From bf63e11021240eac7361c248046283b01a7dd073 Mon Sep 17 00:00:00 2001 From: Park Byung Eun Date: Sat, 11 Jul 2020 19:57:44 +0900 Subject: [PATCH] 0710 sync --- angular.json | 38 - package-lock.json | 760 +++++++++++------- package.json | 94 +-- projects/native-browser/README.md | 24 - projects/native-browser/karma.conf.js | 32 - projects/native-browser/ng-package.json | 11 - projects/native-browser/package.json | 16 - .../lib/services/browser-native.service.ts | 16 - projects/native-browser/src/public-api.ts | 5 - projects/native-browser/src/test.ts | 26 - projects/native-browser/tsconfig.lib.json | 23 - .../native-browser/tsconfig.lib.prod.json | 6 - projects/native-browser/tsconfig.spec.json | 17 - projects/native-browser/tslint.json | 17 - projects/native/ng-package.json | 9 +- projects/native/package.json | 4 +- .../services/browser-native.service.spec.ts | 0 .../lib/services/browser-native.service.ts | 666 +++++++++++++++ .../src/lib/services/notification.service.ts | 44 + projects/native/src/public-api.ts | 3 + projects/store-authentication/ng-package.json | 3 +- projects/store-authentication/package.json | 3 +- .../src/lib/store/login/actions.ts | 27 - .../src/lib/store/login/effects.ts | 62 +- .../src/lib/store/login/reducers.ts | 39 +- projects/store-chat/ng-package.json | 7 + projects/store-chat/package.json | 2 +- .../src/lib/store/chatting/actions.ts | 135 +++- .../src/lib/store/chatting/effects.ts | 481 ++++++++++- .../src/lib/store/chatting/reducers.ts | 185 ++++- .../src/lib/store/chatting/state.ts | 80 +- .../store-chat/src/lib/store/room/actions.ts | 73 +- .../store-chat/src/lib/store/room/effects.ts | 422 +++++++--- .../store-chat/src/lib/store/room/reducers.ts | 217 ++--- .../store-chat/src/lib/store/room/state.ts | 26 +- .../store-chat/src/lib/utils/chat.util.ts | 7 +- projects/store-chat/src/public-api.ts | 3 + projects/store-group/package.json | 2 +- .../src/lib/store/buddy/actions.ts | 12 +- .../src/lib/store/buddy/effects.ts | 72 +- .../src/lib/store/buddy/reducers.ts | 48 +- .../store-group/src/lib/store/buddy/state.ts | 5 +- .../src/lib/store/group/effects.ts | 14 +- projects/store-organization/ng-package.json | 4 +- projects/store-organization/package.json | 4 +- .../src/lib/store/common/actions.ts | 9 +- .../src/lib/store/common/effects.ts | 45 +- .../src/lib/store/company/state.ts | 2 +- .../src/lib/store/department/effects.ts | 44 +- .../src/lib/store/effects.ts | 4 +- .../src/lib/store/presence/actions.ts | 27 +- .../src/lib/store/presence/effects.ts | 25 +- .../src/lib/store/presence/reducers.ts | 12 +- .../src/lib/store/presence/state.ts | 15 +- .../src/lib/store/reducers.ts | 4 +- .../store-organization/src/lib/store/state.ts | 6 + .../src/lib/store/user/actions.ts | 37 + .../src/lib/store/user/effects.ts | 37 + .../src/lib/store/user/reducers.ts | 89 ++ .../src/lib/store/user/state.ts | 20 + projects/store-organization/src/public-api.ts | 9 +- projects/ui-authentication/package.json | 2 +- .../components/change-password.component.html | 6 +- .../src/lib/components/login.component.html | 32 +- projects/ui-authentication/src/public-api.ts | 4 + projects/ui-chat/ng-package.json | 6 +- projects/ui-chat/package.json | 2 +- projects/ui-chat/src/lib/chat-ui.module.ts | 38 +- .../file-list-item-01.component.html | 95 +++ .../file-list-item-01.component.scss | 21 + .../file-list-item-01.component.spec.ts} | 2 +- .../components/file-list-item-01.component.ts | 93 +++ .../lib/components/file-list.component.html | 15 + .../lib/components/file-list.component.scss | 7 + .../components/file-list.component.spec.ts | 25 + .../components/file-list.component.stories.ts | 26 + .../components/file-list.component.theme.scss | 24 + .../src/lib/components/file-list.component.ts | 55 ++ .../image-list-item-01.component.html | 92 +++ .../image-list-item-01.component.scss | 44 + .../image-list-item-01.component.spec.ts | 26 + .../image-list-item-01.component.ts | 122 +++ .../lib/components/image-list.component.html | 15 + .../lib/components/image-list.component.scss | 7 + .../components/image-list.component.spec.ts | 25 + .../image-list.component.stories.ts | 26 + .../image-list.component.theme.scss | 24 + .../lib/components/image-list.component.ts | 55 ++ .../lib/components/item-list.component.html | 17 + .../lib/components/item-list.component.scss | 11 + .../components/item-list.component.spec.ts | 25 + .../components/item-list.component.stories.ts | 22 + .../components/item-list.component.theme.scss | 24 + .../src/lib/components/item-list.component.ts | 71 ++ .../message-box/attach-file.component.html | 161 +++- .../message-box/attach-file.component.scss | 248 +++--- .../message-box/attach-file.component.ts | 66 +- .../message-box/date-splitter.component.scss | 31 + .../message-box/file.component.html | 19 + .../components/message-box/file.component.ts | 32 +- .../message-box/image.component.html | 14 +- .../message-box/image.component.scss | 26 + .../components/message-box/image.component.ts | 24 +- .../message-box/information.component.html | 129 +-- .../message-box/information.component.scss | 10 + .../message-box/information.component.ts | 32 +- .../mass-translation.component.html | 76 +- .../mass-translation.component.scss | 52 ++ .../message-box/mass.component.html | 25 +- .../message-box/mass.component.scss | 22 + .../message-box/read-here.component.scss | 14 + .../message-box/recall.component.html | 33 +- .../message-box/recall.component.scss | 16 + .../message-box/reply.component.html | 33 +- .../message-box/reply.component.scss | 53 ++ .../message-box/schedule.component.html | 80 +- .../message-box/schedule.component.scss | 62 ++ .../message-box/schedule.component.ts | 77 +- .../components/message-box/sms.component.html | 19 +- .../components/message-box/sms.component.scss | 31 + .../message-box/sticker.component.html | 28 +- .../message-box/sticker.component.scss | 10 + .../message-box/text.component.html | 6 +- .../message-box/text.component.scss | 15 + .../message-box/translation.component.html | 40 +- .../message-box/translation.component.scss | 35 + .../video-conference.component.html | 111 +-- .../video-conference.component.scss | 72 ++ .../message-box/video.component.html | 162 +++- .../message-box/video.component.scss | 119 +++ .../components/message-box/video.component.ts | 68 +- .../components/room-expansion.component.html | 16 +- .../components/room-expansion.component.scss | 20 + .../components/room-expansion.component.ts | 35 +- .../room-list-item-01.component.html | 106 ++- .../room-list-item-01.component.scss | 27 + .../components/room-list-item-01.component.ts | 27 +- projects/ui-chat/src/public-api.ts | 25 +- projects/ui-group/package.json | 2 +- .../components/expansion-list.component.html | 91 --- .../components/expansion-list.component.scss | 0 .../expansion-list.component.stories.ts | 16 - .../components/expansion-list.component.ts | 295 ------- .../lib/components/expansion.component.html | 34 +- .../lib/components/expansion.component.scss | 3 + .../src/lib/components/expansion.component.ts | 164 +++- projects/ui-group/src/public-api.ts | 5 +- projects/ui-organization/package.json | 2 +- .../lib/components/profile-01.component.html | 233 ++++-- .../lib/components/profile-01.component.scss | 48 +- .../lib/components/profile-01.component.ts | 64 +- .../profile-image-01.component.html | 3 +- .../profile-image-01.component.scss | 1 + .../components/profile-image-01.component.ts | 32 +- .../profile-list-item-01.component.html | 4 + .../profile-list-item-01.component.scss | 10 +- .../profile-list-item-02.component.html | 25 +- .../profile-list-item-02.component.scss | 46 +- .../profile-list-item.component.html | 1 - .../profile-list-item.component.scss | 0 .../profile-list-item.component.stories.ts | 16 - .../components/profile-list-item.component.ts | 21 - .../components/profile-list.component.html | 25 +- .../components/profile-list.component.scss | 5 + .../lib/components/profile-list.component.ts | 23 +- .../components/profile-menu-01.component.html | 187 +++++ .../components/profile-menu-01.component.scss | 26 + .../profile-menu-01.component.spec.ts | 12 + .../profile-menu-01.component.stories.ts | 26 + .../profile-menu-01.component.theme.scss | 20 + .../components/profile-menu-01.component.ts | 179 +++++ .../profile-selection-01.component.html | 2 +- .../search-for-tenant.component.html | 14 +- .../components/search-for-tenant.component.ts | 15 +- .../src/lib/components/tree.component.html | 27 +- .../src/lib/components/tree.component.ts | 65 +- .../src/lib/organization-ui.module.ts | 23 +- .../src/lib/pipes/translate.pipe.ts | 18 +- .../src/lib/utils/presence.util.ts | 18 +- projects/ui-organization/src/public-api.ts | 7 +- projects/ui/ng-package.json | 2 + projects/ui/package.json | 2 +- .../lib/components/breadcrumb.component.html | 44 + .../lib/components/breadcrumb.component.scss | 33 + .../components/breadcrumb.component.spec.ts | 18 + .../breadcrumb.component.stories.ts | 26 + .../breadcrumb.component.theme.scss | 20 + .../lib/components/breadcrumb.component.ts | 285 +++++++ .../file-upload-queue.component.html | 2 +- .../lib/components/file-viewer.component.html | 30 +- .../lib/components/file-viewer.component.ts | 41 +- .../file-viewer/binary-viewer.component.html | 18 +- .../file-viewer/binary-viewer.component.ts | 3 +- .../file-viewer/document-viewer.component.ts | 3 +- .../file-viewer/image-viewer.component.html | 12 +- .../file-viewer/image-viewer.component.scss | 7 +- .../file-viewer/image-viewer.component.ts | 46 +- .../file-viewer/sound-viewer.component.html | 8 +- .../file-viewer/sound-viewer.component.ts | 6 +- .../file-viewer/video-viewer.component.html | 10 +- .../file-viewer/video-viewer.component.ts | 6 +- .../float-action-button.component.html | 8 +- .../float-action-button.component.ts | 3 + .../inline-edit-input.component.html | 2 +- .../components/media-viewer.component.html | 58 ++ .../components/media-viewer.component.scss | 55 ++ .../media-viewer.component.spec.ts} | 12 +- .../media-viewer.component.stories.ts | 26 + .../media-viewer.component.theme.scss | 20 + .../lib/components/media-viewer.component.ts | 131 +++ .../lib/components/title-bar.component.html | 6 +- .../lib/components/title-bar.component.scss | 9 +- .../components/title-bar.component.stories.ts | 34 + .../src/lib/components/title-bar.component.ts | 44 +- .../virtual-scroll-tree-flat.data-source.ts | 46 +- .../lib/dialogs/alert.dialog.component.html | 2 +- .../lib/dialogs/confirm.dialog.component.html | 4 +- ...virtual-scroll-viewport-patch.directive.ts | 38 - .../directives/file-upload-for.directive.ts | 173 ++-- .../ui/src/lib/directives/image.directive.ts | 51 +- .../ui/src/lib/directives/paginated-header.ts | 544 +++++++++++++ .../ui/src/lib/directives/var.directive.ts | 35 + .../ui/src/lib/models/select-file-info.ts | 4 + .../lib/scrolling/auto-size-virtual-scroll.ts | 562 +++++++++++++ .../scrolling/fixed-size-virtual-scroll.ts | 341 ++++++++ .../scrolling/measure-size-virtual-scroll.ts | 601 ++++++++++++++ .../ui/src/lib/scrolling/scroll-dispatcher.ts | 220 +++++ projects/ui/src/lib/scrolling/scrollable.ts | 241 ++++++ .../ui/src/lib/scrolling/viewport-ruler.ts | 179 +++++ .../ui/src/lib/scrolling/virtual-for-of.ts | 523 ++++++++++++ .../lib/scrolling/virtual-scroll-strategy.ts | 72 ++ .../scrolling/virtual-scroll-viewport.html | 21 + .../scrolling/virtual-scroll-viewport.scss | 87 ++ .../lib/scrolling/virtual-scroll-viewport.ts | 583 ++++++++++++++ .../ui/src/lib/services/clipboard.service.ts | 151 ++++ .../ui/src/lib/services/dialog.service.ts | 52 -- projects/ui/src/lib/services/dom.service.ts | 148 ++++ projects/ui/src/lib/ui.module.ts | 46 +- projects/ui/src/public-api.ts | 29 +- 239 files changed, 12389 insertions(+), 2697 deletions(-) delete mode 100644 projects/native-browser/README.md delete mode 100644 projects/native-browser/karma.conf.js delete mode 100644 projects/native-browser/ng-package.json delete mode 100644 projects/native-browser/package.json delete mode 100644 projects/native-browser/src/lib/services/browser-native.service.ts delete mode 100644 projects/native-browser/src/public-api.ts delete mode 100644 projects/native-browser/src/test.ts delete mode 100644 projects/native-browser/tsconfig.lib.json delete mode 100644 projects/native-browser/tsconfig.lib.prod.json delete mode 100644 projects/native-browser/tsconfig.spec.json delete mode 100644 projects/native-browser/tslint.json rename projects/{native-browser => native}/src/lib/services/browser-native.service.spec.ts (100%) create mode 100644 projects/native/src/lib/services/browser-native.service.ts create mode 100644 projects/native/src/lib/services/notification.service.ts create mode 100644 projects/store-organization/src/lib/store/user/actions.ts create mode 100644 projects/store-organization/src/lib/store/user/effects.ts create mode 100644 projects/store-organization/src/lib/store/user/reducers.ts create mode 100644 projects/store-organization/src/lib/store/user/state.ts create mode 100644 projects/ui-chat/src/lib/components/file-list-item-01.component.html create mode 100644 projects/ui-chat/src/lib/components/file-list-item-01.component.scss rename projects/{ui-organization/src/lib/components/profile-list-item.component.spec.ts => ui-chat/src/lib/components/file-list-item-01.component.spec.ts} (91%) create mode 100644 projects/ui-chat/src/lib/components/file-list-item-01.component.ts create mode 100644 projects/ui-chat/src/lib/components/file-list.component.html create mode 100644 projects/ui-chat/src/lib/components/file-list.component.scss create mode 100644 projects/ui-chat/src/lib/components/file-list.component.spec.ts create mode 100644 projects/ui-chat/src/lib/components/file-list.component.stories.ts create mode 100644 projects/ui-chat/src/lib/components/file-list.component.theme.scss create mode 100644 projects/ui-chat/src/lib/components/file-list.component.ts create mode 100644 projects/ui-chat/src/lib/components/image-list-item-01.component.html create mode 100644 projects/ui-chat/src/lib/components/image-list-item-01.component.scss create mode 100644 projects/ui-chat/src/lib/components/image-list-item-01.component.spec.ts create mode 100644 projects/ui-chat/src/lib/components/image-list-item-01.component.ts create mode 100644 projects/ui-chat/src/lib/components/image-list.component.html create mode 100644 projects/ui-chat/src/lib/components/image-list.component.scss create mode 100644 projects/ui-chat/src/lib/components/image-list.component.spec.ts create mode 100644 projects/ui-chat/src/lib/components/image-list.component.stories.ts create mode 100644 projects/ui-chat/src/lib/components/image-list.component.theme.scss create mode 100644 projects/ui-chat/src/lib/components/image-list.component.ts create mode 100644 projects/ui-chat/src/lib/components/item-list.component.html create mode 100644 projects/ui-chat/src/lib/components/item-list.component.scss create mode 100644 projects/ui-chat/src/lib/components/item-list.component.spec.ts create mode 100644 projects/ui-chat/src/lib/components/item-list.component.stories.ts create mode 100644 projects/ui-chat/src/lib/components/item-list.component.theme.scss create mode 100644 projects/ui-chat/src/lib/components/item-list.component.ts delete mode 100644 projects/ui-group/src/lib/components/expansion-list.component.html delete mode 100644 projects/ui-group/src/lib/components/expansion-list.component.scss delete mode 100644 projects/ui-group/src/lib/components/expansion-list.component.stories.ts delete mode 100644 projects/ui-group/src/lib/components/expansion-list.component.ts delete mode 100644 projects/ui-organization/src/lib/components/profile-list-item.component.html delete mode 100644 projects/ui-organization/src/lib/components/profile-list-item.component.scss delete mode 100644 projects/ui-organization/src/lib/components/profile-list-item.component.stories.ts delete mode 100644 projects/ui-organization/src/lib/components/profile-list-item.component.ts create mode 100644 projects/ui-organization/src/lib/components/profile-menu-01.component.html create mode 100644 projects/ui-organization/src/lib/components/profile-menu-01.component.scss create mode 100644 projects/ui-organization/src/lib/components/profile-menu-01.component.spec.ts create mode 100644 projects/ui-organization/src/lib/components/profile-menu-01.component.stories.ts create mode 100644 projects/ui-organization/src/lib/components/profile-menu-01.component.theme.scss create mode 100644 projects/ui-organization/src/lib/components/profile-menu-01.component.ts create mode 100644 projects/ui/src/lib/components/breadcrumb.component.html create mode 100644 projects/ui/src/lib/components/breadcrumb.component.scss create mode 100644 projects/ui/src/lib/components/breadcrumb.component.spec.ts create mode 100644 projects/ui/src/lib/components/breadcrumb.component.stories.ts create mode 100644 projects/ui/src/lib/components/breadcrumb.component.theme.scss create mode 100644 projects/ui/src/lib/components/breadcrumb.component.ts create mode 100644 projects/ui/src/lib/components/media-viewer.component.html create mode 100644 projects/ui/src/lib/components/media-viewer.component.scss rename projects/{ui-group/src/lib/components/expansion-list.component.spec.ts => ui/src/lib/components/media-viewer.component.spec.ts} (59%) create mode 100644 projects/ui/src/lib/components/media-viewer.component.stories.ts create mode 100644 projects/ui/src/lib/components/media-viewer.component.theme.scss create mode 100644 projects/ui/src/lib/components/media-viewer.component.ts delete mode 100644 projects/ui/src/lib/directives/cdk-virtual-scroll-viewport-patch.directive.ts create mode 100644 projects/ui/src/lib/directives/paginated-header.ts create mode 100644 projects/ui/src/lib/directives/var.directive.ts create mode 100644 projects/ui/src/lib/models/select-file-info.ts create mode 100644 projects/ui/src/lib/scrolling/auto-size-virtual-scroll.ts create mode 100644 projects/ui/src/lib/scrolling/fixed-size-virtual-scroll.ts create mode 100644 projects/ui/src/lib/scrolling/measure-size-virtual-scroll.ts create mode 100644 projects/ui/src/lib/scrolling/scroll-dispatcher.ts create mode 100644 projects/ui/src/lib/scrolling/scrollable.ts create mode 100644 projects/ui/src/lib/scrolling/viewport-ruler.ts create mode 100644 projects/ui/src/lib/scrolling/virtual-for-of.ts create mode 100644 projects/ui/src/lib/scrolling/virtual-scroll-strategy.ts create mode 100644 projects/ui/src/lib/scrolling/virtual-scroll-viewport.html create mode 100644 projects/ui/src/lib/scrolling/virtual-scroll-viewport.scss create mode 100644 projects/ui/src/lib/scrolling/virtual-scroll-viewport.ts create mode 100644 projects/ui/src/lib/services/clipboard.service.ts delete mode 100644 projects/ui/src/lib/services/dialog.service.ts create mode 100644 projects/ui/src/lib/services/dom.service.ts diff --git a/angular.json b/angular.json index dff1cee..0ac2632 100644 --- a/angular.json +++ b/angular.json @@ -1074,44 +1074,6 @@ } } }, - "native-browser": { - "projectType": "library", - "root": "projects/native-browser", - "sourceRoot": "projects/native-browser/src", - "prefix": "lib", - "architect": { - "build": { - "builder": "@angular-devkit/build-ng-packagr:build", - "options": { - "tsConfig": "projects/native-browser/tsconfig.lib.json", - "project": "projects/native-browser/ng-package.json" - }, - "configurations": { - "production": { - "tsConfig": "projects/native-browser/tsconfig.lib.prod.json" - } - } - }, - "test": { - "builder": "@angular-devkit/build-angular:karma", - "options": { - "main": "projects/native-browser/src/test.ts", - "tsConfig": "projects/native-browser/tsconfig.spec.json", - "karmaConfig": "projects/native-browser/karma.conf.js" - } - }, - "lint": { - "builder": "@angular-devkit/build-angular:tslint", - "options": { - "tsConfig": [ - "projects/native-browser/tsconfig.lib.json", - "projects/native-browser/tsconfig.spec.json" - ], - "exclude": ["**/node_modules/**"] - } - } - } - }, "store-authentication": { "projectType": "library", "root": "projects/store-authentication", diff --git a/package-lock.json b/package-lock.json index 37a780f..22cc4dc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -309,20 +309,20 @@ } }, "@angular-devkit/schematics": { - "version": "9.1.3", - "resolved": "https://nexus.loafle.net/repository/npm-all/@angular-devkit/schematics/-/schematics-9.1.3.tgz", - "integrity": "sha512-nNRoQPqoin5jzLKmTabIPLSqVw2Zjnk78XC4AhAM7rSisWMsZ6pw5+BNDITiKfq0LkSXfOs0DoJV/Vr9mLkn/A==", + "version": "9.1.11", + "resolved": "https://nexus.loafle.net/repository/npm-all/@angular-devkit/schematics/-/schematics-9.1.11.tgz", + "integrity": "sha512-1A3Oryhl8hpibJK2J5j2FYNzjfvBJcR4wuNRKzl27kBvVsdRXLQzMD3aAgqFvlMgUWhloQs4tZwuinu0E2VP1A==", "dev": true, "requires": { - "@angular-devkit/core": "9.1.3", + "@angular-devkit/core": "9.1.11", "ora": "4.0.3", "rxjs": "6.5.4" }, "dependencies": { "@angular-devkit/core": { - "version": "9.1.3", - "resolved": "https://nexus.loafle.net/repository/npm-all/@angular-devkit/core/-/core-9.1.3.tgz", - "integrity": "sha512-VRV96prPy0Kdlm6XmI7DITqSMSc1bINimnOhzQre3euDX5OQty+EUqaexHtMv/SPDZX1agP+buHr6viv9YEhzA==", + "version": "9.1.11", + "resolved": "https://nexus.loafle.net/repository/npm-all/@angular-devkit/core/-/core-9.1.11.tgz", + "integrity": "sha512-uiEkDvWfMgPHuO4jVgBEr9Kl/LuxHaWYGD2ZtKsOnnHYZyRGp61ot7UcDF+KNdXTiq01JJH84VTd3IttEewmhQ==", "dev": true, "requires": { "ajv": "6.12.0", @@ -345,9 +345,9 @@ } }, "fast-deep-equal": { - "version": "3.1.1", - "resolved": "https://nexus.loafle.net/repository/npm-all/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz", - "integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==", + "version": "3.1.3", + "resolved": "https://nexus.loafle.net/repository/npm-all/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true }, "fast-json-stable-stringify": { @@ -383,15 +383,15 @@ } }, "@angular/animations": { - "version": "9.1.3", - "resolved": "https://nexus.loafle.net/repository/npm-all/@angular/animations/-/animations-9.1.3.tgz", - "integrity": "sha512-QTQZSnXxr9SCgrN2SZsTMzS/UldrZ65y6Kuqs9yyghNc694eMbabgiPdeegjvcKlDhVPCN2x9Bjfb1vXPOfdvQ==", + "version": "9.1.12", + "resolved": "https://nexus.loafle.net/repository/npm-all/@angular/animations/-/animations-9.1.12.tgz", + "integrity": "sha512-tphpf9QHnOPoL2Jl7KpR+R5aHNW3oifLEmRUTajJYJGvo1uzdUDE82+V9OGOinxJsYseCth9gYJhN24aYTB9NA==", "dev": true }, "@angular/cdk": { - "version": "9.2.1", - "resolved": "https://nexus.loafle.net/repository/npm-all/@angular/cdk/-/cdk-9.2.1.tgz", - "integrity": "sha512-aSG1UNPszkSnpNuDCNd7ZgT29oQ8vqHPmoqjvJI0JkEv3i6uEs5tRuhWl3TK39wDNuwdlq0AY47XTa/0Ppb5RQ==", + "version": "9.2.4", + "resolved": "https://nexus.loafle.net/repository/npm-all/@angular/cdk/-/cdk-9.2.4.tgz", + "integrity": "sha512-iw2+qHMXHYVC6K/fttHeNHIieSKiTEodVutZoOEcBu9rmRTGbLB26V/CRsfIRmA1RBk+uFYWc6UQZnMC3RdnJQ==", "dev": true, "requires": { "parse5": "^5.0.0" @@ -407,16 +407,16 @@ } }, "@angular/cli": { - "version": "9.1.3", - "resolved": "https://nexus.loafle.net/repository/npm-all/@angular/cli/-/cli-9.1.3.tgz", - "integrity": "sha512-/7yHOuiyMgpcoBuADPrF4Eo9VDysA57fsyrMlOH2WZmKdsjW032StS9EIOue5RNQ7y0DwrtgtnkjbpZ6nYo3Pw==", + "version": "9.1.11", + "resolved": "https://nexus.loafle.net/repository/npm-all/@angular/cli/-/cli-9.1.11.tgz", + "integrity": "sha512-EGDd9ZCGitn6F/x5sK7M1PmdfBg+8LoZ6nrfYEhWY8c2CMCVfVrLPLHq6r8uFiflcvd/nlcrVZQ52zt4a2vDaQ==", "dev": true, "requires": { - "@angular-devkit/architect": "0.901.3", - "@angular-devkit/core": "9.1.3", - "@angular-devkit/schematics": "9.1.3", - "@schematics/angular": "9.1.3", - "@schematics/update": "0.901.3", + "@angular-devkit/architect": "0.901.11", + "@angular-devkit/core": "9.1.11", + "@angular-devkit/schematics": "9.1.11", + "@schematics/angular": "9.1.11", + "@schematics/update": "0.901.11", "@yarnpkg/lockfile": "1.1.0", "ansi-colors": "4.1.1", "debug": "4.1.1", @@ -435,19 +435,19 @@ }, "dependencies": { "@angular-devkit/architect": { - "version": "0.901.3", - "resolved": "https://nexus.loafle.net/repository/npm-all/@angular-devkit/architect/-/architect-0.901.3.tgz", - "integrity": "sha512-CFjSj48nOJwejmFFtenIqSZWyxRe4fRQsg16l0R4sagW7YwMJSaW6Yl9hRHM8bviPRrTpGHnxeq1x506v1ARLw==", + "version": "0.901.11", + "resolved": "https://nexus.loafle.net/repository/npm-all/@angular-devkit/architect/-/architect-0.901.11.tgz", + "integrity": "sha512-RmYOq1VEJdQLzwMno+C56WtgscAtoR/7i4tX5b5VxRa2RmQKTxowllYWwgrF5445VGUqzap9H6zJFXvlY2FA0w==", "dev": true, "requires": { - "@angular-devkit/core": "9.1.3", + "@angular-devkit/core": "9.1.11", "rxjs": "6.5.4" } }, "@angular-devkit/core": { - "version": "9.1.3", - "resolved": "https://nexus.loafle.net/repository/npm-all/@angular-devkit/core/-/core-9.1.3.tgz", - "integrity": "sha512-VRV96prPy0Kdlm6XmI7DITqSMSc1bINimnOhzQre3euDX5OQty+EUqaexHtMv/SPDZX1agP+buHr6viv9YEhzA==", + "version": "9.1.11", + "resolved": "https://nexus.loafle.net/repository/npm-all/@angular-devkit/core/-/core-9.1.11.tgz", + "integrity": "sha512-uiEkDvWfMgPHuO4jVgBEr9Kl/LuxHaWYGD2ZtKsOnnHYZyRGp61ot7UcDF+KNdXTiq01JJH84VTd3IttEewmhQ==", "dev": true, "requires": { "ajv": "6.12.0", @@ -476,9 +476,9 @@ "dev": true }, "fast-deep-equal": { - "version": "3.1.1", - "resolved": "https://nexus.loafle.net/repository/npm-all/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz", - "integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==", + "version": "3.1.3", + "resolved": "https://nexus.loafle.net/repository/npm-all/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true }, "fast-json-stable-stringify": { @@ -536,21 +536,21 @@ } }, "@angular/common": { - "version": "9.1.3", - "resolved": "https://nexus.loafle.net/repository/npm-all/@angular/common/-/common-9.1.3.tgz", - "integrity": "sha512-OGrd/x+WoceskeJWZI3ur+KqVoBHn2kmbg57RyJ8Ng/BkdDVYiXzQ/UvvX1WYGH/YzmiiVdlnVq55xkHkzGaXw==", + "version": "9.1.12", + "resolved": "https://nexus.loafle.net/repository/npm-all/@angular/common/-/common-9.1.12.tgz", + "integrity": "sha512-XSIqkbM6VV1yixF9zuzeE5eqN1VsiXS517K2VU0XgCRSAzhVhLOeKsdYjeLf7PdSu/HgW/Tr81H+isi9A9I0YA==", "dev": true }, "@angular/compiler": { - "version": "9.1.3", - "resolved": "https://nexus.loafle.net/repository/npm-all/@angular/compiler/-/compiler-9.1.3.tgz", - "integrity": "sha512-NTqGZUwwdm/RIG1rWPIqQwNyyRcPOmNlv+t1KpydFtycpTut0lAh0shiu57h83Cl2LgaFV4veyiAAtNmxFmqXA==", + "version": "9.1.12", + "resolved": "https://nexus.loafle.net/repository/npm-all/@angular/compiler/-/compiler-9.1.12.tgz", + "integrity": "sha512-suefk0OFkaJpUUKnV+phbL4T8fmVGHvzkereY5eqybQlumOez8NPL1PJcygAylh/E6OIAYm8SWookYwM6ZY9dg==", "dev": true }, "@angular/compiler-cli": { - "version": "9.1.3", - "resolved": "https://nexus.loafle.net/repository/npm-all/@angular/compiler-cli/-/compiler-cli-9.1.3.tgz", - "integrity": "sha512-FqDPmEbJQmzzxqF08CJfqT9XKJFPYdyZuFFStKkJEZ5s1JrjrBlgQSqdF0qp5jWNsjTB87USLHC5zAlzJtMKxQ==", + "version": "9.1.12", + "resolved": "https://nexus.loafle.net/repository/npm-all/@angular/compiler-cli/-/compiler-cli-9.1.12.tgz", + "integrity": "sha512-bbqJ+fbY+aQejSYuHUjE1qYJCXkZBM5Hru9eN7m/j376u83MQ5jWdC290uYx+ipsXcPTa/YRZ44jpL+5cCzIrg==", "dev": true, "requires": { "canonical-path": "1.0.0", @@ -756,57 +756,57 @@ } }, "@angular/core": { - "version": "9.1.3", - "resolved": "https://nexus.loafle.net/repository/npm-all/@angular/core/-/core-9.1.3.tgz", - "integrity": "sha512-r+GyeCuBw9SUBzpPWhKxzTOkNYotYp3gANSWndihLC9P6W5T8Hfyg8uYcxKy7L42bpx8eXZOXFq91cioVwZtqA==", + "version": "9.1.12", + "resolved": "https://nexus.loafle.net/repository/npm-all/@angular/core/-/core-9.1.12.tgz", + "integrity": "sha512-WVA/eh3fzjx0apOzkKot4YRRUsGkHj50zFQWrAOMgivGaj1YVrvhf+m3hpglj5fn/BkLiFDl8RT0wAE8z9X+gQ==", "dev": true }, "@angular/flex-layout": { - "version": "9.0.0-beta.29", - "resolved": "https://nexus.loafle.net/repository/npm-all/@angular/flex-layout/-/flex-layout-9.0.0-beta.29.tgz", - "integrity": "sha512-93sxR+kYfYMOdnlWL0Q77FZ428gg8XnBu0YZm6GsCdkw/vLggIT/G1ZAqHlCPIODt6pxmCJ5KXh4ShvniIYDsA==", + "version": "9.0.0-beta.31", + "resolved": "https://nexus.loafle.net/repository/npm-all/@angular/flex-layout/-/flex-layout-9.0.0-beta.31.tgz", + "integrity": "sha512-g94u2mecDl87ORvFRuOBshV/S/ETE4bybClU2e1xXKWNG+rhRHchChneHSonc29ZLyROTjHhmAtKOYojL92uLA==", "dev": true }, "@angular/forms": { - "version": "9.1.3", - "resolved": "https://nexus.loafle.net/repository/npm-all/@angular/forms/-/forms-9.1.3.tgz", - "integrity": "sha512-t3BexN5EHgCK6q0R04AuyNb6ZUP6sd9fJ+ER98yWws3HXV6FT+J5IB7dVnhelLoU1GTnql4TJSS+LTVradKZ7g==", + "version": "9.1.12", + "resolved": "https://nexus.loafle.net/repository/npm-all/@angular/forms/-/forms-9.1.12.tgz", + "integrity": "sha512-LhjnZlC4WEsEsAJfOZLte+Lks3WBAFVeRv2lzoQNFVr/IMzBNDVfjEaaSqKF1cei3cjY39Df2nYDMJM7HfqbJA==", "dev": true }, "@angular/language-service": { - "version": "9.1.3", - "resolved": "https://nexus.loafle.net/repository/npm-all/@angular/language-service/-/language-service-9.1.3.tgz", - "integrity": "sha512-mcZDO6C2BpDinMjDeMAdohTefpFxNu/S0JEqewYQIQ8TXtKm77xDPUdNdNb4qD4zYW+c46f7UXNMX/tbYCicQw==", + "version": "9.1.12", + "resolved": "https://nexus.loafle.net/repository/npm-all/@angular/language-service/-/language-service-9.1.12.tgz", + "integrity": "sha512-0qfIAn5fP5lD+JW6il5HBHGS89rv+idRv5aooDkHqBhuBo4V2VuB1wNy5eP49GZbHKMW1xPAzv1MqeMdk+zwQA==", "dev": true }, "@angular/material": { - "version": "9.2.1", - "resolved": "https://nexus.loafle.net/repository/npm-all/@angular/material/-/material-9.2.1.tgz", - "integrity": "sha512-nqn/0Eg04DxwnkRGSM1xnmGgtfHYOBcEPbFeTu8c1qAbjFEozd6tpw4y6dQrCCL/JLNIRQPsxsUsVnKeWDF/4Q==", + "version": "9.2.4", + "resolved": "https://nexus.loafle.net/repository/npm-all/@angular/material/-/material-9.2.4.tgz", + "integrity": "sha512-LkoTXE6B0slvMhvfZDdPWaz4yaYLkaAp5VSPunI9pxGsPxzqEV9e210wC1/sjG/76Nk8Ep7/2z9XKac8Q9bMwA==", "dev": true }, "@angular/material-moment-adapter": { - "version": "9.2.1", - "resolved": "https://nexus.loafle.net/repository/npm-all/@angular/material-moment-adapter/-/material-moment-adapter-9.2.1.tgz", - "integrity": "sha512-i3tbL3DJQspO23cPAgW6pAvztuv2D8tkGhxl+bTUlq8FgGBqKrOa0T/maN1fik46AXZojowUVZoCG7EvV2T7nQ==", + "version": "9.2.4", + "resolved": "https://nexus.loafle.net/repository/npm-all/@angular/material-moment-adapter/-/material-moment-adapter-9.2.4.tgz", + "integrity": "sha512-V5xkL+YUec3nDGRaJB72mJTUtdUvGaG9WCQEdr45viDWFGjQaEpS6msuScBLp0PwsN8Wt0n69eZg0ULgxPBa5g==", "dev": true }, "@angular/platform-browser": { - "version": "9.1.3", - "resolved": "https://nexus.loafle.net/repository/npm-all/@angular/platform-browser/-/platform-browser-9.1.3.tgz", - "integrity": "sha512-ts6b7QBp4+UpuQS0Eb+FOT4MZVPUwWtWH4iIfBILJaG/0Bk4jHZeXuLFcpgeZkwMNCZEt9f0JrhnHfvuLCez5g==", + "version": "9.1.12", + "resolved": "https://nexus.loafle.net/repository/npm-all/@angular/platform-browser/-/platform-browser-9.1.12.tgz", + "integrity": "sha512-rPa/hJcLfdId6bYB0b6pFUo3QIgjZlvUlmtKMGdrLNLYR8XQxPa2Y/UdN/5YeZ12htGw6GXrX9U8U7nTbUSpkw==", "dev": true }, "@angular/platform-browser-dynamic": { - "version": "9.1.3", - "resolved": "https://nexus.loafle.net/repository/npm-all/@angular/platform-browser-dynamic/-/platform-browser-dynamic-9.1.3.tgz", - "integrity": "sha512-9U8PkBflnQtKIZ4t0AqYUaKLg7Kx1BvJ1D/S8FZo/V5kBJK6NkGVu58XddWVc+dtO3V+vCC/VLG7fb2PMf/V5g==", + "version": "9.1.12", + "resolved": "https://nexus.loafle.net/repository/npm-all/@angular/platform-browser-dynamic/-/platform-browser-dynamic-9.1.12.tgz", + "integrity": "sha512-NmwUZaQeMnA6f+vP9Fp9P+qjL72H8dKlxLS76ujlKHVf75pP5oahWS8wfl7KXel1tKW3FQWMMffmKf5/NHRiSw==", "dev": true }, "@angular/router": { - "version": "9.1.3", - "resolved": "https://nexus.loafle.net/repository/npm-all/@angular/router/-/router-9.1.3.tgz", - "integrity": "sha512-NmOazeJ2WVT5eTyNHXYxBRAHic6i6Ku5DgsGWBgjUIR/Qmwb/i/xpagbA9WQQZ2OrGbp4ICEYgGkikeIrlQLPA==", + "version": "9.1.12", + "resolved": "https://nexus.loafle.net/repository/npm-all/@angular/router/-/router-9.1.12.tgz", + "integrity": "sha512-+qCaXa9y0nsRhzjAYBqmGoQ2YkrdXgftZwuFDf6t4qEi30EXa0oS97KrlFq0M5GKdLIDGrbUm9PcdHSTOI+ZhA==", "dev": true }, "@babel/cli": { @@ -1004,6 +1004,7 @@ "version": "7.8.3", "resolved": "https://nexus.loafle.net/repository/npm-all/@babel/code-frame/-/code-frame-7.8.3.tgz", "integrity": "sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==", + "dev": true, "requires": { "@babel/highlight": "^7.8.3" } @@ -1180,6 +1181,7 @@ "version": "7.8.3", "resolved": "https://nexus.loafle.net/repository/npm-all/@babel/helper-module-imports/-/helper-module-imports-7.8.3.tgz", "integrity": "sha512-R0Bx3jippsbAEtzkpZ/6FIiuzOURPcMjHp+Z6xPe6DtApDJx+w7UYyOLanZqO8+wKR9G10s/FmHXvxaMd9s6Kg==", + "dev": true, "requires": { "@babel/types": "^7.8.3" } @@ -1270,7 +1272,8 @@ "@babel/helper-validator-identifier": { "version": "7.9.5", "resolved": "https://nexus.loafle.net/repository/npm-all/@babel/helper-validator-identifier/-/helper-validator-identifier-7.9.5.tgz", - "integrity": "sha512-/8arLKUFq882w4tWGj9JYzRpAlZgiWUJ+dtteNTDqrRBz9Iguck9Rn3ykuBDoUwh2TO4tSAJlrxDUOXWklJe4g==" + "integrity": "sha512-/8arLKUFq882w4tWGj9JYzRpAlZgiWUJ+dtteNTDqrRBz9Iguck9Rn3ykuBDoUwh2TO4tSAJlrxDUOXWklJe4g==", + "dev": true }, "@babel/helper-wrap-function": { "version": "7.8.3", @@ -1299,6 +1302,7 @@ "version": "7.9.0", "resolved": "https://nexus.loafle.net/repository/npm-all/@babel/highlight/-/highlight-7.9.0.tgz", "integrity": "sha512-lJZPilxX7Op3Nv/2cvFdnlepPXDxi29wxteT57Q965oc5R9v86ztx0jfxVrTcBk8C2kcPkkDa2Z4T3ZsPPVWsQ==", + "dev": true, "requires": { "@babel/helper-validator-identifier": "^7.9.0", "chalk": "^2.0.0", @@ -1823,6 +1827,7 @@ "version": "7.9.2", "resolved": "https://nexus.loafle.net/repository/npm-all/@babel/runtime/-/runtime-7.9.2.tgz", "integrity": "sha512-NE2DtOdufG7R5vnfQUTehdTfNycfUANEtCa9PssN9O/xmTzP4E08UI797ixaei6hBEVL9BI/PsdJS5x7mWoB9Q==", + "dev": true, "requires": { "regenerator-runtime": "^0.13.4" } @@ -1873,6 +1878,7 @@ "version": "7.9.5", "resolved": "https://nexus.loafle.net/repository/npm-all/@babel/types/-/types-7.9.5.tgz", "integrity": "sha512-XjnvNqenk818r5zMaba+sLQjnbda31UfUURv3ei0qPQw4u+j2jMyJ5b11y8ZHYTRSI3NnInQkkkRT4fLqqPdHg==", + "dev": true, "requires": { "@babel/helper-validator-identifier": "^7.9.5", "lodash": "^4.17.13", @@ -1883,6 +1889,7 @@ "version": "10.0.29", "resolved": "https://nexus.loafle.net/repository/npm-all/@emotion/cache/-/cache-10.0.29.tgz", "integrity": "sha512-fU2VtSVlHiF27empSbxi1O2JFdNWZO+2NFHfwO0pxgTep6Xa3uGb+3pVKfLww2l/IBGLNEZl5Xf/++A4wAYDYQ==", + "dev": true, "requires": { "@emotion/sheet": "0.9.4", "@emotion/stylis": "0.8.5", @@ -1894,6 +1901,7 @@ "version": "10.0.28", "resolved": "https://nexus.loafle.net/repository/npm-all/@emotion/core/-/core-10.0.28.tgz", "integrity": "sha512-pH8UueKYO5jgg0Iq+AmCLxBsvuGtvlmiDCOuv8fGNYn3cowFpLN98L8zO56U0H1PjDIyAlXymgL3Wu7u7v6hbA==", + "dev": true, "requires": { "@babel/runtime": "^7.5.5", "@emotion/cache": "^10.0.27", @@ -1907,6 +1915,7 @@ "version": "10.0.27", "resolved": "https://nexus.loafle.net/repository/npm-all/@emotion/css/-/css-10.0.27.tgz", "integrity": "sha512-6wZjsvYeBhyZQYNrGoR5yPMYbMBNEnanDrqmsqS1mzDm1cOTu12shvl2j4QHNS36UaTE0USIJawCH9C8oW34Zw==", + "dev": true, "requires": { "@emotion/serialize": "^0.11.15", "@emotion/utils": "0.11.3", @@ -1916,12 +1925,14 @@ "@emotion/hash": { "version": "0.8.0", "resolved": "https://nexus.loafle.net/repository/npm-all/@emotion/hash/-/hash-0.8.0.tgz", - "integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==" + "integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==", + "dev": true }, "@emotion/is-prop-valid": { "version": "0.8.8", "resolved": "https://nexus.loafle.net/repository/npm-all/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz", "integrity": "sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==", + "dev": true, "requires": { "@emotion/memoize": "0.7.4" } @@ -1929,12 +1940,14 @@ "@emotion/memoize": { "version": "0.7.4", "resolved": "https://nexus.loafle.net/repository/npm-all/@emotion/memoize/-/memoize-0.7.4.tgz", - "integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==" + "integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==", + "dev": true }, "@emotion/serialize": { "version": "0.11.16", "resolved": "https://nexus.loafle.net/repository/npm-all/@emotion/serialize/-/serialize-0.11.16.tgz", "integrity": "sha512-G3J4o8by0VRrO+PFeSc3js2myYNOXVJ3Ya+RGVxnshRYgsvErfAOglKAiy1Eo1vhzxqtUvjCyS5gtewzkmvSSg==", + "dev": true, "requires": { "@emotion/hash": "0.8.0", "@emotion/memoize": "0.7.4", @@ -1946,12 +1959,14 @@ "@emotion/sheet": { "version": "0.9.4", "resolved": "https://nexus.loafle.net/repository/npm-all/@emotion/sheet/-/sheet-0.9.4.tgz", - "integrity": "sha512-zM9PFmgVSqBw4zL101Q0HrBVTGmpAxFZH/pYx/cjJT5advXguvcgjHFTCaIO3enL/xr89vK2bh0Mfyj9aa0ANA==" + "integrity": "sha512-zM9PFmgVSqBw4zL101Q0HrBVTGmpAxFZH/pYx/cjJT5advXguvcgjHFTCaIO3enL/xr89vK2bh0Mfyj9aa0ANA==", + "dev": true }, "@emotion/styled": { "version": "10.0.27", "resolved": "https://nexus.loafle.net/repository/npm-all/@emotion/styled/-/styled-10.0.27.tgz", "integrity": "sha512-iK/8Sh7+NLJzyp9a5+vIQIXTYxfT4yB/OJbjzQanB2RZpvmzBQOHZWhpAMZWYEKRNNbsD6WfBw5sVWkb6WzS/Q==", + "dev": true, "requires": { "@emotion/styled-base": "^10.0.27", "babel-plugin-emotion": "^10.0.27" @@ -1961,6 +1976,7 @@ "version": "10.0.31", "resolved": "https://nexus.loafle.net/repository/npm-all/@emotion/styled-base/-/styled-base-10.0.31.tgz", "integrity": "sha512-wTOE1NcXmqMWlyrtwdkqg87Mu6Rj1MaukEoEmEkHirO5IoHDJ8LgCQL4MjJODgxWxXibGR3opGp1p7YvkNEdXQ==", + "dev": true, "requires": { "@babel/runtime": "^7.5.5", "@emotion/is-prop-valid": "0.8.8", @@ -1971,27 +1987,32 @@ "@emotion/stylis": { "version": "0.8.5", "resolved": "https://nexus.loafle.net/repository/npm-all/@emotion/stylis/-/stylis-0.8.5.tgz", - "integrity": "sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ==" + "integrity": "sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ==", + "dev": true }, "@emotion/unitless": { "version": "0.7.5", "resolved": "https://nexus.loafle.net/repository/npm-all/@emotion/unitless/-/unitless-0.7.5.tgz", - "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==" + "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==", + "dev": true }, "@emotion/utils": { "version": "0.11.3", "resolved": "https://nexus.loafle.net/repository/npm-all/@emotion/utils/-/utils-0.11.3.tgz", - "integrity": "sha512-0o4l6pZC+hI88+bzuaX/6BgOvQVhbt2PfmxauVaYOGgbsAw14wdKyvMCZXnsnsHys94iadcF+RG/wZyx6+ZZBw==" + "integrity": "sha512-0o4l6pZC+hI88+bzuaX/6BgOvQVhbt2PfmxauVaYOGgbsAw14wdKyvMCZXnsnsHys94iadcF+RG/wZyx6+ZZBw==", + "dev": true }, "@emotion/weak-memoize": { "version": "0.2.5", "resolved": "https://nexus.loafle.net/repository/npm-all/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz", - "integrity": "sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA==" + "integrity": "sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA==", + "dev": true }, "@icons/material": { "version": "0.2.4", "resolved": "https://nexus.loafle.net/repository/npm-all/@icons/material/-/material-0.2.4.tgz", - "integrity": "sha512-QPcGmICAPbGLGb6F/yNf/KzKqvFx8z5qx3D1yFqVAjoFmXK35EgyW+cJ57Te3CNsmzblwtzakLGFqHPqrfb4Tw==" + "integrity": "sha512-QPcGmICAPbGLGb6F/yNf/KzKqvFx8z5qx3D1yFqVAjoFmXK35EgyW+cJ57Te3CNsmzblwtzakLGFqHPqrfb4Tw==", + "dev": true }, "@istanbuljs/schema": { "version": "0.1.2", @@ -2010,33 +2031,33 @@ } }, "@ngrx/effects": { - "version": "9.1.0", - "resolved": "https://nexus.loafle.net/repository/npm-all/@ngrx/effects/-/effects-9.1.0.tgz", - "integrity": "sha512-DPMtp/2YTrvuxMqaehQ39FeoNODuMcRB6xDImvrnMFPIkUcP3mDHnIg6UB5c2JvfHlZZClKR+Fb+RE2nuVub8w==", + "version": "9.2.0", + "resolved": "https://nexus.loafle.net/repository/npm-all/@ngrx/effects/-/effects-9.2.0.tgz", + "integrity": "sha512-8V09zDIPehGpzgfcgyczelovsVYJvDQhN9wHt37K5A+YCG0CI8nj8FmKokHATwv/S62YqFrOVnr/TZacxpDhBw==", "dev": true }, "@ngrx/entity": { - "version": "9.1.0", - "resolved": "https://nexus.loafle.net/repository/npm-all/@ngrx/entity/-/entity-9.1.0.tgz", - "integrity": "sha512-MrjdJeJIaUpPogxzK7S/z5Cc/6XWyyQfUgkXrTS20JZyX4amjVZYL3oU3FF3JhXO9ZkhaBFi07IDPEtvyYl/Mg==", + "version": "9.2.0", + "resolved": "https://nexus.loafle.net/repository/npm-all/@ngrx/entity/-/entity-9.2.0.tgz", + "integrity": "sha512-xSnS4EmksfvIobl2KMpljE1RMYuJGq7j5cCb9TnlsXkEc7cUa0TyGviSsxceSpk9WKtKARPR/AcVrVCESucF6Q==", "dev": true }, "@ngrx/router-store": { - "version": "9.1.0", - "resolved": "https://nexus.loafle.net/repository/npm-all/@ngrx/router-store/-/router-store-9.1.0.tgz", - "integrity": "sha512-zdIBUsfyrrA64mUqajHMARSwttOAOJ5FSyVJLHQfBlz9/vil6Kby1KhVLEG7mGmkz799YYlIM135QsrJhgf7wA==", + "version": "9.2.0", + "resolved": "https://nexus.loafle.net/repository/npm-all/@ngrx/router-store/-/router-store-9.2.0.tgz", + "integrity": "sha512-thu6aU9YWM64oNEk4Srx/mNSeQ2SPJKlTji8MSzfr06qgCMyPSXZBYlfs8HqY+af3eB7XBEhb/4ew4JJ6xC9zw==", "dev": true }, "@ngrx/store": { - "version": "9.1.0", - "resolved": "https://nexus.loafle.net/repository/npm-all/@ngrx/store/-/store-9.1.0.tgz", - "integrity": "sha512-Ah2nApPZXTvKl7ybaFMIFwEdMg8b7PvIKATigbDTUftAsFxked2+T7blNv6aCgSfEqbOVMBTYPk7OsLCYZoz4Q==", + "version": "9.2.0", + "resolved": "https://nexus.loafle.net/repository/npm-all/@ngrx/store/-/store-9.2.0.tgz", + "integrity": "sha512-V8AI3mxbMztVpbZpALkLZYlGkofKcu9GaOCY5e+sZ1VcJ90oxhFjBpnmd6MuVdmhep1XAHALb1B8ZbBFn+xsgQ==", "dev": true }, "@ngrx/store-devtools": { - "version": "9.1.0", - "resolved": "https://nexus.loafle.net/repository/npm-all/@ngrx/store-devtools/-/store-devtools-9.1.0.tgz", - "integrity": "sha512-KDvuNBxfTgDHjESiouEoPXq8GCUYbHnA+jOs8MzzwVDmxNLeZuDy3amaVzKv9Q5nncI6mNGcXOzTKWzapET8Sw==", + "version": "9.2.0", + "resolved": "https://nexus.loafle.net/repository/npm-all/@ngrx/store-devtools/-/store-devtools-9.2.0.tgz", + "integrity": "sha512-/FvgcpjO4IvwNFnRVoHGikAvckr6fxKf4NgYoTQ9giI8xavolLvuQUHxzH20legi5dgZz34ii2m2g1Q7OxEV8w==", "dev": true }, "@ngtools/webpack": { @@ -2072,6 +2093,7 @@ "version": "1.3.3", "resolved": "https://nexus.loafle.net/repository/npm-all/@reach/router/-/router-1.3.3.tgz", "integrity": "sha512-gOIAiFhWdiVGSVjukKeNKkCRBLmnORoTPyBihI/jLunICPgxdP30DroAvPQuf1eVfQbfGJQDJkwhJXsNPMnVWw==", + "dev": true, "requires": { "create-react-context": "0.3.0", "invariant": "^2.2.3", @@ -2146,19 +2168,19 @@ } }, "@schematics/angular": { - "version": "9.1.3", - "resolved": "https://nexus.loafle.net/repository/npm-all/@schematics/angular/-/angular-9.1.3.tgz", - "integrity": "sha512-3vxrPyvcIWvyWgbx/fpeQvMThXlmcnHTXVn2dpTcE2BzstAYdG17W3qouhvmlqd6m28orxkKKiZaJYMNTtnV/A==", + "version": "9.1.11", + "resolved": "https://nexus.loafle.net/repository/npm-all/@schematics/angular/-/angular-9.1.11.tgz", + "integrity": "sha512-nExcWmsQvcj9IrofXuHmXmvmehD36O93uFR/a2LisYxao7j6Pe31Qs4Xzk6K67Kxpbypicqr8wAN3LtCOuy20A==", "dev": true, "requires": { - "@angular-devkit/core": "9.1.3", - "@angular-devkit/schematics": "9.1.3" + "@angular-devkit/core": "9.1.11", + "@angular-devkit/schematics": "9.1.11" }, "dependencies": { "@angular-devkit/core": { - "version": "9.1.3", - "resolved": "https://nexus.loafle.net/repository/npm-all/@angular-devkit/core/-/core-9.1.3.tgz", - "integrity": "sha512-VRV96prPy0Kdlm6XmI7DITqSMSc1bINimnOhzQre3euDX5OQty+EUqaexHtMv/SPDZX1agP+buHr6viv9YEhzA==", + "version": "9.1.11", + "resolved": "https://nexus.loafle.net/repository/npm-all/@angular-devkit/core/-/core-9.1.11.tgz", + "integrity": "sha512-uiEkDvWfMgPHuO4jVgBEr9Kl/LuxHaWYGD2ZtKsOnnHYZyRGp61ot7UcDF+KNdXTiq01JJH84VTd3IttEewmhQ==", "dev": true, "requires": { "ajv": "6.12.0", @@ -2181,9 +2203,9 @@ } }, "fast-deep-equal": { - "version": "3.1.1", - "resolved": "https://nexus.loafle.net/repository/npm-all/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz", - "integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==", + "version": "3.1.3", + "resolved": "https://nexus.loafle.net/repository/npm-all/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true }, "fast-json-stable-stringify": { @@ -2219,13 +2241,13 @@ } }, "@schematics/update": { - "version": "0.901.3", - "resolved": "https://nexus.loafle.net/repository/npm-all/@schematics/update/-/update-0.901.3.tgz", - "integrity": "sha512-kwfc9LMMi6ryN2oiat9vTqGMK1jKwI1c45Nm1xala35nsbQSQ12qeL3MrVmnSCeI14+cinFUbp8mWI9/3M+/Lg==", + "version": "0.901.11", + "resolved": "https://nexus.loafle.net/repository/npm-all/@schematics/update/-/update-0.901.11.tgz", + "integrity": "sha512-btqcX1dFL0R+KrTCDu/W+jF19gmoXEC73HWvbhJhU5CvpOF4RC51msjAs9u3J45KyWC2RoYdQCxuasKQZ962lw==", "dev": true, "requires": { - "@angular-devkit/core": "9.1.3", - "@angular-devkit/schematics": "9.1.3", + "@angular-devkit/core": "9.1.11", + "@angular-devkit/schematics": "9.1.11", "@yarnpkg/lockfile": "1.1.0", "ini": "1.3.5", "npm-package-arg": "^8.0.0", @@ -2236,9 +2258,9 @@ }, "dependencies": { "@angular-devkit/core": { - "version": "9.1.3", - "resolved": "https://nexus.loafle.net/repository/npm-all/@angular-devkit/core/-/core-9.1.3.tgz", - "integrity": "sha512-VRV96prPy0Kdlm6XmI7DITqSMSc1bINimnOhzQre3euDX5OQty+EUqaexHtMv/SPDZX1agP+buHr6viv9YEhzA==", + "version": "9.1.11", + "resolved": "https://nexus.loafle.net/repository/npm-all/@angular-devkit/core/-/core-9.1.11.tgz", + "integrity": "sha512-uiEkDvWfMgPHuO4jVgBEr9Kl/LuxHaWYGD2ZtKsOnnHYZyRGp61ot7UcDF+KNdXTiq01JJH84VTd3IttEewmhQ==", "dev": true, "requires": { "ajv": "6.12.0", @@ -2261,9 +2283,9 @@ } }, "fast-deep-equal": { - "version": "3.1.1", - "resolved": "https://nexus.loafle.net/repository/npm-all/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz", - "integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==", + "version": "3.1.3", + "resolved": "https://nexus.loafle.net/repository/npm-all/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true }, "fast-json-stable-stringify": { @@ -2336,6 +2358,7 @@ "version": "5.3.18", "resolved": "https://nexus.loafle.net/repository/npm-all/@storybook/addon-knobs/-/addon-knobs-5.3.18.tgz", "integrity": "sha512-X0WxGKoso3j5mS4c4enM8BvCjbO6Wwfxc++swQTqtANpBZ8k+w0piiEF1fiJf+ssgEAWe5brgIqnQ9kiBGLqKA==", + "dev": true, "requires": { "@storybook/addons": "5.3.18", "@storybook/api": "5.3.18", @@ -2400,6 +2423,7 @@ "version": "5.3.18", "resolved": "https://nexus.loafle.net/repository/npm-all/@storybook/addons/-/addons-5.3.18.tgz", "integrity": "sha512-ZQjDgTUDFRLvAiBg2d8FgPgghfQ+9uFyXQbtiGlTBLinrPCeQd7J86qiUES0fcGoohCCw0wWKtvB0WF2z1XNDg==", + "dev": true, "requires": { "@storybook/api": "5.3.18", "@storybook/channels": "5.3.18", @@ -2434,6 +2458,7 @@ "version": "5.3.18", "resolved": "https://nexus.loafle.net/repository/npm-all/@storybook/api/-/api-5.3.18.tgz", "integrity": "sha512-QXaccNCARHzPWOuxYndiebGWBZmwiUvRgB9ji0XTJBS3y8K0ZPb5QyuqiKPaEWUj8dBA8rzdDtkW3Yt95Namaw==", + "dev": true, "requires": { "@reach/router": "^1.2.1", "@storybook/channels": "5.3.18", @@ -2461,6 +2486,7 @@ "version": "5.3.18", "resolved": "https://nexus.loafle.net/repository/npm-all/@storybook/channel-postmessage/-/channel-postmessage-5.3.18.tgz", "integrity": "sha512-awxBW/aVfNtY9QvYZgsPaMXgUpC2+W3vEyQcl/w4ce0YVH+7yWx3wt3Ku49lQwxZwDrxP3QoC0U+mkPc9hBJwA==", + "dev": true, "requires": { "@storybook/channels": "5.3.18", "@storybook/client-logger": "5.3.18", @@ -2473,6 +2499,7 @@ "version": "5.3.18", "resolved": "https://nexus.loafle.net/repository/npm-all/@storybook/channels/-/channels-5.3.18.tgz", "integrity": "sha512-scP/6td/BJSEOgfN+qaYGDf3E793xye7tIw6W+sYqwg+xdMFO39wVXgVZNpQL6sLEwpJZTaPywCjC6p6ksErqQ==", + "dev": true, "requires": { "core-js": "^3.0.1" } @@ -2481,6 +2508,7 @@ "version": "5.3.18", "resolved": "https://nexus.loafle.net/repository/npm-all/@storybook/client-api/-/client-api-5.3.18.tgz", "integrity": "sha512-QiXTDUpjdyW19BlocLw07DrkOnEzVaWGJcRze2nSs29IKKuq1Ncv2LOAZt6ySSq0PmIKsjBou3bmS1/aXmDMdw==", + "dev": true, "requires": { "@storybook/addons": "5.3.18", "@storybook/channel-postmessage": "5.3.18", @@ -2505,6 +2533,7 @@ "version": "5.3.18", "resolved": "https://nexus.loafle.net/repository/npm-all/@storybook/client-logger/-/client-logger-5.3.18.tgz", "integrity": "sha512-RZjxw4uqZX3Yk27IirbB/pQG+wRsQSSRlKqYa8KQ5bSanm4IrcV9VA1OQbuySW9njE+CexAnakQJ/fENdmurNg==", + "dev": true, "requires": { "core-js": "^3.0.1" } @@ -2513,6 +2542,7 @@ "version": "5.3.18", "resolved": "https://nexus.loafle.net/repository/npm-all/@storybook/components/-/components-5.3.18.tgz", "integrity": "sha512-LIN4aVCCDY7klOwtuqQhfYz4tHaMADhXEzZpij+3r8N68Inck6IJ1oo9A9umXQPsTioQi8e6FLobH1im90j/2A==", + "dev": true, "requires": { "@storybook/client-logger": "5.3.18", "@storybook/theming": "5.3.18", @@ -2839,6 +2869,7 @@ "version": "5.3.18", "resolved": "https://nexus.loafle.net/repository/npm-all/@storybook/core-events/-/core-events-5.3.18.tgz", "integrity": "sha512-uQ6NYJ5WODXK8DJ7m8y3yUAtWB3n+6XtYztjY+tdkCsLYvTYDXNS+epV+f5Hu9+gB+/Dm+b5Su4jDD+LZB2QWA==", + "dev": true, "requires": { "core-js": "^3.0.1" } @@ -2847,6 +2878,7 @@ "version": "0.0.1", "resolved": "https://nexus.loafle.net/repository/npm-all/@storybook/csf/-/csf-0.0.1.tgz", "integrity": "sha512-USTLkZze5gkel8MYCujSRBVIrUQ3YPBrLOx7GNk/0wttvVtlzWXAq9eLbQ4p/NicGxP+3T7KPEMVV//g+yubpw==", + "dev": true, "requires": { "lodash": "^4.17.15" } @@ -2921,6 +2953,7 @@ "version": "5.3.18", "resolved": "https://nexus.loafle.net/repository/npm-all/@storybook/router/-/router-5.3.18.tgz", "integrity": "sha512-6B2U2C75KTSVaCuYYgcubeJGcCSnwsXuEf50hEd5mGqWgHZfojCtGvB7Ko4X+0h8rEC+eNA4p7YBOhlUv9WNrQ==", + "dev": true, "requires": { "@reach/router": "^1.2.1", "@storybook/csf": "0.0.1", @@ -2937,6 +2970,7 @@ "version": "5.3.18", "resolved": "https://nexus.loafle.net/repository/npm-all/@storybook/theming/-/theming-5.3.18.tgz", "integrity": "sha512-lfFTeLoYwLMKg96N3gn0umghMdAHgJBGuk2OM8Ll84yWtdl9RGnzfiI1Fl7Cr5k95dCF7drLJlJCao1VxUkFSA==", + "dev": true, "requires": { "@emotion/core": "^10.0.20", "@emotion/styled": "^10.0.17", @@ -3068,7 +3102,8 @@ "@types/history": { "version": "4.7.5", "resolved": "https://nexus.loafle.net/repository/npm-all/@types/history/-/history-4.7.5.tgz", - "integrity": "sha512-wLD/Aq2VggCJXSjxEwrMafIP51Z+13H78nXIX0ABEuIGhmB5sNGbR113MOKo+yfw+RDo1ZU3DM6yfnnRF/+ouw==" + "integrity": "sha512-wLD/Aq2VggCJXSjxEwrMafIP51Z+13H78nXIX0ABEuIGhmB5sNGbR113MOKo+yfw+RDo1ZU3DM6yfnnRF/+ouw==", + "dev": true }, "@types/html-minifier-terser": { "version": "5.0.0", @@ -3079,7 +3114,8 @@ "@types/is-function": { "version": "1.0.0", "resolved": "https://nexus.loafle.net/repository/npm-all/@types/is-function/-/is-function-1.0.0.tgz", - "integrity": "sha512-iTs9HReBu7evG77Q4EC8hZnqRt57irBDkK9nvmHroiOIVwYMQc4IvYvdRgwKfYepunIY7Oh/dBuuld+Gj9uo6w==" + "integrity": "sha512-iTs9HReBu7evG77Q4EC8hZnqRt57irBDkK9nvmHroiOIVwYMQc4IvYvdRgwKfYepunIY7Oh/dBuuld+Gj9uo6w==", + "dev": true }, "@types/jasmine": { "version": "3.5.10", @@ -3153,12 +3189,14 @@ "@types/parse-json": { "version": "4.0.0", "resolved": "https://nexus.loafle.net/repository/npm-all/@types/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==" + "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==", + "dev": true }, "@types/prop-types": { "version": "15.7.3", "resolved": "https://nexus.loafle.net/repository/npm-all/@types/prop-types/-/prop-types-15.7.3.tgz", - "integrity": "sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw==" + "integrity": "sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw==", + "dev": true }, "@types/q": { "version": "1.5.2", @@ -3170,6 +3208,7 @@ "version": "1.3.4", "resolved": "https://nexus.loafle.net/repository/npm-all/@types/reach__router/-/reach__router-1.3.4.tgz", "integrity": "sha512-DZgYfxUIlVSjvf0AvBbYNbpXLrTFNNpU1HrvCRbnMtx3nvGUUWC1/zlAe4dD4FCPFtc+LQuIPEsDiTb0zQkthg==", + "dev": true, "requires": { "@types/history": "*", "@types/react": "*" @@ -3179,6 +3218,7 @@ "version": "16.9.34", "resolved": "https://nexus.loafle.net/repository/npm-all/@types/react/-/react-16.9.34.tgz", "integrity": "sha512-8AJlYMOfPe1KGLKyHpflCg5z46n0b5DbRfqDksxBLBTUpB75ypDBAO9eCUcjNwE6LCUslwTz00yyG/X9gaVtow==", + "dev": true, "requires": { "@types/prop-types": "*", "csstype": "^2.2.0" @@ -3188,6 +3228,7 @@ "version": "3.0.1", "resolved": "https://nexus.loafle.net/repository/npm-all/@types/react-color/-/react-color-3.0.1.tgz", "integrity": "sha512-J6mYm43Sid9y+OjZ7NDfJ2VVkeeuTPNVImNFITgQNXodHteKfl/t/5pAR5Z9buodZ2tCctsZjgiMlQOpfntakw==", + "dev": true, "requires": { "@types/react": "*" } @@ -3196,6 +3237,7 @@ "version": "11.0.4", "resolved": "https://nexus.loafle.net/repository/npm-all/@types/react-syntax-highlighter/-/react-syntax-highlighter-11.0.4.tgz", "integrity": "sha512-9GfTo3a0PHwQeTVoqs0g5bS28KkSY48pp5659wA+Dp4MqceDEa8EHBqrllJvvtyusszyJhViUEap0FDvlk/9Zg==", + "dev": true, "requires": { "@types/react": "*" } @@ -3204,6 +3246,7 @@ "version": "4.3.5", "resolved": "https://nexus.loafle.net/repository/npm-all/@types/react-textarea-autosize/-/react-textarea-autosize-4.3.5.tgz", "integrity": "sha512-PiDL83kPMTolyZAWW3lyzO6ktooTb9tFTntVy7CA83/qFLWKLJ5bLeRboy6J6j3b1e8h2Eec6gBTEOOJRjV14A==", + "dev": true, "requires": { "@types/react": "*" } @@ -3286,7 +3329,8 @@ "@types/webpack-env": { "version": "1.15.1", "resolved": "https://nexus.loafle.net/repository/npm-all/@types/webpack-env/-/webpack-env-1.15.1.tgz", - "integrity": "sha512-eWN5ElDTeBc5lRDh95SqA8x18D0ll2pWudU3uWiyfsRmIZcmUXpEsxPU+7+BsdCrO2vfLRC629u/MmjbmF+2tA==" + "integrity": "sha512-eWN5ElDTeBc5lRDh95SqA8x18D0ll2pWudU3uWiyfsRmIZcmUXpEsxPU+7+BsdCrO2vfLRC629u/MmjbmF+2tA==", + "dev": true }, "@types/webpack-sources": { "version": "0.1.7", @@ -3314,9 +3358,9 @@ "dev": true }, "@ucap/api-common": { - "version": "0.0.5", - "resolved": "https://nexus.loafle.net/repository/npm-all/@ucap/api-common/-/api-common-0.0.5.tgz", - "integrity": "sha512-7V/Ea4FbLIKE+/ti414Spm6PQS3ZDcsHTZStLLYkMQrU7x+GbrGnHGOcvzzAQtE2SmpoNd3q6JzoYY+CK1eCkA==", + "version": "0.0.11", + "resolved": "https://nexus.loafle.net/repository/npm-all/@ucap/api-common/-/api-common-0.0.11.tgz", + "integrity": "sha512-QAuFb6APPtL3og5s4z9n5X0ubLxmO4QobbPg6NoFlGGA9LBMwxDrA/8mUe+VF8+ni2lv3JdiqvdxzlzFCphmew==", "dev": true }, "@ucap/api-external": { @@ -3344,9 +3388,9 @@ "dev": true }, "@ucap/core": { - "version": "0.0.7", - "resolved": "https://nexus.loafle.net/repository/npm-all/@ucap/core/-/core-0.0.7.tgz", - "integrity": "sha512-pJkU0u21r7E7RCUTcoKWjbq0fg8FHM6/GuBSeAkC1Zwuc7AHu06njh2oc/7KE38kGQY0+Kjz6T8tJXfS/bLTGQ==", + "version": "0.0.14", + "resolved": "https://nexus.loafle.net/repository/npm-all/@ucap/core/-/core-0.0.14.tgz", + "integrity": "sha512-7JuyGBTRYjzu5fPp0v0MBAW6KwweMCfhoPiTPhGkSpwjJByCWaX1h2QQgn23s1DhKMAZrOiasAZozw72m9fYtg==", "dev": true }, "@ucap/logger": { @@ -3356,15 +3400,9 @@ "dev": true }, "@ucap/native": { - "version": "0.0.6", - "resolved": "https://nexus.loafle.net/repository/npm-all/@ucap/native/-/native-0.0.6.tgz", - "integrity": "sha512-AsR3zwvZ45sah82IxNRip0YyelbJ7qc6vXdyh3qfhFcIFvADPd+THYN1xErDy6qx5mCpIvbUHRb7Ggz3qtUucw==", - "dev": true - }, - "@ucap/native-browser": { - "version": "0.0.5", - "resolved": "https://nexus.loafle.net/repository/npm-all/@ucap/native-browser/-/native-browser-0.0.5.tgz", - "integrity": "sha512-gmf17wyXWCmQQB3CoOpxL66u4XJaNfXb5behlVzJn2lZZjqFmaOi+VTOQ8h8FIK9bkdG1Cc0v9KBLAnOyniTZw==", + "version": "0.0.19", + "resolved": "https://nexus.loafle.net/repository/npm-all/@ucap/native/-/native-0.0.19.tgz", + "integrity": "sha512-IllRnQ4YDb1tfMJcu5W05CNz3tlp2IDrK3c1mL3aG5IF7BkbAaUQ2aens90uzBzJXngNLOIrtV5hMNzveLiWXQ==", "dev": true }, "@ucap/ng-api-common": { @@ -3408,13 +3446,8 @@ "dev": true }, "@ucap/ng-native": { - "version": "file:pack/ucap-ng-native-0.0.1.tgz", - "integrity": "sha512-SE/jxzmBQ/DkAc7CVsA1HcMny6fSRadk65y8EMQDRiMiP5Wzd+n8uzdDlmQdK1zBPK3glYaOi7PP34TQpIkLig==", - "dev": true - }, - "@ucap/ng-native-browser": { - "version": "file:pack/ucap-ng-native-browser-0.0.1.tgz", - "integrity": "sha512-2qqmz8n5DfrHsmYW8OMRcFBRAG60OICIAay3B5KO4nEnZjShelKOZCL2nGAkX+/QSgjtHpgb0h8cj2g6nOZIVg==", + "version": "file:pack/ucap-ng-native-0.0.5.tgz", + "integrity": "sha512-4HJ9AOTCRd7K1EtuvYhHBSB8a9HwjgYSLVxwEkh64AH8eFw4CRKnn4TWHkqX+q5uEuWRzKPBnH7mZBsM27m3IA==", "dev": true }, "@ucap/ng-pi": { @@ -3503,43 +3536,43 @@ "dev": true }, "@ucap/ng-store-authentication": { - "version": "file:pack/ucap-ng-store-authentication-0.0.11.tgz", - "integrity": "sha512-YwLMW+GIR3Rs7LaP+1xOH9KLI5jlpZx8oS7Zl32m6Wbym4ModIcGh21rRrqFhNXky4s9zl+ziaTRpFNCKJRgng==", + "version": "file:pack/ucap-ng-store-authentication-0.0.14.tgz", + "integrity": "sha512-6GUU+lerlrwVB8mT3qDBO5rxo7S5OYaLAb2K5HLxx9T+ZfAC+3maTjc28xeJaVgPfoLapI0jFuGgTlmwlBL6dg==", "dev": true }, "@ucap/ng-store-chat": { - "version": "file:pack/ucap-ng-store-chat-0.0.19.tgz", - "integrity": "sha512-J6jhvJW7GfFOIrPfXnDZ2sqUF10C7cJm8d/9O7ThxJ8muvKhQ/ZN86yJLLtA8Y+sIIupnUikH2MGNW7mgZjHPg==", + "version": "file:pack/ucap-ng-store-chat-0.0.66.tgz", + "integrity": "sha512-fSbIfu0fVcITs+crcKLjcivJSaW0KOqps1TQ5QawZekaQlJYw36rA8TbOF/qL9g1L0Vw9NphKe85Wwm5t2yJOQ==", "dev": true }, "@ucap/ng-store-group": { - "version": "file:pack/ucap-ng-store-group-0.0.14.tgz", - "integrity": "sha512-sUmdHO7TD5B33DMAoEnelvqbLXTsWPnK2HC8XQ0FdlfGyUtf3kGpwS4BxduUi5wiZckR3hfuBdpCShIhf/qmeQ==", + "version": "file:pack/ucap-ng-store-group-0.0.22.tgz", + "integrity": "sha512-XRFBpmJ9SPPwvessI+fxyL4GdWF+p/TU7RSWm6abbBcA7YGO//2gH0ghY8iPwsNVh3UhECy5LxkJx3fOGsJdhA==", "dev": true }, "@ucap/ng-store-organization": { - "version": "file:pack/ucap-ng-store-organization-0.0.8.tgz", - "integrity": "sha512-vWWyPukVWeUPOxXkdWPN9s36V4EuMVuR8vPfOfKwnh09TxV8Efl19jIYYtNo92L1H/lF0OKfaz5eR9Aw76v+FA==", + "version": "file:pack/ucap-ng-store-organization-0.0.20.tgz", + "integrity": "sha512-YpM0+IP6yvv/3B01YHQ/abwK2enamA4Gx1IN2fpD0NyOu7DfFt6mV6EEz32IN0DpY1gZ8jyU/ESchnDbRmPu5w==", "dev": true }, "@ucap/ng-ui": { - "version": "file:pack/ucap-ng-ui-0.0.24.tgz", - "integrity": "sha512-7G+Ee2EPYwDv64yMmy+Ga5Wcj7HK8YC0WLqvRWrLv3XZQtLfZ/oP/lWyjPvCNme0hNQo6EH1MpPn6u+uhoST6w==", + "version": "file:pack/ucap-ng-ui-0.0.97.tgz", + "integrity": "sha512-bmrpdiA1qnDWviV91buXCTOLGb49zeyt3T7AnOrZ87/wuVZAWdGaNUbEJtwYjZxnO3w2I5WOsKFzttq2vYU2LQ==", "dev": true }, "@ucap/ng-ui-authentication": { - "version": "file:pack/ucap-ng-ui-authentication-0.0.25.tgz", - "integrity": "sha512-/KgMR8JHcnOm+XwMQVhHGud61Hivi/fX0NucxUglsOguYiWL6Ms61FdRezB3wuKs5h/5+zI4QIWLdWdT8GT/Dg==", + "version": "file:pack/ucap-ng-ui-authentication-0.0.29.tgz", + "integrity": "sha512-kSQFFqSmtf+8aOmMqL0U+aUzCSmnPaWcJcDQfxuYPdRvlAYd4kMOWRcd/klvUrCpSf7P8jPsI7557rwmYHt3jA==", "dev": true }, "@ucap/ng-ui-chat": { - "version": "file:pack/ucap-ng-ui-chat-0.0.13.tgz", - "integrity": "sha512-ytFXvVxwkaNXH6RY4jdXpwqwfq6+HPiYN+Jqv1o5oddhlpBd7OSzhudUew318GJgclxoXbStcSc0/UODDdTptw==", + "version": "file:pack/ucap-ng-ui-chat-0.0.72.tgz", + "integrity": "sha512-sA5+3/sD9EN2BB2D2HZ9qrpGbkssWN7GF/UAvd8x4ECnVZPXaadNgLsSfOuZkSOVuM+6nTSyuglmnkjrPQBb8w==", "dev": true }, "@ucap/ng-ui-group": { - "version": "file:pack/ucap-ng-ui-group-0.0.33.tgz", - "integrity": "sha512-c//Jq00drbMGE3Cgwlh19ScXllGERX2eMVWkVjm311Y8HN9oBBT6Aq2uCM23/76P866oNrhecVDfDHgGYzPRjA==", + "version": "file:pack/ucap-ng-ui-group-0.0.78.tgz", + "integrity": "sha512-GhHevEcbJJEppdcLOPFSQiAhn0psqrbKHe1I/X+c2wRGeIaJ/+UfseJ5CEqif8IQzU5caigM+q8E0bEFXiSFgQ==", "dev": true }, "@ucap/ng-ui-material": { @@ -3548,13 +3581,14 @@ "dev": true }, "@ucap/ng-ui-organization": { - "version": "file:pack/ucap-ng-ui-organization-0.0.84.tgz", - "integrity": "sha512-T2q3KPCjY1ZZ1bY4UtSvY1XWpb0T8AZ9GbuOFlAJK3rs+grZC+iAmC8NO8iWhUgELdUStN1Mh8ja77RSo9Nqlg==", + "version": "file:pack/ucap-ng-ui-organization-0.0.202.tgz", + "integrity": "sha512-ibK62UXrEFvz5oauxAqsJmaXmu0+iqIqY1W01RNHgl6TrzcWv0ytB54j20viAEhD8rclKfUD8Wa6GvzLolCa6w==", "dev": true }, "@ucap/ng-ui-skin-default": { "version": "file:pack/ucap-ng-ui-skin-default-0.0.1.tgz", - "integrity": "sha512-xYq5hlNUjo39R1TLBaUR59GjgGyCzTdj5CWZyUIBbFicBirsSERoUX5704HlTa0L8YSjW7exOL8VWSgEsMng6g==" + "integrity": "sha512-xYq5hlNUjo39R1TLBaUR59GjgGyCzTdj5CWZyUIBbFicBirsSERoUX5704HlTa0L8YSjW7exOL8VWSgEsMng6g==", + "dev": true }, "@ucap/ng-web-socket": { "version": "file:pack/ucap-ng-web-socket-0.0.2.tgz", @@ -3567,9 +3601,9 @@ "dev": true }, "@ucap/pi": { - "version": "0.0.5", - "resolved": "https://nexus.loafle.net/repository/npm-all/@ucap/pi/-/pi-0.0.5.tgz", - "integrity": "sha512-9zWQzOgwn9Q1VFhNYgD4+hfl728gulSDKjIm/+jplp1zjr5qlmMVyUcywXNd3ClWvA0I5FR+6nwNFv2Q1u3QYg==", + "version": "0.0.8", + "resolved": "https://nexus.loafle.net/repository/npm-all/@ucap/pi/-/pi-0.0.8.tgz", + "integrity": "sha512-exfW6CRRN3XyMrnmoXX0e3lv0ICNHbLeYSZN5/BBYXp+Rilu9UMYlbveotlVZuh6RuDSIUOHZG4xElRApYzzBg==", "dev": true }, "@ucap/protocol": { @@ -3591,15 +3625,15 @@ "dev": true }, "@ucap/protocol-event": { - "version": "0.0.5", - "resolved": "https://nexus.loafle.net/repository/npm-all/@ucap/protocol-event/-/protocol-event-0.0.5.tgz", - "integrity": "sha512-RR74BKQaMIVqPo6TUYxFTUrIVc2ZSrsZXtereqymk24UKfL3eJ/8Oa7Gi7CYqAUPUDV815g4iTilXWr+Ok201w==", + "version": "0.0.6", + "resolved": "https://nexus.loafle.net/repository/npm-all/@ucap/protocol-event/-/protocol-event-0.0.6.tgz", + "integrity": "sha512-nFpNd2jTgzGlrtLWhuWnaPfP3DxxSprHy516BPASlD/iRY2TNmNgFJmZvUY1y5pbEv99azEGjvP5rhy6IECk7w==", "dev": true }, "@ucap/protocol-file": { - "version": "0.0.5", - "resolved": "https://nexus.loafle.net/repository/npm-all/@ucap/protocol-file/-/protocol-file-0.0.5.tgz", - "integrity": "sha512-3XRwtlpcrm2oZeckoOzzAUcqADPCGbgdEb4psfNnphTbGX9nYaBUTpWuLYwD3tVe7Wg4fytaHemcAu8yVZUANw==", + "version": "0.0.6", + "resolved": "https://nexus.loafle.net/repository/npm-all/@ucap/protocol-file/-/protocol-file-0.0.6.tgz", + "integrity": "sha512-iJk/Y5QmAm7GSD0lVjK0EWFhRWWgxE49dBtfyIa04N9NrOHLi0wNlxrwPmiuW1+b8r2wih5OJoJo6zaUZ+KyIw==", "dev": true }, "@ucap/protocol-group": { @@ -3609,9 +3643,9 @@ "dev": true }, "@ucap/protocol-info": { - "version": "0.0.6", - "resolved": "https://nexus.loafle.net/repository/npm-all/@ucap/protocol-info/-/protocol-info-0.0.6.tgz", - "integrity": "sha512-qpt0jfmHDyaMGyADzaDMKbbkfD04yEC0u4KDyoMdjnTi0RXA6cilDRGr9TW/bezB9OxS40yNLK6REfh7aSmcUA==", + "version": "0.0.9", + "resolved": "https://nexus.loafle.net/repository/npm-all/@ucap/protocol-info/-/protocol-info-0.0.9.tgz", + "integrity": "sha512-uRo+B5Xj9jsyVJ5w6Ec6L7a8HJ6PFc5DAzVPkgQEYeHbo6IGsom07udDFFS0/oV6bJNi3BjyL0eakKDTOMwTTA==", "dev": true }, "@ucap/protocol-inner": { @@ -3639,9 +3673,9 @@ "dev": true }, "@ucap/protocol-room": { - "version": "0.0.6", - "resolved": "https://nexus.loafle.net/repository/npm-all/@ucap/protocol-room/-/protocol-room-0.0.6.tgz", - "integrity": "sha512-PaEztUnZgmsH/Vo4JijJxpu9DBSA4SuIBW7y+L8BcBdPtzl49jBolm08+S9vgIr8cf9MtONdU37Al+uOTVki2g==", + "version": "0.0.7", + "resolved": "https://nexus.loafle.net/repository/npm-all/@ucap/protocol-room/-/protocol-room-0.0.7.tgz", + "integrity": "sha512-vGTwAK31CbV49bj6UMpJ2WfxLbVkPDvZV1aaFmwvB0HqqqXgyyaM9Cvlm5xrMguLDE//t9uu1jVi8vBYgALdMA==", "dev": true }, "@ucap/protocol-service": { @@ -3657,9 +3691,9 @@ "dev": true }, "@ucap/protocol-sync": { - "version": "0.0.4", - "resolved": "https://nexus.loafle.net/repository/npm-all/@ucap/protocol-sync/-/protocol-sync-0.0.4.tgz", - "integrity": "sha512-jcRb13WLSBa7s/yeAyRwUPfT+/AgRMTiISMdCZhs271gwoyvqS6I1jgsUxv6s52vqNroUEvX7nsPKX3Xxy6ajA==", + "version": "0.0.6", + "resolved": "https://nexus.loafle.net/repository/npm-all/@ucap/protocol-sync/-/protocol-sync-0.0.6.tgz", + "integrity": "sha512-gGraaSItVbJkgiIPMQ1RKQLVRdYD0B2h7Um00EWxHqHw9kXrHHmEGIGI7y+WnLjlF/ZaksdoWfOGPUmiJhaahQ==", "dev": true }, "@ucap/protocol-umg": { @@ -3669,9 +3703,9 @@ "dev": true }, "@ucap/ui-scss": { - "version": "0.0.3", - "resolved": "https://nexus.loafle.net/repository/npm-all/@ucap/ui-scss/-/ui-scss-0.0.3.tgz", - "integrity": "sha512-E+hrBasMzzm27TuPlLxMCkYin4uMN62kLRmeXi9/qdeX6ty+GkqvfkdzV9lvmyZfNvC0VVFKS0eruLEWW6RDvg==", + "version": "0.0.5", + "resolved": "https://nexus.loafle.net/repository/npm-all/@ucap/ui-scss/-/ui-scss-0.0.5.tgz", + "integrity": "sha512-V67uhXQ/FN27ISwHBy6w/os7fUqsCellgfLabw5aH4lWKpDqL9AX2eao6uhUNedCW9E6Kb/7cxXfv6sUyyLhxw==", "dev": true }, "@ucap/web-socket": { @@ -4087,6 +4121,7 @@ "version": "3.2.1", "resolved": "https://nexus.loafle.net/repository/npm-all/ansi-styles/-/ansi-styles-3.2.1.tgz", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, "requires": { "color-convert": "^1.9.0" } @@ -4673,6 +4708,7 @@ "version": "10.0.33", "resolved": "https://nexus.loafle.net/repository/npm-all/babel-plugin-emotion/-/babel-plugin-emotion-10.0.33.tgz", "integrity": "sha512-bxZbTTGz0AJQDHm8k6Rf3RQJ8tX2scsfsRyKVgAbiUPUNIRtlK+7JxP+TAd1kRLABFxe0CFm2VdK4ePkoA9FxQ==", + "dev": true, "requires": { "@babel/helper-module-imports": "^7.0.0", "@emotion/hash": "0.8.0", @@ -4690,6 +4726,7 @@ "version": "2.8.0", "resolved": "https://nexus.loafle.net/repository/npm-all/babel-plugin-macros/-/babel-plugin-macros-2.8.0.tgz", "integrity": "sha512-SEP5kJpfGYqYKpBrj5XU3ahw5p5GOHJ0U5ssOSQ/WBVdwkD2Dzlce95exQTs3jOVWPPKLBN2rlEWkCK7dSmLvg==", + "dev": true, "requires": { "@babel/runtime": "^7.7.2", "cosmiconfig": "^6.0.0", @@ -4793,7 +4830,8 @@ "babel-plugin-syntax-jsx": { "version": "6.18.0", "resolved": "https://nexus.loafle.net/repository/npm-all/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz", - "integrity": "sha1-CvMqmm4Tyno/1QaeYtew9Y0NiUY=" + "integrity": "sha1-CvMqmm4Tyno/1QaeYtew9Y0NiUY=", + "dev": true }, "babel-plugin-transform-inline-consecutive-adds": { "version": "0.4.3", @@ -5582,7 +5620,8 @@ "callsites": { "version": "3.1.0", "resolved": "https://nexus.loafle.net/repository/npm-all/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==" + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true }, "camel-case": { "version": "4.1.1", @@ -5622,7 +5661,8 @@ "can-use-dom": { "version": "0.1.0", "resolved": "https://nexus.loafle.net/repository/npm-all/can-use-dom/-/can-use-dom-0.1.0.tgz", - "integrity": "sha1-IsxKNKCrxDlQ9CxkEQJKP2NmtFo=" + "integrity": "sha1-IsxKNKCrxDlQ9CxkEQJKP2NmtFo=", + "dev": true }, "caniuse-api": { "version": "3.0.0", @@ -5664,6 +5704,7 @@ "version": "2.4.2", "resolved": "https://nexus.loafle.net/repository/npm-all/chalk/-/chalk-2.4.2.tgz", "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, "requires": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", @@ -5673,17 +5714,20 @@ "character-entities": { "version": "1.2.4", "resolved": "https://nexus.loafle.net/repository/npm-all/character-entities/-/character-entities-1.2.4.tgz", - "integrity": "sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==" + "integrity": "sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==", + "dev": true }, "character-entities-legacy": { "version": "1.1.4", "resolved": "https://nexus.loafle.net/repository/npm-all/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz", - "integrity": "sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==" + "integrity": "sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==", + "dev": true }, "character-reference-invalid": { "version": "1.1.4", "resolved": "https://nexus.loafle.net/repository/npm-all/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz", - "integrity": "sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==" + "integrity": "sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==", + "dev": true }, "chardet": { "version": "0.7.0", @@ -5861,6 +5905,7 @@ "version": "2.0.6", "resolved": "https://nexus.loafle.net/repository/npm-all/clipboard/-/clipboard-2.0.6.tgz", "integrity": "sha512-g5zbiixBRk/wyKakSwCKd7vQXDjFnAMGHoEyBogG/bw9kTD9GvdAvaoRR1ALcEzt3pVKxZR0pViekPMIS0QyGg==", + "dev": true, "optional": true, "requires": { "good-listener": "^1.2.2", @@ -6005,6 +6050,7 @@ "version": "1.9.3", "resolved": "https://nexus.loafle.net/repository/npm-all/color-convert/-/color-convert-1.9.3.tgz", "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, "requires": { "color-name": "1.1.3" } @@ -6012,7 +6058,8 @@ "color-name": { "version": "1.1.3", "resolved": "https://nexus.loafle.net/repository/npm-all/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true }, "color-string": { "version": "1.5.3", @@ -6042,7 +6089,8 @@ "comma-separated-tokens": { "version": "1.0.8", "resolved": "https://nexus.loafle.net/repository/npm-all/comma-separated-tokens/-/comma-separated-tokens-1.0.8.tgz", - "integrity": "sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw==" + "integrity": "sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw==", + "dev": true }, "commander": { "version": "2.20.3", @@ -6401,6 +6449,7 @@ "version": "1.7.0", "resolved": "https://nexus.loafle.net/repository/npm-all/convert-source-map/-/convert-source-map-1.7.0.tgz", "integrity": "sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==", + "dev": true, "requires": { "safe-buffer": "~5.1.1" } @@ -6452,6 +6501,7 @@ "version": "3.3.1", "resolved": "https://nexus.loafle.net/repository/npm-all/copy-to-clipboard/-/copy-to-clipboard-3.3.1.tgz", "integrity": "sha512-i13qo6kIHTTpCm8/Wup+0b1mVWETvu2kIMzKoK8FpkLkFxlt0znUAHcMzox+T8sPlqtZXq3CulEjQHsYiGFJUw==", + "dev": true, "requires": { "toggle-selection": "^1.0.6" } @@ -6548,7 +6598,8 @@ "core-js": { "version": "3.6.5", "resolved": "https://nexus.loafle.net/repository/npm-all/core-js/-/core-js-3.6.5.tgz", - "integrity": "sha512-vZVEEwZoIsI+vPEuoF9Iqf5H7/M3eeQqWlQnYa8FSKKePuYTf5MWnxb5SDAzCa60b3JBRS5g9b+Dq7b1y/RCrA==" + "integrity": "sha512-vZVEEwZoIsI+vPEuoF9Iqf5H7/M3eeQqWlQnYa8FSKKePuYTf5MWnxb5SDAzCa60b3JBRS5g9b+Dq7b1y/RCrA==", + "dev": true }, "core-js-compat": { "version": "3.6.5", @@ -6594,6 +6645,7 @@ "version": "6.0.0", "resolved": "https://nexus.loafle.net/repository/npm-all/cosmiconfig/-/cosmiconfig-6.0.0.tgz", "integrity": "sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg==", + "dev": true, "requires": { "@types/parse-json": "^4.0.0", "import-fresh": "^3.1.0", @@ -6686,6 +6738,7 @@ "version": "0.3.0", "resolved": "https://nexus.loafle.net/repository/npm-all/create-react-context/-/create-react-context-0.3.0.tgz", "integrity": "sha512-dNldIoSuNSvlTJ7slIKC/ZFGKexBMBrrcc+TTe1NdmROnaASuLPvqpwj9v4XS4uXZ8+YPu0sNmShX2rXI5LNsw==", + "dev": true, "requires": { "gud": "^1.0.0", "warning": "^4.0.3" @@ -7086,7 +7139,8 @@ "csstype": { "version": "2.6.10", "resolved": "https://nexus.loafle.net/repository/npm-all/csstype/-/csstype-2.6.10.tgz", - "integrity": "sha512-D34BqZU4cIlMCY93rZHbrq9pjTAQJ3U8S8rfBqjwHxkGPThWFjzZDQpgMJY0QViLxth6ZKYiwFBo14RdN44U/w==" + "integrity": "sha512-D34BqZU4cIlMCY93rZHbrq9pjTAQJ3U8S8rfBqjwHxkGPThWFjzZDQpgMJY0QViLxth6ZKYiwFBo14RdN44U/w==", + "dev": true }, "cuint": { "version": "0.2.2", @@ -7200,6 +7254,7 @@ "version": "1.1.1", "resolved": "https://nexus.loafle.net/repository/npm-all/deep-equal/-/deep-equal-1.1.1.tgz", "integrity": "sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g==", + "dev": true, "requires": { "is-arguments": "^1.0.4", "is-date-object": "^1.0.1", @@ -7218,7 +7273,8 @@ "deep-object-diff": { "version": "1.1.0", "resolved": "https://nexus.loafle.net/repository/npm-all/deep-object-diff/-/deep-object-diff-1.1.0.tgz", - "integrity": "sha512-b+QLs5vHgS+IoSNcUE4n9HP2NwcHj7aqnJWsjPtuG75Rh5TOaGt0OjAYInh77d5T16V5cRDC+Pw/6ZZZiETBGw==" + "integrity": "sha512-b+QLs5vHgS+IoSNcUE4n9HP2NwcHj7aqnJWsjPtuG75Rh5TOaGt0OjAYInh77d5T16V5cRDC+Pw/6ZZZiETBGw==", + "dev": true }, "default-gateway": { "version": "4.2.0", @@ -7266,6 +7322,7 @@ "version": "1.1.3", "resolved": "https://nexus.loafle.net/repository/npm-all/define-properties/-/define-properties-1.1.3.tgz", "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", + "dev": true, "requires": { "object-keys": "^1.0.12" } @@ -7380,6 +7437,7 @@ "version": "3.2.0", "resolved": "https://nexus.loafle.net/repository/npm-all/delegate/-/delegate-3.2.0.tgz", "integrity": "sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw==", + "dev": true, "optional": true }, "delegates": { @@ -7425,7 +7483,8 @@ "detect-node": { "version": "2.0.4", "resolved": "https://nexus.loafle.net/repository/npm-all/detect-node/-/detect-node-2.0.4.tgz", - "integrity": "sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw==" + "integrity": "sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw==", + "dev": true }, "detect-port": { "version": "1.3.0", @@ -7551,6 +7610,7 @@ "version": "5.1.4", "resolved": "https://nexus.loafle.net/repository/npm-all/dom-helpers/-/dom-helpers-5.1.4.tgz", "integrity": "sha512-TjMyeVUvNEnOnhzs6uAn9Ya47GmMo3qq7m+Lr/3ON0Rs5kHvb8I+SQYjLUSYn7qhEm0QjW0yrBkvz9yOrwwz1A==", + "dev": true, "requires": { "@babel/runtime": "^7.8.7", "csstype": "^2.6.7" @@ -7589,7 +7649,8 @@ "dom-walk": { "version": "0.1.2", "resolved": "https://nexus.loafle.net/repository/npm-all/dom-walk/-/dom-walk-0.1.2.tgz", - "integrity": "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==" + "integrity": "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==", + "dev": true }, "domain-browser": { "version": "1.2.0", @@ -7763,6 +7824,7 @@ "version": "10.0.27", "resolved": "https://nexus.loafle.net/repository/npm-all/emotion-theming/-/emotion-theming-10.0.27.tgz", "integrity": "sha512-MlF1yu/gYh8u+sLUqA0YuA9JX0P4Hb69WlKc/9OLo+WCXuX6sy/KoIa+qJimgmr2dWqnypYKYPX37esjDBbhdw==", + "dev": true, "requires": { "@babel/runtime": "^7.5.5", "@emotion/weak-memoize": "0.2.5", @@ -7776,12 +7838,23 @@ "dev": true }, "encoding": { - "version": "0.1.12", - "resolved": "https://nexus.loafle.net/repository/npm-all/encoding/-/encoding-0.1.12.tgz", - "integrity": "sha1-U4tm8+5izRq1HsMjgp0flIDHS+s=", + "version": "0.1.13", + "resolved": "https://nexus.loafle.net/repository/npm-all/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", "dev": true, "requires": { - "iconv-lite": "~0.4.13" + "iconv-lite": "^0.6.2" + }, + "dependencies": { + "iconv-lite": { + "version": "0.6.2", + "resolved": "https://nexus.loafle.net/repository/npm-all/iconv-lite/-/iconv-lite-0.6.2.tgz", + "integrity": "sha512-2y91h5OpQlolefMPmUlivelittSWy0rP+oYVpn6A7GwVHNE8AWzoYOBNmlwks3LobaJxgHCYZAnyNo2GgpNRNQ==", + "dev": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + } + } } }, "end-of-stream": { @@ -7949,6 +8022,7 @@ "version": "1.3.2", "resolved": "https://nexus.loafle.net/repository/npm-all/error-ex/-/error-ex-1.3.2.tgz", "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, "requires": { "is-arrayish": "^0.2.1" } @@ -7957,6 +8031,7 @@ "version": "1.17.5", "resolved": "https://nexus.loafle.net/repository/npm-all/es-abstract/-/es-abstract-1.17.5.tgz", "integrity": "sha512-BR9auzDbySxOcfog0tLECW8l28eRGpDpU3Dm3Hp4q/N+VtLTmyj4EUN088XZWQDW/hzj6sYRDXeOFsaAODKvpg==", + "dev": true, "requires": { "es-to-primitive": "^1.2.1", "function-bind": "^1.1.1", @@ -8004,6 +8079,7 @@ "version": "1.2.1", "resolved": "https://nexus.loafle.net/repository/npm-all/es-to-primitive/-/es-to-primitive-1.2.1.tgz", "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, "requires": { "is-callable": "^1.1.4", "is-date-object": "^1.0.1", @@ -8046,12 +8122,14 @@ "escape-html": { "version": "1.0.3", "resolved": "https://nexus.loafle.net/repository/npm-all/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=", + "dev": true }, "escape-string-regexp": { "version": "1.0.5", "resolved": "https://nexus.loafle.net/repository/npm-all/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true }, "eslint-scope": { "version": "4.0.3", @@ -8111,7 +8189,8 @@ "eventemitter3": { "version": "4.0.0", "resolved": "https://nexus.loafle.net/repository/npm-all/eventemitter3/-/eventemitter3-4.0.0.tgz", - "integrity": "sha512-qerSRB0p+UDEssxTtm6EDKcE7W4OaoisfIMl4CngyEhjpYglocpNg6UEqCvemdGhosAsg4sO2dXJOdyBifPGCg==" + "integrity": "sha512-qerSRB0p+UDEssxTtm6EDKcE7W4OaoisfIMl4CngyEhjpYglocpNg6UEqCvemdGhosAsg4sO2dXJOdyBifPGCg==", + "dev": true }, "events": { "version": "3.1.0", @@ -8403,7 +8482,8 @@ "fast-deep-equal": { "version": "2.0.1", "resolved": "https://nexus.loafle.net/repository/npm-all/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", - "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=" + "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=", + "dev": true }, "fast-glob": { "version": "2.2.7", @@ -8435,6 +8515,7 @@ "version": "1.0.4", "resolved": "https://nexus.loafle.net/repository/npm-all/fault/-/fault-1.0.4.tgz", "integrity": "sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA==", + "dev": true, "requires": { "format": "^0.2.0" } @@ -8705,7 +8786,8 @@ "find-root": { "version": "1.1.0", "resolved": "https://nexus.loafle.net/repository/npm-all/find-root/-/find-root-1.1.0.tgz", - "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==" + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", + "dev": true }, "find-up": { "version": "2.1.0", @@ -8735,7 +8817,8 @@ "focus-lock": { "version": "0.6.7", "resolved": "https://nexus.loafle.net/repository/npm-all/focus-lock/-/focus-lock-0.6.7.tgz", - "integrity": "sha512-KRo93U/afEqt7w5tBm4t0FHf/Li8tEYav3n4GUiZdeRlRfrtMbL8yQg0xRVnY/kmBRmQ4xkqIlbaMvuqlu53kg==" + "integrity": "sha512-KRo93U/afEqt7w5tBm4t0FHf/Li8tEYav3n4GUiZdeRlRfrtMbL8yQg0xRVnY/kmBRmQ4xkqIlbaMvuqlu53kg==", + "dev": true }, "follow-redirects": { "version": "1.11.0", @@ -8807,7 +8890,8 @@ "format": { "version": "0.2.2", "resolved": "https://nexus.loafle.net/repository/npm-all/format/-/format-0.2.2.tgz", - "integrity": "sha1-1hcBB+nv3E7TDJ3DkBbflCtctYs=" + "integrity": "sha1-1hcBB+nv3E7TDJ3DkBbflCtctYs=", + "dev": true }, "forwarded": { "version": "0.1.2", @@ -8913,7 +8997,8 @@ "function-bind": { "version": "1.1.1", "resolved": "https://nexus.loafle.net/repository/npm-all/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true }, "function.prototype.name": { "version": "1.1.2", @@ -9099,6 +9184,7 @@ "version": "4.4.0", "resolved": "https://nexus.loafle.net/repository/npm-all/global/-/global-4.4.0.tgz", "integrity": "sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==", + "dev": true, "requires": { "min-document": "^2.19.0", "process": "^0.11.10" @@ -9183,6 +9269,7 @@ "version": "1.2.2", "resolved": "https://nexus.loafle.net/repository/npm-all/good-listener/-/good-listener-1.2.2.tgz", "integrity": "sha1-1TswzfkxPf+33JoNR3CWqm0UXFA=", + "dev": true, "optional": true, "requires": { "delegate": "^3.1.2" @@ -9216,7 +9303,8 @@ "gud": { "version": "1.0.0", "resolved": "https://nexus.loafle.net/repository/npm-all/gud/-/gud-1.0.0.tgz", - "integrity": "sha512-zGEOVKFM5sVPPrYs7J5/hYEw2Pof8KCyOwyhG8sAF26mCAeUFAcYPu1mwB7hhpIP29zOIBaDqwuHdLp0jvZXjw==" + "integrity": "sha512-zGEOVKFM5sVPPrYs7J5/hYEw2Pof8KCyOwyhG8sAF26mCAeUFAcYPu1mwB7hhpIP29zOIBaDqwuHdLp0jvZXjw==", + "dev": true }, "gzip-size": { "version": "5.1.1", @@ -9254,6 +9342,7 @@ "version": "1.0.3", "resolved": "https://nexus.loafle.net/repository/npm-all/has/-/has-1.0.3.tgz", "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, "requires": { "function-bind": "^1.1.1" } @@ -9293,12 +9382,14 @@ "has-flag": { "version": "3.0.0", "resolved": "https://nexus.loafle.net/repository/npm-all/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true }, "has-symbols": { "version": "1.0.1", "resolved": "https://nexus.loafle.net/repository/npm-all/has-symbols/-/has-symbols-1.0.1.tgz", - "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==" + "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==", + "dev": true }, "has-unicode": { "version": "2.0.1", @@ -9395,12 +9486,14 @@ "hast-util-parse-selector": { "version": "2.2.4", "resolved": "https://nexus.loafle.net/repository/npm-all/hast-util-parse-selector/-/hast-util-parse-selector-2.2.4.tgz", - "integrity": "sha512-gW3sxfynIvZApL4L07wryYF4+C9VvH3AUi7LAnVXV4MneGEgwOByXvFo18BgmTWnm7oHAe874jKbIB1YhHSIzA==" + "integrity": "sha512-gW3sxfynIvZApL4L07wryYF4+C9VvH3AUi7LAnVXV4MneGEgwOByXvFo18BgmTWnm7oHAe874jKbIB1YhHSIzA==", + "dev": true }, "hastscript": { "version": "5.1.2", "resolved": "https://nexus.loafle.net/repository/npm-all/hastscript/-/hastscript-5.1.2.tgz", "integrity": "sha512-WlztFuK+Lrvi3EggsqOkQ52rKbxkXL3RwB6t5lwoa8QLMemoWfBuL43eDrwOamJyR7uKQKdmKYaBH1NZBiIRrQ==", + "dev": true, "requires": { "comma-separated-tokens": "^1.0.0", "hast-util-parse-selector": "^2.0.0", @@ -9423,7 +9516,8 @@ "highlight.js": { "version": "9.13.1", "resolved": "https://nexus.loafle.net/repository/npm-all/highlight.js/-/highlight.js-9.13.1.tgz", - "integrity": "sha512-Sc28JNQNDzaH6PORtRLMvif9RSn1mYuOoX3omVjnb0+HbpPygU2ALBI0R/wsiqCb4/fcp07Gdo8g+fhtFrQl6A==" + "integrity": "sha512-Sc28JNQNDzaH6PORtRLMvif9RSn1mYuOoX3omVjnb0+HbpPygU2ALBI0R/wsiqCb4/fcp07Gdo8g+fhtFrQl6A==", + "dev": true }, "hmac-drbg": { "version": "1.0.1", @@ -9440,6 +9534,7 @@ "version": "3.3.2", "resolved": "https://nexus.loafle.net/repository/npm-all/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "dev": true, "requires": { "react-is": "^16.7.0" } @@ -9451,12 +9546,23 @@ "dev": true }, "hosted-git-info": { - "version": "3.0.4", - "resolved": "https://nexus.loafle.net/repository/npm-all/hosted-git-info/-/hosted-git-info-3.0.4.tgz", - "integrity": "sha512-4oT62d2jwSDBbLLFLZE+1vPuQ1h8p9wjrJ8Mqx5TjsyWmBMV5B13eJqn8pvluqubLf3cJPTfiYCIwNwDNmzScQ==", + "version": "3.0.5", + "resolved": "https://nexus.loafle.net/repository/npm-all/hosted-git-info/-/hosted-git-info-3.0.5.tgz", + "integrity": "sha512-i4dpK6xj9BIpVOTboXIlKG9+8HMKggcrMX7WA24xZtKwX0TPelq/rbaS5rCKeNX8sJXZJGdSxpnEGtta+wismQ==", "dev": true, "requires": { - "lru-cache": "^5.1.1" + "lru-cache": "^6.0.0" + }, + "dependencies": { + "lru-cache": { + "version": "6.0.0", + "resolved": "https://nexus.loafle.net/repository/npm-all/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + } } }, "hpack.js": { @@ -9833,6 +9939,7 @@ "version": "3.2.1", "resolved": "https://nexus.loafle.net/repository/npm-all/import-fresh/-/import-fresh-3.2.1.tgz", "integrity": "sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ==", + "dev": true, "requires": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" @@ -9841,7 +9948,8 @@ "resolve-from": { "version": "4.0.0", "resolved": "https://nexus.loafle.net/repository/npm-all/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==" + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true } } }, @@ -10072,6 +10180,7 @@ "version": "2.2.4", "resolved": "https://nexus.loafle.net/repository/npm-all/invariant/-/invariant-2.2.4.tgz", "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "dev": true, "requires": { "loose-envify": "^1.0.0" } @@ -10129,12 +10238,14 @@ "is-alphabetical": { "version": "1.0.4", "resolved": "https://nexus.loafle.net/repository/npm-all/is-alphabetical/-/is-alphabetical-1.0.4.tgz", - "integrity": "sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==" + "integrity": "sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==", + "dev": true }, "is-alphanumerical": { "version": "1.0.4", "resolved": "https://nexus.loafle.net/repository/npm-all/is-alphanumerical/-/is-alphanumerical-1.0.4.tgz", "integrity": "sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==", + "dev": true, "requires": { "is-alphabetical": "^1.0.0", "is-decimal": "^1.0.0" @@ -10143,12 +10254,14 @@ "is-arguments": { "version": "1.0.4", "resolved": "https://nexus.loafle.net/repository/npm-all/is-arguments/-/is-arguments-1.0.4.tgz", - "integrity": "sha512-xPh0Rmt8NE65sNzvyUmWgI1tz3mKq74lGA0mL8LYZcoIzKOzDh6HmrYm3d18k60nHerC8A9Km8kYu87zfSFnLA==" + "integrity": "sha512-xPh0Rmt8NE65sNzvyUmWgI1tz3mKq74lGA0mL8LYZcoIzKOzDh6HmrYm3d18k60nHerC8A9Km8kYu87zfSFnLA==", + "dev": true }, "is-arrayish": { "version": "0.2.1", "resolved": "https://nexus.loafle.net/repository/npm-all/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=" + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", + "dev": true }, "is-binary-path": { "version": "2.1.0", @@ -10168,7 +10281,8 @@ "is-callable": { "version": "1.1.5", "resolved": "https://nexus.loafle.net/repository/npm-all/is-callable/-/is-callable-1.1.5.tgz", - "integrity": "sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q==" + "integrity": "sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q==", + "dev": true }, "is-ci": { "version": "2.0.0", @@ -10216,12 +10330,14 @@ "is-date-object": { "version": "1.0.2", "resolved": "https://nexus.loafle.net/repository/npm-all/is-date-object/-/is-date-object-1.0.2.tgz", - "integrity": "sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==" + "integrity": "sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==", + "dev": true }, "is-decimal": { "version": "1.0.4", "resolved": "https://nexus.loafle.net/repository/npm-all/is-decimal/-/is-decimal-1.0.4.tgz", - "integrity": "sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==" + "integrity": "sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==", + "dev": true }, "is-descriptor": { "version": "0.1.6", @@ -10285,7 +10401,8 @@ "is-function": { "version": "1.0.2", "resolved": "https://nexus.loafle.net/repository/npm-all/is-function/-/is-function-1.0.2.tgz", - "integrity": "sha512-lw7DUp0aWXYg+CBCN+JKkcE0Q2RayZnSvnZBlwgxHBQhqt5pZNVy4Ri7H9GmmXkdu7LUthszM+Tor1u/2iBcpQ==" + "integrity": "sha512-lw7DUp0aWXYg+CBCN+JKkcE0Q2RayZnSvnZBlwgxHBQhqt5pZNVy4Ri7H9GmmXkdu7LUthszM+Tor1u/2iBcpQ==", + "dev": true }, "is-glob": { "version": "4.0.1", @@ -10299,7 +10416,8 @@ "is-hexadecimal": { "version": "1.0.4", "resolved": "https://nexus.loafle.net/repository/npm-all/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz", - "integrity": "sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==" + "integrity": "sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==", + "dev": true }, "is-installed-globally": { "version": "0.3.2", @@ -10395,6 +10513,7 @@ "version": "3.0.0", "resolved": "https://nexus.loafle.net/repository/npm-all/is-plain-object/-/is-plain-object-3.0.0.tgz", "integrity": "sha512-tZIpofR+P05k8Aocp7UI/2UTa9lTJSebCXpFFoR9aibpokDj/uXBsJ8luUu0tTVYKkMU6URDUuOfJZ7koewXvg==", + "dev": true, "requires": { "isobject": "^4.0.0" } @@ -10426,6 +10545,7 @@ "version": "1.0.5", "resolved": "https://nexus.loafle.net/repository/npm-all/is-regex/-/is-regex-1.0.5.tgz", "integrity": "sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ==", + "dev": true, "requires": { "has": "^1.0.3" } @@ -10473,6 +10593,7 @@ "version": "1.0.3", "resolved": "https://nexus.loafle.net/repository/npm-all/is-symbol/-/is-symbol-1.0.3.tgz", "integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==", + "dev": true, "requires": { "has-symbols": "^1.0.1" } @@ -10531,7 +10652,8 @@ "isobject": { "version": "4.0.0", "resolved": "https://nexus.loafle.net/repository/npm-all/isobject/-/isobject-4.0.0.tgz", - "integrity": "sha512-S/2fF5wH8SJA/kmwr6HYhK/RI/OkhD84k8ntalo0iJjZikgq1XFvR5M8NPT1x5F7fBwCG3qHfnzeP/Vh/ZxCUA==" + "integrity": "sha512-S/2fF5wH8SJA/kmwr6HYhK/RI/OkhD84k8ntalo0iJjZikgq1XFvR5M8NPT1x5F7fBwCG3qHfnzeP/Vh/ZxCUA==", + "dev": true }, "isstream": { "version": "0.1.2", @@ -10780,7 +10902,8 @@ "js-tokens": { "version": "4.0.0", "resolved": "https://nexus.loafle.net/repository/npm-all/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true }, "js-yaml": { "version": "3.13.1", @@ -10813,7 +10936,8 @@ "json-parse-better-errors": { "version": "1.0.2", "resolved": "https://nexus.loafle.net/repository/npm-all/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", - "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==" + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", + "dev": true }, "json-schema": { "version": "0.2.3", @@ -11126,7 +11250,8 @@ "lines-and-columns": { "version": "1.1.6", "resolved": "https://nexus.loafle.net/repository/npm-all/lines-and-columns/-/lines-and-columns-1.1.6.tgz", - "integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=" + "integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=", + "dev": true }, "load-json-file": { "version": "4.0.0", @@ -11188,7 +11313,8 @@ "lodash": { "version": "4.17.15", "resolved": "https://nexus.loafle.net/repository/npm-all/lodash/-/lodash-4.17.15.tgz", - "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", + "dev": true }, "lodash.clonedeep": { "version": "4.5.0", @@ -11199,17 +11325,20 @@ "lodash.debounce": { "version": "4.0.8", "resolved": "https://nexus.loafle.net/repository/npm-all/lodash.debounce/-/lodash.debounce-4.0.8.tgz", - "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=" + "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=", + "dev": true }, "lodash.memoize": { "version": "4.1.2", "resolved": "https://nexus.loafle.net/repository/npm-all/lodash.memoize/-/lodash.memoize-4.1.2.tgz", - "integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=" + "integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=", + "dev": true }, "lodash.throttle": { "version": "4.1.1", "resolved": "https://nexus.loafle.net/repository/npm-all/lodash.throttle/-/lodash.throttle-4.1.1.tgz", - "integrity": "sha1-wj6RtxAkKscMN/HhzaknTMOb8vQ=" + "integrity": "sha1-wj6RtxAkKscMN/HhzaknTMOb8vQ=", + "dev": true }, "lodash.uniq": { "version": "4.5.0", @@ -11255,6 +11384,7 @@ "version": "1.4.0", "resolved": "https://nexus.loafle.net/repository/npm-all/loose-envify/-/loose-envify-1.4.0.tgz", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, "requires": { "js-tokens": "^3.0.0 || ^4.0.0" } @@ -11288,6 +11418,7 @@ "version": "1.11.0", "resolved": "https://nexus.loafle.net/repository/npm-all/lowlight/-/lowlight-1.11.0.tgz", "integrity": "sha512-xrGGN6XLL7MbTMdPD6NfWPwY43SNkjf/d0mecSx/CW36fUZTjRHEq0/Cdug3TWKtRXLWi7iMl1eP0olYxj/a4A==", + "dev": true, "requires": { "fault": "^1.0.2", "highlight.js": "~9.13.0" @@ -11435,7 +11566,8 @@ "map-or-similar": { "version": "1.5.0", "resolved": "https://nexus.loafle.net/repository/npm-all/map-or-similar/-/map-or-similar-1.5.0.tgz", - "integrity": "sha1-beJlMXSt+12e3DPGnT6Sobdvrwg=" + "integrity": "sha1-beJlMXSt+12e3DPGnT6Sobdvrwg=", + "dev": true }, "map-visit": { "version": "1.0.0", @@ -11450,6 +11582,7 @@ "version": "6.11.1", "resolved": "https://nexus.loafle.net/repository/npm-all/markdown-to-jsx/-/markdown-to-jsx-6.11.1.tgz", "integrity": "sha512-FdtDAv8d9/tjyHxdCvWZxxOgK2icwzBkTq/dPk+XlQ2B+DYDcwE89FWGzT92erXQ0CQR/bQbpNK3loNYhYL70g==", + "dev": true, "requires": { "prop-types": "^15.6.2", "unquote": "^1.1.0" @@ -11458,7 +11591,8 @@ "material-colors": { "version": "1.2.6", "resolved": "https://nexus.loafle.net/repository/npm-all/material-colors/-/material-colors-1.2.6.tgz", - "integrity": "sha512-6qE4B9deFBIa9YSpOc9O0Sgc43zTeVYbgDT5veRKSlB2+ZuHNoVVxA1L/ckMUayV9Ay9y7Z/SZCLcGteW9i7bg==" + "integrity": "sha512-6qE4B9deFBIa9YSpOc9O0Sgc43zTeVYbgDT5veRKSlB2+ZuHNoVVxA1L/ckMUayV9Ay9y7Z/SZCLcGteW9i7bg==", + "dev": true }, "md5.js": { "version": "1.3.5", @@ -11497,12 +11631,14 @@ "memoize-one": { "version": "5.1.1", "resolved": "https://nexus.loafle.net/repository/npm-all/memoize-one/-/memoize-one-5.1.1.tgz", - "integrity": "sha512-HKeeBpWvqiVJD57ZUAsJNm71eHTykffzcLZVYWiVfQeI1rJtuEaS7hQiEpWfVVk18donPwJEcFKIkCmPJNOhHA==" + "integrity": "sha512-HKeeBpWvqiVJD57ZUAsJNm71eHTykffzcLZVYWiVfQeI1rJtuEaS7hQiEpWfVVk18donPwJEcFKIkCmPJNOhHA==", + "dev": true }, "memoizerific": { "version": "1.11.3", "resolved": "https://nexus.loafle.net/repository/npm-all/memoizerific/-/memoizerific-1.11.3.tgz", "integrity": "sha1-fIekZGREwy11Q4VwkF8tvRsagFo=", + "dev": true, "requires": { "map-or-similar": "^1.5.0" } @@ -11762,6 +11898,7 @@ "version": "2.19.0", "resolved": "https://nexus.loafle.net/repository/npm-all/min-document/-/min-document-2.19.0.tgz", "integrity": "sha1-e9KC4/WELtKVu3SM3Z8f+iyCRoU=", + "dev": true, "requires": { "dom-walk": "^0.1.0" } @@ -12506,9 +12643,9 @@ } }, "npm-registry-fetch": { - "version": "4.0.3", - "resolved": "https://nexus.loafle.net/repository/npm-all/npm-registry-fetch/-/npm-registry-fetch-4.0.3.tgz", - "integrity": "sha512-WGvUx0lkKFhu9MbiGFuT9nG2NpfQ+4dCJwRwwtK2HK5izJEvwDxMeUyqbuMS7N/OkpVCqDorV6rO5E4V9F8lJw==", + "version": "4.0.5", + "resolved": "https://nexus.loafle.net/repository/npm-all/npm-registry-fetch/-/npm-registry-fetch-4.0.5.tgz", + "integrity": "sha512-yQ0/U4fYpCCqmueB2g8sc+89ckQ3eXpmU4+Yi2j5o/r0WkKvE2+Y0tK3DEILAtn2UaQTkjTHxIXe2/CSdit+/Q==", "dev": true, "requires": { "JSONStream": "^1.3.4", @@ -12539,9 +12676,9 @@ } }, "safe-buffer": { - "version": "5.2.0", - "resolved": "https://nexus.loafle.net/repository/npm-all/safe-buffer/-/safe-buffer-5.2.0.tgz", - "integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==", + "version": "5.2.1", + "resolved": "https://nexus.loafle.net/repository/npm-all/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", "dev": true }, "semver": { @@ -12648,7 +12785,8 @@ "object-assign": { "version": "4.1.1", "resolved": "https://nexus.loafle.net/repository/npm-all/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "dev": true }, "object-component": { "version": "0.0.3", @@ -12690,12 +12828,14 @@ "object-inspect": { "version": "1.7.0", "resolved": "https://nexus.loafle.net/repository/npm-all/object-inspect/-/object-inspect-1.7.0.tgz", - "integrity": "sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw==" + "integrity": "sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw==", + "dev": true }, "object-is": { "version": "1.1.2", "resolved": "https://nexus.loafle.net/repository/npm-all/object-is/-/object-is-1.1.2.tgz", "integrity": "sha512-5lHCz+0uufF6wZ7CRFWJN3hp8Jqblpgve06U5CMQ3f//6iDjPr2PEo9MWCjEssDsa+UZEL4PkFpr+BMop6aKzQ==", + "dev": true, "requires": { "define-properties": "^1.1.3", "es-abstract": "^1.17.5" @@ -12704,7 +12844,8 @@ "object-keys": { "version": "1.1.1", "resolved": "https://nexus.loafle.net/repository/npm-all/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==" + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true }, "object-visit": { "version": "1.0.1", @@ -12727,6 +12868,7 @@ "version": "4.1.0", "resolved": "https://nexus.loafle.net/repository/npm-all/object.assign/-/object.assign-4.1.0.tgz", "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", + "dev": true, "requires": { "define-properties": "^1.1.2", "function-bind": "^1.1.1", @@ -13258,6 +13400,7 @@ "version": "1.0.1", "resolved": "https://nexus.loafle.net/repository/npm-all/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, "requires": { "callsites": "^3.0.0" } @@ -13280,6 +13423,7 @@ "version": "1.2.2", "resolved": "https://nexus.loafle.net/repository/npm-all/parse-entities/-/parse-entities-1.2.2.tgz", "integrity": "sha512-NzfpbxW/NPrzZ/yYSoQxyqUZMZXIdCfE0OIN4ESsnptHJECoUk3FZktxNuzQf4tjt5UEopnxpYJbvYuxIFDdsg==", + "dev": true, "requires": { "character-entities": "^1.0.0", "character-entities-legacy": "^1.0.0", @@ -13293,6 +13437,7 @@ "version": "5.0.0", "resolved": "https://nexus.loafle.net/repository/npm-all/parse-json/-/parse-json-5.0.0.tgz", "integrity": "sha512-OOY5b7PAEFV0E2Fir1KOkxchnZNCdowAJgQ5NuxjpBKTRP3pQhwkrkxqQjeoKJ+fO7bCpmIZaogI4eZGDMEGOw==", + "dev": true, "requires": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", @@ -13385,7 +13530,8 @@ "path-parse": { "version": "1.0.6", "resolved": "https://nexus.loafle.net/repository/npm-all/path-parse/-/path-parse-1.0.6.tgz", - "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==" + "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", + "dev": true }, "path-to-regexp": { "version": "0.1.7", @@ -13396,7 +13542,8 @@ "path-type": { "version": "4.0.0", "resolved": "https://nexus.loafle.net/repository/npm-all/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==" + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true }, "pbkdf2": { "version": "3.0.17", @@ -13538,6 +13685,7 @@ "version": "3.5.2", "resolved": "https://nexus.loafle.net/repository/npm-all/polished/-/polished-3.5.2.tgz", "integrity": "sha512-vWoRDg3gY5RQBtUfcj9MRN10VCIf4EkdUikGxyXItg2Hnwk+eIVtdBiLajN0ldFeT3Vq4r/QNbjrQdhqBKrTug==", + "dev": true, "requires": { "@babel/runtime": "^7.8.7" } @@ -13545,7 +13693,8 @@ "popper.js": { "version": "1.16.1", "resolved": "https://nexus.loafle.net/repository/npm-all/popper.js/-/popper.js-1.16.1.tgz", - "integrity": "sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ==" + "integrity": "sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ==", + "dev": true }, "portfinder": { "version": "1.0.25", @@ -14324,6 +14473,7 @@ "version": "1.20.0", "resolved": "https://nexus.loafle.net/repository/npm-all/prismjs/-/prismjs-1.20.0.tgz", "integrity": "sha512-AEDjSrVNkynnw6A+B1DsFkd6AVdTnp+/WoUixFRULlCLZVRZlVQMVWio/16jv7G1FscUxQxOQhWwApgbnxr6kQ==", + "dev": true, "requires": { "clipboard": "^2.0.0" } @@ -14337,7 +14487,8 @@ "process": { "version": "0.11.10", "resolved": "https://nexus.loafle.net/repository/npm-all/process/-/process-0.11.10.tgz", - "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=" + "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=", + "dev": true }, "process-nextick-args": { "version": "2.0.1", @@ -14407,6 +14558,7 @@ "version": "15.7.2", "resolved": "https://nexus.loafle.net/repository/npm-all/prop-types/-/prop-types-15.7.2.tgz", "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==", + "dev": true, "requires": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", @@ -14417,6 +14569,7 @@ "version": "5.4.0", "resolved": "https://nexus.loafle.net/repository/npm-all/property-information/-/property-information-5.4.0.tgz", "integrity": "sha512-nmMWAm/3vKFGmmOWOcdLjgq/Hlxa+hsuR/px1Lp/UGEyc5A22A6l78Shc2C0E71sPmAqglni+HrS7L7VJ7AUCA==", + "dev": true, "requires": { "xtend": "^4.0.0" } @@ -14699,7 +14852,8 @@ "qs": { "version": "6.9.3", "resolved": "https://nexus.loafle.net/repository/npm-all/qs/-/qs-6.9.3.tgz", - "integrity": "sha512-EbZYNarm6138UKKq46tdx08Yo/q9ZhFoAXAI1meAFd2GtbRDhbZY2WQSICskT0c5q99aFzLG1D4nvTk9tqfXIw==" + "integrity": "sha512-EbZYNarm6138UKKq46tdx08Yo/q9ZhFoAXAI1meAFd2GtbRDhbZY2WQSICskT0c5q99aFzLG1D4nvTk9tqfXIw==", + "dev": true }, "query-string": { "version": "4.3.4", @@ -14856,6 +15010,7 @@ "version": "16.13.1", "resolved": "https://nexus.loafle.net/repository/npm-all/react/-/react-16.13.1.tgz", "integrity": "sha512-YMZQQq32xHLX0bz5Mnibv1/LHb3Sqzngu7xstSM+vrkE5Kzr9xE0yMByK5kMoTK30YVJE61WfbxIFFvfeDKT1w==", + "dev": true, "requires": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1", @@ -14866,6 +15021,7 @@ "version": "1.2.2", "resolved": "https://nexus.loafle.net/repository/npm-all/react-clientside-effect/-/react-clientside-effect-1.2.2.tgz", "integrity": "sha512-nRmoyxeok5PBO6ytPvSjKp9xwXg9xagoTK1mMjwnQxqM9Hd7MNPl+LS1bOSOe+CV2+4fnEquc7H/S8QD3q697A==", + "dev": true, "requires": { "@babel/runtime": "^7.0.0" } @@ -14874,6 +15030,7 @@ "version": "2.18.0", "resolved": "https://nexus.loafle.net/repository/npm-all/react-color/-/react-color-2.18.0.tgz", "integrity": "sha512-FyVeU1kQiSokWc8NPz22azl1ezLpJdUyTbWL0LPUpcuuYDrZ/Y1veOk9rRK5B3pMlyDGvTk4f4KJhlkIQNRjEA==", + "dev": true, "requires": { "@icons/material": "^0.2.4", "lodash": "^4.17.11", @@ -15325,6 +15482,7 @@ "version": "16.13.1", "resolved": "https://nexus.loafle.net/repository/npm-all/react-dom/-/react-dom-16.13.1.tgz", "integrity": "sha512-81PIMmVLnCNLO/fFOQxdQkvEq/+Hfpv24XNJfpyZhTRfO0QcmQIF/PgCa1zCOj2w1hrn12MFLyaJ/G0+Mxtfag==", + "dev": true, "requires": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1", @@ -15351,12 +15509,14 @@ "react-fast-compare": { "version": "3.0.1", "resolved": "https://nexus.loafle.net/repository/npm-all/react-fast-compare/-/react-fast-compare-3.0.1.tgz", - "integrity": "sha512-C5vP0J644ofZGd54P8++O7AvrqMEbrGf8Ue0eAUJLJyw168dAX2aiYyX/zcY/eSNwO0IDjsKUaLE6n83D+TnEg==" + "integrity": "sha512-C5vP0J644ofZGd54P8++O7AvrqMEbrGf8Ue0eAUJLJyw168dAX2aiYyX/zcY/eSNwO0IDjsKUaLE6n83D+TnEg==", + "dev": true }, "react-focus-lock": { "version": "2.3.1", "resolved": "https://nexus.loafle.net/repository/npm-all/react-focus-lock/-/react-focus-lock-2.3.1.tgz", "integrity": "sha512-j15cWLPzH0gOmRrUg01C09Peu8qbcdVqr6Bjyfxj80cNZmH+idk/bNBYEDSmkAtwkXI+xEYWSmHYqtaQhZ8iUQ==", + "dev": true, "requires": { "@babel/runtime": "^7.0.0", "focus-lock": "^0.6.7", @@ -15370,6 +15530,7 @@ "version": "1.0.5", "resolved": "https://nexus.loafle.net/repository/npm-all/react-helmet-async/-/react-helmet-async-1.0.5.tgz", "integrity": "sha512-nqGA5a1HRZsw1lzDn+bYuUN2FyHRiY+DgjRVhEOKVBDTrrqJCpCIOuY/IRHdobr+KD1gGTP0WabZsTrIHnFKJA==", + "dev": true, "requires": { "@babel/runtime": "^7.9.2", "invariant": "^2.2.4", @@ -15391,6 +15552,7 @@ "version": "2.2.2", "resolved": "https://nexus.loafle.net/repository/npm-all/react-input-autosize/-/react-input-autosize-2.2.2.tgz", "integrity": "sha512-jQJgYCA3S0j+cuOwzuCd1OjmBmnZLdqQdiLKRYrsMMzbjUrVDS5RvJUDwJqA7sKuksDuzFtm6hZGKFu7Mjk5aw==", + "dev": true, "requires": { "prop-types": "^15.5.8" } @@ -15409,17 +15571,20 @@ "react-is": { "version": "16.13.1", "resolved": "https://nexus.loafle.net/repository/npm-all/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true }, "react-lifecycles-compat": { "version": "3.0.4", "resolved": "https://nexus.loafle.net/repository/npm-all/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", - "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" + "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==", + "dev": true }, "react-popper": { "version": "1.3.7", "resolved": "https://nexus.loafle.net/repository/npm-all/react-popper/-/react-popper-1.3.7.tgz", "integrity": "sha512-nmqYTx7QVjCm3WUZLeuOomna138R1luC4EqkW3hxJUrAe+3eNz3oFCLYdnPwILfn0mX1Ew2c3wctrjlUMYYUww==", + "dev": true, "requires": { "@babel/runtime": "^7.1.2", "create-react-context": "^0.3.0", @@ -15434,6 +15599,7 @@ "version": "2.11.1", "resolved": "https://nexus.loafle.net/repository/npm-all/react-popper-tooltip/-/react-popper-tooltip-2.11.1.tgz", "integrity": "sha512-04A2f24GhyyMicKvg/koIOQ5BzlrRbKiAgP6L+Pdj1MVX3yJ1NeZ8+EidndQsbejFT55oW1b++wg2Z8KlAyhfQ==", + "dev": true, "requires": { "@babel/runtime": "^7.9.2", "react-popper": "^1.3.7" @@ -15443,6 +15609,7 @@ "version": "3.1.0", "resolved": "https://nexus.loafle.net/repository/npm-all/react-select/-/react-select-3.1.0.tgz", "integrity": "sha512-wBFVblBH1iuCBprtpyGtd1dGMadsG36W5/t2Aj8OE6WbByDg5jIFyT7X5gT+l0qmT5TqWhxX+VsKJvCEl2uL9g==", + "dev": true, "requires": { "@babel/runtime": "^7.4.4", "@emotion/cache": "^10.0.9", @@ -15470,6 +15637,7 @@ "version": "11.0.2", "resolved": "https://nexus.loafle.net/repository/npm-all/react-syntax-highlighter/-/react-syntax-highlighter-11.0.2.tgz", "integrity": "sha512-kqmpM2OH5OodInbEADKARwccwSQWBfZi0970l5Jhp4h39q9Q65C4frNcnd6uHE5pR00W8pOWj9HDRntj2G4Rww==", + "dev": true, "requires": { "@babel/runtime": "^7.3.1", "highlight.js": "~9.13.0", @@ -15482,6 +15650,7 @@ "version": "7.1.2", "resolved": "https://nexus.loafle.net/repository/npm-all/react-textarea-autosize/-/react-textarea-autosize-7.1.2.tgz", "integrity": "sha512-uH3ORCsCa3C6LHxExExhF4jHoXYCQwE5oECmrRsunlspaDAbS4mGKNlWZqjLfInWtFQcf0o1n1jC/NGXFdUBCg==", + "dev": true, "requires": { "@babel/runtime": "^7.1.2", "prop-types": "^15.6.0" @@ -15491,6 +15660,7 @@ "version": "4.3.0", "resolved": "https://nexus.loafle.net/repository/npm-all/react-transition-group/-/react-transition-group-4.3.0.tgz", "integrity": "sha512-1qRV1ZuVSdxPlPf4O8t7inxUGpdyO5zG9IoNfJxSO0ImU2A1YWkEQvFPuIPZmMLkg5hYs7vv5mMOyfgSkvAwvw==", + "dev": true, "requires": { "@babel/runtime": "^7.5.5", "dom-helpers": "^5.0.1", @@ -15502,6 +15672,7 @@ "version": "1.2.3", "resolved": "https://nexus.loafle.net/repository/npm-all/reactcss/-/reactcss-1.2.3.tgz", "integrity": "sha512-KiwVUcFu1RErkI97ywr8nvx8dNOpT03rbnma0SSalTYjkrPYaEajR4a/MRt6DZ46K6arDRbWMNHF+xH7G7n/8A==", + "dev": true, "requires": { "lodash": "^4.0.1" } @@ -15702,6 +15873,7 @@ "version": "2.10.1", "resolved": "https://nexus.loafle.net/repository/npm-all/refractor/-/refractor-2.10.1.tgz", "integrity": "sha512-Xh9o7hQiQlDbxo5/XkOX6H+x/q8rmlmZKr97Ie1Q8ZM32IRRd3B/UxuA/yXDW79DBSXGWxm2yRTbcTVmAciJRw==", + "dev": true, "requires": { "hastscript": "^5.0.0", "parse-entities": "^1.1.2", @@ -15712,6 +15884,7 @@ "version": "1.17.1", "resolved": "https://nexus.loafle.net/repository/npm-all/prismjs/-/prismjs-1.17.1.tgz", "integrity": "sha512-PrEDJAFdUGbOP6xK/UsfkC5ghJsPJviKgnQOoxaDbBjwc8op68Quupwt1DeAFoG8GImPhiKXAvvsH7wDSLsu1Q==", + "dev": true, "requires": { "clipboard": "^2.0.0" } @@ -15736,7 +15909,8 @@ "regenerator-runtime": { "version": "0.13.5", "resolved": "https://nexus.loafle.net/repository/npm-all/regenerator-runtime/-/regenerator-runtime-0.13.5.tgz", - "integrity": "sha512-ZS5w8CpKFinUzOwW3c83oPeVXoNsrLsaCoLtJvAClH135j/R77RuymhiSErhm2lKcwSCIpmvIWSbDkIfAqKQlA==" + "integrity": "sha512-ZS5w8CpKFinUzOwW3c83oPeVXoNsrLsaCoLtJvAClH135j/R77RuymhiSErhm2lKcwSCIpmvIWSbDkIfAqKQlA==", + "dev": true }, "regenerator-transform": { "version": "0.14.4", @@ -15762,6 +15936,7 @@ "version": "1.3.0", "resolved": "https://nexus.loafle.net/repository/npm-all/regexp.prototype.flags/-/regexp.prototype.flags-1.3.0.tgz", "integrity": "sha512-2+Q0C5g951OlYlJz6yu5/M33IcsESLlLfsyIaLJaG4FA2r4yP8MvVMJUUP/fVBkSpbbbZlS5gynbEWLipiiXiQ==", + "dev": true, "requires": { "define-properties": "^1.1.3", "es-abstract": "^1.17.0-next.1" @@ -15946,12 +16121,14 @@ "resize-observer-polyfill": { "version": "1.5.1", "resolved": "https://nexus.loafle.net/repository/npm-all/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", - "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==" + "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==", + "dev": true }, "resolve": { "version": "1.17.0", "resolved": "https://nexus.loafle.net/repository/npm-all/resolve/-/resolve-1.17.0.tgz", "integrity": "sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==", + "dev": true, "requires": { "path-parse": "^1.0.6" } @@ -15976,7 +16153,8 @@ "resolve-from": { "version": "5.0.0", "resolved": "https://nexus.loafle.net/repository/npm-all/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==" + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true }, "resolve-url": { "version": "0.2.1", @@ -16103,7 +16281,8 @@ "safe-buffer": { "version": "5.1.2", "resolved": "https://nexus.loafle.net/repository/npm-all/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true }, "safe-regex": { "version": "1.1.0", @@ -16214,6 +16393,7 @@ "version": "0.19.1", "resolved": "https://nexus.loafle.net/repository/npm-all/scheduler/-/scheduler-0.19.1.tgz", "integrity": "sha512-n/zwRWRYSUj0/3g/otKDRPMh6qv2SYMWNq85IEa8iZyAv8od9zDYpGSnpBEjNgcMNq6Scbu5KfIPxNF72R/2EA==", + "dev": true, "requires": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1" @@ -16338,6 +16518,7 @@ "version": "1.1.2", "resolved": "https://nexus.loafle.net/repository/npm-all/select/-/select-1.1.2.tgz", "integrity": "sha1-DnNQrN7ICxEIUoeG7B1EGNEbOW0=", + "dev": true, "optional": true }, "select-hose": { @@ -16390,7 +16571,8 @@ "semver": { "version": "6.3.0", "resolved": "https://nexus.loafle.net/repository/npm-all/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true }, "semver-diff": { "version": "3.1.1", @@ -16666,12 +16848,14 @@ "shallow-equal": { "version": "1.2.1", "resolved": "https://nexus.loafle.net/repository/npm-all/shallow-equal/-/shallow-equal-1.2.1.tgz", - "integrity": "sha512-S4vJDjHHMBaiZuT9NPb616CSmLf618jawtv3sufLl6ivK8WocjAo58cXwbRV1cgqxH0Qbv+iUt6m05eqEa2IRA==" + "integrity": "sha512-S4vJDjHHMBaiZuT9NPb616CSmLf618jawtv3sufLl6ivK8WocjAo58cXwbRV1cgqxH0Qbv+iUt6m05eqEa2IRA==", + "dev": true }, "shallowequal": { "version": "1.1.0", "resolved": "https://nexus.loafle.net/repository/npm-all/shallowequal/-/shallowequal-1.1.0.tgz", - "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==" + "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==", + "dev": true }, "shebang-command": { "version": "1.2.0", @@ -16750,6 +16934,7 @@ "version": "4.2.3", "resolved": "https://nexus.loafle.net/repository/npm-all/simplebar/-/simplebar-4.2.3.tgz", "integrity": "sha512-9no0pK7/1y+8/oTF3sy/+kx0PjQ3uk4cYwld5F1CJGk2gx+prRyUq8GRfvcVLq5niYWSozZdX73a2wIr1o9l/g==", + "dev": true, "requires": { "can-use-dom": "^0.1.0", "core-js": "^3.0.1", @@ -16763,6 +16948,7 @@ "version": "1.2.3", "resolved": "https://nexus.loafle.net/repository/npm-all/simplebar-react/-/simplebar-react-1.2.3.tgz", "integrity": "sha512-1EOWJzFC7eqHUp1igD1/tb8GBv5aPQA5ZMvpeDnVkpNJ3jAuvmrL2kir3HuijlxhG7njvw9ssxjjBa89E5DrJg==", + "dev": true, "requires": { "prop-types": "^15.6.1", "simplebar": "^4.2.3" @@ -17123,7 +17309,8 @@ "source-map": { "version": "0.5.7", "resolved": "https://nexus.loafle.net/repository/npm-all/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true }, "source-map-loader": { "version": "0.2.4", @@ -17181,7 +17368,8 @@ "space-separated-tokens": { "version": "1.1.5", "resolved": "https://nexus.loafle.net/repository/npm-all/space-separated-tokens/-/space-separated-tokens-1.1.5.tgz", - "integrity": "sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA==" + "integrity": "sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA==", + "dev": true }, "spawn-command": { "version": "0.0.2-1", @@ -17315,7 +17503,8 @@ "stable": { "version": "0.1.8", "resolved": "https://nexus.loafle.net/repository/npm-all/stable/-/stable-0.1.8.tgz", - "integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==" + "integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==", + "dev": true }, "static-extend": { "version": "0.1.2", @@ -17347,7 +17536,8 @@ "store2": { "version": "2.11.1", "resolved": "https://nexus.loafle.net/repository/npm-all/store2/-/store2-2.11.1.tgz", - "integrity": "sha512-llZqXAXjG2E4FvWsZxFmBDfh6kqQuGFZm64TX23qW02Hf4dyElhDEbYx1IIVTEMKWrrDnDA9oqOjY8WHo2NgcA==" + "integrity": "sha512-llZqXAXjG2E4FvWsZxFmBDfh6kqQuGFZm64TX23qW02Hf4dyElhDEbYx1IIVTEMKWrrDnDA9oqOjY8WHo2NgcA==", + "dev": true }, "stream-browserify": { "version": "2.0.2", @@ -17494,6 +17684,7 @@ "version": "1.0.1", "resolved": "https://nexus.loafle.net/repository/npm-all/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz", "integrity": "sha512-LRPxFUaTtpqYsTeNKaFOw3R4bxIzWOnbQ837QfBylo8jIxtcbK/A/sMV7Q+OAV/vWo+7s25pOE10KYSjaSO06g==", + "dev": true, "requires": { "define-properties": "^1.1.3", "es-abstract": "^1.17.5" @@ -17503,6 +17694,7 @@ "version": "2.1.2", "resolved": "https://nexus.loafle.net/repository/npm-all/string.prototype.trimleft/-/string.prototype.trimleft-2.1.2.tgz", "integrity": "sha512-gCA0tza1JBvqr3bfAIFJGqfdRTyPae82+KTnm3coDXkZN9wnuW3HjGgN386D7hfv5CHQYCI022/rJPVlqXyHSw==", + "dev": true, "requires": { "define-properties": "^1.1.3", "es-abstract": "^1.17.5", @@ -17513,6 +17705,7 @@ "version": "2.1.2", "resolved": "https://nexus.loafle.net/repository/npm-all/string.prototype.trimright/-/string.prototype.trimright-2.1.2.tgz", "integrity": "sha512-ZNRQ7sY3KroTaYjRS6EbNiiHrOkjihL9aQE/8gfQ4DtAC/aEBRHFJa44OmoWxGGqXuJlfKkZW4WcXErGr+9ZFg==", + "dev": true, "requires": { "define-properties": "^1.1.3", "es-abstract": "^1.17.5", @@ -17523,6 +17716,7 @@ "version": "1.0.1", "resolved": "https://nexus.loafle.net/repository/npm-all/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz", "integrity": "sha512-XxZn+QpvrBI1FOcg6dIpxUPgWCPuNXvMD72aaRaUQv1eD4e/Qy8i/hFTe0BUmD60p/QA6bh1avmuPTfNjqVWRw==", + "dev": true, "requires": { "define-properties": "^1.1.3", "es-abstract": "^1.17.5" @@ -17700,6 +17894,7 @@ "version": "5.5.0", "resolved": "https://nexus.loafle.net/repository/npm-all/supports-color/-/supports-color-5.5.0.tgz", "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, "requires": { "has-flag": "^3.0.0" } @@ -17793,6 +17988,7 @@ "version": "3.3.0", "resolved": "https://nexus.loafle.net/repository/npm-all/telejson/-/telejson-3.3.0.tgz", "integrity": "sha512-er08AylQ+LEbDLp1GRezORZu5wKOHaBczF6oYJtgC3Idv10qZ8A3p6ffT+J5BzDKkV9MqBvu8HAKiIIOp6KJ2w==", + "dev": true, "requires": { "@types/is-function": "^1.0.0", "global": "^4.4.0", @@ -18038,12 +18234,14 @@ "version": "2.1.0", "resolved": "https://nexus.loafle.net/repository/npm-all/tiny-emitter/-/tiny-emitter-2.1.0.tgz", "integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==", + "dev": true, "optional": true }, "tinycolor2": { "version": "1.4.1", "resolved": "https://nexus.loafle.net/repository/npm-all/tinycolor2/-/tinycolor2-1.4.1.tgz", - "integrity": "sha1-9PrTM0R7wLB9TcjpIJ2POaisd+g=" + "integrity": "sha1-9PrTM0R7wLB9TcjpIJ2POaisd+g=", + "dev": true }, "tmp": { "version": "0.0.33", @@ -18069,7 +18267,8 @@ "to-fast-properties": { "version": "2.0.0", "resolved": "https://nexus.loafle.net/repository/npm-all/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=" + "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", + "dev": true }, "to-object-path": { "version": "0.3.0", @@ -18121,7 +18320,8 @@ "toggle-selection": { "version": "1.0.6", "resolved": "https://nexus.loafle.net/repository/npm-all/toggle-selection/-/toggle-selection-1.0.6.tgz", - "integrity": "sha1-bkWxJj8gF/oKzH2J14sVuL932jI=" + "integrity": "sha1-bkWxJj8gF/oKzH2J14sVuL932jI=", + "dev": true }, "toidentifier": { "version": "1.0.0", @@ -18170,7 +18370,8 @@ "ts-dedent": { "version": "1.1.1", "resolved": "https://nexus.loafle.net/repository/npm-all/ts-dedent/-/ts-dedent-1.1.1.tgz", - "integrity": "sha512-UGTRZu1evMw4uTPyYF66/KFd22XiU+jMaIuHrkIHQ2GivAXVlLV0v/vHrpOuTRf9BmpNHi/SO7Vd0rLu0y57jg==" + "integrity": "sha512-UGTRZu1evMw4uTPyYF66/KFd22XiU+jMaIuHrkIHQ2GivAXVlLV0v/vHrpOuTRf9BmpNHi/SO7Vd0rLu0y57jg==", + "dev": true }, "ts-loader": { "version": "6.2.2", @@ -18242,7 +18443,8 @@ "tslib": { "version": "1.11.1", "resolved": "https://nexus.loafle.net/repository/npm-all/tslib/-/tslib-1.11.1.tgz", - "integrity": "sha512-aZW88SY8kQbU7gpV19lN24LtXh/yD4ZZg6qieAJDDg+YBsJcSmLGK9QpnUjAKVG/xefmvJGd1WUmfpT/g6AJGA==" + "integrity": "sha512-aZW88SY8kQbU7gpV19lN24LtXh/yD4ZZg6qieAJDDg+YBsJcSmLGK9QpnUjAKVG/xefmvJGd1WUmfpT/g6AJGA==", + "dev": true }, "tslint": { "version": "5.18.0", @@ -18334,7 +18536,8 @@ "typed-styles": { "version": "0.0.7", "resolved": "https://nexus.loafle.net/repository/npm-all/typed-styles/-/typed-styles-0.0.7.tgz", - "integrity": "sha512-pzP0PWoZUhsECYjABgCGQlRGL1n7tOHsgwYv3oIiEpJwGhFTuty/YNeduxQYzXXa3Ge5BdT6sHYIQYpl4uJ+5Q==" + "integrity": "sha512-pzP0PWoZUhsECYjABgCGQlRGL1n7tOHsgwYv3oIiEpJwGhFTuty/YNeduxQYzXXa3Ge5BdT6sHYIQYpl4uJ+5Q==", + "dev": true }, "typedarray": { "version": "0.0.6", @@ -18485,7 +18688,8 @@ "unquote": { "version": "1.1.1", "resolved": "https://nexus.loafle.net/repository/npm-all/unquote/-/unquote-1.1.1.tgz", - "integrity": "sha1-j97XMk7G6IoP+LkF58CYzcCG1UQ=" + "integrity": "sha1-j97XMk7G6IoP+LkF58CYzcCG1UQ=", + "dev": true }, "unset-value": { "version": "1.0.0", @@ -18728,12 +18932,14 @@ "use-callback-ref": { "version": "1.2.3", "resolved": "https://nexus.loafle.net/repository/npm-all/use-callback-ref/-/use-callback-ref-1.2.3.tgz", - "integrity": "sha512-DPBPh1i2adCZoIArRlTuKRy7yue7QogtEnfv0AKrWsY+GA+4EKe37zhRDouNnyWMoNQFYZZRF+2dLHsWE4YvJA==" + "integrity": "sha512-DPBPh1i2adCZoIArRlTuKRy7yue7QogtEnfv0AKrWsY+GA+4EKe37zhRDouNnyWMoNQFYZZRF+2dLHsWE4YvJA==", + "dev": true }, "use-sidecar": { "version": "1.0.2", "resolved": "https://nexus.loafle.net/repository/npm-all/use-sidecar/-/use-sidecar-1.0.2.tgz", "integrity": "sha512-287RZny6m5KNMTb/Kq9gmjafi7lQL0YHO1lYolU6+tY1h9+Z3uCtkJJ3OSOq3INwYf2hBryCcDh4520AhJibMA==", + "dev": true, "requires": { "detect-node": "^2.0.4", "tslib": "^1.9.3" @@ -18787,7 +18993,8 @@ "util-deprecate": { "version": "1.0.2", "resolved": "https://nexus.loafle.net/repository/npm-all/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "dev": true }, "util-promisify": { "version": "2.1.0", @@ -18886,6 +19093,7 @@ "version": "4.0.3", "resolved": "https://nexus.loafle.net/repository/npm-all/warning/-/warning-4.0.3.tgz", "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", + "dev": true, "requires": { "loose-envify": "^1.0.0" } @@ -19756,7 +19964,8 @@ "xtend": { "version": "4.0.2", "resolved": "https://nexus.loafle.net/repository/npm-all/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true }, "xxhashjs": { "version": "0.2.2", @@ -19783,6 +19992,7 @@ "version": "1.9.2", "resolved": "https://nexus.loafle.net/repository/npm-all/yaml/-/yaml-1.9.2.tgz", "integrity": "sha512-HPT7cGGI0DuRcsO51qC1j9O16Dh1mZ2bnXwsi0jrSpsLz0WxOLSLXfkABVl6bZO629py3CU+OMJtpNHDLB97kg==", + "dev": true, "requires": { "@babel/runtime": "^7.9.2" } diff --git a/package.json b/package.json index 400468e..c59167e 100644 --- a/package.json +++ b/package.json @@ -16,9 +16,8 @@ "build:pi": "node ./scripts/build.js pi", "build:core": "node ./scripts/build.js core", "build:logger": "node ./scripts/build.js logger", - "build:native:all": "npm-run-all -s build:native build:native-browser", + "build:native:all": "npm-run-all -s build:native", "build:native": "node ./scripts/build.js native", - "build:native-browser": "node ./scripts/build.js native-browser", "build:protocol:all": "npm-run-all -s build:protocol build:protocol-authentication build:protocol-buddy build:protocol-event build:protocol-file build:protocol-group build:protocol-status build:protocol-info build:protocol-inner build:protocol-option build:protocol-ping build:protocol-query build:protocol-room build:protocol-service build:protocol-sync build:protocol-umg", "build:protocol": "node ./scripts/build.js protocol", "build:protocol-authentication": "node ./scripts/build.js protocol-authentication", @@ -65,9 +64,8 @@ "publish:pi": "cd ./dist/pi && npm publish", "publish:core": "cd ./dist/core && npm publish", "publish:logger": "cd ./dist/logger && npm publish", - "publish:native:all": "npm-run-all -s publish:native publish:native-browser", + "publish:native:all": "npm-run-all -s publish:native", "publish:native": "cd ./dist/native && npm publish", - "publish:native-browser": "cd ./dist/native-browser && npm publish", "publish:protocol:all": "npm-run-all -s publish:protocol publish:protocol-authentication publish:protocol-buddy publish:protocol-event publish:protocol-file publish:protocol-group publish:protocol-status publish:protocol-info publish:protocol-inner publish:protocol-option publish:protocol-ping publish:protocol-query publish:protocol-room publish:protocol-service publish:protocol-sync publish:protocol-umg", "publish:protocol": "cd ./dist/protocol && npm publish", "publish:protocol-authentication": "cd ./dist/protocol-authentication && npm publish", @@ -110,35 +108,33 @@ "storybook": "start-storybook -p 6006", "build-storybook": "build-storybook" }, - "dependencies": { - "@storybook/addon-knobs": "^5.3.18", - "tslib": "^1.10.0" - }, + "dependencies": {}, "devDependencies": { "@angular-devkit/build-angular": "^0.900.6", "@angular-devkit/build-ng-packagr": "^0.900.6", - "@angular/animations": "^9.0.6", - "@angular/cdk": "^9.1.2", - "@angular/cli": "^9.0.6", - "@angular/common": "^9.0.6", - "@angular/compiler": "^9.0.6", - "@angular/compiler-cli": "^9.0.6", - "@angular/core": "^9.0.6", - "@angular/flex-layout": "^9.0.0-beta.29", - "@angular/forms": "^9.0.6", - "@angular/language-service": "^9.0.6", - "@angular/material": "^9.1.2", - "@angular/material-moment-adapter": "^9.1.2", - "@angular/platform-browser": "^9.0.6", - "@angular/platform-browser-dynamic": "^9.0.6", - "@angular/router": "^9.0.6", + "@angular/animations": "^9.1.11", + "@angular/cdk": "^9.2.4", + "@angular/cli": "^9.1.9", + "@angular/common": "^9.1.11", + "@angular/compiler": "^9.1.11", + "@angular/compiler-cli": "^9.1.11", + "@angular/core": "^9.1.11", + "@angular/flex-layout": "^9.0.0-beta.31", + "@angular/forms": "^9.1.11", + "@angular/language-service": "^9.1.11", + "@angular/material": "^9.2.4", + "@angular/material-moment-adapter": "^9.2.4", + "@angular/platform-browser": "^9.1.11", + "@angular/platform-browser-dynamic": "^9.1.11", + "@angular/router": "^9.1.11", "@babel/core": "^7.9.0", - "@ngrx/effects": "^9.0.0", - "@ngrx/entity": "^9.0.0", - "@ngrx/router-store": "^9.0.0", - "@ngrx/store": "^9.0.0", - "@ngrx/store-devtools": "^9.0.0", + "@ngrx/effects": "^9.2.0", + "@ngrx/entity": "^9.2.0", + "@ngrx/router-store": "^9.2.0", + "@ngrx/store": "^9.2.0", + "@ngrx/store-devtools": "^9.2.0", "@storybook/addon-actions": "^5.3.18", + "@storybook/addon-knobs": "^5.3.18", "@storybook/addon-links": "^5.3.18", "@storybook/addon-notes": "^5.3.18", "@storybook/addons": "^5.3.18", @@ -148,15 +144,14 @@ "@types/moment-timezone": "^0.5.12", "@types/node": "^12.12.30", "@ucap/api": "~0.0.1", - "@ucap/api-common": "~0.0.5", + "@ucap/api-common": "~0.0.7", "@ucap/api-external": "~0.0.2", "@ucap/api-message": "~0.0.1", "@ucap/api-prompt": "~0.0.1", "@ucap/api-public": "~0.0.1", - "@ucap/core": "~0.0.6", + "@ucap/core": "~0.0.14", "@ucap/logger": "~0.0.12", - "@ucap/native": "~0.0.1", - "@ucap/native-browser": "~0.0.1", + "@ucap/native": "~0.0.19", "@ucap/ng-api-common": "file:pack/ucap-ng-api-common-0.0.1.tgz", "@ucap/ng-api-external": "file:pack/ucap-ng-api-external-0.0.1.tgz", "@ucap/ng-api-message": "file:pack/ucap-ng-api-message-0.0.1.tgz", @@ -165,8 +160,7 @@ "@ucap/ng-core": "file:pack/ucap-ng-core-0.0.7.tgz", "@ucap/ng-i18n": "file:pack/ucap-ng-i18n-0.0.6.tgz", "@ucap/ng-logger": "file:pack/ucap-ng-logger-0.0.2.tgz", - "@ucap/ng-native": "file:pack/ucap-ng-native-0.0.1.tgz", - "@ucap/ng-native-browser": "file:pack/ucap-ng-native-browser-0.0.1.tgz", + "@ucap/ng-native": "file:pack/ucap-ng-native-0.0.5.tgz", "@ucap/ng-pi": "file:pack/ucap-ng-pi-0.0.1.tgz", "@ucap/ng-protocol": "file:pack/ucap-ng-protocol-0.0.3.tgz", "@ucap/ng-protocol-authentication": "file:pack/ucap-ng-protocol-authentication-0.0.3.tgz", @@ -184,37 +178,37 @@ "@ucap/ng-protocol-status": "file:pack/ucap-ng-protocol-status-0.0.3.tgz", "@ucap/ng-protocol-sync": "file:pack/ucap-ng-protocol-sync-0.0.3.tgz", "@ucap/ng-protocol-umg": "file:pack/ucap-ng-protocol-umg-0.0.3.tgz", - "@ucap/ng-store-authentication": "file:pack/ucap-ng-store-authentication-0.0.11.tgz", - "@ucap/ng-store-chat": "file:pack/ucap-ng-store-chat-0.0.19.tgz", - "@ucap/ng-store-group": "file:pack/ucap-ng-store-group-0.0.14.tgz", - "@ucap/ng-store-organization": "file:pack/ucap-ng-store-organization-0.0.8.tgz", - "@ucap/ng-ui": "file:pack/ucap-ng-ui-0.0.24.tgz", - "@ucap/ng-ui-authentication": "file:pack/ucap-ng-ui-authentication-0.0.25.tgz", - "@ucap/ng-ui-chat": "file:pack/ucap-ng-ui-chat-0.0.13.tgz", - "@ucap/ng-ui-group": "file:pack/ucap-ng-ui-group-0.0.33.tgz", + "@ucap/ng-store-authentication": "file:pack/ucap-ng-store-authentication-0.0.14.tgz", + "@ucap/ng-store-chat": "file:pack/ucap-ng-store-chat-0.0.66.tgz", + "@ucap/ng-store-group": "file:pack/ucap-ng-store-group-0.0.22.tgz", + "@ucap/ng-store-organization": "file:pack/ucap-ng-store-organization-0.0.20.tgz", + "@ucap/ng-ui": "file:pack/ucap-ng-ui-0.0.97.tgz", + "@ucap/ng-ui-authentication": "file:pack/ucap-ng-ui-authentication-0.0.29.tgz", + "@ucap/ng-ui-chat": "file:pack/ucap-ng-ui-chat-0.0.72.tgz", + "@ucap/ng-ui-group": "file:pack/ucap-ng-ui-group-0.0.78.tgz", "@ucap/ng-ui-material": "file:pack/ucap-ng-ui-material-0.0.4.tgz", - "@ucap/ng-ui-organization": "file:pack/ucap-ng-ui-organization-0.0.84.tgz", + "@ucap/ng-ui-organization": "file:pack/ucap-ng-ui-organization-0.0.202.tgz", "@ucap/ng-ui-skin-default": "file:pack/ucap-ng-ui-skin-default-0.0.1.tgz", "@ucap/ng-web-socket": "file:pack/ucap-ng-web-socket-0.0.2.tgz", "@ucap/ng-web-storage": "file:pack/ucap-ng-web-storage-0.0.3.tgz", - "@ucap/pi": "~0.0.5", + "@ucap/pi": "~0.0.8", "@ucap/protocol": "~0.0.11", "@ucap/protocol-authentication": "~0.0.5", "@ucap/protocol-buddy": "~0.0.5", - "@ucap/protocol-event": "~0.0.5", - "@ucap/protocol-file": "~0.0.5", + "@ucap/protocol-event": "~0.0.6", + "@ucap/protocol-file": "~0.0.6", "@ucap/protocol-group": "~0.0.5", - "@ucap/protocol-info": "~0.0.6", + "@ucap/protocol-info": "~0.0.9", "@ucap/protocol-inner": "~0.0.4", "@ucap/protocol-option": "~0.0.7", "@ucap/protocol-ping": "~0.0.4", "@ucap/protocol-query": "~0.0.5", - "@ucap/protocol-room": "~0.0.6", + "@ucap/protocol-room": "~0.0.7", "@ucap/protocol-service": "~0.0.4", "@ucap/protocol-status": "~0.0.5", - "@ucap/protocol-sync": "~0.0.4", + "@ucap/protocol-sync": "~0.0.6", "@ucap/protocol-umg": "~0.0.5", - "@ucap/ui-scss": "~0.0.3", + "@ucap/ui-scss": "~0.0.5", "@ucap/web-socket": "~0.0.5", "@ucap/web-storage": "~0.0.5", "autolinker": "^3.13.0", diff --git a/projects/native-browser/README.md b/projects/native-browser/README.md deleted file mode 100644 index 03a92d8..0000000 --- a/projects/native-browser/README.md +++ /dev/null @@ -1,24 +0,0 @@ -# NativeBrowser - -This library was generated with [Angular CLI](https://github.com/angular/angular-cli) version 9.0.2. - -## Code scaffolding - -Run `ng generate component component-name --project native-browser` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module --project native-browser`. -> Note: Don't forget to add `--project native-browser` or else it will be added to the default project in your `angular.json` file. - -## Build - -Run `ng build native-browser` to build the project. The build artifacts will be stored in the `dist/` directory. - -## Publishing - -After building your library with `ng build native-browser`, go to the dist folder `cd dist/native-browser` and run `npm publish`. - -## Running unit tests - -Run `ng test native-browser` to execute the unit tests via [Karma](https://karma-runner.github.io). - -## Further help - -To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md). diff --git a/projects/native-browser/karma.conf.js b/projects/native-browser/karma.conf.js deleted file mode 100644 index 3db63cf..0000000 --- a/projects/native-browser/karma.conf.js +++ /dev/null @@ -1,32 +0,0 @@ -// Karma configuration file, see link for more information -// https://karma-runner.github.io/1.0/config/configuration-file.html - -module.exports = function (config) { - config.set({ - basePath: '', - frameworks: ['jasmine', '@angular-devkit/build-angular'], - plugins: [ - require('karma-jasmine'), - require('karma-chrome-launcher'), - require('karma-jasmine-html-reporter'), - require('karma-coverage-istanbul-reporter'), - require('@angular-devkit/build-angular/plugins/karma') - ], - client: { - clearContext: false // leave Jasmine Spec Runner output visible in browser - }, - coverageIstanbulReporter: { - dir: require('path').join(__dirname, '../../coverage/native-browser'), - reports: ['html', 'lcovonly', 'text-summary'], - fixWebpackSourcePaths: true - }, - reporters: ['progress', 'kjhtml'], - port: 9876, - colors: true, - logLevel: config.LOG_INFO, - autoWatch: true, - browsers: ['Chrome'], - singleRun: false, - restartOnFileChange: true - }); -}; diff --git a/projects/native-browser/ng-package.json b/projects/native-browser/ng-package.json deleted file mode 100644 index db3da68..0000000 --- a/projects/native-browser/ng-package.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", - "dest": "../../dist/native-browser", - "lib": { - "entryFile": "src/public-api.ts", - "umdModuleIds": { - "@ucap/native-browser": "@ucap/native-browser", - "@ucap/ng-core": "@ucap/ng-core" - } - } -} diff --git a/projects/native-browser/package.json b/projects/native-browser/package.json deleted file mode 100644 index 048e66b..0000000 --- a/projects/native-browser/package.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "name": "@ucap/ng-native-browser", - "version": "0.0.1", - "publishConfig": { - "registry": "https://nexus.loafle.net/repository/npm-ucap/" - }, - "peerDependencies": { - "@angular/common": "^9.0.2", - "@angular/core": "^9.0.2", - "@ucap/native-browser": "~0.0.1", - "@ucap/ng-core": "~0.0.1", - "axios": "^0.19.2", - "rxjs": "~6.5.4", - "tslib": "^1.10.0" - } -} diff --git a/projects/native-browser/src/lib/services/browser-native.service.ts b/projects/native-browser/src/lib/services/browser-native.service.ts deleted file mode 100644 index c87d100..0000000 --- a/projects/native-browser/src/lib/services/browser-native.service.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Injectable, Inject } from '@angular/core'; - -import { AxiosInstance } from 'axios'; - -import { BrowserNativeService as UcapBrowserNativeService } from '@ucap/native-browser'; - -import { AXIOS_INSTANCE } from '@ucap/ng-core'; - -@Injectable({ - providedIn: 'root' -}) -export class BrowserNativeService extends UcapBrowserNativeService { - constructor(@Inject(AXIOS_INSTANCE) axios: AxiosInstance) { - super(axios); - } -} diff --git a/projects/native-browser/src/public-api.ts b/projects/native-browser/src/public-api.ts deleted file mode 100644 index d7da042..0000000 --- a/projects/native-browser/src/public-api.ts +++ /dev/null @@ -1,5 +0,0 @@ -/* - * Public API Surface of native-browser - */ - -export * from './lib/services/browser-native.service'; diff --git a/projects/native-browser/src/test.ts b/projects/native-browser/src/test.ts deleted file mode 100644 index 303b32a..0000000 --- a/projects/native-browser/src/test.ts +++ /dev/null @@ -1,26 +0,0 @@ -// This file is required by karma.conf.js and loads recursively all the .spec and framework files - -import 'zone.js/dist/zone'; -import 'zone.js/dist/zone-testing'; -import { getTestBed } from '@angular/core/testing'; -import { - BrowserDynamicTestingModule, - platformBrowserDynamicTesting -} from '@angular/platform-browser-dynamic/testing'; - -declare const require: { - context(path: string, deep?: boolean, filter?: RegExp): { - keys(): string[]; - (id: string): T; - }; -}; - -// First, initialize the Angular testing environment. -getTestBed().initTestEnvironment( - BrowserDynamicTestingModule, - platformBrowserDynamicTesting() -); -// Then we find all the tests. -const context = require.context('./', true, /\.spec\.ts$/); -// And load the modules. -context.keys().map(context); diff --git a/projects/native-browser/tsconfig.lib.json b/projects/native-browser/tsconfig.lib.json deleted file mode 100644 index 4b5d4af..0000000 --- a/projects/native-browser/tsconfig.lib.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "extends": "../../tsconfig.json", - "compilerOptions": { - "outDir": "../../out-tsc/lib", - "target": "es2015", - "declaration": true, - "inlineSources": true, - "types": [], - "lib": [ - "dom", - "es2018" - ] - }, - "angularCompilerOptions": { - "skipTemplateCodegen": true, - "strictMetadataEmit": true, - "enableResourceInlining": true - }, - "exclude": [ - "src/test.ts", - "**/*.spec.ts" - ] -} diff --git a/projects/native-browser/tsconfig.lib.prod.json b/projects/native-browser/tsconfig.lib.prod.json deleted file mode 100644 index cbae794..0000000 --- a/projects/native-browser/tsconfig.lib.prod.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "extends": "./tsconfig.lib.json", - "angularCompilerOptions": { - "enableIvy": false - } -} diff --git a/projects/native-browser/tsconfig.spec.json b/projects/native-browser/tsconfig.spec.json deleted file mode 100644 index 16da33d..0000000 --- a/projects/native-browser/tsconfig.spec.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "extends": "../../tsconfig.json", - "compilerOptions": { - "outDir": "../../out-tsc/spec", - "types": [ - "jasmine", - "node" - ] - }, - "files": [ - "src/test.ts" - ], - "include": [ - "**/*.spec.ts", - "**/*.d.ts" - ] -} diff --git a/projects/native-browser/tslint.json b/projects/native-browser/tslint.json deleted file mode 100644 index 124133f..0000000 --- a/projects/native-browser/tslint.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "extends": "../../tslint.json", - "rules": { - "directive-selector": [ - true, - "attribute", - "lib", - "camelCase" - ], - "component-selector": [ - true, - "element", - "lib", - "kebab-case" - ] - } -} diff --git a/projects/native/ng-package.json b/projects/native/ng-package.json index db18b23..c8eece6 100644 --- a/projects/native/ng-package.json +++ b/projects/native/ng-package.json @@ -2,6 +2,11 @@ "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", "dest": "../../dist/native", "lib": { - "entryFile": "src/public-api.ts" + "entryFile": "src/public-api.ts", + "umdModuleIds": { + "@ucap/core": "@ucap/core", + "@ucap/native": "@ucap/native", + "@ucap/ng-core": "@ucap/ng-core" + } } -} \ No newline at end of file +} diff --git a/projects/native/package.json b/projects/native/package.json index ca433b7..8d7f76e 100644 --- a/projects/native/package.json +++ b/projects/native/package.json @@ -1,13 +1,15 @@ { "name": "@ucap/ng-native", - "version": "0.0.1", + "version": "0.0.5", "publishConfig": { "registry": "https://nexus.loafle.net/repository/npm-ucap/" }, "peerDependencies": { "@angular/common": "^9.0.2", "@angular/core": "^9.0.2", + "@ucap/core": "~0.0.1", "@ucap/native": "~0.0.1", + "axios": "^0.19.2", "rxjs": "~6.5.4", "tslib": "^1.10.0" } diff --git a/projects/native-browser/src/lib/services/browser-native.service.spec.ts b/projects/native/src/lib/services/browser-native.service.spec.ts similarity index 100% rename from projects/native-browser/src/lib/services/browser-native.service.spec.ts rename to projects/native/src/lib/services/browser-native.service.spec.ts diff --git a/projects/native/src/lib/services/browser-native.service.ts b/projects/native/src/lib/services/browser-native.service.ts new file mode 100644 index 0000000..23bf113 --- /dev/null +++ b/projects/native/src/lib/services/browser-native.service.ts @@ -0,0 +1,666 @@ +import { Subject, BehaviorSubject } from 'rxjs'; + +import { AxiosInstance } from 'axios'; + +import { StatusCode, FileUtil } from '@ucap/core'; +import { + NativeService, + UpdateCheckConfig, + UpdateInfo, + NativeType, + WindowState, + WindowIdle, + NotificationRequest, + NotificationType +} from '@ucap/native'; + +import { NotificationService } from './notification.service'; + +export class BrowserNativeService extends NativeService { + // tslint:disable-next-line: variable-name + private _notificationService: NotificationService; + // tslint:disable-next-line: variable-name + private _idle_checker: WindowIdleChecker; + + constructor(private axios: AxiosInstance) { + super(); + + this._notificationService = new NotificationService(); + } + + platform_nativeType(): Promise { + return new Promise((resolve, reject) => { + try { + resolve(NativeType.Browser); + } catch (error) { + reject(error); + } + }); + } + platform_networkInfo(): Promise { + return new Promise((resolve, reject) => { + try { + resolve([{ ip: 'Browser', mac: 'browser' }]); + } catch (error) { + reject(error); + } + }); + } + platform_execute(executableName: string): Promise { + return new Promise((resolve, reject) => { + try { + resolve(-1); + } catch (error) { + reject(error); + } + }); + } + platform_openDefaultBrowser( + url: string, + options?: { + name?: string; + features?: string; + replace?: boolean; + } + ): Promise { + return new Promise((resolve, reject) => { + try { + const name = !!options && !!options.name ? options.name : null; + const features = + !!options && !!options.features ? options.features : null; + window.open(url, name, features); + resolve(); + } catch (error) { + reject(error); + } + }); + } + platform_readFromClipboard(): Promise<{ + text?: string; + rtf?: string; + html?: string; + image?: Buffer; + imageDataUrl?: string; + }> { + return new Promise<{ + text?: string; + rtf?: string; + html?: string; + image?: Buffer; + imageDataUrl?: string; + }>((resolve, reject) => { + try { + navigator.permissions + .query({ name: 'clipboard-read' as PermissionName }) + .then((result) => { + if ('granted' === result.state || 'prompt' === result.state) { + navigator.clipboard + .readText() + .then((value) => { + resolve({ text: value }); + }) + .catch((reason) => { + reject(reason); + }); + } + }) + .catch((reason) => { + reject(reason); + }); + } catch (error) { + reject(error); + } + }); + } + + file_save( + buffer: Buffer, + fileName: string, + mimeType: string, + path?: string + ): Promise { + return new Promise((resolve, reject) => { + try { + FileUtil.save(buffer, fileName, mimeType) + .then((fn) => { + resolve(fn); + }) + .catch((reason) => { + reject(reason); + }); + } catch (error) { + reject(error); + } + }); + } + file_read(path: string): Promise { + return new Promise((resolve, reject) => { + try { + this.axios + .get(path, { responseType: 'arraybuffer' }) + .then((res) => { + resolve(Buffer.from(res.data)); + }) + .catch((reason) => reject(reason)); + } catch (error) { + reject(error); + } + }); + } + file_openFolder(folderPath?: string, make?: boolean): Promise { + return new Promise((resolve, reject) => { + try { + resolve(false); + } catch (error) { + reject(error); + } + }); + } + file_openItem(filePath?: string): Promise { + return new Promise((resolve, reject) => { + try { + resolve(false); + } catch (error) { + reject(error); + } + }); + } + file_path( + name: 'home' | 'documents' | 'downloads' | 'music' | 'pictures' | 'videos', + ...appendPaths: string[] + ): Promise { + return new Promise((resolve, reject) => { + try { + resolve(undefined); + } catch (error) { + reject(error); + } + }); + } + file_selectForOpen(option: { + title?: string; + defaultPath?: string; + filters?: { + extensions: string[]; + name: string; + }[]; + properties?: Array< + | 'openFile' + | 'openDirectory' + | 'multiSelections' + | 'showHiddenFiles' + | 'createDirectory' + | 'promptToCreate' + | 'noResolveAliases' + | 'treatPackageAsDirectory' + >; + message?: string; + }): Promise { + return new Promise((resolve, reject) => { + try { + resolve(undefined); + } catch (error) { + reject(error); + } + }); + } + file_selectForSave(option: { + title?: string; + defaultPath?: string; + filters?: { + extensions: string[]; + name: string; + }[]; + message?: string; + }): Promise<{ + canceled: boolean; + filePath: string; + }> { + return new Promise<{ + canceled: boolean; + filePath: string; + }>((resolve, reject) => { + try { + resolve(undefined); + } catch (error) { + reject(error); + } + }); + } + + window_onState$(): BehaviorSubject { + return super.window_onState$(); + } + window_onFocus$(): BehaviorSubject { + if (!this._window_onFocusSubject) { + // tslint:disable-next-line: variable-name + const __this = this; + const onFocus = (event: Event) => { + __this._window_onFocusSubject.next(true); + }; + const onBlur = (event: Event) => { + __this._window_onFocusSubject.next(false); + }; + window.addEventListener('focus', onFocus); + window.addEventListener('blur', onBlur); + + super.window_onFocus$().subscribe( + (focus) => {}, + (error) => {}, + () => { + window.removeEventListener('focus', onFocus); + window.removeEventListener('blur', onBlur); + } + ); + } + + return super.window_onFocus$(); + } + window_close(): Promise { + return new Promise((resolve, reject) => { + try { + resolve(); + } catch (error) { + reject(error); + } + }); + } + window_minimize(): Promise { + return new Promise((resolve, reject) => { + try { + resolve(); + } catch (error) { + reject(error); + } + }); + } + window_maximize(): Promise { + return new Promise((resolve, reject) => { + try { + resolve(); + } catch (error) { + reject(error); + } + }); + } + + idle_startCheck(limitTime: number): Promise { + return new Promise((resolve, reject) => { + try { + if (!!this._idle_onStateSubject) { + return; + } + + this._idle_checker = new WindowIdleChecker({ + limitTime, + onIdle: () => { + if (!!this._idle_onStateSubject) { + this._idle_onStateSubject.next(WindowIdle.Idle); + } + }, + onActive: () => { + if (!!this._idle_onStateSubject) { + this._idle_onStateSubject.next(WindowIdle.Active); + } + }, + onHide: () => { + if (!!this._idle_onStateSubject) { + this._idle_onStateSubject.next(WindowIdle.Idle); + } + }, + onShow: () => { + if (!!this._idle_onStateSubject) { + this._idle_onStateSubject.next(WindowIdle.Active); + } + } + }); + this._idle_checker.start(); + + resolve(); + } catch (error) { + reject(error); + } + }); + } + idle_onState$(): BehaviorSubject { + return super.idle_onState$(); + } + idle_stopCheck(): Promise { + return new Promise((resolve, reject) => { + try { + if (!!this._idle_checker) { + this._idle_checker.stop(); + this._idle_checker = undefined; + } + if (!!this._idle_onStateSubject) { + this._idle_onStateSubject.complete(); + this._idle_onStateSubject = undefined; + } + + resolve(); + } catch (error) { + reject(error); + } + }); + } + idle_changeLimitTime(limitTime: number): Promise { + return new Promise((resolve, reject) => { + try { + if (!this._idle_checker) { + return; + } + this._idle_checker.changeLimitTime(limitTime); + + resolve(); + } catch (error) { + reject(error); + } + }); + } + + app_version(): Promise { + return new Promise((resolve, reject) => { + try { + resolve(''); + } catch (error) { + reject(error); + } + }); + } + app_postInit(): Promise { + return new Promise((resolve, reject) => { + try { + resolve(); + } catch (error) { + reject(error); + } + }); + } + app_postLogin(): Promise { + return new Promise((resolve, reject) => { + try { + this._notificationService.requestPermission(); + + resolve(); + } catch (error) { + reject(error); + } + }); + } + app_postLogout(): Promise { + return new Promise((resolve, reject) => { + try { + resolve(); + } catch (error) { + reject(error); + } + }); + } + app_postDestroy(): Promise { + return new Promise((resolve, reject) => { + try { + resolve(); + } catch (error) { + reject(error); + } + }); + } + app_changeAutoLaunch(autoLaunch: boolean): Promise { + return new Promise((resolve, reject) => { + try { + resolve(true); + } catch (error) { + reject(error); + } + }); + } + app_showNotify(req: NotificationRequest): Promise { + return new Promise((resolve, reject) => { + try { + this._notificationService.notify(req, () => { + window.focus(); + + switch (req.type) { + case NotificationType.Event: + if (!!this._chat_onOpenSubject) { + this._chat_onOpenSubject.next(req.seq); + } + break; + case NotificationType.Message: + if (!!this._message_onOpenSubject) { + this._message_onOpenSubject.next(req.seq); + } + break; + default: + break; + } + }); + + resolve(); + } catch (error) { + reject(error); + } + }); + } + app_closeAllNotify(): Promise { + return new Promise((resolve, reject) => { + try { + resolve(); + } catch (error) { + reject(error); + } + }); + } + app_checkForUpdates(currentVersion: string): Promise { + return new Promise((resolve, reject) => { + try { + resolve(); + } catch (error) { + reject(error); + } + }); + } + app_startCheckForUpdate(config: UpdateCheckConfig): Promise { + return new Promise((resolve, reject) => { + try { + resolve(); + } catch (error) { + reject(error); + } + }); + } + app_onUpdate$(): BehaviorSubject { + return super.app_onUpdate$(); + } + app_stopCheckForUpdate(): Promise { + return new Promise((resolve, reject) => { + try { + resolve(); + } catch (error) { + reject(error); + } + }); + } + app_applyInstantUpdates(): Promise { + return new Promise((resolve, reject) => { + try { + resolve(); + } catch (error) { + reject(error); + } + }); + } + app_exit(): Promise { + return new Promise((resolve, reject) => { + try { + window.close(); + resolve(); + } catch (error) { + reject(error); + } + }); + } + + app_onLogout$(): Subject { + return super.app_onLogout$(); + } + app_onStatus$(): BehaviorSubject { + return super.app_onStatus$(); + } + app_onShowSetting$(): Subject { + return super.app_onShowSetting$(); + } + + chat_onOpen$(): BehaviorSubject { + return super.chat_onOpen$(); + } + + message_onOpen$(): BehaviorSubject { + return super.message_onOpen$(); + } +} + +interface WindowIdleCheckerConfig { + limitTime?: number; + events?: string[]; + onIdle?: () => void; + onActive?: () => void; + visibilityEvents?: string[]; + onHide?: () => void; + onShow?: () => void; +} + +const defaultWindowIdleCheckerConfig: WindowIdleCheckerConfig = { + limitTime: 5 * 60 * 1000, + events: ['mousemove', 'keydown', 'mousedown', 'touchstart'], + onIdle: () => {}, + onActive: () => {}, + onHide: () => {}, + onShow: () => {}, + visibilityEvents: [ + 'visibilitychange', + 'webkitvisibilitychange', + 'mozvisibilitychange', + 'msvisibilitychange' + ] +}; + +const WEBKIT_HIDDEN = 'webkitHidden'; +const MOZ_HIDDEN = 'mozHidden'; +const MS_HIDDEN = 'msHidden'; + +class WindowIdleChecker { + // tslint:disable-next-line: variable-name + private _timerId: any; + // tslint:disable-next-line: variable-name + private _status: WindowIdle; + // tslint:disable-next-line: variable-name + private _visible: boolean; + // tslint:disable-next-line: variable-name + private _started: boolean; + + constructor( + // tslint:disable-next-line: variable-name + private _config: WindowIdleCheckerConfig + ) { + this._config = Object.assign({}, defaultWindowIdleCheckerConfig, _config); + this._status = WindowIdle.Active; + this._visible = true; + } + + start() { + if (!!this._timerId) { + clearInterval(this._timerId); + } + + this._timerId = setInterval(() => { + if (WindowIdle.Active === this._status) { + this._status = WindowIdle.Idle; + this._config.onIdle(); + } + }, this._config.limitTime); + + if (!this._started) { + this._started = true; + } + + this._addEventListerner(this._config.events, this._onActive); + + if (!!this._config.onShow || !!this._config.onHide) { + this._addEventListerner( + this._config.visibilityEvents, + this._onVisibility + ); + } + } + + stop() { + if (!!this._timerId) { + clearInterval(this._timerId); + this._timerId = undefined; + } + + if (!!this._started) { + this._removeEventListerner(this._config.events, this._onActive); + + if (!!this._config.onShow || !!this._config.onHide) { + this._removeEventListerner( + this._config.visibilityEvents, + this._onVisibility + ); + } + } + } + + changeLimitTime(limitTime: number) { + this._config = { + ...this._config, + limitTime + }; + this.start(); + } + + private _onActive(event: Event) { + if (WindowIdle.Idle === this._status) { + this._status = WindowIdle.Active; + this._config.onActive(); + } + } + + private _onVisibility(event: Event) { + if ( + !!document.hidden || + !!document[WEBKIT_HIDDEN] || + !!document[MOZ_HIDDEN] || + !!document[MS_HIDDEN] + ) { + if (this._visible) { + this._visible = false; + this._config.onHide(); + } + } else { + if (!this._visible) { + this._visible = true; + this._config.onShow(); + } + } + } + + private _addEventListerner( + events: string[], + callback: (event: Event) => void + ) { + events.forEach((e) => { + window.addEventListener(e, callback.bind(this)); + }); + } + + private _removeEventListerner( + events: string[], + callback: (event: Event) => void + ) { + events.forEach((e) => { + window.removeEventListener(e, callback.bind(this)); + }); + } +} diff --git a/projects/native/src/lib/services/notification.service.ts b/projects/native/src/lib/services/notification.service.ts new file mode 100644 index 0000000..2713e46 --- /dev/null +++ b/projects/native/src/lib/services/notification.service.ts @@ -0,0 +1,44 @@ +import { NotificationRequest } from '@ucap/native'; + +export class NotificationService { + notificationPermission: NotificationPermission; + + constructor() { + this.notificationPermission = this.isSupported() ? 'default' : 'denied'; + } + + public isSupported(): boolean { + return 'Notification' in window; + } + + requestPermission(): void { + const self = this; + if ('Notification' in window) { + Notification.requestPermission().then((result) => { + self.notificationPermission = result; + }); + } + } + + notify(noti: NotificationRequest, click?: () => void) { + if (!this.isSupported()) { + return; + } + const notification = new Notification(noti.title, { + body: noti.contents, + icon: noti.image || 'assets/images/img_nophoto_50.png' + }); + notification.onclick = (e) => { + console.log('notification.onclick'); + if (!!click) { + click(); + } + }; + notification.onclose = (e) => { + console.log('notification.onclose'); + }; + notification.onerror = (e) => { + console.log('notification.onerror'); + }; + } +} diff --git a/projects/native/src/public-api.ts b/projects/native/src/public-api.ts index 781a13d..b467b8e 100644 --- a/projects/native/src/public-api.ts +++ b/projects/native/src/public-api.ts @@ -2,4 +2,7 @@ * Public API Surface of native */ +export * from './lib/services/notification.service'; +export * from './lib/services/browser-native.service'; + export * from './lib/types/token'; diff --git a/projects/store-authentication/ng-package.json b/projects/store-authentication/ng-package.json index b791a0d..18c565a 100644 --- a/projects/store-authentication/ng-package.json +++ b/projects/store-authentication/ng-package.json @@ -18,7 +18,8 @@ "@ucap/ng-protocol-authentication": "@ucap/ng-protocol-authentication", "@ucap/ng-protocol-info": "@ucap/ng-protocol-info", "@ucap/ng-protocol-option": "@ucap/ng-protocol-option", - "@ucap/ng-protocol-query": "@ucap/ng-protocol-query" + "@ucap/ng-protocol-query": "@ucap/ng-protocol-query", + "@ucap/ng-store-organization": "@ucap/ng-store-organization" } } } diff --git a/projects/store-authentication/package.json b/projects/store-authentication/package.json index 0da71fe..577275c 100644 --- a/projects/store-authentication/package.json +++ b/projects/store-authentication/package.json @@ -1,6 +1,6 @@ { "name": "@ucap/ng-store-authentication", - "version": "0.0.11", + "version": "0.0.14", "publishConfig": { "registry": "https://nexus.loafle.net/repository/npm-ucap/" }, @@ -23,6 +23,7 @@ "@ucap/ng-protocol-option": "~0.0.1", "@ucap/ng-protocol-query": "~0.0.1", "@ucap/ng-protocol-info": "~0.0.1", + "@ucap/ng-store-organization": "~0.0.1", "rxjs": "~6.5.4", "tslib": "^1.10.0" } diff --git a/projects/store-authentication/src/lib/store/login/actions.ts b/projects/store-authentication/src/lib/store/login/actions.ts index 40024cc..c8c9078 100644 --- a/projects/store-authentication/src/lib/store/login/actions.ts +++ b/projects/store-authentication/src/lib/store/login/actions.ts @@ -7,7 +7,6 @@ import { LoginResponse, LogoutResponse } from '@ucap/protocol-authentication'; -import { UserResponse, UserRequest } from '@ucap/protocol-info'; /** * request of web login @@ -107,29 +106,3 @@ export const sessionDestroyed = createAction( '[ucap::authentication::login] session Destroyed', props<{ error: any }>() ); - -/** - * info user request - */ -export const infoUser = createAction( - '[ucap::authentication::login] Info User', - props<{ req: UserRequest }>() -); - -/** - * Success of info user request - */ -export const infoUserSuccess = createAction( - '[ucap::authentication::login] Info User Success', - props<{ - res: UserResponse; - }>() -); - -/** - * Failure of info user request - */ -export const infoUserFailure = createAction( - '[ucap::authentication::login] Info User Failure', - props<{ error: any }>() -); diff --git a/projects/store-authentication/src/lib/store/login/effects.ts b/projects/store-authentication/src/lib/store/login/effects.ts index ca404d7..ae5169a 100644 --- a/projects/store-authentication/src/lib/store/login/effects.ts +++ b/projects/store-authentication/src/lib/store/login/effects.ts @@ -1,22 +1,16 @@ import { of } from 'rxjs'; -import { catchError, map, switchMap, exhaustMap } from 'rxjs/operators'; +import { catchError, map, switchMap, tap } from 'rxjs/operators'; import { Injectable } from '@angular/core'; +import { Store } from '@ngrx/store'; import { Actions, ofType, createEffect } from '@ngrx/effects'; -import { PiService } from '@ucap/ng-pi'; import { AuthenticationProtocolService } from '@ucap/ng-protocol-authentication'; -import { InfoProtocolService } from '@ucap/ng-protocol-info'; -import { - logout, - logoutSuccess, - infoUser, - infoUserSuccess, - infoUserFailure -} from './actions'; -import { UserResponse } from '@ucap/protocol-info'; +import { UserActions } from '@ucap/ng-store-organization'; + +import { logout, logoutSuccess, loginSuccess } from './actions'; @Injectable() export class Effects { @@ -32,27 +26,37 @@ export class Effects { ); }); - infoUser$ = createEffect(() => - this.actions$.pipe( - ofType(infoUser), - map((action) => action.req), - exhaustMap((req) => - this.infoProtocolService.user(req).pipe( - map((res: UserResponse) => { - return infoUserSuccess({ - res - }); - }), - catchError((error) => of(infoUserFailure({ error }))) - ) - ) - ) + loginSuccessForOrganizationUserInit$ = createEffect( + () => + this.actions$.pipe( + ofType(loginSuccess), + map((params) => params.res), + tap((loginRes) => { + this.store.dispatch( + UserActions.init({ + user: { + info: loginRes.userInfo, + companyCode: loginRes.companyCode, + departmentCode: loginRes.departmentCode, + statusMessage1: loginRes.statusMessage1, + statusMessage2: loginRes.statusMessage2, + statusMessage3: loginRes.statusMessage3, + madn: loginRes.madn, + hardPhoneSadn: loginRes.hardPhoneSadn, + fmcSadn: loginRes.fmcSadn, + pbxIndex: loginRes.pbxIndex, + talkWithMeBotSeq: loginRes.talkWithMeBotSeq + } + }) + ); + }) + ), + { dispatch: false } ); constructor( private actions$: Actions, - private piService: PiService, - private authenticationProtocolService: AuthenticationProtocolService, - private infoProtocolService: InfoProtocolService + private store: Store, + private authenticationProtocolService: AuthenticationProtocolService ) {} } diff --git a/projects/store-authentication/src/lib/store/login/reducers.ts b/projects/store-authentication/src/lib/store/login/reducers.ts index 1f12c05..aee7688 100644 --- a/projects/store-authentication/src/lib/store/login/reducers.ts +++ b/projects/store-authentication/src/lib/store/login/reducers.ts @@ -1,8 +1,7 @@ import { createReducer, on } from '@ngrx/store'; import { initialState } from './state'; -import { loginSuccess, logoutSuccess, infoUserSuccess } from './actions'; -import { UserInfoUpdateType } from '@ucap/protocol-info'; +import { loginSuccess, logoutSuccess } from './actions'; export const reducer = createReducer( initialState, @@ -17,41 +16,5 @@ export const reducer = createReducer( return { ...initialState }; - }), - - on(infoUserSuccess, (state, action) => { - let loginRes = { - ...state.loginRes - }; - - switch (action.res.type) { - case UserInfoUpdateType.Image: - loginRes = { - ...loginRes - }; - break; - case UserInfoUpdateType.Intro: - loginRes = { - ...loginRes, - userInfo: { - ...loginRes.userInfo, - intro: action.res.info - } - }; - break; - case UserInfoUpdateType.TelephoneVisible: - loginRes = { - ...loginRes - }; - break; - default: - break; - } - return { - ...state, - loginRes: { - ...loginRes - } - }; }) ); diff --git a/projects/store-chat/ng-package.json b/projects/store-chat/ng-package.json index 9b1f32c..be06674 100644 --- a/projects/store-chat/ng-package.json +++ b/projects/store-chat/ng-package.json @@ -8,13 +8,20 @@ "@ngrx/store": "@ngrx/store", "@ngrx/entity": "@ngrx/entity", "@ngrx/effects": "@ngrx/effects", + "@ucap/api": "@ucap/api", "@ucap/pi": "@ucap/pi", "@ucap/protocol-event": "@ucap/protocol-event", "@ucap/protocol-file": "@ucap/protocol-file", + "@ucap/protocol-info": "@ucap/protocol-info", "@ucap/protocol-room": "@ucap/protocol-room", "@ucap/protocol-sync": "@ucap/protocol-sync", + "@ucap/ng-i18n": "@ucap/ng-i18n", + "@ucap/ng-api-common": "@ucap/ng-api-common", "@ucap/ng-protocol-room": "@ucap/ng-protocol-room", + "@ucap/ng-protocol-event": "@ucap/ng-protocol-event", + "@ucap/ng-protocol-file": "@ucap/ng-protocol-file", "@ucap/ng-protocol-sync": "@ucap/ng-protocol-sync", + "@ucap/ng-store-organization": "@ucap/ng-store-organization", "@ucap/ng-store-authentication": "@ucap/ng-store-authentication" } } diff --git a/projects/store-chat/package.json b/projects/store-chat/package.json index 756c875..10f2c79 100644 --- a/projects/store-chat/package.json +++ b/projects/store-chat/package.json @@ -1,6 +1,6 @@ { "name": "@ucap/ng-store-chat", - "version": "0.0.19", + "version": "0.0.66", "publishConfig": { "registry": "https://nexus.loafle.net/repository/npm-ucap/" }, diff --git a/projects/store-chat/src/lib/store/chatting/actions.ts b/projects/store-chat/src/lib/store/chatting/actions.ts index d98a703..2abe134 100644 --- a/projects/store-chat/src/lib/store/chatting/actions.ts +++ b/projects/store-chat/src/lib/store/chatting/actions.ts @@ -1,5 +1,7 @@ import { createAction, props } from '@ngrx/store'; +import { DeviceType } from '@ucap/core'; + import { Info, InfoRequest as EventInfoRequest, @@ -16,14 +18,17 @@ import { CancelNotification, DelNotification, EventJson, - ReadResponse + ReadResponse, + SendRequest } from '@ucap/protocol-event'; import { InfoRequest as FileInfoRequest, InfoResponse as FileInfoResponse, FileDownloadInfo, - FileInfo + FileInfo, + DownCheckRequest, + DownCheckResponse } from '@ucap/protocol-file'; /** @@ -51,6 +56,13 @@ export const eventsFailure = createAction( '[ucap::chat::chatting] events Failure', props<{ roomId: string; error: any }>() ); +/** + * retrieve list of event + */ +export const moreEvents = createAction( + '[ucap::chat::chatting] events more', + props<{ roomId: string }>() +); /** * retrieve list of file information @@ -78,6 +90,19 @@ export const fileInfosFailure = createAction( props<{ roomId: string; error: any }>() ); +export const fileDownCheck = createAction( + '[ucap::chat::chatting] fileDownCheck', + props<{ req: DownCheckRequest }>() +); +export const fileDownCheckSuccess = createAction( + '[ucap::chat::chatting] fileDownCheck Success', + props<{ res: DownCheckResponse }>() +); +export const fileDownCheckFailure = createAction( + '[ucap::chat::chatting] fileDownCheck Failure', + props<{ error: any }>() +); + /** * add new event */ @@ -143,3 +168,109 @@ export const sendNotification = createAction( '[ucap::chat::chatting] Send Notification', props<{ noti: SendNotification }>() ); + +/** 대화 삭제 */ +export const del = createAction( + '[ucap::chat::chatting] Delete', + props() +); +export const delFailure = createAction( + '[ucap::chat::chatting] Delete Failure', + props<{ error: any }>() +); +export const delNotification = createAction( + '[ucap::chat::chatting] Delete Notification || Response', + props<{ noti: DelNotification | DelResponse }>() +); +/** 대화 삭제시 열린 대화방의 대화 내용 갱신 */ +export const delEventList = createAction( + '[ucap::chat::chatting] Delete InfoList', + props<{ + roomId: string; + eventSeqs: number[]; + }>() +); + +/** Clear event for TimerRoom */ +export const intervalClearEvent = createAction( + '[ucap::chat::chatting] Clear events interval', + props<{ + roomId: string; + }>() +); + +/** forward */ +export const forward = createAction( + '[ucap::chat::chatting] Forward', + props<{ + senderSeq: string; + deviceType: DeviceType; + req: SendRequest; + trgtUserSeqs?: string[]; + trgtRoomId?: string; + }>() +); +/** chat forward failure */ +export const forwardFailure = createAction( + '[ucap::chat::chatting] Forward failure', + props<{ error: any }>() +); + +export const forwardAfterRoomOpen = createAction( + '[ucap::chat::chatting] Forward after room open', + props<{ + senderSeq: string; + deviceType: DeviceType; + req: SendRequest; + trgtUserSeqs?: string[]; + trgtRoomId?: string; + }>() +); + +export const roomOpenAfterForward = createAction( + '[ucap::chat::chatting] Room open after forward', + props<{ + senderSeq: string; + req: SendRequest; + trgtUserSeqs?: string[]; + trgtRoomId?: string; + }>() +); + +export const forwarForFileEvent = createAction( + '[ucap::chat::chatting] Forward for file event', + props<{ + forwardType: string; + deviceType: DeviceType; + trgtRoomId?: string; + sendReq: SendRequest; + }>() +); + +/** 대화 회수 */ +export const cancel = createAction( + '[ucap::chat::chatting] Cancel', + props() +); +export const cancelFailure = createAction( + '[ucap::chat::chatting] Cancel Failure', + props<{ error: any }>() +); +export const cancelNotification = createAction( + '[ucap::chat::chatting] Cancel Notification || Response', + props<{ noti: CancelNotification | CancelResponse }>() +); +/** 대화 회수시 열린 대화방의 대화 내용 갱신 */ +export const updateEventList = createAction( + '[ucap::chat::chatting] Update InfoList', + props<{ + roomId: string; + eventSeq: number; + sentMessage: string; + }>() +); + +export const clearActiveRoomId = createAction( + '[ucap::chat::chatting] Clear activeRoomId', + props() +); diff --git a/projects/store-chat/src/lib/store/chatting/effects.ts b/projects/store-chat/src/lib/store/chatting/effects.ts index 405b5f1..5c5bb1a 100644 --- a/projects/store-chat/src/lib/store/chatting/effects.ts +++ b/projects/store-chat/src/lib/store/chatting/effects.ts @@ -7,7 +7,9 @@ import { exhaustMap, concatMap, withLatestFrom, - debounceTime + debounceTime, + mergeMap, + take } from 'rxjs/operators'; import { Injectable, Inject } from '@angular/core'; @@ -16,11 +18,46 @@ import { Store, select } from '@ngrx/store'; import { Actions, createEffect, ofType } from '@ngrx/effects'; import { Dictionary } from '@ngrx/entity'; +import { + RoomType, + OpenResponse as CreateResponse, + Open3Response as CreateTimerResponse, + ExitResponse as DeleteResponse, + ExitAllResponse as DeleteMultiResponse +} from '@ucap/protocol-room'; +import { + InfoRequest, + ReadResponse, + FileType, + EventType, + DelResponse, + SendResponse, + CancelResponse, + FileEventJson, + decodeFileEventJson, + Info, + EventJson +} from '@ucap/protocol-event'; + +import moment from 'moment'; + +import { LocaleCode } from '@ucap/core'; +import { StatusCode } from '@ucap/api'; +import { FileTalkShareRequest, FileTalkShareResponse } from '@ucap/api-common'; + +import { I18nService } from '@ucap/ng-i18n'; +import { CommonApiService } from '@ucap/ng-api-common'; import { EventProtocolService } from '@ucap/ng-protocol-event'; import { RoomProtocolService } from '@ucap/ng-protocol-room'; import { FileProtocolService } from '@ucap/ng-protocol-file'; +import { UserSelector } from '@ucap/ng-store-organization'; +import { LoginSelector } from '@ucap/ng-store-authentication'; +import { ModuleConfig } from '../../config/module-config'; +import { _MODULE_CONFIG } from '../../config/token'; +import { ChattingSelector, RoomSelector } from '../state'; import * as RoomActions from '../room/actions'; + import { events, eventsFailure, @@ -35,18 +72,26 @@ import { sendSuccess, sendFailure, addEvent, - addEventSuccess + addEventSuccess, + del, + delNotification, + delFailure, + delEventList, + moreEvents, + forward, + forwardFailure, + forwardAfterRoomOpen, + roomOpenAfterForward, + forwarForFileEvent, + cancel, + cancelFailure, + cancelNotification, + updateEventList, + intervalClearEvent, + fileDownCheck, + fileDownCheckSuccess, + fileDownCheckFailure } from './actions'; - -import { - InfoRequest, - ReadResponse, - FileType, - EventType -} from '@ucap/protocol-event'; - -import { ModuleConfig } from '../../config/module-config'; -import { _MODULE_CONFIG } from '../../config/token'; import { Chatting } from './state'; @Injectable() @@ -110,6 +155,40 @@ export class Effects { { dispatch: false } ); + moreEvents$ = createEffect( + () => { + return this.actions$.pipe( + ofType(moreEvents), + mergeMap( + (action) => + of(action).pipe( + withLatestFrom( + this.store.pipe( + select(ChattingSelector.eventList, action.roomId) + ) + ) + ), + (action, latestStoreData) => latestStoreData + ), + tap(([req, eventList]) => { + if (!!eventList && eventList.length > 0) { + this.store.dispatch( + events({ + req: { + roomId: req.roomId, + baseSeq: eventList.sort((a, b) => a.seq - b.seq)[0].seq, + requestCount: + this.moduleConfig?.eventRequestDefaultCount || 50 + } as InfoRequest + }) + ); + } + }) + ); + }, + { dispatch: false } + ); + read$ = createEffect( () => { return this.actions$.pipe( @@ -117,7 +196,14 @@ export class Effects { exhaustMap((req) => this.eventProtocolService.read(req).pipe( map((res: ReadResponse) => { + // room user lastReadEventSeq reset. this.store.dispatch(readSuccess(res)); + // room noReadCount reset. + this.store.dispatch( + RoomActions.updateUnreadCount({ + roomId: res.roomId + }) + ); }), catchError((error) => of(readFailure({ error }))) ) @@ -152,6 +238,23 @@ export class Effects { { dispatch: false } ); + fileDownCheck$ = createEffect( + () => { + return this.actions$.pipe( + ofType(fileDownCheck), + switchMap((action) => { + return this.fileProtocolService.downCheck(action.req).pipe( + map((res) => { + this.store.dispatch(fileDownCheckSuccess({ res })); + }), + catchError((error) => of(fileDownCheckFailure({ error }))) + ); + }) + ); + }, + { dispatch: false } + ); + addEvent$ = createEffect( () => { return this.actions$.pipe( @@ -234,6 +337,352 @@ export class Effects { { dispatch: false } ); + del$ = createEffect(() => + this.actions$.pipe( + ofType(del), + exhaustMap((req) => + this.eventProtocolService.del(req).pipe( + map((res: DelResponse) => { + return delNotification({ noti: res }); + }), + catchError((error) => of(delFailure({ error }))) + ) + ) + ) + ); + + delNotification$ = createEffect( + () => { + return this.actions$.pipe( + ofType(delNotification), + map((action) => action.noti), + withLatestFrom(this.store.pipe(select(RoomSelector.rooms))), + tap(([noti, rooms]) => { + const notiRoomId = noti.roomId; + + if (!!rooms && rooms.length > 0) { + // 현재 방이 오픈되어 있으면 방내용 갱신 + const idx = rooms.findIndex( + (roomInfo) => roomInfo.roomId === notiRoomId + ); + + if (idx > -1) { + this.store.dispatch( + delEventList({ roomId: notiRoomId, eventSeqs: [noti.eventSeq] }) + ); + } + } + + // 대화 > 리스트의 항목 갱신 + this.store.dispatch( + RoomActions.room({ + req: { + roomId: notiRoomId, + isDetail: false, + localeCode: this.i18nService.currentLng.toUpperCase() as LocaleCode + } + }) + ); + }) + ); + }, + { dispatch: false } + ); + + intervalClearEvent$ = createEffect( + () => { + return this.actions$.pipe( + ofType(intervalClearEvent), + withLatestFrom( + this.store.pipe(select(RoomSelector.rooms)), + this.store.pipe(select(ChattingSelector.chattings)) + ), + tap(([action, rooms, chattings]) => { + const roomId = action.roomId; + if ( + !!rooms && + rooms.length > 0 && + !!chattings && + chattings.length > 0 + ) { + const roomInfo = rooms.find((item) => item.roomId === roomId); + const chatting = chattings.find((item) => item.roomId === roomId); + + if ( + !!roomInfo && + !!roomInfo.timeRoomInterval && + !!chatting && + !!chatting.eventList + ) { + const eventList = chatting.eventList; + const delEventSeq: number[] = []; + + eventList.ids.forEach((id) => { + const event: Info = eventList.entities[id]; + + if ( + !event || + event.type === EventType.NotificationForTimerRoom + ) { + //ignore.. + } else { + if ( + new Date().getTime() - + moment(event.sendDate).toDate().getTime() >= + roomInfo.timeRoomInterval * 1000 + ) { + delEventSeq.push(event.seq); + } + } + }); + + if (delEventSeq.length > 0) { + this.store.dispatch( + delEventList({ + roomId, + eventSeqs: delEventSeq + }) + ); + } + } + } + }) + ); + }, + { dispatch: false } + ); + + forward$ = createEffect( + () => { + return this.actions$.pipe( + ofType(forward), + tap((action) => { + if (!!action.trgtRoomId) { + // 대화전달 후 방오픈. Exist roomSeq. + if (action.req.eventType === EventType.File) { + // file share request action + this.store.dispatch( + forwarForFileEvent({ + forwardType: 'F', + deviceType: action.deviceType, + sendReq: action.req, + trgtRoomId: action.trgtRoomId + }) + ); + } else { + this.store.dispatch(roomOpenAfterForward(action)); + } + } else if (!!action.trgtUserSeqs && action.trgtUserSeqs.length > 0) { + // 방오픈 후 대화전달. + this.store.dispatch(forwardAfterRoomOpen(action)); + } + }) + ); + }, + { dispatch: false } + ); + + forwarForFileEvent$ = createEffect( + () => { + return this.actions$.pipe( + ofType(forwarForFileEvent), + withLatestFrom( + this.store.pipe(select(UserSelector.user)), + this.store.pipe(select(LoginSelector.loginRes)) + ), + tap(([actionReq, user, loginRes]) => { + const fileEventJson: FileEventJson = decodeFileEventJson( + actionReq.sendReq.sentMessage + ); + + const req: FileTalkShareRequest = { + userSeq: String(user.info.seq), + deviceType: actionReq.deviceType, + token: loginRes.tokenString, + attachmentsSeq: fileEventJson.attachmentSeq.toString(), + roomId: actionReq.trgtRoomId, + synapKey: '' + }; + + return this.commonApiService + .fileTalkShare(req) + .pipe( + take(1), + map((res: FileTalkShareResponse) => { + const sedRequest = { + senderSeq: String(user.info.seq), + req: { + roomId: actionReq.trgtRoomId, + eventType: actionReq.sendReq.eventType, + sentMessage: res.returnJson + } + }; + if (actionReq.forwardType === 'O') { + if (res.statusCode === StatusCode.Success) { + this.store.dispatch(send(sedRequest)); + } + } else if (actionReq.forwardType === 'F') { + if (res.statusCode === StatusCode.Success) { + this.store.dispatch( + roomOpenAfterForward({ + ...sedRequest, + trgtRoomId: actionReq.trgtRoomId + }) + ); + } + } + }), + catchError((error) => of(forwardFailure({ error }))) + ) + .subscribe(); + }) + ); + }, + { dispatch: false } + ); + + forwardAfterRoomOpen$ = createEffect( + () => { + let createRes: CreateResponse; + return this.actions$.pipe( + ofType(forwardAfterRoomOpen), + exhaustMap((actionReq) => { + return this.roomProtocolService + .open({ divCd: 'forwardOpen', userSeqs: actionReq.trgtUserSeqs }) + .pipe( + map((res: CreateResponse) => { + createRes = res; + this.store.dispatch( + RoomActions.createRoomSuccess({ res, isForward: true }) + ); + }), + map(() => { + if (actionReq.req.eventType === EventType.File) { + // file share request action + this.store.dispatch( + forwarForFileEvent({ + forwardType: 'O', + deviceType: actionReq.deviceType, + sendReq: actionReq.req, + trgtRoomId: createRes.roomId + }) + ); + } else { + this.store.dispatch( + send({ + senderSeq: actionReq.senderSeq, + req: { + roomId: createRes.roomId, + eventType: actionReq.req.eventType, + sentMessage: actionReq.req.sentMessage + } + }) + ); + } + }), + catchError((error) => + of(RoomActions.createRoomFailure({ error })) + ) + ); + }) + ); + }, + { dispatch: false } + ); + + roomOpenAfterForward$ = createEffect( + () => { + return this.actions$.pipe( + ofType(roomOpenAfterForward), + exhaustMap((action) => { + return this.eventProtocolService + .send({ + roomId: action.trgtRoomId, + eventType: action.req.eventType, + sentMessage: action.req.sentMessage + }) + .pipe( + map((res: SendResponse) => { + this.store.dispatch( + addEvent({ + roomId: res.roomId, + info: res.info, + SVC_TYPE: res.SVC_TYPE, + SSVC_TYPE: res.SSVC_TYPE + }) + ); + this.store.dispatch( + RoomActions.selectedRoom({ + roomId: res.roomId, + localeCode: this.i18nService.currentLng.toUpperCase() as LocaleCode + }) + ); + }), + catchError((error) => + of(RoomActions.createRoomFailure({ error })) + ) + ); + }) + ); + }, + { dispatch: false } + ); + + cancel$ = createEffect(() => + this.actions$.pipe( + ofType(cancel), + exhaustMap((req) => + this.eventProtocolService.cancel(req).pipe( + map((res: CancelResponse) => { + return cancelNotification({ noti: res }); + }), + catchError((error) => of(delFailure({ error }))) + ) + ) + ) + ); + + cancelNotification$ = createEffect( + () => { + return this.actions$.pipe( + ofType(cancelNotification), + withLatestFrom(this.store.pipe(select(RoomSelector.rooms))), + tap(([action, rooms]) => { + const notiRoomId = action.noti.roomId; + + if (!!rooms && rooms.length > 0) { + // 현재 방이 오픈되어 있으면 방내용 갱신 + const idx = rooms.findIndex( + (roomInfo) => roomInfo.roomId === notiRoomId + ); + + if (idx > -1) { + this.store.dispatch( + updateEventList({ + roomId: notiRoomId, + eventSeq: action.noti.eventSeq, + sentMessage: this.i18nService.t('event.recalled') + }) + ); + } + } + + // 대화 > 리스트의 항목 갱신 + this.store.dispatch( + RoomActions.room({ + req: { + roomId: action.noti.roomId, + isDetail: false, + localeCode: this.i18nService.currentLng.toUpperCase() as LocaleCode + } + }) + ); + }) + ); + }, + { dispatch: false } + ); + /******************************************************************* * [Room Action watching.] *******************************************************************/ @@ -263,6 +712,10 @@ export class Effects { @Inject(_MODULE_CONFIG) private moduleConfig: ModuleConfig, private roomProtocolService: RoomProtocolService, private eventProtocolService: EventProtocolService, - private fileProtocolService: FileProtocolService - ) {} + private fileProtocolService: FileProtocolService, + private i18nService: I18nService, + private commonApiService: CommonApiService + ) { + this.i18nService.setDefaultNamespace('chat'); + } } diff --git a/projects/store-chat/src/lib/store/chatting/reducers.ts b/projects/store-chat/src/lib/store/chatting/reducers.ts index 6f0a564..4eb83a5 100644 --- a/projects/store-chat/src/lib/store/chatting/reducers.ts +++ b/projects/store-chat/src/lib/store/chatting/reducers.ts @@ -1,21 +1,28 @@ import { createReducer, on } from '@ngrx/store'; +import { FileInfo } from '@ucap/protocol-file'; +import { Info, EventJson, EventType } from '@ucap/protocol-event'; + +import * as RoomActions from '../room/actions'; + import { initialState, adapterChatting, adapterEventList, Chatting, - adapterFileInfoList, - adapterFileInfoCheckList + adapterFileInfoList + // adapterFileInfoCheckList } from './state'; -import * as RoomActions from '../room/actions'; import { eventsSuccess, eventsFailure, fileInfosSuccess, fileInfosFailure, - addEvent + addEvent, + delEventList, + updateEventList, + clearActiveRoomId } from './actions'; export const reducer = createReducer( @@ -31,22 +38,53 @@ export const reducer = createReducer( eventListProcessing: false, eventList: adapterEventList.getInitialState(), eventStatus: null, - remainEvent: false, + remainEvent: null, fileInfoListProcessing: false, fileInfoList: adapterFileInfoList.getInitialState(), - fileInfoCheckList: adapterFileInfoCheckList.getInitialState(), + // fileInfoCheckList: adapterFileInfoCheckList.getInitialState(), + fileInfoCheckList: [], fileInfoSyncDate: '', ...chatting }; + // dupliaction event process + const trgtEventInfoList: Info[] = []; + if (!!chatting && !!chatting.eventList && !!chatting.eventList.entities) { + const filteredList = action.eventInfoList.filter((item) => { + let notExistOrDiff = true; // added target flag. + + // tslint:disable-next-line: forin + for (const key in chatting.eventList.entities) { + const event = chatting.eventList.entities[key]; + if ( + item.seq === event.seq && + item.type === event.type && + item.sentMessage === event.sentMessage + ) { + notExistOrDiff = false; + break; + } + } + + return notExistOrDiff; + }); + + if (!!filteredList && filteredList.length > 0) { + trgtEventInfoList.push(...filteredList); + } + } else { + trgtEventInfoList.push(...action.eventInfoList); + } + trgtChatting = { ...trgtChatting, - eventList: adapterEventList.upsertMany(action.eventInfoList, { + eventList: adapterEventList.upsertMany(trgtEventInfoList, { ...trgtChatting.eventList }), eventStatus: action.res, - remainEvent: action.remainEvent, + remainEvent: + trgtChatting.remainEvent === false ? false : action.remainEvent, // 재조회를 위한 처리. eventListProcessing: false }; @@ -92,11 +130,12 @@ export const reducer = createReducer( eventListProcessing: false, eventList: adapterEventList.getInitialState(), eventStatus: null, - remainEvent: false, + remainEvent: null, fileInfoListProcessing: false, fileInfoList: adapterFileInfoList.getInitialState(), - fileInfoCheckList: adapterFileInfoCheckList.getInitialState(), + // fileInfoCheckList: adapterFileInfoCheckList.getInitialState(), + fileInfoCheckList: [], fileInfoSyncDate: '', ...chatting }; @@ -111,11 +150,12 @@ export const reducer = createReducer( ...trgtChatting.fileInfoList }) : trgtChatting.fileInfoList, - fileInfoCheckList: !!fileInfoCheckList - ? adapterFileInfoCheckList.upsertMany(fileInfoCheckList, { - ...trgtChatting.fileInfoCheckList - }) - : trgtChatting.fileInfoCheckList, + // fileInfoCheckList: !!fileInfoCheckList + // ? adapterFileInfoCheckList.upsertMany(fileInfoCheckList, { + // ...trgtChatting.fileInfoCheckList + // }) + // : trgtChatting.fileInfoCheckList, + fileInfoCheckList, fileInfoListProcessing: false }; @@ -171,6 +211,117 @@ export const reducer = createReducer( } }), + on(delEventList, (state, action) => { + const roomId = action.roomId; + const chatting = state.chattings.entities[roomId]; + + // checked Valid array + if ( + !chatting || + !chatting.eventList || + chatting.eventList.ids.length === 0 + ) { + return state; + } + + const fileInfoList = chatting.fileInfoList; + const trgtDelFileInfoSeqs = []; + if (!!fileInfoList) { + fileInfoList.ids.forEach((id) => { + const fileInfo: FileInfo = fileInfoList.entities[id]; + if (action.eventSeqs.indexOf(fileInfo.eventSeq) > -1) { + trgtDelFileInfoSeqs.push(id); + } + }); + } + + return { + ...state, + chattings: adapterChatting.upsertOne( + { + ...chatting, + eventList: adapterEventList.removeMany(action.eventSeqs, { + ...chatting.eventList + }), + fileInfoList: + trgtDelFileInfoSeqs.length > 0 + ? adapterFileInfoList.removeMany(trgtDelFileInfoSeqs, { + ...chatting.fileInfoList + }) + : chatting.fileInfoList + }, + { ...state.chattings } + ) + }; + }), + + on(updateEventList, (state, action) => { + const roomId = action.roomId; + const eventSeq = action.eventSeq; + const sentMessage = action.sentMessage; + const chatting = state.chattings.entities[roomId]; + let fileInfoSeq; + + // checked Valid array + if ( + !chatting || + !chatting.eventList || + chatting.eventList.ids.length === 0 + ) { + return state; + } + + const statusEventInfo: Info = { + ...chatting.eventList[eventSeq], + type: EventType.RecalledMessage, + sentMessage + }; + + // 파일이 회수되었을 경우 fileInfoList 에서도 삭제 한다. + if (chatting.eventList.entities[eventSeq].type === EventType.File) { + const fileInfoList = chatting.fileInfoList; + if (!!chatting && !!fileInfoList) { + fileInfoList.ids.forEach((id) => { + const fileInfo: FileInfo = fileInfoList.entities[id]; + if (action.eventSeq === fileInfo.eventSeq) { + fileInfoSeq = id; + } + }); + } + } + + return { + ...state, + chattings: adapterChatting.upsertOne( + { + ...chatting, + eventList: adapterEventList.updateOne( + { + id: eventSeq, + changes: statusEventInfo + }, + { + ...chatting.eventList + } + ), + fileInfoList: !!fileInfoSeq + ? adapterFileInfoList.removeOne(fileInfoSeq, { + ...chatting.fileInfoList + }) + : chatting.fileInfoList + }, + { ...state.chattings } + ) + }; + }), + + on(clearActiveRoomId, (state, action) => { + return { + ...state, + activeRoomId: null + }; + }), + /******************************************************************* * [Room Action watching.] *******************************************************************/ @@ -197,7 +348,9 @@ export const reducer = createReducer( return { ...state, - chattings: adapterChatting.removeMany(roomIds, { ...state.chattings }) + chattings: adapterChatting.removeMany(roomIds, { + ...state.chattings + }) }; }) ); diff --git a/projects/store-chat/src/lib/store/chatting/state.ts b/projects/store-chat/src/lib/store/chatting/state.ts index a5cc03e..2329448 100644 --- a/projects/store-chat/src/lib/store/chatting/state.ts +++ b/projects/store-chat/src/lib/store/chatting/state.ts @@ -2,12 +2,11 @@ import { Selector, createSelector } from '@ngrx/store'; import { EntityState, createEntityAdapter, Dictionary } from '@ngrx/entity'; import { InfoResponse, Info, EventJson } from '@ucap/protocol-event'; - import { FileInfo, FileDownloadInfo } from '@ucap/protocol-file'; export interface EventListState extends EntityState> {} export interface FileInfoListState extends EntityState {} -export interface FileInfoCheckListState extends EntityState {} +// export interface FileInfoCheckListState extends EntityState {} export const adapterEventList = createEntityAdapter>({ selectId: (info) => info.seq, @@ -21,12 +20,12 @@ export const adapterFileInfoList = createEntityAdapter({ return b.seq - a.seq; } }); -export const adapterFileInfoCheckList = createEntityAdapter({ - selectId: (info) => info.seq, - sortComparer: (a, b) => { - return b.seq - a.seq; - } -}); +// export const adapterFileInfoCheckList = createEntityAdapter({ +// selectId: (info) => info.seq, +// sortComparer: (a, b) => { +// return b.seq - a.seq; +// } +// }); const eventListInitialState: EventListState = adapterEventList.getInitialState( {} @@ -34,9 +33,9 @@ const eventListInitialState: EventListState = adapterEventList.getInitialState( const fileInfoListInitialState: FileInfoListState = adapterFileInfoList.getInitialState( {} ); -const fileInfoCheckListInitialState: FileInfoCheckListState = adapterFileInfoCheckList.getInitialState( - {} -); +// const fileInfoCheckListInitialState: FileInfoCheckListState = adapterFileInfoCheckList.getInitialState( +// {} +// ); const { selectAll: selectAllForEventList, @@ -52,12 +51,12 @@ const { selectTotal: selectTotalForFileInfoList } = adapterFileInfoList.getSelectors(); -const { - selectAll: selectAllForFileInfoCheckList, - selectEntities: selectEntitiesForFileInfoCheckList, - selectIds: selectIdsForFileInfoCheckList, - selectTotal: selectTotalForFileInfoCheckList -} = adapterFileInfoCheckList.getSelectors(); +// const { +// selectAll: selectAllForFileInfoCheckList, +// selectEntities: selectEntitiesForFileInfoCheckList, +// selectIds: selectIdsForFileInfoCheckList, +// selectTotal: selectTotalForFileInfoCheckList +// } = adapterFileInfoCheckList.getSelectors(); export interface Chatting { roomId?: string; @@ -65,11 +64,12 @@ export interface Chatting { eventListProcessing?: boolean; eventList?: EventListState; eventStatus?: InfoResponse | null; - remainEvent?: boolean; + remainEvent?: boolean | null; fileInfoListProcessing?: boolean; fileInfoList?: FileInfoListState; - fileInfoCheckList?: FileInfoCheckListState; + // fileInfoCheckList?: FileInfoCheckListState; + fileInfoCheckList?: FileDownloadInfo[]; fileInfoSyncDate?: string; } @@ -139,12 +139,37 @@ export function selectors(selector: Selector) { (state) => state.fileInfoListProcessing ); const selectChattingFileInfoList = createSelector( - selectChatting, - (state) => state.fileInfoList + selectChattings, + (state: ChattingState, roomId: string) => { + const chatting = state.entities && state.entities[roomId]; + if (!!chatting) { + return chatting?.fileInfoList; + } else { + return adapterFileInfoList.getInitialState(); + } + } ); + // const selectChattingFileInfoCheckList = createSelector( + // selectChattings, + // (state: ChattingState, roomId: string) => { + // const chatting = state.entities && state.entities[roomId]; + // if (!!chatting) { + // return chatting?.fileInfoCheckList; + // } else { + // return adapterFileInfoCheckList.getInitialState(); + // } + // } + // ); const selectChattingFileInfoCheckList = createSelector( - selectChatting, - (state) => state.fileInfoCheckList + selectChattings, + (state: ChattingState, roomId: string) => { + const chatting = state.entities && state.entities[roomId]; + if (!!chatting) { + return chatting?.fileInfoCheckList; + } else { + return []; + } + } ); const selectChattingFileInfoSyncDate = createSelector( selectChatting, @@ -167,10 +192,11 @@ export function selectors(selector: Selector) { selectChattingFileInfoList, selectAllForFileInfoList ), - fileInfoCheckList: createSelector( - selectChattingFileInfoCheckList, - selectAllForFileInfoCheckList - ), + // fileInfoCheckList: createSelector( + // selectChattingFileInfoCheckList, + // selectAllForFileInfoCheckList + // ), + fileInfoCheckList: selectChattingFileInfoCheckList, fileInfoSyncDate: selectChattingFileInfoSyncDate }; } diff --git a/projects/store-chat/src/lib/store/room/actions.ts b/projects/store-chat/src/lib/store/room/actions.ts index d6af7f6..23d69c5 100644 --- a/projects/store-chat/src/lib/store/room/actions.ts +++ b/projects/store-chat/src/lib/store/room/actions.ts @@ -160,21 +160,6 @@ export const create = createAction( '[ucap::chat::room] create', props<{ req: CreateRequest }>() ); -/** - * Success of create request - */ -export const createSuccess = createAction( - '[ucap::chat::room] create Success', - props<{ res: CreateResponse }>() -); -/** - * Failure of create request - */ -export const createFailure = createAction( - '[ucap::chat::room] create Failure', - props<{ error: any }>() -); - /** * create timer room */ @@ -183,17 +168,17 @@ export const createTimer = createAction( props<{ req: CreateTimerRequest }>() ); /** - * Success of openTimer request + * Success of create room / timer room request */ -export const createTimerSuccess = createAction( - '[ucap::chat::room] createTimer Success', - props<{ res: CreateTimerResponse }>() +export const createRoomSuccess = createAction( + '[ucap::chat::room] create room Success', + props<{ res: CreateResponse; isForward?: boolean }>() ); /** - * Failure of createTimer request + * Failure of create room / timer room request */ -export const createTimerFailure = createAction( - '[ucap::chat::room] createTimer Failure', +export const createRoomFailure = createAction( + '[ucap::chat::room] create room Failure', props<{ error: any }>() ); @@ -264,6 +249,15 @@ export const updateFailure = createAction( '[ucap::chat::room] update Failure', props<{ error: any }>() ); +/** + * Success of update RoomName + */ +export const updateRoomName = createAction( + '[ucap::chat::room] update Roomname for Notification', + props<{ + res: UpdateResponse; + }>() +); /** * update isJoinRoom of user information from true to false @@ -331,18 +325,6 @@ export const closeFailure = createAction( props<{ error: any }>() ); -/** - * Invite conversation partner to room or create room - */ -export const inviteOrCreate = createAction( - '[ucap::chat::room] inviteOrCreate', - props<{ - roomInfo: RoomInfo; - localeCode: LocaleCode; - req: CreateRequest; - }>() -); - /** * Invite conversation partner to room */ @@ -386,6 +368,13 @@ export const expelFailure = createAction( '[ucap::chat::room] expel Failure', props<{ error: any }>() ); +/** + * expel notification + */ +export const expelNotification = createAction( + '[ucap::chat::room] expel Notification', + props<{ res: ExitForcingResponse }>() +); /** * update interval of timer room @@ -422,10 +411,24 @@ export const inviteNotification = createAction( */ export const exitNotification = createAction( '[ucap::chat::room] Exit Notification', - props<{ roomId: string; userSeq: string; senderSeq: string }>() + props<{ roomId: string; senderSeq: string }>() ); export const updateUnreadCount = createAction( '[ucap::chat::room] Update unread count', props<{ roomId: string; noReadCnt?: number }>() ); + +/** + * room user info update by Notification + */ +// export const userInfoListUpdate = createAction( +// '[ucap::chat::room] user info update', +// props<{ +// roomInfo: RoomInfo; +// roomUserInfo: { +// userInfoShortList: UserInfoShort[]; +// userInfoList: RoomUserInfo[]; +// }; +// }>() +// ); diff --git a/projects/store-chat/src/lib/store/room/effects.ts b/projects/store-chat/src/lib/store/room/effects.ts index 053c5f9..819f3f7 100644 --- a/projects/store-chat/src/lib/store/room/effects.ts +++ b/projects/store-chat/src/lib/store/room/effects.ts @@ -14,8 +14,8 @@ import { Injectable } from '@angular/core'; import { Store, select } from '@ngrx/store'; import { Actions, ofType, createEffect } from '@ngrx/effects'; +import { LocaleCode } from '@ucap/core'; import { - RoomType, OpenResponse as CreateResponse, Open3Response as CreateTimerResponse, ExitResponse as DeleteResponse, @@ -24,22 +24,30 @@ import { InviteResponse, ExitForcingResponse, UpdateTimerSetResponse, - InfoRequest + InfoRequest, + UserInfoShort, + UserInfo, + RoomInfo } from '@ucap/protocol-room'; +import { UserNotification, UserInfoUpdateType } from '@ucap/protocol-info'; +import { I18nService } from '@ucap/ng-i18n'; import { RoomProtocolService } from '@ucap/ng-protocol-room'; import { SyncProtocolService } from '@ucap/ng-protocol-sync'; +import { + PresenceActions, + CommonActions, + UserSelector +} from '@ucap/ng-store-organization'; import { LoginActions } from '@ucap/ng-store-authentication'; -import * as ChattingAction from '../Chatting/actions'; -import { RoomSelector } from '../state'; + +import * as ChattingAction from '../chatting/actions'; +import { RoomSelector, ChattingSelector } from '../state'; import { rooms, - roomsFailure, - roomsSuccess, room, - roomSuccess, roomFailure, inviteNotification, exitNotification, @@ -48,11 +56,9 @@ import { close, delSuccess, create, - createSuccess, - createFailure, createTimer, - createTimerSuccess, - createTimerFailure, + createRoomSuccess, + createRoomFailure, del, delFailure, update, @@ -61,7 +67,6 @@ import { open, openSuccess, closeSuccess, - inviteOrCreate, invite, inviteSuccess, inviteFailure, @@ -79,10 +84,9 @@ import { selectedRoom, room2Success, selectedRoomSuccess, - clearSelectedRoom + clearSelectedRoom, + expelNotification } from './actions'; -import { Router } from '@angular/router'; -import { LocaleCode } from '@ucap/core'; @Injectable() export class Effects { @@ -121,14 +125,25 @@ export class Effects { roomInfo2Res: res }) ); + + // Buddy Presence + const targetUserInfos = req.isDetail + ? res.roomUserInfo.userInfoList.map( + (userInfo) => userInfo.seq + '' + ) + : res.roomUserInfo.userInfoShortList.map( + (userInfo) => userInfo.seq + '' + ); + if (!!targetUserInfos && targetUserInfos.length > 0) { + this.store.dispatch( + PresenceActions.bulkInfo({ + divCd: 'roomBulk', + userSeqs: targetUserInfos + }) + ); + } } else { // is not join room. so, redirect chat main. - this.router.navigate([ - 'chat', - { - outlets: { content: 'index' } - } - ]); this.store.dispatch( clearSelectedRoom({ roomId: req.roomId }) ); @@ -237,27 +252,9 @@ export class Effects { exhaustMap((req) => { return this.roomProtocolService.open(req).pipe( map((res: CreateResponse) => { - console.log('CreateResponse', res); - - this.store.dispatch(createSuccess({ res })); - - this.router.navigate( - [ - 'chat', - { - outlets: { content: 'chatroom' } - } - ], - { - queryParams: { roomId: res.roomId } - } - ); - - // if (!res.newRoom) { - // } else { - // } + this.store.dispatch(createRoomSuccess({ res })); }), - catchError((error) => of(createFailure({ error }))) + catchError((error) => of(createRoomFailure({ error }))) ); }) ); @@ -265,33 +262,53 @@ export class Effects { { dispatch: false } ); - createTimer$ = createEffect(() => - this.actions$.pipe( - ofType(createTimer), - map((action) => action.req), - exhaustMap((req) => { - return this.roomProtocolService.open3(req).pipe( - map((res: CreateTimerResponse) => { - return createTimerSuccess({ res }); - }), - catchError((error) => of(createTimerFailure({ error }))) - ); - }) - ) + createTimer$ = createEffect( + () => { + return this.actions$.pipe( + ofType(createTimer), + map((action) => action.req), + exhaustMap((req) => { + return this.roomProtocolService.open3(req).pipe( + map((res: CreateTimerResponse) => { + // return createTimerSuccess({ res }); + this.store.dispatch(createRoomSuccess({ res })); + }), + catchError((error) => of(createRoomFailure({ error }))) + ); + }) + ); + }, + { dispatch: false } ); del$ = createEffect(() => this.actions$.pipe( ofType(del), map((action) => action.req), - exhaustMap((req) => { + withLatestFrom(this.store.pipe(select(ChattingSelector.activeRoomId))), + exhaustMap(([req, activeRoomId]) => { + const existActiveRoomId = req.roomId === activeRoomId; + return this.roomProtocolService.exit(req).pipe( - switchMap((res: DeleteResponse) => [ - // close room, clear chatting - close({ roomIds: [res.roomId] }), - // clear room in rooms. - delSuccess({ res }) - ]), + switchMap((res: DeleteResponse) => { + if (!!existActiveRoomId) { + return [ + // clear activeRoomId + clearSelectedRoom({ roomId: res.roomId }), + // close room, clear chatting + close({ roomIds: [res.roomId] }), + // clear room in rooms. + delSuccess({ res }) + ]; + } else { + return [ + // close room, clear chatting + close({ roomIds: [res.roomId] }), + // clear room in rooms. + delSuccess({ res }) + ]; + } + }), catchError((error) => of(delFailure({ error }))) ); }) @@ -302,14 +319,32 @@ export class Effects { this.actions$.pipe( ofType(delMulti), map((action) => action.req), - exhaustMap((req) => { + withLatestFrom(this.store.pipe(select(ChattingSelector.activeRoomId))), + exhaustMap(([req, activeRoomId]) => { + const existActiveRoomId = req.roomIds.find( + (roomId) => roomId === activeRoomId + ); + return this.roomProtocolService.exitAll(req).pipe( - switchMap((res: DeleteMultiResponse) => [ - // close room, clear chatting - close({ roomIds: res.roomIds }), - // clear room in rooms. - delMultiSuccess({ res }) - ]), + switchMap((res: DeleteMultiResponse) => { + if (!!existActiveRoomId) { + return [ + // clear selected room + clearSelectedRoom({ roomId: existActiveRoomId }), + // close room, clear chatting + close({ roomIds: res.roomIds }), + // clear room in rooms. + delMultiSuccess({ res }) + ]; + } else { + return [ + // close room, clear chatting + close({ roomIds: res.roomIds }), + // clear room in rooms. + delMultiSuccess({ res }) + ]; + } + }), catchError((error) => of(delMultiFailure({ error }))) ); }) @@ -357,33 +392,6 @@ export class Effects { ); }); - inviteOrCreate$ = createEffect(() => - this.actions$.pipe( - ofType(inviteOrCreate), - map((action) => { - const roomInfo = action.roomInfo; - const localeCode = action.localeCode; - - switch (roomInfo.roomType) { - case RoomType.Single: - return create({ req: action.req }); - case RoomType.Multi: - return invite({ - req: { - roomId: roomInfo.roomId, - inviteUserSeqs: [...action.req.userSeqs] - }, - localeCode - }); - default: - return inviteFailure({ - error: `type[${roomInfo.roomType}] of room is not valid` - }); - } - }) - ) - ); - invite$ = createEffect(() => this.actions$.pipe( ofType(invite), @@ -398,7 +406,7 @@ export class Effects { room({ req: { roomId: req.roomId, - isDetail: true, + isDetail: false, localeCode } }) @@ -425,6 +433,47 @@ export class Effects { ) ); + expelSuccess$ = createEffect( + () => { + return this.actions$.pipe( + ofType(expelSuccess), + map((action) => action.res), + tap((res) => { + this.store.dispatch( + excludeUser({ roomId: res.roomId, userSeqs: res.userSeqs }) + ); + }) + ); + }, + { dispatch: false } + ); + + expelNotification$ = createEffect(() => { + return this.actions$.pipe( + ofType(expelNotification), + withLatestFrom(this.store.pipe(select(UserSelector.user))), + exhaustMap(([action, user]) => { + const roomId = action.res.roomId; + const userSeqs = action.res.userSeqs; + const existMe = userSeqs.indexOf(String(user.info.seq)) > -1; + + // 내가 강퇴 대상에 포함되어 있으면 우선 처리. + if (!!existMe) { + return [ + close({ roomIds: [roomId] }), + clearSelectedRoom({ roomId }), + delSuccess({ res: { roomId } }) + ]; + } else { + // 나를 제외한 강퇴 인원 처리. + if (!!userSeqs && userSeqs.length > 0) { + return [excludeUser({ roomId, userSeqs })]; + } + } + }) + ); + }); + updateTimeRoomInterval$ = createEffect(() => this.actions$.pipe( ofType(updateTimeRoomInterval), @@ -440,28 +489,48 @@ export class Effects { ) ); - inviteNotification$ = createEffect(() => { - return this.actions$.pipe( - ofType(inviteNotification), - map((action) => - room({ - req: { - roomId: action.noti.roomId, - isDetail: true, - localeCode: action.localeCode + /** + * @discription Call by notifications case in SSVC_TYPE_ROOM_INVITE_RES, SSVC_TYPE_ROOM_INVITE_NOTI + * 1. roomlist 를 체크하여 없을경우 내가 초대된 경우라 간주하고 방 조회하여 갱신하지 않도록 한다.(첫 대화가 들어오면 그때 조회.) + * 2. roomlist 를 체크하여 있을 경우 기존방에 다른 인원이 추가되었을 경우이므로 방 조회하여 갱신한다. + */ + inviteNotification$ = createEffect( + () => { + return this.actions$.pipe( + ofType(inviteNotification), + withLatestFrom(this.store.pipe(select(RoomSelector.rooms))), + map(([action, roomList]) => { + const roomId = action.noti.roomId; + if (!!roomList && roomList.length > 0) { + if (roomList.some((roomInfo) => roomId === roomInfo.roomId)) { + this.store.dispatch( + room({ + req: { + roomId, + isDetail: false, + localeCode: action.localeCode + } + }) + ); + } } }) - ) - ); - }); + ); + }, + { + dispatch: false + } + ); exitNotification$ = createEffect(() => { return this.actions$.pipe( ofType(exitNotification), - switchMap((action) => { - if (action.userSeq === action.senderSeq) { + withLatestFrom(this.store.pipe(select(UserSelector.user))), + switchMap(([action, user]) => { + if (String(user.info.seq) === String(action.senderSeq)) { return [ close({ roomIds: [action.roomId] }), + clearSelectedRoom({ roomId: action.roomId }), delSuccess({ res: { roomId: action.roomId } }) @@ -490,7 +559,134 @@ export class Effects { const roomId = action.roomId; if (!roomList.find((roomInfo) => roomInfo.roomId === roomId)) { - this.store.dispatch(rooms({ localeCode: LocaleCode.Korean })); + this.store.dispatch( + rooms({ + localeCode: this.i18nService.currentLng.toUpperCase() as LocaleCode + }) + ); + } + }) + ); + }, + { dispatch: false } + ); + + userNotificationForRoom$ = createEffect( + () => { + return this.actions$.pipe( + ofType(CommonActions.userNotification), + withLatestFrom( + this.store.pipe(select(UserSelector.user)), + this.store.pipe(select(RoomSelector.rooms)), + this.store.pipe(select(RoomSelector.roomUsers)), + this.store.pipe(select(RoomSelector.roomUsersShort)) + ), + tap(([action, user, roomList, roomUsers, roomUsersShort]) => { + const noti = action.noti as UserNotification; + + if ( + Number(action.noti.SENDER_SEQ) !== Number(user.info.seq) && + noti.type === UserInfoUpdateType.Image + ) { + roomUsers = (roomUsers || []).filter( + (userMap) => + roomList.findIndex((rInfo) => rInfo.roomId === userMap.roomId) > + -1 + ); + + roomUsersShort = (roomUsersShort || []).filter( + (userMap) => + roomList.findIndex((rInfo) => rInfo.roomId === userMap.roomId) > + -1 + ); + + const tempRoomList: { + roomInfo: RoomInfo; + uInfos?: UserInfo[]; + userInfoS?: UserInfoShort[]; + }[] = []; + + const findIdx = noti.info.indexOf('ProfileImage'); + let imgInfo: string = noti.info; + + if (findIdx > -1) { + const startIdx = noti.info.indexOf('/', findIdx); + imgInfo = noti.info.substring(startIdx); + } + + for (const ru of roomUsers) { + ru.userInfos.every((u) => { + if (Number(u.seq) === Number(noti.SENDER_SEQ)) { + const tempInfos: UserInfo[] = []; + const roomInfo = roomList.filter( + (r) => r.roomId === ru.roomId + )[0]; + const cUser = { + ...u, + profileImageFile: imgInfo + }; + + ru.userInfos.map((u2) => { + if (Number(u2.seq) === Number(u.seq)) { + tempInfos.push(cUser); + } else { + tempInfos.push(u2); + } + }); + + tempRoomList.push({ + roomInfo, + uInfos: tempInfos + }); + + return false; + } + return true; + }); + } + + for (const ru of roomUsersShort) { + ru.userInfos.every((u) => { + if (Number(u.seq) === Number(noti.SENDER_SEQ)) { + const tempShorts: UserInfoShort[] = []; + const roomInfo = roomList.filter( + (r) => r.roomId === ru.roomId + )[0]; + + const cUser = { + ...u, + profileImageFile: imgInfo + }; + ru.userInfos.map((u2) => { + if (Number(u2.seq) === Number(u.seq)) { + tempShorts.push(cUser); + } else { + tempShorts.push(u2); + } + }); + + tempRoomList.push({ + roomInfo, + userInfoS: tempShorts + }); + + return false; + } + return true; + }); + } + + tempRoomList.map((obj) => { + this.store.dispatch( + room2Success({ + roomInfo: obj.roomInfo, + roomUserInfo: { + userInfoList: obj?.uInfos, + userInfoShortList: obj?.userInfoS + } + }) + ); + }); } }) ); @@ -501,8 +697,8 @@ export class Effects { constructor( private actions$: Actions, private store: Store, - private router: Router, private syncProtocolService: SyncProtocolService, - private roomProtocolService: RoomProtocolService + private roomProtocolService: RoomProtocolService, + private i18nService: I18nService ) {} } diff --git a/projects/store-chat/src/lib/store/room/reducers.ts b/projects/store-chat/src/lib/store/room/reducers.ts index fc59df7..9086ed3 100644 --- a/projects/store-chat/src/lib/store/room/reducers.ts +++ b/projects/store-chat/src/lib/store/room/reducers.ts @@ -1,12 +1,10 @@ import { createReducer, on } from '@ngrx/store'; -import { - UserInfo as RoomUserInfo, - UserInfoShort as RoomUserInfoShort, - RoomInfo -} from '@ucap/protocol-room'; +import { RoomInfo } from '@ucap/protocol-room'; +import { Info, EventJson } from '@ucap/protocol-event'; -import * as chattingActions from '../chatting/actions'; +import * as ChattingActions from '../chatting/actions'; +import { ChatUtil } from '../../utils/chat.util'; import { initialState, @@ -19,18 +17,17 @@ import { import { roomsSuccess, roomSuccess, - excludeUser, excludeUserSuccess, delSuccess, rooms2Success, delMultiSuccess, updateSuccess, room2Success, - createSuccess, - updateUnreadCount + createRoomSuccess, + updateUnreadCount, + updateRoomName, + updateTimeRoomIntervalSuccess } from './actions'; -import { Info, EventJson } from '@ucap/protocol-event'; -import { ChatUtil } from '../../utils/chat.util'; export const reducer = createReducer( initialState, @@ -184,63 +181,88 @@ export const reducer = createReducer( on(excludeUserSuccess, (state, action) => { const roomId = action.roomId; - const roomUserMap = state.roomUsers.entities[roomId]; - const roomUserMapShort = state.roomUsersShort.entities[roomId]; - const userInfos: RoomUserInfo[] = [...roomUserMap.userInfos]; - const userInfosShort: RoomUserInfoShort[] = [...roomUserMapShort.userInfos]; + let roomUser: RoomUserMap = state.roomUsers.entities[roomId]; + if (!!roomUser) { + roomUser = { + ...roomUser, + userInfos: state.roomUsers.entities[roomId].userInfos.map( + (roomUserInfo) => { + if ( + action.userSeqs.some((seq) => seq + '' === roomUserInfo.seq + '') + ) { + return { + ...roomUserInfo, + isJoinRoom: false + }; + } else { + return roomUserInfo; + } + } + ) + }; + } + let roomUserShort: RoomUserShortMap = state.roomUsersShort.entities[roomId]; + if (!!roomUserShort) { + roomUserShort = { + ...roomUserShort, + userInfos: state.roomUsersShort.entities[roomId].userInfos.map( + (roomUserInfo) => { + if ( + action.userSeqs.some((seq) => seq + '' === roomUserInfo.seq + '') + ) { + return { + ...roomUserInfo, + isJoinRoom: false + }; + } else { + return roomUserInfo; + } + } + ) + }; + } - action.userSeqs.forEach((userSeq) => { - const userInfo: RoomUserInfo = userInfos.find( - (u) => userSeq === String(u.seq) - ); - - if (!!userInfo && !!userInfo.seq) { - userInfo.isJoinRoom = false; - } - - const userInfoShort: RoomUserInfoShort = userInfosShort.find( - (u) => userSeq === String(u.seq) - ); - - if (!!userInfoShort && !!userInfoShort.seq) { - userInfoShort.isJoinRoom = false; - } - }); + const currentRoomInfo = state.rooms.entities[roomId]; + let roomInfo: RoomInfo; + if (!!currentRoomInfo) { + roomInfo = { + ...currentRoomInfo, + joinUserCount: !!roomUser + ? roomUser.userInfos.filter((item) => !!item.isJoinRoom).length + : !!roomUserShort + ? roomUserShort.userInfos.filter((item) => !!item.isJoinRoom).length + : currentRoomInfo.joinUserCount + }; + } return { ...state, - roomUsers: adapterRoomUser.updateOne( - { - id: roomId, - changes: { - roomId, - userInfos - } - }, - { - ...state.roomUsers - } - ), - roomUsersShort: adapterRoomUserShort.updateOne( - { - id: roomId, - changes: { - roomId, - userInfos: userInfosShort - } - }, - { - ...state.roomUsersShort - } - ) + rooms: !!roomInfo + ? adapterRoom.upsertOne(roomInfo, { ...state.rooms }) + : state.rooms, + roomUsers: !!roomUser + ? adapterRoomUser.upsertOne(roomUser, { + ...state.roomUsers + }) + : state.roomUsers, + roomUsersShort: !!roomUserShort + ? adapterRoomUserShort.upsertOne(roomUserShort, { + ...state.roomUsersShort + }) + : state.roomUsersShort }; }), - on(createSuccess, (state, action) => { + on(createRoomSuccess, (state, action) => { const standby = state.standbyRooms; const curRoomId = action.res.roomId; - if (standby.findIndex((roomId) => roomId === curRoomId) > -1) { + + if ( + !!action.isForward || + standby.findIndex((roomId) => roomId === curRoomId) > -1 || + !!state.rooms.entities[curRoomId] + ) { // Exist. return state; } else { @@ -252,26 +274,6 @@ export const reducer = createReducer( } }), - on(delSuccess, (state, action) => { - const roomId = action.res.roomId; - const room = state.rooms.entities[roomId]; - - if (!room) { - return state; - } - - return { - ...state, - rooms: adapterRoom.removeOne(roomId, { ...state.rooms }), - roomUsers: adapterRoomUser.removeOne(roomId, { - ...state.roomUsers - }), - roomUsersShort: adapterRoomUserShort.removeOne(roomId, { - ...state.roomUsersShort - }) - }; - }), - on(updateSuccess, (state, action) => { const roomInfo = { ...state.rooms.entities[action.res.roomId], @@ -285,6 +287,30 @@ export const reducer = createReducer( }; }), + on(updateTimeRoomIntervalSuccess, (state, action) => { + const roomInfo = { + ...state.rooms.entities[action.res.roomId], + timeRoomInterval: action.res.timerInterval + } as RoomInfo; + + return { + ...state, + rooms: adapterRoom.upsertOne(roomInfo, { ...state.rooms }) + }; + }), + + on(updateRoomName, (state, action) => { + const roomInfo = { + ...state.rooms.entities[action.res.roomId], + roomName: action.res.roomName + } as RoomInfo; + + return { + ...state, + rooms: adapterRoom.upsertOne(roomInfo, { ...state.rooms }) + }; + }), + on(delSuccess, (state, action) => { const roomId = action.res.roomId; const room = state.rooms.entities[roomId]; @@ -353,7 +379,7 @@ export const reducer = createReducer( /******************************************************************* * [Chatting Action watching.] *******************************************************************/ - on(chattingActions.readSuccess, (state, action) => { + on(ChattingActions.readSuccess, (state, action) => { const roomId = action.roomId; const trgtUserSeq = action.SENDER_SEQ; @@ -413,7 +439,7 @@ export const reducer = createReducer( }; }), - on(chattingActions.addEventSuccess, (state, action) => { + on(ChattingActions.addEventSuccess, (state, action) => { const roomId = action.roomId; const info: Info = action.info; @@ -434,19 +460,26 @@ export const reducer = createReducer( action.info.sentMessageJson || action.info.sentMessage ); - const roomInfo = { - ...currentRoomInfo, - finalEventType: info.type, - finalEventDate: info.sendDate, - finalEventMessage, - finalEventSeq: info.seq - } as RoomInfo; + if (!finalEventMessage) { + /** + * 해당 타입은 메시지를 갱신하지 않는다. + */ + return state; + } else { + const roomInfo = { + ...currentRoomInfo, + finalEventType: info.type, + finalEventDate: info.sendDate, + finalEventMessage, + finalEventSeq: info.seq + } as RoomInfo; - return { - ...state, - rooms: adapterRoom.upsertOne(roomInfo, { ...state.rooms }), - standbyRooms: fixedStandByRooms - }; + return { + ...state, + rooms: adapterRoom.upsertOne(roomInfo, { ...state.rooms }), + standbyRooms: fixedStandByRooms + }; + } } else { return state; } diff --git a/projects/store-chat/src/lib/store/room/state.ts b/projects/store-chat/src/lib/store/room/state.ts index f4123e0..34eae34 100644 --- a/projects/store-chat/src/lib/store/room/state.ts +++ b/projects/store-chat/src/lib/store/room/state.ts @@ -1,9 +1,8 @@ import moment from 'moment'; import { Selector, createSelector } from '@ngrx/store'; -import { EntityState, createEntityAdapter, Dictionary } from '@ngrx/entity'; +import { EntityState, createEntityAdapter } from '@ngrx/entity'; -import { RoomUserDetailData, RoomUserData } from '@ucap/protocol-sync'; import { RoomInfo, UserInfo as RoomUserInfo, @@ -134,16 +133,23 @@ export function selectors(selector: Selector) { selector, (state: State) => state.standbyRooms ), - unreadTotal: createSelector( - selectRooms, - selectAllForRoom, - (roomState: RoomState, rooms: RoomInfo[]) => { - let unreadTotal = 0; - for (const room of rooms) { + unreadTotal: createSelector(selectRooms, (roomState: RoomState) => { + let unreadTotal = 0; + + // tslint:disable-next-line: forin + for (const key in roomState.ids) { + const roomId = roomState.ids[key]; + if (roomId === undefined) { + continue; + } + + if (roomState.entities.hasOwnProperty(roomId)) { + const room = roomState.entities[roomId]; unreadTotal += room.noReadCnt; } - return unreadTotal; } - ) + + return unreadTotal; + }) }; } diff --git a/projects/store-chat/src/lib/utils/chat.util.ts b/projects/store-chat/src/lib/utils/chat.util.ts index 11a6cc2..ddaf59e 100644 --- a/projects/store-chat/src/lib/utils/chat.util.ts +++ b/projects/store-chat/src/lib/utils/chat.util.ts @@ -128,7 +128,12 @@ export class ChatUtil { default: { const m = finalEventMessage as string; - eventMessage = m; + + if (eventType === EventType.Character && m.trim().length === 0) { + eventMessage = '최근 대화 없음'; + } else { + eventMessage = m; + } } break; diff --git a/projects/store-chat/src/public-api.ts b/projects/store-chat/src/public-api.ts index d327ecc..433a57e 100644 --- a/projects/store-chat/src/public-api.ts +++ b/projects/store-chat/src/public-api.ts @@ -10,6 +10,9 @@ export * from './lib/config/module-config'; export { CommonActions, RoomActions, ChattingActions }; +export * from './lib/utils/chat.util'; export * from './lib/store/state'; +export { Chatting } from './lib/store/chatting/state'; +export { RoomUserMap, RoomUserShortMap } from './lib/store/room/state'; export * from './lib/chat-store.module'; diff --git a/projects/store-group/package.json b/projects/store-group/package.json index 86133cc..8a3b090 100644 --- a/projects/store-group/package.json +++ b/projects/store-group/package.json @@ -1,6 +1,6 @@ { "name": "@ucap/ng-store-group", - "version": "0.0.14", + "version": "0.0.22", "publishConfig": { "registry": "https://nexus.loafle.net/repository/npm-ucap/" }, diff --git a/projects/store-group/src/lib/store/buddy/actions.ts b/projects/store-group/src/lib/store/buddy/actions.ts index dfef27a..99a612f 100644 --- a/projects/store-group/src/lib/store/buddy/actions.ts +++ b/projects/store-group/src/lib/store/buddy/actions.ts @@ -1,6 +1,6 @@ import { createAction, props } from '@ngrx/store'; -import { UserInfo } from '@ucap/protocol-sync'; +import { UserInfo } from '@ucap/protocol-sync'; import { AddRequest as BuddyAddRequest, AddResponse as BuddyAddResponse, @@ -11,7 +11,8 @@ import { } from '@ucap/protocol-buddy'; import { UserNicknameRequest as NicknameRequest, - UserNicknameResponse as NicknameResponse + UserNicknameResponse as NicknameResponse, + UserNotification } from '@ucap/protocol-info'; /** @@ -142,3 +143,10 @@ export const nicknameFailure = createAction( '[ucap::group::buddy] user nickname Failure', props<{ error: any }>() ); +/** + * buddy info update by Notification + */ +export const buddyInfoUpdate = createAction( + '[ucap::group::buddy] buddy info update notification', + props<{ noti: UserNotification }>() +); diff --git a/projects/store-group/src/lib/store/buddy/effects.ts b/projects/store-group/src/lib/store/buddy/effects.ts index 2a561fa..ffc97c2 100644 --- a/projects/store-group/src/lib/store/buddy/effects.ts +++ b/projects/store-group/src/lib/store/buddy/effects.ts @@ -30,8 +30,11 @@ import { BuddyProtocolService } from '@ucap/ng-protocol-buddy'; import { DepartmentSelector, - PresenceActions + PresenceActions, + CommonActions, + UserSelector } from '@ucap/ng-store-organization'; + import { LoginActions } from '@ucap/ng-store-authentication'; import * as groupActions from '../group/actions'; @@ -51,7 +54,8 @@ import { nickname, nicknameSuccess, nicknameFailure, - delAndClear + delAndClear, + buddyInfoUpdate } from './actions'; import { BuddySelector, GroupSelector } from '../state'; @@ -135,7 +139,7 @@ export class Effects { ), tap(([req, groupList, myDeptUserList]) => { for (const group of groupList) { - if (group.userSeqs.indexOf(req.seq as any) > -1) { + if (group.userSeqs.indexOf(String(req.seq)) > -1) { // 소속부서(내부서) 고정그룹 사용시 소속부서원을 삭제하지 않는다. if ( !!this.moduleConfig.useMyDeptGroup && @@ -154,7 +158,7 @@ export class Effects { groupSeq: group.seq, groupName: group.name, userSeqs: group.userSeqs.filter( - (userSeq) => userSeq !== (req.seq as any) + (userSeq) => userSeq !== String(req.seq) ) } }) @@ -169,7 +173,7 @@ export class Effects { this.moduleConfig.useMyDeptGroup && !!myDeptUserList && myDeptUserList.filter( - (deptUser) => deptUser.seq === (req.seq as any) + (deptUser) => deptUser.seq === String(req.seq) ).length > 0 ) { // skip;; @@ -268,25 +272,42 @@ export class Effects { const userSeqsForDelete = action.userSeqsForDelete; if (!!targetUserSeqs && 0 < targetUserSeqs.length) { - const addBuddyList: string[] = []; - targetUserSeqs.forEach((userSeq) => { - if (!buddyList) { - addBuddyList.push(userSeq); - return; - } + // Add Buddy + const userSeqsForAdd: string[] = []; - const index = buddyList.findIndex( - (b) => b.seq === Number(userSeq) + targetUserSeqs.map((seq) => { + const findBuddy = buddyList.filter( + (user) => Number(user.seq) === Number(seq) ); - if (-1 < index) { - addBuddyList.push(userSeq); - return; + + if (!!findBuddy && findBuddy.length === 0) { + userSeqsForAdd.push(seq); } }); - if (addBuddyList.length > 0) { - this.store.dispatch(add({ req: { userSeqs: addBuddyList } })); + if (userSeqsForAdd.length > 0) { + this.store.dispatch(add({ req: { userSeqs: userSeqsForAdd } })); } + + // const addBuddyList: string[] = []; + // targetUserSeqs.forEach((userSeq) => { + // if (!buddyList) { + // addBuddyList.push(userSeq); + // return; + // } + + // const index = buddyList.findIndex( + // (b) => b.seq === Number(userSeq) + // ); + // if (-1 < index) { + // addBuddyList.push(userSeq); + // return; + // } + // }); + + // if (addBuddyList.length > 0) { + // this.store.dispatch(add({ req: { userSeqs: addBuddyList } })); + // } } if (!!userSeqsForDelete && 0 < userSeqsForDelete.length) { @@ -304,6 +325,21 @@ export class Effects { { dispatch: false } ); + userNotificationForBuddy$ = createEffect( + () => { + return this.actions$.pipe( + ofType(CommonActions.userNotification), + withLatestFrom(this.store.pipe(select(UserSelector.user))), + tap(([action, user]) => { + if (Number(action.noti.SENDER_SEQ) !== Number(user.info.seq)) { + this.store.dispatch(buddyInfoUpdate({ noti: action.noti })); + } + }) + ); + }, + { dispatch: false } + ); + constructor( private actions$: Actions, private store: Store, diff --git a/projects/store-group/src/lib/store/buddy/reducers.ts b/projects/store-group/src/lib/store/buddy/reducers.ts index 84ebf9b..ff0f236 100644 --- a/projects/store-group/src/lib/store/buddy/reducers.ts +++ b/projects/store-group/src/lib/store/buddy/reducers.ts @@ -1,5 +1,6 @@ import { createReducer, on } from '@ngrx/store'; +import { UserInfoUpdateType } from '@ucap/protocol-info'; import { UserInfo } from '@ucap/protocol-sync'; import { initialState, adapterBuddy } from './state'; @@ -7,7 +8,8 @@ import { buddy2Success, delSuccess, updateSuccess, - nicknameSuccess + nicknameSuccess, + buddyInfoUpdate } from './actions'; export const reducer = createReducer( @@ -56,5 +58,49 @@ export const reducer = createReducer( ...state, buddies: adapterBuddy.upsertOne(userInfo, { ...state.buddies }) }; + }), + + on(buddyInfoUpdate, (state, action) => { + const noti = action.noti; + let buddyInfo: UserInfo; + + switch (noti.type) { + case UserInfoUpdateType.Image: + { + const findIdx = noti.info.indexOf('ProfileImage'); + let imgInfo: string = noti.info; + + if (findIdx > -1) { + const startIdx = noti.info.indexOf('/', findIdx); + imgInfo = noti.info.substring(startIdx); + } + + buddyInfo = { + ...state.buddies.entities[noti.SENDER_SEQ], + profileImageFile: imgInfo + }; + } + break; + case UserInfoUpdateType.Intro: + { + buddyInfo = { + ...state.buddies.entities[noti.SENDER_SEQ], + intro: noti.info + }; + } + break; + case UserInfoUpdateType.TelephoneVisible: + { + buddyInfo = { + ...state.buddies.entities[noti.SENDER_SEQ] + }; + } + break; + } + + return { + ...state, + buddies: adapterBuddy.upsertOne(buddyInfo, { ...state.buddies }) + }; }) ); diff --git a/projects/store-group/src/lib/store/buddy/state.ts b/projects/store-group/src/lib/store/buddy/state.ts index c2e96f0..8ba7a68 100644 --- a/projects/store-group/src/lib/store/buddy/state.ts +++ b/projects/store-group/src/lib/store/buddy/state.ts @@ -1,12 +1,13 @@ import { Selector, createSelector } from '@ngrx/store'; import { EntityState, createEntityAdapter } from '@ngrx/entity'; + import { UserInfo } from '@ucap/protocol-sync'; export interface BuddyState extends EntityState { syncDate: string; } export const adapterBuddy = createEntityAdapter({ - selectId: userInfo => userInfo.seq + selectId: (userInfo) => userInfo.seq }); export interface State { @@ -38,7 +39,7 @@ export function selectors(selector: Selector) { buddies: createSelector(selectBuddies, selectAllForBuddy), buddySyncDate: createSelector( selectBuddies, - buddyState => buddyState.syncDate + (buddyState) => buddyState.syncDate ) }; } diff --git a/projects/store-group/src/lib/store/group/effects.ts b/projects/store-group/src/lib/store/group/effects.ts index 39af7f4..8081506 100644 --- a/projects/store-group/src/lib/store/group/effects.ts +++ b/projects/store-group/src/lib/store/group/effects.ts @@ -20,7 +20,6 @@ import { } from '@ucap/protocol-group'; import { GroupProtocolService } from '@ucap/ng-protocol-group'; - import { SyncProtocolService } from '@ucap/ng-protocol-sync'; import { DepartmentSelector } from '@ucap/ng-store-organization'; @@ -132,17 +131,20 @@ export class Effects { return this.actions$.pipe( ofType(updateMember), withLatestFrom( + this.store.pipe(select(BuddySelector.buddies)), this.store.pipe(select(GroupSelector.groups)), this.store.pipe(select(DepartmentSelector.myDepartmentUserInfoList)) // 내 부서원 비교. ), - switchMap(([action, groupList, myDeptUserList]) => { + switchMap(([action, buddyList, groupList, myDeptUserList]) => { const targetGroup = action.targetGroup; - const targetUserSeqs = action.targetUserSeqs as any; + const targetUserSeqs = action.targetUserSeqs; // Del Buddy - let userSeqsForDelete: string[] = targetGroup.userSeqs.filter( - (v) => targetUserSeqs.indexOf(v + '') < 0 - ); + let userSeqsForDelete: string[] = targetGroup.userSeqs.filter((v) => { + if (!targetUserSeqs.includes(v)) { + return v; + } + }); // 소속부서(내부서) 고정그룹 사용시 소속부서원을 삭제하지 않는다. if ( diff --git a/projects/store-organization/ng-package.json b/projects/store-organization/ng-package.json index d50ff9f..3259cf2 100644 --- a/projects/store-organization/ng-package.json +++ b/projects/store-organization/ng-package.json @@ -11,9 +11,9 @@ "@ucap/protocol-query": "@ucap/protocol-query", "@ucap/protocol-status": "@ucap/protocol-status", "@ucap/ng-api-external": "@ucap/ng-api-external", + "@ucap/ng-protocol-info": "@ucap/ng-protocol-info", "@ucap/ng-protocol-query": "@ucap/ng-protocol-query", - "@ucap/ng-protocol-status": "@ucap/ng-protocol-status", - "@ucap/ng-store-authentication": "@ucap/ng-store-authentication" + "@ucap/ng-protocol-status": "@ucap/ng-protocol-status" } } } diff --git a/projects/store-organization/package.json b/projects/store-organization/package.json index a33cd82..f72e221 100644 --- a/projects/store-organization/package.json +++ b/projects/store-organization/package.json @@ -1,6 +1,6 @@ { "name": "@ucap/ng-store-organization", - "version": "0.0.8", + "version": "0.0.20", "publishConfig": { "registry": "https://nexus.loafle.net/repository/npm-ucap/" }, @@ -10,8 +10,8 @@ "@ucap/core": "~0.0.1", "@ucap/protocol-query": "~0.0.1", "@ucap/ng-api-external": "~0.0.1", + "@ucap/ng-protocol-info": "~0.0.1", "@ucap/ng-protocol-query": "~0.0.1", - "@ucap/ng-store-authentication": "~0.0.1", "tslib": "^1.10.0" } } diff --git a/projects/store-organization/src/lib/store/common/actions.ts b/projects/store-organization/src/lib/store/common/actions.ts index 38cd23d..8f1a80b 100644 --- a/projects/store-organization/src/lib/store/common/actions.ts +++ b/projects/store-organization/src/lib/store/common/actions.ts @@ -1,3 +1,10 @@ -import { createAction } from '@ngrx/store'; +import { createAction, props } from '@ngrx/store'; + +import { UserNotification } from '@ucap/protocol-info'; export const init = createAction('[ucap::organization::common] init'); + +export const userNotification = createAction( + '[ucap::organization::common] user Notification', + props<{ noti: UserNotification }>() +); diff --git a/projects/store-organization/src/lib/store/common/effects.ts b/projects/store-organization/src/lib/store/common/effects.ts index 4306694..aa700f0 100644 --- a/projects/store-organization/src/lib/store/common/effects.ts +++ b/projects/store-organization/src/lib/store/common/effects.ts @@ -1,8 +1,49 @@ +import { withLatestFrom, tap } from 'rxjs/operators'; + import { Injectable } from '@angular/core'; -import { Actions } from '@ngrx/effects'; +import { select, Store } from '@ngrx/store'; +import { Actions, ofType, createEffect } from '@ngrx/effects'; + +import { modifyInfoSuccess } from '../user/actions'; +import { UserSelector } from '../state'; + +import { userNotification } from './actions'; @Injectable() export class Effects { - constructor(private actions$: Actions) {} + userNotification$ = createEffect( + () => { + return this.actions$.pipe( + ofType(userNotification), + withLatestFrom(this.store.pipe(select(UserSelector.user))), + tap(([action, user]) => { + if (Number(action.noti.SENDER_SEQ) === Number(user.info.seq)) { + // my + const noti = action.noti; + + const findIdx = noti.info.indexOf('ProfileImage'); + let notInfo: string = noti.info; + + if (findIdx > -1) { + const startIdx = noti.info.indexOf('/', findIdx); + notInfo = noti.info.substring(startIdx); + } + this.store.dispatch( + modifyInfoSuccess({ + res: { + SENDER_SEQ: noti.SENDER_SEQ, + info: notInfo, + type: noti.type + } + }) + ); + } + }) + ); + }, + { dispatch: false } + ); + + constructor(private actions$: Actions, private store: Store) {} } diff --git a/projects/store-organization/src/lib/store/company/state.ts b/projects/store-organization/src/lib/store/company/state.ts index 0d7bfe2..5d451ec 100644 --- a/projects/store-organization/src/lib/store/company/state.ts +++ b/projects/store-organization/src/lib/store/company/state.ts @@ -1,5 +1,5 @@ import { Selector, createSelector } from '@ngrx/store'; -import { DeptInfo, UserInfoSS } from '@ucap/protocol-query'; + import { Company } from '@ucap/api-external'; export interface State { diff --git a/projects/store-organization/src/lib/store/department/effects.ts b/projects/store-organization/src/lib/store/department/effects.ts index 07afb26..b5aa8aa 100644 --- a/projects/store-organization/src/lib/store/department/effects.ts +++ b/projects/store-organization/src/lib/store/department/effects.ts @@ -7,7 +7,8 @@ import { Store } from '@ngrx/store'; import { Actions, ofType, createEffect } from '@ngrx/effects'; import { QueryProtocolService } from '@ucap/ng-protocol-query'; -import { LoginActions } from '@ucap/ng-store-authentication'; + +import { init as userInit } from '../user/actions'; import { dept, @@ -23,12 +24,12 @@ import { DeptDivisionCode } from '@ucap/protocol-query'; export class Effects { sessionCreatedForGroups$ = createEffect(() => { return this.actions$.pipe( - ofType(LoginActions.sessionCreated), - map(action => + ofType(userInit), + map((action) => dept({ req: { divCd: DeptDivisionCode.Organization, - companyCode: action.loginSession.companyCode + companyCode: action.user.companyCode } }) ) @@ -38,17 +39,13 @@ export class Effects { dept$ = createEffect(() => { return this.actions$.pipe( ofType(dept), - map(action => action.req), - switchMap(req => { + map((action) => action.req), + switchMap((req) => { return this.queryProtocolService.dept(req).pipe( - map(res => { - const departmentInfoList = res.departmentInfoList.sort((a, b) => - a.order < b.order ? -1 : a.order > b.order ? 1 : 0 - ); - - return deptSuccess({ departmentInfoList }); + map((res) => { + return deptSuccess({ departmentInfoList: res.departmentInfoList }); }), - catchError(error => of(deptFailure({ error }))) + catchError((error) => of(deptFailure({ error }))) ); }) ); @@ -57,24 +54,13 @@ export class Effects { myDeptUser$ = createEffect(() => { return this.actions$.pipe( ofType(myDeptUser), - map(action => action.req), - switchMap(req => { + map((action) => action.req), + switchMap((req) => { return this.queryProtocolService.deptUser(req).pipe( - map(res => { - const userInfos = res.userInfos.sort((a, b) => - a.order < b.order - ? -1 - : a.order > b.order - ? 1 - : a.name < b.name - ? -1 - : a.name > b.name - ? 1 - : 0 - ); - return myDeptUserSuccess({ userInfos }); + map((res) => { + return myDeptUserSuccess({ userInfos: res.userInfos }); }), - catchError(error => of(myDeptUserFailure({ error }))) + catchError((error) => of(myDeptUserFailure({ error }))) ); }) ); diff --git a/projects/store-organization/src/lib/store/effects.ts b/projects/store-organization/src/lib/store/effects.ts index 3a15dbc..47528f3 100644 --- a/projects/store-organization/src/lib/store/effects.ts +++ b/projects/store-organization/src/lib/store/effects.ts @@ -4,10 +4,12 @@ import { Effects as CommonEffects } from './common/effects'; import { Effects as CompanyEffects } from './company/effects'; import { Effects as DepartmentEffects } from './department/effects'; import { Effects as PresenceEffects } from './presence/effects'; +import { Effects as UserEffects } from './user/effects'; export const effects: Type[] = [ CommonEffects, CompanyEffects, DepartmentEffects, - PresenceEffects + PresenceEffects, + UserEffects ]; diff --git a/projects/store-organization/src/lib/store/presence/actions.ts b/projects/store-organization/src/lib/store/presence/actions.ts index ba717b6..3290a60 100644 --- a/projects/store-organization/src/lib/store/presence/actions.ts +++ b/projects/store-organization/src/lib/store/presence/actions.ts @@ -5,54 +5,59 @@ import { StatusNotification, StatusRequest, StatusResponse, - MessageUpdateRequest + MessageUpdateRequest, + MessageUpdateResponse } from '@ucap/protocol-status'; export const bulkInfo = createAction( - '[ucap::organization::presence] Bulk Info', + '[ucap::organization::presence] bulkInfo', props() ); export const bulkInfoSuccess = createAction( - '[ucap::organization::presence] Bulk Info Success', + '[ucap::organization::presence] bulkInfo Success', props<{ statusBulkInfoList: StatusBulkInfo[] }>() ); export const bulkInfoFailure = createAction( - '[ucap::organization::presence] Bulk Info Failure', + '[ucap::organization::presence] bulkInfo Failure', props<{ error: any }>() ); export const statusNotification = createAction( - '[ucap::organization::presence] Status Notification', + '[ucap::organization::presence] statusNotification', props<{ noti: StatusNotification }>() ); export const status = createAction( - '[ucap::organization::presence] Status', + '[ucap::organization::presence] status', props<{ req: StatusRequest }>() ); export const statusSuccess = createAction( - '[ucap::organization::presence] Status Success', + '[ucap::organization::presence] statusSuccess', props<{ res: StatusResponse; }>() ); export const statusFailure = createAction( - '[ucap::organization::presence] Status Failure', + '[ucap::organization::presence] statusFailure', props<{ error: any }>() ); export const changeMyIdleCheckTime = createAction( - '[ucap::organization::presence] Change MyIdleCheckTime', + '[ucap::organization::presence] changeMyIdleCheckTime', props<{ checkTime: number }>() ); export const messageUpdate = createAction( - '[ucap::organization::presence] status message update of Others', + '[ucap::organization::presence] messageUpdate', props<{ req: MessageUpdateRequest }>() ); +export const messageUpdateSuccess = createAction( + '[ucap::organization::presence] messageUpdate Success', + props<{ res: MessageUpdateResponse }>() +); export const messageUpdateFailure = createAction( - '[ucap::organization::presence] status message update of Others Failure', + '[ucap::organization::presence] messageUpdate Failure', props<{ error: any }>() ); diff --git a/projects/store-organization/src/lib/store/presence/effects.ts b/projects/store-organization/src/lib/store/presence/effects.ts index ce88228..52815a7 100644 --- a/projects/store-organization/src/lib/store/presence/effects.ts +++ b/projects/store-organization/src/lib/store/presence/effects.ts @@ -1,5 +1,5 @@ import { of } from 'rxjs'; -import { map, exhaustMap, catchError, tap, switchMap } from 'rxjs/operators'; +import { map, exhaustMap, catchError, concatMap } from 'rxjs/operators'; import { Injectable } from '@angular/core'; @@ -12,10 +12,12 @@ import { statusFailure, bulkInfo, bulkInfoSuccess, - bulkInfoFailure + bulkInfoFailure, + messageUpdate, + messageUpdateSuccess, + messageUpdateFailure } from './actions'; import { StatusProtocolService } from '@ucap/ng-protocol-status'; -import { StatusBulkInfo } from '@ucap/protocol-status'; @Injectable() export class Effects { @@ -23,7 +25,7 @@ export class Effects { () => { return this.actions$.pipe( ofType(bulkInfo), - switchMap((req) => { + concatMap((req) => { return this.statusProtocolService.bulkInfo(req).pipe( map((res) => { this.store.dispatch( @@ -55,6 +57,21 @@ export class Effects { ) ); + messageUpdate$ = createEffect(() => + this.actions$.pipe( + ofType(messageUpdate), + map((action) => action.req), + exhaustMap((req) => { + return this.statusProtocolService.messageUpdate(req).pipe( + map((res) => { + return messageUpdateSuccess({ res }); + }), + catchError((error) => of(messageUpdateFailure({ error }))) + ); + }) + ) + ); + constructor( private actions$: Actions, private store: Store, diff --git a/projects/store-organization/src/lib/store/presence/reducers.ts b/projects/store-organization/src/lib/store/presence/reducers.ts index e397f41..05a4e83 100644 --- a/projects/store-organization/src/lib/store/presence/reducers.ts +++ b/projects/store-organization/src/lib/store/presence/reducers.ts @@ -1,4 +1,11 @@ import { createReducer, on } from '@ngrx/store'; + +import { + StatusBulkInfo, + TerminalStatusInfo, + TerminalStatusNumber +} from '@ucap/protocol-status'; + import { initialState, State, adapterStatusBulkInfo } from './state'; import { bulkInfoSuccess, @@ -6,11 +13,6 @@ import { statusSuccess, bulkInfo } from './actions'; -import { - StatusBulkInfo, - TerminalStatusInfo, - TerminalStatusNumber -} from '@ucap/protocol-status'; export const reducer = createReducer( initialState, diff --git a/projects/store-organization/src/lib/store/presence/state.ts b/projects/store-organization/src/lib/store/presence/state.ts index f189b7e..63c19ad 100644 --- a/projects/store-organization/src/lib/store/presence/state.ts +++ b/projects/store-organization/src/lib/store/presence/state.ts @@ -1,5 +1,6 @@ import { Selector, createSelector } from '@ngrx/store'; import { EntityState, createEntityAdapter } from '@ngrx/entity'; + import { StatusBulkInfo } from '@ucap/protocol-status'; export interface StatusBulkInfoState extends EntityState {} @@ -42,12 +43,14 @@ export function selectors(selector: Selector) { selectStatusBulkInfo, ngeSelectEntitiesStatusBulkInfo ), - selectStatusBulkInfo: (userSeq: number) => - createSelector( - selectStatusBulkInfo, - ngeSelectEntitiesStatusBulkInfo, - (_, entities) => (!!entities ? entities[userSeq] : undefined) - ), + selectStatusBulkInfo: createSelector( + selectStatusBulkInfo, + (statusBulkInfoState: StatusBulkInfoState, userSeq: number) => { + return ( + statusBulkInfoState.entities && statusBulkInfoState.entities[userSeq] + ); + } + ), statusBulkInfoProcessing: createSelector( selector, (state: State) => state.statusBulkInfoProcessing diff --git a/projects/store-organization/src/lib/store/reducers.ts b/projects/store-organization/src/lib/store/reducers.ts index a0918f9..6d597fb 100644 --- a/projects/store-organization/src/lib/store/reducers.ts +++ b/projects/store-organization/src/lib/store/reducers.ts @@ -4,12 +4,14 @@ import { reducer as CommonReducer } from './common/reducers'; import { reducer as CompanyReducer } from './company/reducers'; import { reducer as DepartmentReducer } from './department/reducers'; import { reducer as PresenceReducer } from './presence/reducers'; +import { reducer as UserReducer } from './user/reducers'; export function reducers(state: any | undefined, action: Action) { return combineReducers({ common: CommonReducer, company: CompanyReducer, department: DepartmentReducer, - presence: PresenceReducer + presence: PresenceReducer, + user: UserReducer })(state, action); } diff --git a/projects/store-organization/src/lib/store/state.ts b/projects/store-organization/src/lib/store/state.ts index baff1de..05a0495 100644 --- a/projects/store-organization/src/lib/store/state.ts +++ b/projects/store-organization/src/lib/store/state.ts @@ -4,6 +4,7 @@ import * as CommonState from './common/state'; import * as CompanyState from './company/state'; import * as DepartmentState from './department/state'; import * as PresenceState from './presence/state'; +import * as UserState from './user/state'; export const KEY_FEATURE = 'organization'; @@ -12,6 +13,7 @@ export interface State { company: CompanyState.State; department: DepartmentState.State; presence: PresenceState.State; + user: UserState.State; } export const Selector = createFeatureSelector(KEY_FEATURE); @@ -31,3 +33,7 @@ export const DepartmentSelector = DepartmentState.selectors( export const PresenceSelector = PresenceState.selectors( createSelector(Selector, (state: State) => state.presence) ); + +export const UserSelector = UserState.selectors( + createSelector(Selector, (state: State) => state.user) +); diff --git a/projects/store-organization/src/lib/store/user/actions.ts b/projects/store-organization/src/lib/store/user/actions.ts new file mode 100644 index 0000000..194ae8f --- /dev/null +++ b/projects/store-organization/src/lib/store/user/actions.ts @@ -0,0 +1,37 @@ +import { createAction, props } from '@ngrx/store'; + +import { UserResponse, UserRequest, User } from '@ucap/protocol-info'; + +/** + * info user request + */ +export const init = createAction( + '[ucap::organization::user] init', + props<{ user: User }>() +); + +/** + * modifyInfo user request + */ +export const modifyInfo = createAction( + '[ucap::organization::user] modifyInfo', + props<{ req: UserRequest }>() +); + +/** + * Success of modifyInfo request + */ +export const modifyInfoSuccess = createAction( + '[ucap::organization::user] modifyInfo Success', + props<{ + res: UserResponse; + }>() +); + +/** + * Failure of modifyInfo request + */ +export const modifyInfoFailure = createAction( + '[ucap::organization::user] modifyInfo Failure', + props<{ error: any }>() +); diff --git a/projects/store-organization/src/lib/store/user/effects.ts b/projects/store-organization/src/lib/store/user/effects.ts new file mode 100644 index 0000000..288ae41 --- /dev/null +++ b/projects/store-organization/src/lib/store/user/effects.ts @@ -0,0 +1,37 @@ +import { of } from 'rxjs'; +import { map, exhaustMap, catchError } from 'rxjs/operators'; + +import { Injectable } from '@angular/core'; + +import { Actions, createEffect, ofType } from '@ngrx/effects'; + +import { UserResponse } from '@ucap/protocol-info'; + +import { InfoProtocolService } from '@ucap/ng-protocol-info'; + +import { modifyInfo, modifyInfoSuccess, modifyInfoFailure } from './actions'; + +@Injectable() +export class Effects { + infoUser$ = createEffect(() => + this.actions$.pipe( + ofType(modifyInfo), + map((action) => action.req), + exhaustMap((req) => + this.infoProtocolService.user(req).pipe( + map((res: UserResponse) => { + return modifyInfoSuccess({ + res + }); + }), + catchError((error) => of(modifyInfoFailure({ error }))) + ) + ) + ) + ); + + constructor( + private actions$: Actions, + private infoProtocolService: InfoProtocolService + ) {} +} diff --git a/projects/store-organization/src/lib/store/user/reducers.ts b/projects/store-organization/src/lib/store/user/reducers.ts new file mode 100644 index 0000000..9802fed --- /dev/null +++ b/projects/store-organization/src/lib/store/user/reducers.ts @@ -0,0 +1,89 @@ +import { createReducer, on } from '@ngrx/store'; + +import { UserInfoUpdateType } from '@ucap/protocol-info'; +import { MessageIndexType } from '@ucap/protocol-status'; + +import { messageUpdateSuccess } from '../presence/actions'; + +import { init, modifyInfoSuccess } from './actions'; +import { State, initialState } from './state'; + +export const reducer = createReducer( + initialState, + on(init, (state, action) => { + return { + ...state, + user: action.user + } as State; + }), + + on(modifyInfoSuccess, (state, action) => { + let user = { + ...state.user + }; + + switch (action.res.type) { + case UserInfoUpdateType.Image: + user = { + ...user, + info: { + ...user.info, + profileImageFile: action.res.info + } + }; + break; + case UserInfoUpdateType.Intro: + user = { + ...user, + info: { + ...user.info, + intro: action.res.info + } + }; + break; + case UserInfoUpdateType.TelephoneVisible: + break; + default: + break; + } + return { + ...state, + user + }; + }), + + on(messageUpdateSuccess, (state, action) => { + let user = { + ...state.user + }; + + switch (action.res.index) { + case MessageIndexType.First: + user = { + ...user, + statusMessage1: action.res.statusMessage + }; + break; + case MessageIndexType.Second: + user = { + ...user, + statusMessage2: action.res.statusMessage + }; + break; + case MessageIndexType.Third: + user = { + ...user, + statusMessage3: action.res.statusMessage + }; + break; + + default: + break; + } + + return { + ...state, + user + } as State; + }) +); diff --git a/projects/store-organization/src/lib/store/user/state.ts b/projects/store-organization/src/lib/store/user/state.ts new file mode 100644 index 0000000..973f803 --- /dev/null +++ b/projects/store-organization/src/lib/store/user/state.ts @@ -0,0 +1,20 @@ +import { Selector, createSelector } from '@ngrx/store'; +import { EntityState } from '@ngrx/entity'; +import { StatusBulkInfo } from '@ucap/protocol-status'; +import { User } from '@ucap/protocol-info'; + +export interface StatusBulkInfoState extends EntityState {} + +export interface State { + user: User | undefined; +} + +export const initialState: State = { + user: undefined +}; + +export function selectors(selector: Selector) { + return { + user: createSelector(selector, (state: State) => state.user) + }; +} diff --git a/projects/store-organization/src/public-api.ts b/projects/store-organization/src/public-api.ts index bf49c75..f66b94e 100644 --- a/projects/store-organization/src/public-api.ts +++ b/projects/store-organization/src/public-api.ts @@ -6,10 +6,17 @@ import * as CommonActions from './lib/store/common/actions'; import * as CompanyActions from './lib/store/company/actions'; import * as DepartmentActions from './lib/store/department/actions'; import * as PresenceActions from './lib/store/presence/actions'; +import * as UserActions from './lib/store/user/actions'; export * from './lib/config/module-config'; -export { CommonActions, CompanyActions, DepartmentActions, PresenceActions }; +export { + CommonActions, + CompanyActions, + DepartmentActions, + PresenceActions, + UserActions +}; export * from './lib/store/state'; diff --git a/projects/ui-authentication/package.json b/projects/ui-authentication/package.json index 166ff6e..382ac62 100644 --- a/projects/ui-authentication/package.json +++ b/projects/ui-authentication/package.json @@ -1,6 +1,6 @@ { "name": "@ucap/ng-ui-authentication", - "version": "0.0.25", + "version": "0.0.29", "publishConfig": { "registry": "https://nexus.loafle.net/repository/npm-ucap/" }, diff --git a/projects/ui-authentication/src/lib/components/change-password.component.html b/projects/ui-authentication/src/lib/components/change-password.component.html index 2697856..c545fbb 100644 --- a/projects/ui-authentication/src/lib/components/change-password.component.html +++ b/projects/ui-authentication/src/lib/components/change-password.component.html @@ -1,6 +1,6 @@
- + {{ 'accounts.fieldCurrentPassword' | ucapI18n }} - + {{ 'accounts.fieldNewPassword' | ucapI18n }} - + {{ 'accounts.fieldNewPasswordConfirm' | ucapI18n }} - {{ 'login.fields.company' | ucapI18n }} + {{ + 'authentication:login.fields.company' | ucapI18n + }} - - {{ 'login.fields.loginId' | ucapI18n }} + + {{ + 'authentication:login.fields.loginId' | ucapI18n + }}
@@ -95,8 +101,10 @@ - - {{ 'login.fields.loginPw' | ucapI18n }} + + {{ + 'authentication:login.fields.loginPw' | ucapI18n + }} - {{ 'login.errors.requireCompany' | ucapI18n }} + {{ 'authentication:login.errors.requireCompany' | ucapI18n }} @@ -156,11 +164,11 @@ - {{ 'login.labels.doLogin' | ucapI18n }} + {{ 'authentication:login.labels.doLogin' | ucapI18n }} - {{ 'login.errors.attemptsExceeded' | ucapI18n }} + {{ 'authentication:login.errors.attemptsExceeded' | ucapI18n }} ( {{ moment diff --git a/projects/ui-authentication/src/public-api.ts b/projects/ui-authentication/src/public-api.ts index 3de17ef..99e37fd 100644 --- a/projects/ui-authentication/src/public-api.ts +++ b/projects/ui-authentication/src/public-api.ts @@ -2,6 +2,10 @@ * Public API Surface of ui-authentication */ +export * from './lib/components/change-password.component'; +export * from './lib/components/login.component'; + export * from './lib/config/module-config'; +export * from './lib/config/token'; export * from './lib/authentication-ui.module'; diff --git a/projects/ui-chat/ng-package.json b/projects/ui-chat/ng-package.json index b98dadd..c072cd4 100644 --- a/projects/ui-chat/ng-package.json +++ b/projects/ui-chat/ng-package.json @@ -5,12 +5,16 @@ "entryFile": "src/public-api.ts", "styleIncludePaths": ["./src/assets/scss"], "umdModuleIds": { + "moment": "moment", "ngx-perfect-scrollbar": "ngx-perfect-scrollbar", "@ucap/core": "@ucap/core", + "@ucap/api": "@ucap/api", + "@ucap/protocol-event": "@ucap/protocol-event", "@ucap/protocol-room": "@ucap/protocol-room", "@ucap/ng-logger": "@ucap/ng-logger", "@ucap/ng-i18n": "@ucap/ng-i18n", - "@ucap/ng-ui": "@ucap/ng-ui" + "@ucap/ng-ui": "@ucap/ng-ui", + "@ucap/ng-ui-organization": "@ucap/ng-ui-organization" } } } diff --git a/projects/ui-chat/package.json b/projects/ui-chat/package.json index 6d7f06e..5674b47 100644 --- a/projects/ui-chat/package.json +++ b/projects/ui-chat/package.json @@ -1,6 +1,6 @@ { "name": "@ucap/ng-ui-chat", - "version": "0.0.13", + "version": "0.0.72", "publishConfig": { "registry": "https://nexus.loafle.net/repository/npm-ucap/" }, diff --git a/projects/ui-chat/src/lib/chat-ui.module.ts b/projects/ui-chat/src/lib/chat-ui.module.ts index 565e7aa..ef984d7 100644 --- a/projects/ui-chat/src/lib/chat-ui.module.ts +++ b/projects/ui-chat/src/lib/chat-ui.module.ts @@ -3,25 +3,36 @@ import { CommonModule } from '@angular/common'; import { FlexLayoutModule } from '@angular/flex-layout'; -import { ScrollingModule } from '@angular/cdk/scrolling'; - import { MatBadgeModule } from '@angular/material/badge'; import { MatButtonModule } from '@angular/material/button'; import { MatCheckboxModule } from '@angular/material/checkbox'; import { MatIconModule } from '@angular/material/icon'; import { MatRippleModule } from '@angular/material/core'; import { MatTreeModule } from '@angular/material/tree'; - +import { MatProgressBarModule } from '@angular/material/progress-bar'; +import { MatTooltipModule } from '@angular/material/tooltip'; import { MatMenuModule } from '@angular/material/menu'; + import { PerfectScrollbarModule } from 'ngx-perfect-scrollbar'; import { UiModule } from '@ucap/ng-ui'; +import { OrganizationUiModule } from '@ucap/ng-ui-organization'; import { I18nModule, UCAP_I18N_NAMESPACE } from '@ucap/ng-i18n'; import { ModuleConfig } from './config/module-config'; import { _MODULE_CONFIG } from './config/token'; +import { + ImageListComponent, + ImageListNodeDirective +} from './components/image-list.component'; +import { + FileListComponent, + FileListNodeDirective +} from './components/file-list.component'; +import { ImageListItem01Component } from './components/image-list-item-01.component'; +import { FileListItem01Component } from './components/file-list-item-01.component'; import { RoomExpansionComponent, RoomExpansionHeaderDirective, @@ -45,10 +56,19 @@ import { ReadHereComponent } from './components/message-box/read-here.component' import { DateSplitterComponent } from './components/message-box/date-splitter.component'; import { SmsComponent } from './components/message-box/sms.component'; import { ReplyComponent } from './components/message-box/reply.component'; +import { + ChatItemListComponent, + ChatItemListNodeDirective +} from './components/item-list.component'; const COMPONENTS = [ + ImageListItem01Component, + FileListItem01Component, + FileListComponent, + ImageListComponent, RoomExpansionComponent, RoomListItem01Component, + ChatItemListComponent, InformationComponent, TextComponent, @@ -72,7 +92,13 @@ const COMPONENTS = [ ]; const DIALOGS = []; const PIPES = []; -const DIRECTIVES = [RoomExpansionHeaderDirective, RoomExpansionNodeDirective]; +const DIRECTIVES = [ + RoomExpansionHeaderDirective, + RoomExpansionNodeDirective, + FileListNodeDirective, + ImageListNodeDirective, + ChatItemListNodeDirective +]; const SERVICES = []; @NgModule({ @@ -86,7 +112,6 @@ export class ChatUiRootModule {} imports: [ CommonModule, FlexLayoutModule, - ScrollingModule, MatBadgeModule, MatButtonModule, @@ -95,10 +120,13 @@ export class ChatUiRootModule {} MatRippleModule, MatTreeModule, MatMenuModule, + MatProgressBarModule, + MatTooltipModule, PerfectScrollbarModule, I18nModule, + OrganizationUiModule, UiModule ], exports: [...COMPONENTS, ...DIRECTIVES, ...PIPES], diff --git a/projects/ui-chat/src/lib/components/file-list-item-01.component.html b/projects/ui-chat/src/lib/components/file-list-item-01.component.html new file mode 100644 index 0000000..65a4c2e --- /dev/null +++ b/projects/ui-chat/src/lib/components/file-list-item-01.component.html @@ -0,0 +1,95 @@ +
+
+ + +
+
+
+
    +
  • + {{ + fileInfo.sentMessageJson.fileName + }} + {{ + senderInfo | ucapOrganizationTranslate: 'name' + }} +
  • +
  • + {{ + fileInfo.sentMessageJson.attachmentSize | ucapBytes + }} + + {{ + fileInfo.sentMessageJson.attachmentRegDate | ucapDate: 'MM.DD' + }} + + ~ + {{ + fileInfo.sentMessageJson.attachmentRegDate + | ucapDate: 'MM.DD':fileRetentionPeriodOptions + }} + + +
  • +
  • + {{ downloadedCount }}/{{ downloadTotalCount }} +
  • +
  • + + +
  • +
+
+
+ + + +
+ {{ + 'common:file.downloading' | ucapI18n + }} + {{ fileDownloadItem.downloadingProgress$ | async }}% +
+
+
diff --git a/projects/ui-chat/src/lib/components/file-list-item-01.component.scss b/projects/ui-chat/src/lib/components/file-list-item-01.component.scss new file mode 100644 index 0000000..21e717b --- /dev/null +++ b/projects/ui-chat/src/lib/components/file-list-item-01.component.scss @@ -0,0 +1,21 @@ +@import '~@ucap/ng-ui-material/material'; + +.ucap-chat-file-list-item-01-container { + display: flex; + flex-direction: row; + align-items: center; + .mime-icon { + flex: 0 0 60px; + } + ul { + flex: 1 1 auto; + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + li { + display: flex; + flex-direction: column; + } + } +} diff --git a/projects/ui-organization/src/lib/components/profile-list-item.component.spec.ts b/projects/ui-chat/src/lib/components/file-list-item-01.component.spec.ts similarity index 91% rename from projects/ui-organization/src/lib/components/profile-list-item.component.spec.ts rename to projects/ui-chat/src/lib/components/file-list-item-01.component.spec.ts index 80e7d67..9965458 100644 --- a/projects/ui-organization/src/lib/components/profile-list-item.component.spec.ts +++ b/projects/ui-chat/src/lib/components/file-list-item-01.component.spec.ts @@ -4,7 +4,7 @@ import { DebugElement } from '@angular/core'; import { ProfileListItemComponent } from './profile-list-item.component'; -describe('ucap::ui-organization::ProfileListItemComponent', () => { +describe('ucap::ucap::organization::ProfileListItemComponent', () => { let component: ProfileListItemComponent; let fixture: ComponentFixture; diff --git a/projects/ui-chat/src/lib/components/file-list-item-01.component.ts b/projects/ui-chat/src/lib/components/file-list-item-01.component.ts new file mode 100644 index 0000000..4a1bf66 --- /dev/null +++ b/projects/ui-chat/src/lib/components/file-list-item-01.component.ts @@ -0,0 +1,93 @@ +import { + Component, + OnInit, + OnDestroy, + ChangeDetectionStrategy, + ChangeDetectorRef, + Input, + EventEmitter, + Output +} from '@angular/core'; + +import { Subject } from 'rxjs'; +import { + UserInfo as RoomUserInfo, + UserInfoShort as RoomUserInfoShort +} from '@ucap/protocol-room'; +import { FileInfo } from '@ucap/protocol-file'; +import { DateOptions } from '@ucap/ng-ui'; +import { FileDownloadItem } from '@ucap/api'; + +@Component({ + selector: 'ucap-chat-file-list-item-01', + templateUrl: './file-list-item-01.component.html', + styleUrls: ['./file-list-item-01.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class FileListItem01Component implements OnInit, OnDestroy { + @Input() + fileInfo: FileInfo; + + @Input() + senderInfo: RoomUserInfo | RoomUserInfoShort; + + @Input() + downloadTotalCount: number; + @Input() + downloadedCount: number; + + @Input() + fileRetentionPeriod: number; + + @Input() + expired = false; + + @Input() + checked = false; + + @Input() + checkable = true; + + @Input() + fileDownloadItem: FileDownloadItem; + + @Output() + changeCheck = new EventEmitter<{ + checked: boolean; + fileInfo: FileInfo; + fileDownloadItem: FileDownloadItem; + }>(); + + @Output() + showDownCheck = new EventEmitter(); + + private ngOnDestroySubject: Subject = new Subject(); + constructor(private changeDetectorRef: ChangeDetectorRef) {} + + ngOnInit(): void {} + + ngOnDestroy(): void { + if (!!this.ngOnDestroySubject) { + this.ngOnDestroySubject.next(); + this.ngOnDestroySubject.complete(); + } + } + + onClickShowDownCheck() { + this.showDownCheck.emit(this.fileInfo); + } + + onChangeCheck(checked: boolean) { + this.changeCheck.emit({ + checked, + fileInfo: this.fileInfo, + fileDownloadItem: this.fileDownloadItem + }); + } + + get fileRetentionPeriodOptions(): DateOptions { + return { + manipulate: { amount: this.fileRetentionPeriod, unit: 'days' } + }; + } +} diff --git a/projects/ui-chat/src/lib/components/file-list.component.html b/projects/ui-chat/src/lib/components/file-list.component.html new file mode 100644 index 0000000..f1d1c02 --- /dev/null +++ b/projects/ui-chat/src/lib/components/file-list.component.html @@ -0,0 +1,15 @@ +
+ + + + + +
diff --git a/projects/ui-chat/src/lib/components/file-list.component.scss b/projects/ui-chat/src/lib/components/file-list.component.scss new file mode 100644 index 0000000..0520f96 --- /dev/null +++ b/projects/ui-chat/src/lib/components/file-list.component.scss @@ -0,0 +1,7 @@ +@import '~@ucap/ng-ui-material/material'; + +.ucap-chat-file-list-container { + display: flex; + flex-direction: row; + flex-wrap: wrap; +} diff --git a/projects/ui-chat/src/lib/components/file-list.component.spec.ts b/projects/ui-chat/src/lib/components/file-list.component.spec.ts new file mode 100644 index 0000000..3b9025e --- /dev/null +++ b/projects/ui-chat/src/lib/components/file-list.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { DebugElement } from '@angular/core'; +import { FileListComponent } from './file-list.component'; + +describe('ucap::chat::FileListComponent', () => { + let component: FileListComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [FileListComponent] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(FileListComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/projects/ui-chat/src/lib/components/file-list.component.stories.ts b/projects/ui-chat/src/lib/components/file-list.component.stories.ts new file mode 100644 index 0000000..5430b3f --- /dev/null +++ b/projects/ui-chat/src/lib/components/file-list.component.stories.ts @@ -0,0 +1,26 @@ +import { moduleMetadata } from '@storybook/angular'; +import { action } from '@storybook/addon-actions'; +import { linkTo } from '@storybook/addon-links'; + +import { BrowserModule } from '@angular/platform-browser'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; + +import { ChatUiModule } from '../chat-ui.module'; + +import { FileListComponent } from './file-list.component'; + +export default { + title: 'ui-chat::FileListComponent', + component: FileListComponent, + decorators: [ + moduleMetadata({ + imports: [BrowserModule, BrowserAnimationsModule, ChatUiModule], + providers: [] + }) + ] +}; + +export const Default = () => ({ + component: FileListComponent, + props: {} +}); diff --git a/projects/ui-chat/src/lib/components/file-list.component.theme.scss b/projects/ui-chat/src/lib/components/file-list.component.theme.scss new file mode 100644 index 0000000..0603234 --- /dev/null +++ b/projects/ui-chat/src/lib/components/file-list.component.theme.scss @@ -0,0 +1,24 @@ +@import '~@ucap/ng-ui-material/material'; + +@mixin ucap-organization-profile-list-theme($theme) { + $is-dark-theme: map-get($theme, is-dark); + $primary: map-get($theme, primary); + $accent: map-get($theme, accent); + $warn: map-get($theme, warn); + $background: map-get($theme, background); + $foreground: map-get($theme, foreground); + + .ucap-organization-profile-list-container { + border-color: mat-color($foreground, secondary-text); + } +} + +@mixin ucap-organization-profile-list-typography($config) { + .ucap-organization-profile-list-container { + .searchword-container { + .form-field-input-searchword { + font-family: mat-font-family($config); + } + } + } +} diff --git a/projects/ui-chat/src/lib/components/file-list.component.ts b/projects/ui-chat/src/lib/components/file-list.component.ts new file mode 100644 index 0000000..cbf2d9b --- /dev/null +++ b/projects/ui-chat/src/lib/components/file-list.component.ts @@ -0,0 +1,55 @@ +import { Subject } from 'rxjs'; + +import { + Component, + OnInit, + OnDestroy, + ChangeDetectionStrategy, + ChangeDetectorRef, + Input, + Directive, + ContentChild, + TemplateRef +} from '@angular/core'; + +import { FileInfo } from '@ucap/protocol-file'; + +@Directive({ + selector: '[ucapChatFileListNode]' +}) +export class FileListNodeDirective {} + +@Component({ + selector: 'ucap-chat-file-list', + templateUrl: './file-list.component.html', + styleUrls: ['./file-list.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class FileListComponent implements OnInit, OnDestroy { + @Input() + fileInfos: FileInfo[]; + + @Input() + vsItemSize: number; + + @ContentChild(FileListNodeDirective, { + read: TemplateRef, + static: false + }) + nodeTemplate: TemplateRef; + + private ngOnDestroySubject: Subject; + + constructor(private changeDetectorRef: ChangeDetectorRef) {} + + ngOnInit(): void { + this.ngOnDestroySubject = new Subject(); + } + + ngOnDestroy(): void { + if (!!this.ngOnDestroySubject) { + this.ngOnDestroySubject.next(); + this.ngOnDestroySubject.complete(); + } + } +} diff --git a/projects/ui-chat/src/lib/components/image-list-item-01.component.html b/projects/ui-chat/src/lib/components/image-list-item-01.component.html new file mode 100644 index 0000000..c324cb9 --- /dev/null +++ b/projects/ui-chat/src/lib/components/image-list-item-01.component.html @@ -0,0 +1,92 @@ +
+
+ + + +
+
+
    +
  • + +
  • +
  • + +
  • +
  • + +
  • +
+
+
+ + + +
+ {{ + 'common:file.downloading' | ucapI18n + }} + {{ fileDownloadItem.downloadingProgress$ | async }}% +
+
+
diff --git a/projects/ui-chat/src/lib/components/image-list-item-01.component.scss b/projects/ui-chat/src/lib/components/image-list-item-01.component.scss new file mode 100644 index 0000000..23b8c6d --- /dev/null +++ b/projects/ui-chat/src/lib/components/image-list-item-01.component.scss @@ -0,0 +1,44 @@ +@import '~@ucap/ng-ui-material/material'; + +.ucap-chat-image-list-item-01-container { + width: 100%; + height: 100%; + //padding-left: 4px; + //margin-left: -4px; + .contents { + width: 100%; + height: 100%; + display: flex; + flex-direction: row; + align-items: center; + img { + } + .mat-checkbox { + position: absolute; + //right: 0; + //top: 0; + } + } + .btn-box { + position: absolute; + width: 100%; + height: 100%; + bottom: 0; + //background-color: rgba(0, 0, 0, 0.5); + ul { + width: 100%; + display: flex; + flex-direction: row; + align-items: center; + height: 30px; + position: absolute; + //bottom: 0; + li { + button { + //width: 30%; + //height: 30px; + } + } + } + } +} diff --git a/projects/ui-chat/src/lib/components/image-list-item-01.component.spec.ts b/projects/ui-chat/src/lib/components/image-list-item-01.component.spec.ts new file mode 100644 index 0000000..9965458 --- /dev/null +++ b/projects/ui-chat/src/lib/components/image-list-item-01.component.spec.ts @@ -0,0 +1,26 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { DebugElement } from '@angular/core'; + +import { ProfileListItemComponent } from './profile-list-item.component'; + +describe('ucap::ucap::organization::ProfileListItemComponent', () => { + let component: ProfileListItemComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ProfileListItemComponent] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ProfileListItemComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/projects/ui-chat/src/lib/components/image-list-item-01.component.ts b/projects/ui-chat/src/lib/components/image-list-item-01.component.ts new file mode 100644 index 0000000..abb625f --- /dev/null +++ b/projects/ui-chat/src/lib/components/image-list-item-01.component.ts @@ -0,0 +1,122 @@ +import { + Component, + OnInit, + OnDestroy, + ChangeDetectionStrategy, + ChangeDetectorRef, + Input, + EventEmitter, + Output +} from '@angular/core'; + +import { Subject } from 'rxjs'; +import { FileInfo } from '@ucap/protocol-file'; +import { FileDownloadItem } from '@ucap/api'; +import { FileEventJson } from '@ucap/protocol-event'; + +@Component({ + selector: 'ucap-chat-image-list-item-01', + templateUrl: './image-list-item-01.component.html', + styleUrls: ['./image-list-item-01.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class ImageListItem01Component implements OnInit, OnDestroy { + private ngOnDestroySubject: Subject = new Subject(); + + @Input() + fileInfo: FileInfo; + + @Input() + expired = false; + + @Input() + checked = false; + + @Input() + checkable = true; + + @Input() + fileDownloadItem: FileDownloadItem; + + @Output() + changeCheck = new EventEmitter<{ + checked: boolean; + fileInfo: FileInfo; + fileDownloadItem: FileDownloadItem; + }>(); + + @Output() + save = new EventEmitter<{ + fileInfo: FileEventJson; + fileDownloadItem: FileDownloadItem; + type: string; + }>(); + + @Output() + openViewer = new EventEmitter(); + + @Output() + delete = new EventEmitter(); + + showOverButtons = false; + + constructor(private changeDetectorRef: ChangeDetectorRef) {} + + ngOnInit(): void {} + + ngOnDestroy(): void { + if (!!this.ngOnDestroySubject) { + this.ngOnDestroySubject.next(); + this.ngOnDestroySubject.complete(); + } + } + + onMouseover(event: MouseEvent): void { + if (!this.expired) { + this.showOverButtons = true; + } + event.preventDefault(); + event.stopPropagation(); + } + onMouseleave(event: MouseEvent): void { + this.showOverButtons = false; + event.preventDefault(); + event.stopPropagation(); + } + + onChangeCheck(checked: boolean) { + this.changeCheck.emit({ + checked, + fileInfo: this.fileInfo, + fileDownloadItem: this.fileDownloadItem + }); + } + + onClickSave(event: MouseEvent) { + event.stopPropagation(); + event.preventDefault(); + + const param = { + fileInfo: { + // require parameters. + fileName: this.fileInfo.name, + attachmentSeq: this.fileInfo.seq + } as FileEventJson, + fileDownloadItem: this.fileDownloadItem, + type: 'save' + }; + this.save.emit(param); + } + onClickOpenViewer(event: MouseEvent) { + event.stopPropagation(); + event.preventDefault(); + + this.openViewer.emit(this.fileInfo); + } + onClickDelete(event: MouseEvent) { + event.stopPropagation(); + event.preventDefault(); + + this.delete.emit(this.fileInfo); + } +} diff --git a/projects/ui-chat/src/lib/components/image-list.component.html b/projects/ui-chat/src/lib/components/image-list.component.html new file mode 100644 index 0000000..55d1a26 --- /dev/null +++ b/projects/ui-chat/src/lib/components/image-list.component.html @@ -0,0 +1,15 @@ +
+ + + + + +
diff --git a/projects/ui-chat/src/lib/components/image-list.component.scss b/projects/ui-chat/src/lib/components/image-list.component.scss new file mode 100644 index 0000000..5b10d68 --- /dev/null +++ b/projects/ui-chat/src/lib/components/image-list.component.scss @@ -0,0 +1,7 @@ +@import '~@ucap/ng-ui-material/material'; + +.ucap-chat-image-list-container { + display: flex; + flex-direction: row; + flex-wrap: wrap; +} diff --git a/projects/ui-chat/src/lib/components/image-list.component.spec.ts b/projects/ui-chat/src/lib/components/image-list.component.spec.ts new file mode 100644 index 0000000..e70d02c --- /dev/null +++ b/projects/ui-chat/src/lib/components/image-list.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { DebugElement } from '@angular/core'; +import { ImageListComponent } from './image-list.component'; + +describe('ucap::chat::ImageListComponent', () => { + let component: ImageListComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ImageListComponent] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ImageListComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/projects/ui-chat/src/lib/components/image-list.component.stories.ts b/projects/ui-chat/src/lib/components/image-list.component.stories.ts new file mode 100644 index 0000000..42370cc --- /dev/null +++ b/projects/ui-chat/src/lib/components/image-list.component.stories.ts @@ -0,0 +1,26 @@ +import { moduleMetadata } from '@storybook/angular'; +import { action } from '@storybook/addon-actions'; +import { linkTo } from '@storybook/addon-links'; + +import { BrowserModule } from '@angular/platform-browser'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; + +import { ChatUiModule } from '../chat-ui.module'; + +import { ImageListComponent } from './image-list.component'; + +export default { + title: 'ui-chat::ImageListComponent', + component: ImageListComponent, + decorators: [ + moduleMetadata({ + imports: [BrowserModule, BrowserAnimationsModule, ChatUiModule], + providers: [] + }) + ] +}; + +export const Default = () => ({ + component: ImageListComponent, + props: {} +}); diff --git a/projects/ui-chat/src/lib/components/image-list.component.theme.scss b/projects/ui-chat/src/lib/components/image-list.component.theme.scss new file mode 100644 index 0000000..0603234 --- /dev/null +++ b/projects/ui-chat/src/lib/components/image-list.component.theme.scss @@ -0,0 +1,24 @@ +@import '~@ucap/ng-ui-material/material'; + +@mixin ucap-organization-profile-list-theme($theme) { + $is-dark-theme: map-get($theme, is-dark); + $primary: map-get($theme, primary); + $accent: map-get($theme, accent); + $warn: map-get($theme, warn); + $background: map-get($theme, background); + $foreground: map-get($theme, foreground); + + .ucap-organization-profile-list-container { + border-color: mat-color($foreground, secondary-text); + } +} + +@mixin ucap-organization-profile-list-typography($config) { + .ucap-organization-profile-list-container { + .searchword-container { + .form-field-input-searchword { + font-family: mat-font-family($config); + } + } + } +} diff --git a/projects/ui-chat/src/lib/components/image-list.component.ts b/projects/ui-chat/src/lib/components/image-list.component.ts new file mode 100644 index 0000000..caa5d4e --- /dev/null +++ b/projects/ui-chat/src/lib/components/image-list.component.ts @@ -0,0 +1,55 @@ +import { Subject } from 'rxjs'; + +import { + Component, + OnInit, + OnDestroy, + ChangeDetectionStrategy, + ChangeDetectorRef, + Input, + Directive, + ContentChild, + TemplateRef +} from '@angular/core'; + +import { FileInfo } from '@ucap/protocol-file'; + +@Directive({ + selector: '[ucapChatImageListNode]' +}) +export class ImageListNodeDirective {} + +@Component({ + selector: 'ucap-chat-image-list', + templateUrl: './image-list.component.html', + styleUrls: ['./image-list.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class ImageListComponent implements OnInit, OnDestroy { + @Input() + fileInfos: FileInfo[]; + + @Input() + vsItemSize: number; + + @ContentChild(ImageListNodeDirective, { + read: TemplateRef, + static: false + }) + nodeTemplate: TemplateRef; + + private ngOnDestroySubject: Subject; + + constructor(private changeDetectorRef: ChangeDetectorRef) {} + + ngOnInit(): void { + this.ngOnDestroySubject = new Subject(); + } + + ngOnDestroy(): void { + if (!!this.ngOnDestroySubject) { + this.ngOnDestroySubject.next(); + this.ngOnDestroySubject.complete(); + } + } +} diff --git a/projects/ui-chat/src/lib/components/item-list.component.html b/projects/ui-chat/src/lib/components/item-list.component.html new file mode 100644 index 0000000..89da1a8 --- /dev/null +++ b/projects/ui-chat/src/lib/components/item-list.component.html @@ -0,0 +1,17 @@ +
+ + +
+ +
+
+
+
diff --git a/projects/ui-chat/src/lib/components/item-list.component.scss b/projects/ui-chat/src/lib/components/item-list.component.scss new file mode 100644 index 0000000..0d07f4b --- /dev/null +++ b/projects/ui-chat/src/lib/components/item-list.component.scss @@ -0,0 +1,11 @@ +@import '~@ucap/ng-ui-material/material'; + +.ucap-chat-item-list-container { + width: 100%; + height: 100%; + + ucap-virtual-scroll-viewport { + width: 100%; + height: 100%; + } +} diff --git a/projects/ui-chat/src/lib/components/item-list.component.spec.ts b/projects/ui-chat/src/lib/components/item-list.component.spec.ts new file mode 100644 index 0000000..54872d3 --- /dev/null +++ b/projects/ui-chat/src/lib/components/item-list.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { DebugElement } from '@angular/core'; +import { ChatItemListComponent } from './item-list.component'; + +describe('ucap::organization::ChatItemListComponent', () => { + let component: ChatItemListComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ChatItemListComponent] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ChatItemListComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/projects/ui-chat/src/lib/components/item-list.component.stories.ts b/projects/ui-chat/src/lib/components/item-list.component.stories.ts new file mode 100644 index 0000000..9012a01 --- /dev/null +++ b/projects/ui-chat/src/lib/components/item-list.component.stories.ts @@ -0,0 +1,22 @@ +import { moduleMetadata } from '@storybook/angular'; + +import { BrowserModule } from '@angular/platform-browser'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; + +import { ChatItemListComponent } from './item-list.component'; + +export default { + title: 'ui-chat::ChatItemListComponent', + component: ChatItemListComponent, + decorators: [ + moduleMetadata({ + imports: [BrowserModule, BrowserAnimationsModule], + providers: [] + }) + ] +}; + +export const Default = () => ({ + component: ChatItemListComponent, + props: {} +}); diff --git a/projects/ui-chat/src/lib/components/item-list.component.theme.scss b/projects/ui-chat/src/lib/components/item-list.component.theme.scss new file mode 100644 index 0000000..9b10e18 --- /dev/null +++ b/projects/ui-chat/src/lib/components/item-list.component.theme.scss @@ -0,0 +1,24 @@ +@import '~@ucap/ng-ui-material/material'; + +@mixin ucap-chat-item-list-theme($theme) { + $is-dark-theme: map-get($theme, is-dark); + $primary: map-get($theme, primary); + $accent: map-get($theme, accent); + $warn: map-get($theme, warn); + $background: map-get($theme, background); + $foreground: map-get($theme, foreground); + + .ucap-chat-item-list-container { + border-color: mat-color($foreground, secondary-text); + } +} + +@mixin ucap-chat-item-list-typography($config) { + .ucap-chat-item-list-container { + .searchword-container { + .form-field-input-searchword { + font-family: mat-font-family($config); + } + } + } +} diff --git a/projects/ui-chat/src/lib/components/item-list.component.ts b/projects/ui-chat/src/lib/components/item-list.component.ts new file mode 100644 index 0000000..078bf4a --- /dev/null +++ b/projects/ui-chat/src/lib/components/item-list.component.ts @@ -0,0 +1,71 @@ +import { Subject } from 'rxjs'; + +import { + Component, + OnInit, + OnDestroy, + ChangeDetectionStrategy, + ChangeDetectorRef, + Input, + Directive, + ContentChild, + TemplateRef, + ViewChild, + ElementRef +} from '@angular/core'; + +import { ObjectUtil } from '@ucap/core'; + +import { UserInfo, UserInfoShort, RoomInfo } from '@ucap/protocol-room'; + +import { VirtualScrollViewportComponent } from '@ucap/ng-ui'; + +export type ItemInfoTypes = UserInfo | UserInfoShort | RoomInfo; + +@Directive({ + selector: '[ucapChatItemListNode]' +}) +export class ChatItemListNodeDirective {} + +@Component({ + selector: 'ucap-chat-item-list', + templateUrl: './item-list.component.html', + styleUrls: ['./item-list.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class ChatItemListComponent implements OnInit, OnDestroy { + @Input() + itemInfos: ItemInfoTypes[]; + + @Input() + vsItemSize: number; + + @ContentChild(ChatItemListNodeDirective, { + read: TemplateRef, + static: false + }) + nodeTemplate: TemplateRef; + + @ViewChild('container', { static: true }) + container: ElementRef; + + @ViewChild('cvsvList', { static: true }) + cvsvList: VirtualScrollViewportComponent; + + ObjectUtil = ObjectUtil; + + private ngOnDestroySubject: Subject; + + constructor(private changeDetectorRef: ChangeDetectorRef) {} + + ngOnInit(): void { + this.ngOnDestroySubject = new Subject(); + } + + ngOnDestroy(): void { + if (!!this.ngOnDestroySubject) { + this.ngOnDestroySubject.next(); + this.ngOnDestroySubject.complete(); + } + } +} diff --git a/projects/ui-chat/src/lib/components/message-box/attach-file.component.html b/projects/ui-chat/src/lib/components/message-box/attach-file.component.html index 3afaee4..6e43bc1 100644 --- a/projects/ui-chat/src/lib/components/message-box/attach-file.component.html +++ b/projects/ui-chat/src/lib/components/message-box/attach-file.component.html @@ -1,37 +1,128 @@ -
- - -
-
+
+
+
+ + +
+
+
+
    +
  • + {{ fileInfo.fileName }} +
  • +
  • + {{ fileInfo.attachmentRegDate | ucapDate }} + + ~ + {{ + fileInfo.attachmentRegDate + | ucapDate: 'YYYY.MM.DD':fileRetentionPeriodOptions + }} + +
  • +
+
+
+
    +
  • + +
  • +
  • + +
  • +
  • + +
  • +
  • + +
  • +
  • + +
  • +
+
+
+ + + +
+ {{ 'common:file.downloading' | ucapI18n }}{{ fileDownloadItem.downloadingProgress$ | async }}% +
+
-
    -
  • - {{ fileInfo.fileName }} -
  • -
  • - {{ fileInfo.attachmentRegDate | ucapDate }} -
  • -
-
-
-
    -
  • - -
  • -
  • - -
  • -
  • - -
  • -
-
-
- 프로그레스 영역
diff --git a/projects/ui-chat/src/lib/components/message-box/attach-file.component.scss b/projects/ui-chat/src/lib/components/message-box/attach-file.component.scss index 2aa05cb..3c31aef 100644 --- a/projects/ui-chat/src/lib/components/message-box/attach-file.component.scss +++ b/projects/ui-chat/src/lib/components/message-box/attach-file.component.scss @@ -1,126 +1,124 @@ -// $tablet-s-width: 768px; +$tablet-s-width: 768px; -// .bubble-main { -// display: flex; -// flex-direction: row; -// padding: 14px; -// min-width: 300px; -// .file-img { -// display: inline-flex; -// width: 50px; -// height: 50px; -// float: left; -// margin-right: 14px; -// background-repeat: no-repeat; -// &.doc { -// background-image: url(/assets/images/file/icon_talk_doc.png); -// &.disable { -// background-image: url(/assets/images/file/icon_talk_doc_d.png); -// } -// } -// &.exe { -// background-image: url(/assets/images/file/icon_talk_exe.png); -// &.disable { -// background-image: url(/assets/images/file/icon_talk_exe_d.png); -// } -// } -// &.file { -// background-image: url(/assets/images/file/icon_talk_file.png); -// &.disable { -// background-image: url(/assets/images/file/icon_talk_file_d.png); -// } -// } -// &.hwp { -// background-image: url(/assets/images/file/icon_talk_hwp.png); -// &.disable { -// background-image: url(/assets/images/file/icon_talk_hwp_d.png); -// } -// } -// &.ppt, -// &.pptx { -// background-image: url(/assets/images/file/icon_talk_ppt.png); -// &.disable { -// background-image: url(/assets/images/file/icon_talk_ppt_d.png); -// } -// } -// &.xls, -// &.xlsm, -// &.xlsx { -// background-image: url(/assets/images/file/icon_talk_xls.png); -// &.disable { -// background-image: url(/assets/images/file/icon_talk_xls_d.png); -// } -// } -// &.zip { -// background-image: url(/assets/images/file/icon_talk_zip.png); -// &.disable { -// background-image: url(/assets/images/file/icon_talk_doc_d.png); -// } -// } -// } -// .file-info { -// display: inline-flex; -// flex-direction: column; -// text-align: left; -// float: left; -// line-height: 1.6em; -// .file-name { -// font-size: 1em; -// font-weight: bold; -// display: inline-flex; -// } -// .file-size { -// display: inline-flex; -// font-size: 11px; -// color: #666666; -// } -// .file-ext { -// font-size: 12px; -// color: #848d95; -// } -// } -// } -// .btn-box { -// width: 100%; -// height: 40px; -// border-top: 1px solid #dddddd; -// display: flex; -// width: 100%; -// text-align: center; -// font-size: 1em; -// ul { -// width: 100%; -// li { -// width: 50%; -// height: 100%; -// display: inline-block; -// text-align: center; -// align-items: center; -// border-right: 1px solid #dddddd; -// @media screen and (max-width: #{$tablet-s-width}) { -// width: 30%; -// } -// &:last-child { -// border-right: none; -// @media screen and (max-width: #{$tablet-s-width}) { -// width: 70%; -// } -// } -// .mat-button { -// width: 100%; -// display: block; -// height: 100%; -// font-size: 1em; -// } -// } -// &.expired { -// li { -// width: 100%; -// white-space: nowrap; -// color: #999999; -// align-items: center; -// line-height: 40px; -// } -// } -// } -// } +.ucap-chat-message-box-attach-file-bubble { + display: flex; + flex-direction: row; + align-items: center; + //padding: 6px 12px 0 0; + //border-radius: 2px; + cursor: pointer; + .file-info-box { + display: flex; + flex-direction: row; + //align-items: center; + //padding: 6px 12px 0 0; + //border-radius: 2px; + cursor: pointer; + .file-info { + //border-radius: 2px; + .file-name { + font-weight: 600; + //line-height: 1.2; + //font-size: 0.929em; + } + .date { + //color: #c92b5c; + //font-size: 0.875em; + //margin-top: 4px; + } + } + } + .btn-box { + width: 100%; + height: 100%; + //background-color: rgba(0, 0, 0, 0.6); + display: none; + //transition: all 0.2s linear; + //opacity: 0; + //border-radius: 2px; + cursor: default; + &.over { + position: absolute; + display: block; + z-index: 5; + top: 0; + left: 0; + //transition: all 0.3s linear; + opacity: 1; + } + ul { + display: flex; + flex-direction: row; + align-items: center; + flex-wrap: nowrap; + justify-content: center; + width: 100%; + height: 100%; + li { + //padding-left: 16px; + &:first-child { + padding-left: 0; + } + .btn-file-ctrl { + //@include matbtnCtrl(36px, 36px, 50%, 36px); + width: 36px; + } + } + } + } + .progress { + //display: flex; + display: none; + flex-direction: column; + justify-content: center; + width: 100%; + height: 100%; + //background-color: rgba(0, 0, 0, 0.6); + padding: 0 20px; + border-radius: 2px; + cursor: default; + &:before { + content: ''; + //width: 16px; + // height: 16px; + position: absolute; + z-index: 0; + //right: 22px; + //top: calc(50% - 25px); + //background-color: $white; + display: inline-block; + //border-radius: 50%; + } + &.over { + display: flex; + position: absolute; + z-index: 5; + top: 0; + left: 0; + } + .btn-close { + align-self: flex-end; + //@include matbtnCtrl(20px, 20px, 0, 20px); + // width: 20px; + //margin-bottom: 6px; + } + .progress-info-area { + display: flex; + flex-direction: row; + justify-content: space-between; + //color: $white; + //font-size: 0.875em; + //margin-top: 4px; + //font-weight: 600; + } + } + + .desibled { + .ucap-file-bubble { + .ucap-chat-message-box-attach-file { + // color: #999 !important; + // background: #f1f2f6 !important; + } + } + } +} diff --git a/projects/ui-chat/src/lib/components/message-box/attach-file.component.ts b/projects/ui-chat/src/lib/components/message-box/attach-file.component.ts index df974ca..07be118 100644 --- a/projects/ui-chat/src/lib/components/message-box/attach-file.component.ts +++ b/projects/ui-chat/src/lib/components/message-box/attach-file.component.ts @@ -1,5 +1,8 @@ import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core'; import { FileEventJson } from '@ucap/protocol-event'; +import { DateOptions } from '@ucap/ng-ui'; +import { FileDownloadItem } from '@ucap/api'; +import { DeviceType } from '@ucap/core'; @Component({ selector: 'ucap-chat-message-box-attach-file', @@ -10,26 +13,83 @@ export class AttachFileComponent implements OnInit { @Input() fileInfo: FileEventJson; + @Input() + fileDownloadItem: FileDownloadItem; + + @Input() + existFile = false; + + @Input() + deviceType: DeviceType; + @Input() expired = false; + @Input() + fileRetentionPeriod?: number; + @Output() save = new EventEmitter(); + @Output() + openFile = new EventEmitter(); + + @Output() + openFolder = new EventEmitter(); + @Output() openViewer = new EventEmitter(); + showOverButtons = false; + + DeviceType = DeviceType; + constructor() {} ngOnInit() {} onClickSave() { - this.save.emit('save'); + if (!this.expired) { + this.save.emit('save'); + } } onClickSaveAs() { - this.save.emit('saveAs'); + if (!this.expired) { + this.save.emit('saveAs'); + } + } + onClickOpenFile() { + if (!this.expired) { + this.openFile.emit(); + } + } + onClickOpenFolder() { + if (!this.expired) { + this.openFolder.emit(); + } } onClickOpenViewer() { - this.openViewer.emit(); + if (!this.expired) { + this.openViewer.emit(); + } + } + + mouseEnter(event: MouseEvent): void { + if (!this.expired) { + this.showOverButtons = true; + } + event.stopPropagation(); + event.preventDefault(); + } + mouseLeave(event: MouseEvent): void { + this.showOverButtons = false; + event.stopPropagation(); + event.preventDefault(); + } + + get fileRetentionPeriodOptions(): DateOptions { + return { + manipulate: { amount: this.fileRetentionPeriod, unit: 'days' } + }; } } diff --git a/projects/ui-chat/src/lib/components/message-box/date-splitter.component.scss b/projects/ui-chat/src/lib/components/message-box/date-splitter.component.scss index e69de29..2c09035 100644 --- a/projects/ui-chat/src/lib/components/message-box/date-splitter.component.scss +++ b/projects/ui-chat/src/lib/components/message-box/date-splitter.component.scss @@ -0,0 +1,31 @@ +@import '~@ucap/ng-ui-material/material'; + +.ucap-chat-message-box-date-splitter { + display: flex; + align-content: center; + flex-flow: row; + //height: 30px; + position: relative; + justify-content: center; + //margin: 20px 0; + &:before { + content: ''; + // height: 1px; + // background-color: $gray-re3; + width: 100%; + position: absolute; + z-index: 3; + // top: 50%; + } + .date { + min-width: 160px; + // font-size: 0.979em; + // text-align: center; + // font-weight: 600; + // color: $gray-re3; + position: relative; + z-index: 5; + // padding: 0 36px; + // align-self: center; + } +} diff --git a/projects/ui-chat/src/lib/components/message-box/file.component.html b/projects/ui-chat/src/lib/components/message-box/file.component.html index c1cb732..fbcd2b9 100644 --- a/projects/ui-chat/src/lib/components/message-box/file.component.html +++ b/projects/ui-chat/src/lib/components/message-box/file.component.html @@ -6,8 +6,14 @@ @@ -15,8 +21,14 @@ @@ -25,6 +37,7 @@ *ngSwitchCase="FileType.Image" [fileInfo]="fileInfo" [expired]="getExpiredFile()" + [fileRetentionPeriod]="fileRetentionPeriod" (openViewer)="onClickFileViewer(fileInfo)" (save)="onSave($event)" > @@ -32,8 +45,14 @@ diff --git a/projects/ui-chat/src/lib/components/message-box/file.component.ts b/projects/ui-chat/src/lib/components/message-box/file.component.ts index 6a37e17..875123a 100644 --- a/projects/ui-chat/src/lib/components/message-box/file.component.ts +++ b/projects/ui-chat/src/lib/components/message-box/file.component.ts @@ -2,6 +2,8 @@ import { Component, OnInit, Output, Input, EventEmitter } from '@angular/core'; import { FileType, Info, FileEventJson } from '@ucap/protocol-event'; import { RoomInfo } from '@ucap/protocol-room'; import { FileDownloadItem, StatusCode } from '@ucap/api'; +import { SelectFileInfo } from '@ucap/ng-ui'; +import { DeviceType } from '@ucap/core'; @Component({ selector: 'ucap-chat-message-box-file', @@ -15,14 +17,30 @@ export class FileComponent implements OnInit { @Input() roomInfo: RoomInfo; + @Input() + existFile = false; + + @Input() + deviceType?: DeviceType; + + @Input() + fileRetentionPeriod?: number; + @Output() save = new EventEmitter<{ fileInfo: FileEventJson; fileDownloadItem: FileDownloadItem; type: string; }>(); + @Output() - fileViewer = new EventEmitter(); + fileViewer = new EventEmitter(); + + @Output() + openFile = new EventEmitter(); + + @Output() + openFolder = new EventEmitter(); fileInfo?: FileEventJson; fileDownloadItem: FileDownloadItem; @@ -55,7 +73,7 @@ export class FileComponent implements OnInit { onClickFileViewer(fileInfo: FileEventJson) { if (!this.getExpiredFile()) { - this.fileViewer.emit(fileInfo); + this.fileViewer.emit({ attachmentSeq: this.fileInfo.attachmentSeq }); } } @@ -68,4 +86,14 @@ export class FileComponent implements OnInit { }); } } + onClickOpenFile(fileInfo: FileEventJson): void { + if (!this.getExpiredFile()) { + this.openFile.emit(fileInfo); + } + } + onClickOpenFolder(fileInfo: FileEventJson): void { + if (!this.getExpiredFile()) { + this.openFolder.emit(fileInfo); + } + } } diff --git a/projects/ui-chat/src/lib/components/message-box/image.component.html b/projects/ui-chat/src/lib/components/message-box/image.component.html index 65240f9..79561cc 100644 --- a/projects/ui-chat/src/lib/components/message-box/image.component.html +++ b/projects/ui-chat/src/lib/components/message-box/image.component.html @@ -1,10 +1,8 @@ -
-
- 만기된 파일입니다. +
+
+
-
diff --git a/projects/ui-chat/src/lib/components/message-box/image.component.scss b/projects/ui-chat/src/lib/components/message-box/image.component.scss index e69de29..c1ca776 100644 --- a/projects/ui-chat/src/lib/components/message-box/image.component.scss +++ b/projects/ui-chat/src/lib/components/message-box/image.component.scss @@ -0,0 +1,26 @@ +.ucap-chat-message-box-image-bubble { + //padding: 0 16px 16px; + position: relative; + + img { + max-height: 220px; + min-height: 20px; + width: auto; + } + + .expired-text { + position: absolute; + display: flex; + justify-content: center; + z-index: 5; + top: 0; + left: 0; + //background-color: rgb(255, 255, 255, 0.6); + //width: 100%; + //height: 100%; + + span { + align-self: center; + } + } +} diff --git a/projects/ui-chat/src/lib/components/message-box/image.component.ts b/projects/ui-chat/src/lib/components/message-box/image.component.ts index 841ed4c..1bf842e 100644 --- a/projects/ui-chat/src/lib/components/message-box/image.component.ts +++ b/projects/ui-chat/src/lib/components/message-box/image.component.ts @@ -1,4 +1,4 @@ -import { Component, OnInit, Input } from '@angular/core'; +import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core'; import { FileEventJson } from '@ucap/protocol-event'; @Component({ @@ -9,27 +9,23 @@ import { FileEventJson } from '@ucap/protocol-event'; export class ImageComponent implements OnInit { @Input() fileInfo: FileEventJson; + @Input() expired = false; - showExpired = false; + @Input() + fileRetentionPeriod?: number; + + @Output() + openViewer = new EventEmitter(); constructor() {} ngOnInit() {} - mouseEnter(event: MouseEvent): void { - if (this.expired) { - this.showExpired = true; + onClickOpenViewer() { + if (!this.expired) { + this.openViewer.emit(); } - event.stopPropagation(); - event.preventDefault(); - } - mouseLeave(event: MouseEvent): void { - if (this.expired) { - this.showExpired = false; - } - event.stopPropagation(); - event.preventDefault(); } } diff --git a/projects/ui-chat/src/lib/components/message-box/information.component.html b/projects/ui-chat/src/lib/components/message-box/information.component.html index 9bb75c9..c2bb742 100644 --- a/projects/ui-chat/src/lib/components/message-box/information.component.html +++ b/projects/ui-chat/src/lib/components/message-box/information.component.html @@ -1,53 +1,80 @@ -
- - - {{ - 'event.inviteToRoomWith' - | ucapI18n - : { - owner: getOwnerForJoinEventJson(), - inviter: getInviterForJoinEventJson() - } - }} +
+
+ + +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
- - {{ 'event.exitFromRoomWith' | ucapI18n: { exitor: message.sentMessage } }} - - - {{ - 'event.ejectedFromRoomWith' - | ucapI18n - : { - requester: message.exitForcingRequestUserName, - ejected: message.sentMessage - } - }} - - - {{ - 'event.renamedRoomWith' - | ucapI18n - : { - requester: getRequesterForRenameRoom(), - roomName: getRoomNameForRenameRoom() - } - }} - - - {{ message.sentMessage }} - - - {{ - 'event.setTimerWith' - | ucapI18n - : { - requester: senderName, - timer: getCalcTimerForGuideForRoomTimerChanged() - } - }} - - - {{ 'event.iosCapture' | ucapI18n: { requester: message.sentMessage } }} - - +
diff --git a/projects/ui-chat/src/lib/components/message-box/information.component.scss b/projects/ui-chat/src/lib/components/message-box/information.component.scss index e69de29..ff7ee23 100644 --- a/projects/ui-chat/src/lib/components/message-box/information.component.scss +++ b/projects/ui-chat/src/lib/components/message-box/information.component.scss @@ -0,0 +1,10 @@ +@import '~@ucap/ng-ui-material/material'; + +//구조가 달라요 +.ucap-chat-message-box-information { + .ucap-chat-info-eventinfo { + min-height: 30px; + text-align: center; + margin-top: 20px; + } +} diff --git a/projects/ui-chat/src/lib/components/message-box/information.component.ts b/projects/ui-chat/src/lib/components/message-box/information.component.ts index 5a08672..52ef7d3 100644 --- a/projects/ui-chat/src/lib/components/message-box/information.component.ts +++ b/projects/ui-chat/src/lib/components/message-box/information.component.ts @@ -50,37 +50,35 @@ export class InformationComponent implements OnInit { Number( (this.message.sentMessageJson as GuideForRoomTimerChangedEventJson).time ) * 1000; - const langs = this.i18nService.t([ - 'common.units.hourFrom', - 'common.units.minute', - 'common.units.second' - ]); + const second = this.i18nService.t('common:units.second'); + const minute = this.i18nService.t('common:units.minute'); + const hourFrom = this.i18nService.t('common:units.hourFrom'); switch (millisec) { case 5000: - return `5 ${langs['common.units.second']}`; + return `5 ${second}`; case 10000: - return `10 ${langs['common.units.second']}`; + return `10 ${second}`; case 30000: - return `30 ${langs['common.units.second']}`; + return `30 ${second}`; case 60000: - return `1 ${langs['common.units.minute']}`; + return `1 ${minute}`; case 300000: - return `5 ${langs['common.units.minute']}`; + return `5 ${minute}`; case 600000: - return `10 ${langs['common.units.minute']}`; + return `10 ${minute}`; case 1800000: - return `30 ${langs['common.units.minute']}`; + return `30 ${minute}`; case 3600000: - return `1 ${langs['common.units.hourFrom']}`; + return `1 ${hourFrom}`; case 21600000: - return `6 ${langs['common.units.hourFrom']}`; + return `6 ${hourFrom}`; case 43200000: - return `12 ${langs['common.units.hourFrom']}`; + return `12 ${hourFrom}`; case 86400000: - return `24 ${langs['common.units.hourFrom']}`; + return `24 ${hourFrom}`; default: - return `0 ${langs['common.units.second']}`; + return `0 ${second}`; } } diff --git a/projects/ui-chat/src/lib/components/message-box/mass-translation.component.html b/projects/ui-chat/src/lib/components/message-box/mass-translation.component.html index ae520ee..b9ad4a6 100644 --- a/projects/ui-chat/src/lib/components/message-box/mass-translation.component.html +++ b/projects/ui-chat/src/lib/components/message-box/mass-translation.component.html @@ -1,44 +1,48 @@ -
-
-
- {{ message.sentMessageJson.destLocale }} - +
+
+
- + {{ message.sentMessageJson.destLocale }} + + +
+
+
    +
  • + +
  • +
  • + +
  • +
+
-
-
    -
  • - -
  • -
  • - -
  • -
-
diff --git a/projects/ui-chat/src/lib/components/message-box/mass-translation.component.scss b/projects/ui-chat/src/lib/components/message-box/mass-translation.component.scss index e69de29..f6b6701 100644 --- a/projects/ui-chat/src/lib/components/message-box/mass-translation.component.scss +++ b/projects/ui-chat/src/lib/components/message-box/mass-translation.component.scss @@ -0,0 +1,52 @@ +.ucap-chat-message-box-mass-translation-bubble { + .content { + //padding: 0 16px 16px; + //font-size: 0.929em; + //color: #333; + //line-height: 1.54; + text-align: left; + } + + .btn-box { + width: 100%; + //height: 40px; + //border-top: 1px solid #f998a0; + //background-color: #fff2f3; + ul { + width: 100%; + display: flex; + flex-direction: row; + li { + width: 50%; + height: 100%; + display: inline-block; + text-align: center; + align-items: center; + //font-size: 0.979em; + //border-left: 1px solid #f998a0; + //&:first-child { + //border-left: none; + //} + .mat-button { + width: 100%; + height: 100%; + display: block; + //padding: 0 6px; + //height: 40px; + //border-radius: 0; + //color: #584f52; + } + .language { + //padding: 3px 8px; + //background-color: #e86773; + //border-radius: 2px; + //font-size: 0.786em; + //color: #fff; + //margin-right: 4px; + //line-height: 1.2; + //transform: translateY(-2px); + } + } + } + } +} diff --git a/projects/ui-chat/src/lib/components/message-box/mass.component.html b/projects/ui-chat/src/lib/components/message-box/mass.component.html index 0e97ee2..ffa2a3b 100644 --- a/projects/ui-chat/src/lib/components/message-box/mass.component.html +++ b/projects/ui-chat/src/lib/components/message-box/mass.component.html @@ -1,12 +1,15 @@ -
- - -
-
- +
+
+ + + +
+ +
+
diff --git a/projects/ui-chat/src/lib/components/message-box/mass.component.scss b/projects/ui-chat/src/lib/components/message-box/mass.component.scss index e69de29..5577e2f 100644 --- a/projects/ui-chat/src/lib/components/message-box/mass.component.scss +++ b/projects/ui-chat/src/lib/components/message-box/mass.component.scss @@ -0,0 +1,22 @@ +.ucap-chat-message-box-mass-bubble { + .content { + //padding: 0 16px 16px; + //font-size: 0.929em; + //color: #333; + //line-height: 1.54; + text-align: left; + } + + .btn-box { + //border-top: 1px solid #f998a0; + //height: 40px; + //background-color: #fff2f3; + .mat-button { + width: 100%; + display: block; + //height: 40px; + //border-radius: 0; + //color: #584f52; + } + } +} diff --git a/projects/ui-chat/src/lib/components/message-box/read-here.component.scss b/projects/ui-chat/src/lib/components/message-box/read-here.component.scss index e69de29..9ddd372 100644 --- a/projects/ui-chat/src/lib/components/message-box/read-here.component.scss +++ b/projects/ui-chat/src/lib/components/message-box/read-here.component.scss @@ -0,0 +1,14 @@ +.ucap-chat-message-box-read-here { + display: flex; + align-content: center; + flex-flow: row; + //background-color: rgba(0, 0, 0, 0.4); + //line-height: 30px; + //min-height: 30px; + //padding: 0 8px; + //border-radius: 2px; + justify-content: center; + //margin: 20px 0 0; + //font-size: 12px; + //font-weight: 600; +} diff --git a/projects/ui-chat/src/lib/components/message-box/recall.component.html b/projects/ui-chat/src/lib/components/message-box/recall.component.html index f84fdcc..5ca6cff 100644 --- a/projects/ui-chat/src/lib/components/message-box/recall.component.html +++ b/projects/ui-chat/src/lib/components/message-box/recall.component.html @@ -1,4 +1,31 @@ -
- redo - 회수된 메시지 +
+
+ + + + + + + + + + + + + + {{ 'event.recalled' | ucapI18n }} +
diff --git a/projects/ui-chat/src/lib/components/message-box/recall.component.scss b/projects/ui-chat/src/lib/components/message-box/recall.component.scss index e69de29..ea57e2c 100644 --- a/projects/ui-chat/src/lib/components/message-box/recall.component.scss +++ b/projects/ui-chat/src/lib/components/message-box/recall.component.scss @@ -0,0 +1,16 @@ +.ucap-chat-message-box-recall-bubble { + // padding: 0 16px 16px; + display: flex; + flex-direction: row; + align-items: flex-start; + .icon-recall { + flex: 0 0 30px; + height: 30px; + } + .recall-msg { + // color: #999999; + font-weight: 600; + // margin-left: 10px; + // line-height: 1.8; + } +} diff --git a/projects/ui-chat/src/lib/components/message-box/reply.component.html b/projects/ui-chat/src/lib/components/message-box/reply.component.html index 19a0bfc..211ec86 100644 --- a/projects/ui-chat/src/lib/components/message-box/reply.component.html +++ b/projects/ui-chat/src/lib/components/message-box/reply.component.html @@ -1,14 +1,23 @@ -
-
-
    -
  • 나에게 답장
  • -
  • - 올려 놓았습니다. 확인해 보시기 바랍니다. -
  • -
-
-
- - 네 감사합니다. 수고가 많으시네요. 오늘 중으로 확인해 보겠습니다. +
+
+
+
+
나에게 답장
+
+ 올려 놓았습니다. 확인해 보시기 바랍니다. +
+
+
+
+
+ + subdirectory_arrow_right + + 네 감사합니다. 수고가 많으시네요. 오늘 중으로 확인해 + 보겠습니다. +
+
diff --git a/projects/ui-chat/src/lib/components/message-box/reply.component.scss b/projects/ui-chat/src/lib/components/message-box/reply.component.scss index e69de29..83af572 100644 --- a/projects/ui-chat/src/lib/components/message-box/reply.component.scss +++ b/projects/ui-chat/src/lib/components/message-box/reply.component.scss @@ -0,0 +1,53 @@ +.ucap-chat-message-box-reply-bubble { + display: flex; + flex-direction: column; + .reply-header { + //padding: 10px 16px; + //background-color: #f7f8fa; + //border-top: 1px solid #f998a0; + //border-bottom: 1px solid #f998a0; + dl { + display: flex; + flex-direction: column; + align-items: flex-start; + //margin: 0; + //padding: 5px 0 8px; + dt { + font-weight: 600; + //color: #ec5361; + //margin: 0; + } + dd { + //color: #666; + //font-size: 0.979em; + //margin: 5px 0 0; + } + } + } + .content { + //padding: 12px 16px 16px; + .content-inner { + display: flex; + flex-direction: row; + align-items: flex-start; + .icon-reply { + //background-color: #e86773; + //width: 30px; + //height: 18px; + //border-radius: 2px; + //margin-right: 8px; + flex: 0 0 38px; + text-align: center; + .ico-reply { + //color: #fff; + //font-size: 16px; + } + } + .reply-text { + //line-height: 18px; + //font-size: 0.979em; + //color: #333; + } + } + } +} diff --git a/projects/ui-chat/src/lib/components/message-box/schedule.component.html b/projects/ui-chat/src/lib/components/message-box/schedule.component.html index fff45f2..446ad44 100644 --- a/projects/ui-chat/src/lib/components/message-box/schedule.component.html +++ b/projects/ui-chat/src/lib/components/message-box/schedule.component.html @@ -1,38 +1,48 @@ -
-
- - - - {{ 'chat.event.scheduleTypeNew' | ucapI18n }} +
+
+
+
+ date_range + + + + {{ 'chat:event.scheduleTypeNew' | ucapI18n }} + + + {{ 'chat:event.scheduleTypeUpdate' | ucapI18n }} + + + {{ 'chat:event.scheduleTypeDelete' | ucapI18n }} + + + + {{ getAlertLeftTime() }} + + - - {{ 'chat.event.scheduleTypeUpdate' | ucapI18n }} - - - {{ 'chat.event.scheduleTypeDelete' | ucapI18n }} - - - - {{ getAlertLeftTime() }} - - - +
+
    +
  • + {{ message.sentMessageJson.title }} +
  • +
  • + {{ date | ucapDate: 'L LT' }} ~ {{ endDate | ucapDate: 'L LT' }} +
  • + +
+
+
+ +
-
    -
  • - 마곡 E14동 906호 -
  • -
  • - 2020.05.23 9:30 ~ 2020.05.23 11:30 -
  • -
  • - {{ message.sentMessageJson.title }} - {{ message.sentMessageJson.contents }} -
  • -
-
-
-
diff --git a/projects/ui-chat/src/lib/components/message-box/schedule.component.scss b/projects/ui-chat/src/lib/components/message-box/schedule.component.scss index e69de29..a93c988 100644 --- a/projects/ui-chat/src/lib/components/message-box/schedule.component.scss +++ b/projects/ui-chat/src/lib/components/message-box/schedule.component.scss @@ -0,0 +1,62 @@ +.ucap-chat-message-box-schedule-bubble { + display: flex; + flex-direction: column; + .event-box { + //padding: 0 16px; + display: flex; + flex-direction: column; + .event-header { + //background-color: rgba(232, 103, 115, 0.9); + //border-radius: 4px; + //height: 36px; + display: flex; + flex-direction: row; + align-items: center; + //color: #fff; + //font-weight: 600; + .ico-event { + margin: 0 10px; + } + } + .event-info { + display: flex; + flex-direction: column; + .place { + //min-height: 32px; + //font-size: 0.929em; + //color: #666; + //font-weight: 600; + //padding: 6px 0; + //border-bottom: 1px dashed #ccc; + } + .date { + //min-height: 32px; + //font-size: 0.929em; + //color: #c92b5c; + //padding: 6px 0; + //border-bottom: 1px dashed #ccc; + } + .event-content { + //min-height: 32px; + //font-size: 0.929em; + //color: #333; + //padding: 8px 0; + word-wrap: break-word; + white-space: pre-wrap; + word-break: break-word; + } + } + } + .btn-box { + //height: 40px; + //border-top: 1px solid #f998a0; + //background-color: #fff2f3; + .mat-button { + width: 100%; + display: block; + //height: 40px; + //border-radius: 0; + //color: #584f52; + } + } +} diff --git a/projects/ui-chat/src/lib/components/message-box/schedule.component.ts b/projects/ui-chat/src/lib/components/message-box/schedule.component.ts index 98b9712..6bd72cd 100644 --- a/projects/ui-chat/src/lib/components/message-box/schedule.component.ts +++ b/projects/ui-chat/src/lib/components/message-box/schedule.component.ts @@ -1,6 +1,7 @@ -import { Component, OnInit, Input } from '@angular/core'; +import { Component, OnInit, Input, EventEmitter, Output } from '@angular/core'; import { Info, PlanEventJson, PlanContentType } from '@ucap/protocol-event'; -import { TranslateService } from '@ucap/ng-ui-organization'; + +import { I18nService } from '@ucap/ng-i18n'; import moment from 'moment'; @@ -13,52 +14,78 @@ export class ScheduleComponent implements OnInit { @Input() message: Info; + @Output() + scheduleDetail = new EventEmitter(); + PlanContentType = PlanContentType; date: any; endDate: any; - constructor(private translateService: TranslateService) {} + constructor(private i18nService: I18nService) {} ngOnInit() { if (!!this.message && !!this.message.sentMessageJson) { if (!!this.message.sentMessageJson.date) { - let strDate = this.message.sentMessageJson.date - .replace(/ /g, '') - .replace(/\n/g, '') - .replace(/(\([월,화,수,목,금,토,일]\))/g, ''); - if (strDate.indexOf('오전') > -1) { - strDate = strDate.replace('오전', ' '); - } else if (strDate.indexOf('오후') > -1) { - strDate = strDate.replace('오후', ' '); - const arr = strDate.split(' '); - const h = Number(arr[1].split(':')[0]) + 12; - strDate = arr[0] + ' ' + h + ':' + arr[1].split(':')[1]; - } + this.date = this._transeDateTime(this.message.sentMessageJson.date); + } - this.date = moment(strDate).toDate(); - if (this.date === 'Invalid Date') { - this.date = this.message.sentMessageJson.date.replace(/\n/g, ''); - } + if (!!this.message.sentMessageJson.endDate) { + this.endDate = this._transeDateTime( + this.message.sentMessageJson.endDate + ); } } } - onClickSave(): void {} - getAlertLeftTime(): string { let content = this.message.sentMessageJson.contents; if (content.indexOf('PLAN_CONTENTS_MINUTE') > -1) { - content = content.replace('PLAN_CONTENTS_MINUTE', '분'); + content = content.replace( + 'PLAN_CONTENTS_MINUTE', + this.i18nService.t('common:units.minute') + ); } else if (content.indexOf('PLAN_CONTENTS_HOUR') > -1) { - content = content.replace('PLAN_CONTENTS_HOUR', '시간'); + content = content.replace( + 'PLAN_CONTENTS_HOUR', + this.i18nService.t('common:units.time') + ); } if (content.indexOf('PLAN_CONTENTS_AFTER') > -1) { - content = content.replace('PLAN_CONTENTS_AFTER', ' 전 알림'); + content = content.replace( + 'PLAN_CONTENTS_AFTER', + this.i18nService.t('chat:event.scheduleTypeSurfixLeft') + ); } - content = '[이벤트] ' + content; + content = this.i18nService.t('chat:event.scheduleTypePrefix') + content; return content; } + + onClickDetailView() { + this.scheduleDetail.emit(this.message.sentMessageJson.planSeq); + } + + private _transeDateTime(time: string): any { + let tempDate = time + .replace(/ /g, '') + .replace(/\n/g, '') + .replace(/(\([월,화,수,목,금,토,일]\))/g, ''); + if (tempDate.indexOf('오전') > -1) { + tempDate = tempDate.replace('오전', ' '); + } else if (tempDate.indexOf('오후') > -1) { + tempDate = tempDate.replace('오후', ' '); + const arr = tempDate.split(' '); + const h = Number(arr[1].split(':')[0]) + 12; + tempDate = arr[0] + ' ' + h + ':' + arr[1].split(':')[1]; + } + + let rtnDate: any = moment(tempDate).toDate(); + if (rtnDate === 'Invalid Date') { + rtnDate = time.replace(/\n/g, ''); + } + + return rtnDate; + } } diff --git a/projects/ui-chat/src/lib/components/message-box/sms.component.html b/projects/ui-chat/src/lib/components/message-box/sms.component.html index a16d272..96ff537 100644 --- a/projects/ui-chat/src/lib/components/message-box/sms.component.html +++ b/projects/ui-chat/src/lib/components/message-box/sms.component.html @@ -1,9 +1,14 @@ -
-
- SMS -
-
- SMS 내용 공간입니다. 내용을 보여주세요 SMS 내용 공간입니다. 내용을 - 보여주세요 SMS 내용 공간입니다. 내용을 보여주세요 +
+
+
+
+ textsms + SMS +
+
+ SMS 내용 공간입니다. 내용을 보여주세요 SMS 내용 공간입니다. 내용을 + 보여주세요 SMS 내용 공간입니다. 내용을 보여주세요 +
+
diff --git a/projects/ui-chat/src/lib/components/message-box/sms.component.scss b/projects/ui-chat/src/lib/components/message-box/sms.component.scss index e69de29..cd67480 100644 --- a/projects/ui-chat/src/lib/components/message-box/sms.component.scss +++ b/projects/ui-chat/src/lib/components/message-box/sms.component.scss @@ -0,0 +1,31 @@ +.ucap-chat-message-box-sms-bubble { + display: flex; + flex-direction: column; + .sms-box { + //padding: 0 16px 16px; + display: flex; + flex-direction: column; + .sms-header { + //background-color: rgba(232, 103, 115, 0.9); + //border-radius: 4px; + //height: 36px; + display: flex; + flex-direction: row; + align-items: center; + //color: #fff; + //font-weight: 600; + .ico-sms { + margin: 0 10px; + } + } + .content { + //min-height: 32px; + //font-size: 0.857em; + //color: #333; + //padding: 8px 0 0; + word-wrap: break-word; + white-space: pre-wrap; + word-break: break-word; + } + } +} diff --git a/projects/ui-chat/src/lib/components/message-box/sticker.component.html b/projects/ui-chat/src/lib/components/message-box/sticker.component.html index b8936ba..e26b21a 100644 --- a/projects/ui-chat/src/lib/components/message-box/sticker.component.html +++ b/projects/ui-chat/src/lib/components/message-box/sticker.component.html @@ -1,14 +1,16 @@ -
-
    -
  • - - -
  • -
  • -
+
+
+
    +
  • + + +
  • +
  • +
+
diff --git a/projects/ui-chat/src/lib/components/message-box/sticker.component.scss b/projects/ui-chat/src/lib/components/message-box/sticker.component.scss index e69de29..1bb2eb5 100644 --- a/projects/ui-chat/src/lib/components/message-box/sticker.component.scss +++ b/projects/ui-chat/src/lib/components/message-box/sticker.component.scss @@ -0,0 +1,10 @@ +.ucap-chat-message-box-sticker-bubble { + //padding: 0 16px 16px; + li { + text-align: left; + margin-top: 10px; + &:first-child { + margin-top: 0; + } + } +} diff --git a/projects/ui-chat/src/lib/components/message-box/text.component.html b/projects/ui-chat/src/lib/components/message-box/text.component.html index bce0ecc..ea70631 100644 --- a/projects/ui-chat/src/lib/components/message-box/text.component.html +++ b/projects/ui-chat/src/lib/components/message-box/text.component.html @@ -1,3 +1,5 @@ -
- +
+
+ +
diff --git a/projects/ui-chat/src/lib/components/message-box/text.component.scss b/projects/ui-chat/src/lib/components/message-box/text.component.scss index e69de29..902d571 100644 --- a/projects/ui-chat/src/lib/components/message-box/text.component.scss +++ b/projects/ui-chat/src/lib/components/message-box/text.component.scss @@ -0,0 +1,15 @@ +.ucap-chat-message-box-text-bubble { + //padding: 0 16px 16px; + text-align: left; + span { + //font-size: 13px; + //line-height: 20px; + //color: #333; + word-wrap: break-word; + white-space: pre-wrap; + word-break: break-word; + a { + //color: #349adf; + } + } +} diff --git a/projects/ui-chat/src/lib/components/message-box/translation.component.html b/projects/ui-chat/src/lib/components/message-box/translation.component.html index 64f065a..8c922cf 100644 --- a/projects/ui-chat/src/lib/components/message-box/translation.component.html +++ b/projects/ui-chat/src/lib/components/message-box/translation.component.html @@ -1,20 +1,24 @@ -
-
- -
- -
-
- {{ message.sentMessageJson.locale }} - +
+
+
+
+ +
+
+
+
+ {{ message.sentMessageJson.locale }} + + +
diff --git a/projects/ui-chat/src/lib/components/message-box/translation.component.scss b/projects/ui-chat/src/lib/components/message-box/translation.component.scss index e69de29..176f646 100644 --- a/projects/ui-chat/src/lib/components/message-box/translation.component.scss +++ b/projects/ui-chat/src/lib/components/message-box/translation.component.scss @@ -0,0 +1,35 @@ +.ucap-chat-message-box-translation-bubble { + display: flex; + flex-direction: column; + .translation-original-box { + //padding: 0 16px; + .sticker { + padding: 10px 0; + } + .original { + // font-size: 0.929em; + // line-height: 1.47; + // padding-bottom: 10px; + } + } + .translation-box { + width: 100%; + min-height: 40px; + //border-top: 1px solid #f998a0; + //background-color: #fff2f3; + //padding: 10px 16px; + // font-size: 0.929em; + //line-height: 1.4; + //color: #333; + .language { + // padding: 3px 8px; + // background-color: #e86773; + // border-radius: 2px; + // font-size: 0.786em; + // color: #fff; + // margin-right: 8px; + // line-height: 1.2; + // transform: translateY(-2px); + } + } +} diff --git a/projects/ui-chat/src/lib/components/message-box/video-conference.component.html b/projects/ui-chat/src/lib/components/message-box/video-conference.component.html index 6d4f036..88511fc 100644 --- a/projects/ui-chat/src/lib/components/message-box/video-conference.component.html +++ b/projects/ui-chat/src/lib/components/message-box/video-conference.component.html @@ -1,54 +1,63 @@ -
-
- - - - {{ 'conference.videoConferenceTypeNow' | ucapI18n }} +
+
+
+
+ voice_chat + + + + {{ 'conference.videoConferenceTypeNow' | ucapI18n }} + + + {{ 'conference.videoConferenceTypeNew' | ucapI18n }} + + + {{ 'conference.videoConferenceTypeUpdate' | ucapI18n }} + + + {{ 'conference.videoConferenceTypeDelete' | ucapI18n }} + + - - {{ 'conference.videoConferenceTypeNew' | ucapI18n }} - - - {{ 'conference.videoConferenceTypeUpdate' | ucapI18n }} - - - {{ 'conference.videoConferenceTypeDelete' | ucapI18n }} - - - +
+
    +
  • + {{ + 'conference.videoConferenceRegister' | ucapI18n + }} + {{ message.sentMessageJson.register }} +
  • +
  • + {{ + 'conference.videoConferenceAttendee' | ucapI18n + }} + {{ message.sentMessageJson.attendee }} +
  • +
  • + {{ message.sentMessageJson.startDate | ucapDate }} ~ + {{ message.sentMessageJson.endDate | ucapDate }} +
  • +
+
+
+
    +
  • + +
  • +
  • + +
  • +
+
-
    -
  • - {{ - 'conference.videoConferenceRegister' | ucapI18n - }} - {{ message.sentMessageJson.register }} -
  • -
  • - {{ - 'conference.videoConferenceAttendee' | ucapI18n - }} - {{ message.sentMessageJson.attendee }} -
  • -
  • - {{ message.sentMessageJson.startDate | ucapDate }} ~ - {{ message.sentMessageJson.endDate | ucapDate }} -
  • -
-
-
-
    -
  • - -
  • -
  • - -
  • -
diff --git a/projects/ui-chat/src/lib/components/message-box/video-conference.component.scss b/projects/ui-chat/src/lib/components/message-box/video-conference.component.scss index e69de29..224022f 100644 --- a/projects/ui-chat/src/lib/components/message-box/video-conference.component.scss +++ b/projects/ui-chat/src/lib/components/message-box/video-conference.component.scss @@ -0,0 +1,72 @@ +.ucap-chat-message-box-video-conference-bubble { + display: flex; + flex-direction: column; + .conference-box { + //padding: 0 16px; + display: flex; + flex-direction: column; + .conference-header { + //background-color: rgba(232, 103, 115, 0.9); + //border-radius: 4px; + //height: 36px; + display: flex; + flex-direction: row; + align-items: center; + //color: #fff; + //font-weight: 600; + .ico-conference { + margin: 0 10px; + } + } + .conference-info { + li { + //min-height: 32px; + //font-size: 0.929em; + //color: #2d2a2c; + //padding: 6px 0; + //border-top: 1px dashed #ccc; + .label { + //font-weight: 600; + } + &:first-child { + //@error: none; + } + &.date { + //color: #c92b5c; + } + } + } + } + .btn-box { + width: 100%; + //height: 40px; + //border-top: 1px solid #f998a0; + //background-color: #fff2f3; + ul { + width: 100%; + display: flex; + flex-direction: row; + li { + //width: 50%; + height: 100%; + display: inline-block; + text-align: center; + align-items: center; + //font-size: 0.979em; + //border-left: 1px solid #f998a0; + &:first-child { + border-left: none; + } + .mat-button { + width: 100%; + height: 100%; + display: block; + //padding: 0 6px; + //height: 40px; + //border-radius: 0; + //color: #584f52; + } + } + } + } +} diff --git a/projects/ui-chat/src/lib/components/message-box/video.component.html b/projects/ui-chat/src/lib/components/message-box/video.component.html index 67e4b77..2af3fd0 100644 --- a/projects/ui-chat/src/lib/components/message-box/video.component.html +++ b/projects/ui-chat/src/lib/components/message-box/video.component.html @@ -1,41 +1,131 @@ -
-
- +
+
+
+
+ +
+
+
+
+
    +
  • + {{ fileInfo.fileName }} +
  • +
  • + {{ fileInfo.attachmentRegDate | ucapDate }} + + ~ + {{ + fileInfo.attachmentRegDate + | ucapDate: 'YYYY.MM.DD':fileRetentionPeriodOptions + }} + +
  • +
+
+
+
    +
  • + +
  • +
  • + +
  • +
  • + +
  • +
  • + +
  • +
  • + +
  • +
+
-
+ + + +
+ {{ 'common:file.downloading' | ucapI18n }}{{ fileDownloadItem.downloadingProgress$ | async }}% +
-
    -
  • - {{ fileInfo.fileName }} -
  • -
  • - 2020.01.01 ~ 2020.12.12 -
  • -
-
-
-
    -
  • - -
  • -
  • - -
  • -
  • - -
  • -
-
-
- 프로그레스 영역
diff --git a/projects/ui-chat/src/lib/components/message-box/video.component.scss b/projects/ui-chat/src/lib/components/message-box/video.component.scss index e69de29..14ac507 100644 --- a/projects/ui-chat/src/lib/components/message-box/video.component.scss +++ b/projects/ui-chat/src/lib/components/message-box/video.component.scss @@ -0,0 +1,119 @@ +$tablet-s-width: 768px; + +.ucap-chat-message-box-video-bubble { + //margin: 0 16px 16px; + position: relative; + overflow: hidden; + .video-info-box { + display: flex; + flex-direction: row; + align-items: center; + //@include: 10px; + //border-radius: 2px; + cursor: pointer; + .video-thumbimg { + //flex: 0 0 80px; + align-self: start; + } + .video-info { + //padding-left: 10px; + flex-grow: 1; + //border-radius: 2px; + .video-subject { + font-weight: 600; + //line-height: 1.2; + //font-size: 0.929em; + } + .date { + //color: #c92b5c; + //font-size: 0.875em; + //margin-top: 4px; + } + } + } + + .btn-box { + width: 100%; + height: 100%; + //background-color: rgba(0, 0, 0, 0.6); + //display: none; + //border-radius: 2px; + //transition: all 0.2s linear; + opacity: 0; + cursor: default; + &.over { + position: absolute; + display: block; + z-index: 5; + top: 0; + left: 0; + // transition: all 0.3s linear; + opacity: 1; + } + ul { + display: flex; + flex-direction: row; + align-items: center; + flex-wrap: nowrap; + justify-content: center; + width: 100%; + height: 100%; + li { + padding-left: 16px; + &:first-child { + padding-left: 0; + } + .btn-file-ctrl { + // @include matbtnCtrl(36px, 36px, 50%, 36px); + // width: 36px; + } + } + } + } + .progress { + //display: flex; + display: none; + flex-direction: column; + justify-content: center; + width: 100%; + height: 100%; + // border-radius: 2px; + // background-color: rgba(0, 0, 0, 0.6); + // padding: 0 20px; + cursor: default; + &:before { + content: ''; + // width: 16px; + // height: 16px; + position: absolute; + z-index: 0; + // right: 22px; + // top: calc(50% - 25px); + // background-color: $white; + display: inline-block; + // border-radius: 50%; + } + &.over { + display: flex; + position: absolute; + z-index: 5; + top: 0; + left: 0; + } + .btn-close { + align-self: flex-end; + // @include matbtnCtrl(20px, 20px, 0, 20px); + // width: 20px; + // margin-bottom: 6px; + } + .progress-info-area { + display: flex; + flex-direction: row; + justify-content: space-between; + // color: $white; + // font-size: 0.875em; + // margin-top: 4px; + // font-weight: 600; + } + } +} diff --git a/projects/ui-chat/src/lib/components/message-box/video.component.ts b/projects/ui-chat/src/lib/components/message-box/video.component.ts index 987bad2..2ed554c 100644 --- a/projects/ui-chat/src/lib/components/message-box/video.component.ts +++ b/projects/ui-chat/src/lib/components/message-box/video.component.ts @@ -1,5 +1,8 @@ import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core'; import { FileEventJson } from '@ucap/protocol-event'; +import { DateOptions } from '@ucap/ng-ui'; +import { FileDownloadItem } from '@ucap/api'; +import { DeviceType } from '@ucap/core'; @Component({ selector: 'ucap-chat-message-box-video', @@ -9,25 +12,84 @@ import { FileEventJson } from '@ucap/protocol-event'; export class VideoComponent implements OnInit { @Input() fileInfo: FileEventJson; + + @Input() + fileDownloadItem: FileDownloadItem; + + @Input() + existFile = false; + + @Input() + deviceType: DeviceType; + @Input() expired = false; + @Input() + fileRetentionPeriod?: number; + @Output() save = new EventEmitter(); + + @Output() + openFile = new EventEmitter(); + + @Output() + openFolder = new EventEmitter(); + @Output() openViewer = new EventEmitter(); + showOverButtons = false; + + DeviceType = DeviceType; + constructor() {} ngOnInit() {} onClickSave() { - this.save.emit('save'); + if (!this.expired) { + this.save.emit('save'); + } } onClickSaveAs() { - this.save.emit('saveAs'); + if (!this.expired) { + this.save.emit('saveAs'); + } + } + onClickOpenFile() { + if (!this.expired) { + this.openFile.emit(); + } + } + onClickOpenFolder() { + if (!this.expired) { + this.openFolder.emit(); + } } onClickOpenViewer() { - this.openViewer.emit(); + if (!this.expired) { + this.openViewer.emit(); + } + } + + mouseEnter(event: MouseEvent): void { + if (!this.expired) { + this.showOverButtons = true; + } + event.stopPropagation(); + event.preventDefault(); + } + mouseLeave(event: MouseEvent): void { + this.showOverButtons = false; + event.stopPropagation(); + event.preventDefault(); + } + + get fileRetentionPeriodOptions(): DateOptions { + return { + manipulate: { amount: this.fileRetentionPeriod, unit: 'days' } + }; } } diff --git a/projects/ui-chat/src/lib/components/room-expansion.component.html b/projects/ui-chat/src/lib/components/room-expansion.component.html index 3a5e632..001d4a4 100644 --- a/projects/ui-chat/src/lib/components/room-expansion.component.html +++ b/projects/ui-chat/src/lib/components/room-expansion.component.html @@ -1,16 +1,19 @@
- +
  • @@ -27,6 +30,7 @@ *matTreeNodeDef="let node; when: isHeader" class="tree-node-frame ucap-clickable" [attr.node-type]="node?.nodeType" + [style.height.px]="vsItemSize" matRipple >
  • @@ -59,5 +63,5 @@
  • -
    +
    diff --git a/projects/ui-chat/src/lib/components/room-expansion.component.scss b/projects/ui-chat/src/lib/components/room-expansion.component.scss index 1c674b1..f48cc6c 100644 --- a/projects/ui-chat/src/lib/components/room-expansion.component.scss +++ b/projects/ui-chat/src/lib/components/room-expansion.component.scss @@ -1,2 +1,22 @@ .ucap-chat-room-expansion-container { } + +.ucap-clickable { + display: flex; + min-height: 60px; + border-top: 10px solid #f1f2f6; + //border-bottom: 1px solid $gray-rec; + li { + width: 100%; + .path { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + .group-info { + justify-self: self-start; + flex-grow: 1; + } + } + } +} diff --git a/projects/ui-chat/src/lib/components/room-expansion.component.ts b/projects/ui-chat/src/lib/components/room-expansion.component.ts index c7d8562..1fe8790 100644 --- a/projects/ui-chat/src/lib/components/room-expansion.component.ts +++ b/projects/ui-chat/src/lib/components/room-expansion.component.ts @@ -11,10 +11,10 @@ import { TemplateRef, ChangeDetectionStrategy, ChangeDetectorRef, - Directive + Directive, + AfterViewInit } from '@angular/core'; -import { CdkVirtualScrollViewport } from '@angular/cdk/scrolling'; import { FlatTreeControl } from '@angular/cdk/tree'; import { MatTreeFlattener, MatTree } from '@angular/material/tree'; @@ -23,7 +23,10 @@ import { PerfectScrollbarDirective } from 'ngx-perfect-scrollbar'; import { RoomInfo } from '@ucap/protocol-room'; -import { VirtualScrollTreeFlatDataSource } from '@ucap/ng-ui'; +import { + VirtualScrollTreeFlatDataSource, + VirtualScrollViewportComponent +} from '@ucap/ng-ui'; export interface RoomNode { nodeType: string; @@ -53,7 +56,8 @@ export class RoomExpansionHeaderDirective {} styleUrls: ['./room-expansion.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush }) -export class RoomExpansionComponent implements OnInit, OnDestroy { +export class RoomExpansionComponent + implements OnInit, OnDestroy, AfterViewInit { @Input() set roomGroup(list: { division: string; roomList: RoomInfo[] }[]) { if (!list || 0 === list.length) { @@ -108,11 +112,14 @@ export class RoomExpansionComponent implements OnInit, OnDestroy { this.refreshNodes(); } + @Input() + vsItemSize: number; + @ViewChild('treeList', { static: false }) treeList: MatTree; @ViewChild('cvsvList', { static: false }) - cvsvList: CdkVirtualScrollViewport; + cvsvList: VirtualScrollViewportComponent; @ViewChild(PerfectScrollbarDirective, { static: false }) psDirectiveRef?: PerfectScrollbarDirective; @@ -135,7 +142,7 @@ export class RoomExpansionComponent implements OnInit, OnDestroy { private nodeMap: Map = new Map(); // tslint:disable-next-line: variable-name - private _ngOnDestroySubject: Subject; + private _ngOnDestroySubject: Subject = new Subject(); constructor(private changeDetectorRef: ChangeDetectorRef) { this.treeControl = new FlatTreeControl( @@ -164,14 +171,12 @@ export class RoomExpansionComponent implements OnInit, OnDestroy { } ngOnInit(): void { - this._ngOnDestroySubject = new Subject(); - - this.dataSource.cdkVirtualScrollViewport = this.cvsvList; - this.treeControl.expansionModel.changed + this.dataSource.expandedData$ .pipe(takeUntil(this._ngOnDestroySubject)) - .subscribe(() => { - this.cvsvList.checkViewportSize(); - this.psDirectiveRef.update(); + .subscribe((datas) => { + if (!!this.psDirectiveRef) { + this.psDirectiveRef.update(); + } }); } @@ -182,6 +187,10 @@ export class RoomExpansionComponent implements OnInit, OnDestroy { } } + ngAfterViewInit(): void { + this.dataSource.virtualScrollViewport = this.cvsvList; + } + isHeader = (_: number, node: FlatNode) => 0 === node.level; private refreshNodes() { diff --git a/projects/ui-chat/src/lib/components/room-list-item-01.component.html b/projects/ui-chat/src/lib/components/room-list-item-01.component.html index 86f055c..47bfbf8 100644 --- a/projects/ui-chat/src/lib/components/room-list-item-01.component.html +++ b/projects/ui-chat/src/lib/components/room-list-item-01.component.html @@ -1,28 +1,46 @@ -
    +
    - +
    + +
    - {{ roomName }} - ({{ roomInfo.joinUserCount }}) + timer +
    +
    {{ roomName }}
    +
    + ({{ roomInfo.joinUserCount }}) +
    + notifications_off - / {{ roomInfo.receiveAlarm }}
    -
    {{ roomInfo.finalEventMessage }}
    +
    + {{ finalEventMessage() }} +
    {{ roomInfo.finalEventDate | ucapDate: 'LT' }}
    - more_vert + more_horiz
    - + {{ roomName }} diff --git a/projects/ui-chat/src/lib/components/room-list-item-01.component.scss b/projects/ui-chat/src/lib/components/room-list-item-01.component.scss index 546b1d8..b209828 100644 --- a/projects/ui-chat/src/lib/components/room-list-item-01.component.scss +++ b/projects/ui-chat/src/lib/components/room-list-item-01.component.scss @@ -2,6 +2,33 @@ width: 100%; height: 100%; + display: flex; + flex-direction: row; + align-content: center; + justify-content: space-between; + .ucap-chat-room-list-item1-profile-image { + display: inline-flex; + position: relative; + align-self: center; + } + .ucap-chat-room-list-item1-info { + display: flex; + flex-direction: column; + justify-content: center; + .roomName { + display: flex; + flex-direction: row; + .ico-chat-list { + .ico-off { + &-false { + display: inline-block; + } + &-true { + display: none; + } + } + } + } } } diff --git a/projects/ui-chat/src/lib/components/room-list-item-01.component.ts b/projects/ui-chat/src/lib/components/room-list-item-01.component.ts index 89a53a6..3d300ff 100644 --- a/projects/ui-chat/src/lib/components/room-list-item-01.component.ts +++ b/projects/ui-chat/src/lib/components/room-list-item-01.component.ts @@ -9,6 +9,8 @@ import { ChangeDetectorRef } from '@angular/core'; import { RoomInfo, RoomType } from '@ucap/protocol-room'; +import { I18nService } from '@ucap/ng-i18n'; +import { EventType } from '@ucap/protocol-event'; @Component({ selector: 'ucap-chat-room-list-item-01', @@ -20,6 +22,9 @@ export class RoomListItem01Component implements OnInit, OnDestroy { @Input() roomInfo: RoomInfo; + @Input() + isCurrentRoom = false; + @Input() profileImageRoot: string; @@ -38,6 +43,9 @@ export class RoomListItem01Component implements OnInit, OnDestroy { @Input() checked = false; + @Input() + moreable = true; + @Output() toggleItem = new EventEmitter<{ checked: boolean; @@ -55,12 +63,29 @@ export class RoomListItem01Component implements OnInit, OnDestroy { RoomType = RoomType; - constructor(private changeDetectorRef: ChangeDetectorRef) {} + constructor( + private i18nService: I18nService, + private changeDetectorRef: ChangeDetectorRef + ) {} ngOnInit(): void {} ngOnDestroy(): void {} + finalEventMessage(): string { + if (!!this.roomInfo.isTimeRoom) { + return this.i18nService.t('chat:event.isRoomTypeSecret'); + } else if ( + this.roomInfo.finalEventType === EventType.Character && + !!this.roomInfo.finalEventMessage && + this.roomInfo.finalEventMessage.trim().length === 0 + ) { + return this.i18nService.t('chat:event.noRecentChat'); + } else { + return this.roomInfo.finalEventMessage; + } + } + onToggleItem(value: boolean): void { this.toggleItem.emit({ checked: value, diff --git a/projects/ui-chat/src/public-api.ts b/projects/ui-chat/src/public-api.ts index 9655d82..b48b89b 100644 --- a/projects/ui-chat/src/public-api.ts +++ b/projects/ui-chat/src/public-api.ts @@ -2,9 +2,32 @@ * Public API Surface of ui-chat */ -export * from './lib/config/module-config'; +export * from './lib/components/message-box/attach-file.component'; +export * from './lib/components/message-box/date-splitter.component'; +export * from './lib/components/message-box/file.component'; +export * from './lib/components/message-box/image.component'; +export * from './lib/components/message-box/information.component'; +export * from './lib/components/message-box/mass-translation.component'; +export * from './lib/components/message-box/mass.component'; +export * from './lib/components/message-box/read-here.component'; +export * from './lib/components/message-box/recall.component'; +export * from './lib/components/message-box/reply.component'; +export * from './lib/components/message-box/schedule.component'; +export * from './lib/components/message-box/sms.component'; +export * from './lib/components/message-box/sticker.component'; +export * from './lib/components/message-box/text.component'; +export * from './lib/components/message-box/translation.component'; +export * from './lib/components/message-box/video-conference.component'; +export * from './lib/components/message-box/video.component'; +export * from './lib/components/file-list-item-01.component'; +export * from './lib/components/image-list-item-01.component'; +export * from './lib/components/image-list.component'; +export * from './lib/components/item-list.component'; export * from './lib/components/room-expansion.component'; export * from './lib/components/room-list-item-01.component'; +export * from './lib/config/module-config'; +export * from './lib/config/token'; + export * from './lib/chat-ui.module'; diff --git a/projects/ui-group/package.json b/projects/ui-group/package.json index 0fde3b2..4c8f4b9 100644 --- a/projects/ui-group/package.json +++ b/projects/ui-group/package.json @@ -1,6 +1,6 @@ { "name": "@ucap/ng-ui-group", - "version": "0.0.33", + "version": "0.0.78", "publishConfig": { "registry": "https://nexus.loafle.net/repository/npm-ucap/" }, diff --git a/projects/ui-group/src/lib/components/expansion-list.component.html b/projects/ui-group/src/lib/components/expansion-list.component.html deleted file mode 100644 index f13294b..0000000 --- a/projects/ui-group/src/lib/components/expansion-list.component.html +++ /dev/null @@ -1,91 +0,0 @@ -
    - - - - - -
  • -
    - -
    -
  • -
    - - -
  • -
    - -
    - - - - - - - - -
    - - - - -
    -
      -
      -
      - -
      -
    -
  • -
    -
    -
    -
    diff --git a/projects/ui-group/src/lib/components/expansion-list.component.scss b/projects/ui-group/src/lib/components/expansion-list.component.scss deleted file mode 100644 index e69de29..0000000 diff --git a/projects/ui-group/src/lib/components/expansion-list.component.stories.ts b/projects/ui-group/src/lib/components/expansion-list.component.stories.ts deleted file mode 100644 index 7a717ae..0000000 --- a/projects/ui-group/src/lib/components/expansion-list.component.stories.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { action } from '@storybook/addon-actions'; -import { linkTo } from '@storybook/addon-links'; - -import { ExpansionListComponent } from './expansion-list.component'; - -export default { - title: 'ExpansionListComponent', - component: ExpansionListComponent -}; - -export const Text = () => ({ - component: ExpansionListComponent, - props: { - text: 'Hello ExpansionListComponent' - } -}); diff --git a/projects/ui-group/src/lib/components/expansion-list.component.ts b/projects/ui-group/src/lib/components/expansion-list.component.ts deleted file mode 100644 index eafbf9f..0000000 --- a/projects/ui-group/src/lib/components/expansion-list.component.ts +++ /dev/null @@ -1,295 +0,0 @@ -import { - Component, - OnInit, - OnDestroy, - Input, - Output, - EventEmitter, - ViewChild, - ContentChild, - TemplateRef, - ChangeDetectionStrategy, - ChangeDetectorRef -} from '@angular/core'; - -import { CdkVirtualScrollViewport } from '@angular/cdk/scrolling'; -import { FlatTreeControl } from '@angular/cdk/tree'; - -import { MatTreeFlattener, MatTree } from '@angular/material/tree'; - -import { UserInfo, GroupDetailData } from '@ucap/protocol-sync'; - -import { VirtualScrollTreeFlatDataSource } from '@ucap/ng-ui'; -import { UserInfoSS, UserInfoF, UserInfoDN } from '@ucap/protocol-query'; - -export enum NodeType { - None = 'None', - Profile = 'Profile', - Favorite = 'Favorite', - Buddy = 'Buddy', - Default = 'Default' -} - -export interface GroupNode { - nodeType: NodeType; - userInfo?: UserInfo; - groupDetail?: GroupDetailData; - children?: GroupNode[]; -} - -export interface FlatNode { - expandable: boolean; - level: number; - node: GroupNode; -} - -@Component({ - selector: 'ucap-group-expansion-list', - templateUrl: './expansion-list.component.html', - styleUrls: ['./expansion-list.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush -}) -export class ExpansionListComponent implements OnInit, OnDestroy { - @Input() - displayOrder: NodeType[] = []; - - @Input() - set profile(userInfo: UserInfo) { - if (!userInfo) { - this.nodeMap.set(NodeType.Profile, []); - } else { - const node: GroupNode = { - nodeType: NodeType.Profile, - userInfo, - children: [] - }; - - this.nodeMap.set(NodeType.Profile, [node]); - } - - this.refreshNodes(); - } - - @Input() - set favorites(userInfos: UserInfo[]) { - if (!userInfos || 0 === userInfos.length) { - this.nodeMap.set(NodeType.Favorite, []); - } else { - const node: GroupNode = { - nodeType: NodeType.Favorite, - groupDetail: { - seq: -9999, - name: NodeType.Favorite, - isActive: true, - userSeqs: userInfos.map((userInfo) => String(userInfo.seq)) - } as GroupDetailData, - children: [] - }; - - userInfos.forEach((userInfo) => { - node.children.push({ - nodeType: NodeType.Favorite, - userInfo - }); - }); - - this.nodeMap.set(NodeType.Favorite, [node]); - } - - this.refreshNodes(); - } - - @Input() - set groupBuddies(list: { group: GroupDetailData; buddyList: UserInfo[] }[]) { - if (!list || 0 === list.length) { - this.nodeMap.set(NodeType.Default, []); - this.nodeMap.set(NodeType.Buddy, []); - } else { - this.nodeMap.set(NodeType.Default, []); - this.nodeMap.set(NodeType.Buddy, []); - - for (const item of list) { - let nodeType = NodeType.Buddy; - if (0 === item.group.seq) { - nodeType = NodeType.Default; - } - - const node: GroupNode = { - nodeType, - groupDetail: item.group, - children: [] - }; - - item.buddyList.sort((a, b) => - a.order < b.order - ? -1 - : a.order > b.order - ? 1 - : a.name < b.name - ? -1 - : a.name > b.name - ? 1 - : 0 - ); - - item.buddyList.forEach((userInfo) => { - node.children.push({ - nodeType, - groupDetail: item.group, - userInfo - }); - }); - - if (NodeType.Buddy === nodeType) { - this.nodeMap.get(NodeType.Buddy).push(node); - } else { - this.nodeMap.get(NodeType.Default).push(node); - } - } - } - - this.refreshNodes(); - } - - @Input() - checkable = false; - - @Input() - selectedUserList?: (UserInfo | UserInfoSS | UserInfoF | UserInfoDN)[] = []; - - @Input() - unselectableUserList?: ( - | UserInfo - | UserInfoSS - | UserInfoF - | UserInfoDN - )[] = []; - - @ViewChild('treeList', { static: false }) - treeList: MatTree; - - @ViewChild('cvsvList', { static: false }) - cvsvList: CdkVirtualScrollViewport; - - @ContentChild('[ucapGroupExpansionList="node"]', { read: TemplateRef }) - nodeTemplate: TemplateRef; - - @ContentChild('[ucapGroupExpansionList="favoriteHeader"]', { - read: TemplateRef - }) - favoriteHeaderTemplate: TemplateRef; - - @ContentChild('[ucapGroupExpansionList="defaultHeader"]', { - read: TemplateRef - }) - defaultHeaderTemplate: TemplateRef; - - @ContentChild('[ucapGroupExpansionList="buddyHeader"]', { read: TemplateRef }) - buddyHeaderTemplate: TemplateRef; - - treeControl: FlatTreeControl; - treeFlattener: MatTreeFlattener; - dataSource: VirtualScrollTreeFlatDataSource; - - NodeType = NodeType; - - private nodeMap: Map; - - constructor(private changeDetectorRef: ChangeDetectorRef) { - this.treeControl = new FlatTreeControl( - (node) => node.level, - (node) => node.expandable - ); - - this.treeFlattener = new MatTreeFlattener( - (node: GroupNode, level: number) => { - return { - expandable: !!node.children && node.children.length > 0, - level, - nodeType: node.nodeType, - node - }; - }, - (node) => node.level, - (node) => node.expandable, - (node) => node.children - ); - - this.dataSource = new VirtualScrollTreeFlatDataSource( - this.treeControl, - this.treeFlattener - ); - } - - ngOnInit(): void {} - - ngOnDestroy(): void {} - - onClickHeaderMenu(event: MouseEvent, node: FlatNode) {} - - isCheckedGroup(node: FlatNode): boolean { - const groupDetail = node.node.groupDetail; - - if (!groupDetail || groupDetail === undefined) { - return false; - } - - if (groupDetail.userSeqs.length === 0) { - return false; - } - - if (!!this.selectedUserList && this.selectedUserList.length > 0) { - let allExist = true; - groupDetail.userSeqs.some((seq) => { - if ( - this.selectedUserList.filter((item) => item.seq === seq).length === 0 - ) { - allExist = false; - return true; - } - }); - return allExist; - } - return false; - } - - isCheckableGroup(node: FlatNode): boolean { - if (!!this.unselectableUserList && this.unselectableUserList.length > 0) { - const groupDetail = node.node.groupDetail; - let allExist = true; - groupDetail.userSeqs.some((seq) => { - if ( - this.unselectableUserList.filter((item) => item.seq === seq) - .length === 0 - ) { - allExist = false; - return true; - } - }); - - if (allExist) { - return false; - } - } - - return true; - } - - onChangeCheckGroup(value: boolean, node: FlatNode) {} - - isHeader = (_: number, node: FlatNode) => - NodeType.Profile !== node.node.nodeType && 0 === node.level; - - private refreshNodes() { - const rootNode: GroupNode[] = []; - - for (const nodeType of this.displayOrder) { - const subNode = this.nodeMap.get(nodeType); - if (!!subNode && 0 < subNode.length) { - rootNode.push(...subNode); - } - } - - this.dataSource.data = rootNode; - } -} diff --git a/projects/ui-group/src/lib/components/expansion.component.html b/projects/ui-group/src/lib/components/expansion.component.html index d343959..c62f8db 100644 --- a/projects/ui-group/src/lib/components/expansion.component.html +++ b/projects/ui-group/src/lib/components/expansion.component.html @@ -1,19 +1,22 @@
    - + -
  • +
  • -
  • +
  • diff --git a/projects/ui-group/src/lib/components/expansion.component.scss b/projects/ui-group/src/lib/components/expansion.component.scss index cbc5e38..b100b4f 100644 --- a/projects/ui-group/src/lib/components/expansion.component.scss +++ b/projects/ui-group/src/lib/components/expansion.component.scss @@ -3,6 +3,9 @@ --ucap-group-expansion-container-backgroupd-position, absolute ); + + .ucap-selected-groupt-header { + } } .ucap-clickable { display: flex; diff --git a/projects/ui-group/src/lib/components/expansion.component.ts b/projects/ui-group/src/lib/components/expansion.component.ts index b0fb215..8eb5ec5 100644 --- a/projects/ui-group/src/lib/components/expansion.component.ts +++ b/projects/ui-group/src/lib/components/expansion.component.ts @@ -1,3 +1,6 @@ +import { Subject } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; + import { Component, OnInit, @@ -10,20 +13,22 @@ import { TemplateRef, ChangeDetectionStrategy, ChangeDetectorRef, - Directive + Directive, + AfterViewInit } from '@angular/core'; -import { CdkVirtualScrollViewport } from '@angular/cdk/scrolling'; import { FlatTreeControl } from '@angular/cdk/tree'; import { MatTreeFlattener, MatTree } from '@angular/material/tree'; import { UserInfo, GroupDetailData } from '@ucap/protocol-sync'; - -import { VirtualScrollTreeFlatDataSource } from '@ucap/ng-ui'; import { UserInfoSS, UserInfoF, UserInfoDN } from '@ucap/protocol-query'; -import { Subject } from 'rxjs'; -import { takeUntil } from 'rxjs/operators'; + +import { + VirtualScrollTreeFlatDataSource, + VirtualScrollViewportComponent +} from '@ucap/ng-ui'; + import { PerfectScrollbarDirective } from 'ngx-perfect-scrollbar'; export enum NodeType { @@ -73,10 +78,16 @@ export class ExpansionBuddyHeaderDirective {} styleUrls: ['./expansion.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush }) -export class ExpansionComponent implements OnInit, OnDestroy { +export class ExpansionComponent implements OnInit, OnDestroy, AfterViewInit { @Input() displayOrder: NodeType[] = []; + @Input() + vsItemSize: number; + + @Input() + isAllExpand = false; + @Input() set profile(userInfo: UserInfo) { if (!userInfo) { @@ -112,7 +123,8 @@ export class ExpansionComponent implements OnInit, OnDestroy { userInfos.forEach((userInfo) => { node.children.push({ - nodeType: NodeType.Favorite, + nodeType: NodeType.Profile, + groupDetail: node.groupDetail, userInfo }); }); @@ -160,7 +172,7 @@ export class ExpansionComponent implements OnInit, OnDestroy { item.buddyList.forEach((userInfo) => { node.children.push({ - nodeType, + nodeType: NodeType.Profile, groupDetail: item.group, userInfo }); @@ -177,9 +189,15 @@ export class ExpansionComponent implements OnInit, OnDestroy { this.refreshNodes(); } + @Input() + selectedGroupHeader: GroupDetailData; + @Input() checkable = false; + @Input() + moreable = false; + @Input() selectedUserList?: (UserInfo | UserInfoSS | UserInfoF | UserInfoDN)[] = []; @@ -203,14 +221,17 @@ export class ExpansionComponent implements OnInit, OnDestroy { groupBuddyList: { group: GroupDetailData; buddyList: UserInfo[] }; }>(); + @Output() + clickNode = new EventEmitter<{ node: FlatNode; isExpand: boolean }>(); + @ViewChild('treeList', { static: false }) treeList: MatTree; @ViewChild('cvsvList', { static: false }) - cvsvList: CdkVirtualScrollViewport; + cvsvList: VirtualScrollViewportComponent; @ViewChild(PerfectScrollbarDirective, { static: false }) - psDirectiveRef?: PerfectScrollbarDirective; + psDirectiveRef: PerfectScrollbarDirective; @ContentChild(ExpansionNodeDirective, { read: TemplateRef, @@ -242,10 +263,11 @@ export class ExpansionComponent implements OnInit, OnDestroy { NodeType = NodeType; groupList: { group: GroupDetailData; buddyList: UserInfo[] }[]; + groupOpenInfos: number[]; private nodeMap: Map = new Map(); // tslint:disable-next-line: variable-name - private _ngOnDestroySubject: Subject; + private _ngOnDestroySubject: Subject = new Subject(); constructor(private changeDetectorRef: ChangeDetectorRef) { this.treeControl = new FlatTreeControl( @@ -274,14 +296,12 @@ export class ExpansionComponent implements OnInit, OnDestroy { } ngOnInit(): void { - this._ngOnDestroySubject = new Subject(); - - this.dataSource.cdkVirtualScrollViewport = this.cvsvList; - this.treeControl.expansionModel.changed + this.dataSource.expandedData$ .pipe(takeUntil(this._ngOnDestroySubject)) - .subscribe(() => { - this.cvsvList.checkViewportSize(); - this.psDirectiveRef.update(); + .subscribe((datas) => { + if (!!this.psDirectiveRef) { + this.psDirectiveRef.update(); + } }); } @@ -292,6 +312,12 @@ export class ExpansionComponent implements OnInit, OnDestroy { } } + ngAfterViewInit(): void { + this.dataSource.virtualScrollViewport = this.cvsvList; + } + + // trackBy = (_: number, node: FlatNode) => node.node.groupDetail.seq; + onClickHeaderMenu(event: MouseEvent, node: FlatNode) { this.clickMoreMenu.emit({ event, node }); } @@ -311,7 +337,9 @@ export class ExpansionComponent implements OnInit, OnDestroy { let allExist = true; groupDetail.userSeqs.some((seq) => { if ( - this.selectedUserList.filter((item) => item.seq === seq).length === 0 + this.selectedUserList.filter( + (item) => String(item.seq) === String(seq) + ).length === 0 ) { allExist = false; return true; @@ -356,12 +384,92 @@ export class ExpansionComponent implements OnInit, OnDestroy { isChecked: value, groupBuddyList: groupInfos[0] }); + } else if (node.node.nodeType === NodeType.Favorite) { + this.checkGroup.emit({ + isChecked: value, + groupBuddyList: { + group: trgGroup, + buddyList: node.node.children.map((child) => child.userInfo) + } + }); } } isHeader = (_: number, node: FlatNode) => NodeType.Profile !== node.node.nodeType && 0 === node.level; + expandMore() { + // this.treeList.treeControl.expandAll(); + this.treeControl.expandAll(); + if (!!this.psDirectiveRef) { + this.psDirectiveRef.scrollToTop(); + } + } + + expandLess() { + this.treeList.treeControl.collapseAll(); + if (!!this.psDirectiveRef) { + this.psDirectiveRef.scrollToTop(); + } + } + + onClickNode(event: Event, node: FlatNode) { + event.stopPropagation(); + + this.clickNode.emit({ node, isExpand: this.treeControl.isExpanded(node) }); + } + + expand(groupSeqs: number[], lastGroupSeq: number) { + if (!this.treeControl.dataNodes || !groupSeqs || 0 === groupSeqs.length) { + return; + } + this.groupOpenInfos = groupSeqs; + + const flatNodes: FlatNode[] = []; + groupSeqs.forEach((s) => { + // const node = this.treeControl.dataNodes.find( + // (n) => n.node.groupDetail.seq === s + // ); + let node: FlatNode; + + this.treeControl.dataNodes.every((n) => { + if ( + n.node.nodeType !== NodeType.Profile && + n.node.groupDetail.seq === s + ) { + node = n; + return false; + } + return true; + }); + if (!!node) { + flatNodes.push(node); + } + }); + if (0 < flatNodes.length) { + this.treeControl.expansionModel.select(...flatNodes); + } + + // setTimeout( + // () => { + // const curIndex = this.dataSource.expandedDataSubject.value.findIndex( + // (node) => node.node.groupDetail.seq === lastGroupSeq + // ); + // if (-1 < curIndex) { + // if ('vertical' === this.cvsvList.orientation) { + // this.psDirectiveRef.scrollToY( + // this.cvsvList.offsetForIndex(curIndex) + // ); + // } + // } + // }, + // 0 < flatNodes.length && + // 0 === this.dataSource.expandedDataSubject.value.length + // ? 100 + // : 0 + // ); + } + private refreshNodes() { const rootNode: GroupNode[] = []; @@ -373,20 +481,14 @@ export class ExpansionComponent implements OnInit, OnDestroy { } this.dataSource.data = rootNode; - this.changeDetectorRef.detectChanges(); - } + this.changeDetectorRef.markForCheck(); - expandMore() { - this.treeList.treeControl.expandAll(); - if (!!this.psDirectiveRef) { - this.psDirectiveRef.scrollToTop(); + if (!!this.groupOpenInfos && this.groupOpenInfos.length > 0) { + this.expand(this.groupOpenInfos, 0); } - } - expandLess() { - this.treeList.treeControl.collapseAll(); - if (!!this.psDirectiveRef) { - this.psDirectiveRef.scrollToTop(); + if (!!this.isAllExpand) { + this.expandMore(); } } } diff --git a/projects/ui-group/src/public-api.ts b/projects/ui-group/src/public-api.ts index 59f0846..7e1ac1c 100644 --- a/projects/ui-group/src/public-api.ts +++ b/projects/ui-group/src/public-api.ts @@ -2,8 +2,9 @@ * Public API Surface of ui-group */ -export * from './lib/config/module-config'; - export * from './lib/components/expansion.component'; +export * from './lib/config/module-config'; +export * from './lib/config/token'; + export * from './lib/group-ui.module'; diff --git a/projects/ui-organization/package.json b/projects/ui-organization/package.json index 47d12d5..f23ff36 100644 --- a/projects/ui-organization/package.json +++ b/projects/ui-organization/package.json @@ -1,6 +1,6 @@ { "name": "@ucap/ng-ui-organization", - "version": "0.0.84", + "version": "0.0.202", "publishConfig": { "registry": "https://nexus.loafle.net/repository/npm-ucap/" }, diff --git a/projects/ui-organization/src/lib/components/profile-01.component.html b/projects/ui-organization/src/lib/components/profile-01.component.html index f0150ca..95c7d7e 100644 --- a/projects/ui-organization/src/lib/components/profile-01.component.html +++ b/projects/ui-organization/src/lib/components/profile-01.component.html @@ -9,36 +9,53 @@ aria-label="Example icon-button with a heart icon" *ngIf="isBuddy" (click)="onToggleFavorit()" + [ngClass]="!!isFavorite ? 'btn-star-chk' : ''" > star_border -
  • -
    -
    @@ -46,20 +63,18 @@ - -
    {{ userInfo?.intro }}
    - -
    - - - - - -
    -
    + + + +
    + + + + {{ myProfileIntro.value?.length || 0 }}/20 + + +
    +
    @@ -201,24 +272,24 @@ diff --git a/projects/ui-organization/src/lib/components/profile-01.component.scss b/projects/ui-organization/src/lib/components/profile-01.component.scss index 61edf51..897cf46 100644 --- a/projects/ui-organization/src/lib/components/profile-01.component.scss +++ b/projects/ui-organization/src/lib/components/profile-01.component.scss @@ -64,42 +64,22 @@ align-items: center; justify-content: space-between; } - .my-input { - display: flex; - flex-flow: row nowrap; - justify-content: space-between; - margin: 0; - width: 100%; - - .my-in-input { - flex-grow: 1; - } - button { - } - } - .user-profile-info-list { - ul { - display: flex; - flex-direction: column; - justify-content: space-between; - } - } } - - ///////////////////////////////////////////////////////////////////////////////// 제거 - .profile-card { + .my-input { + display: flex; + flex-flow: row nowrap; + justify-content: space-between; + margin: 0; width: 100%; - background-color: transparent; - box-shadow: none; - .profileImage { - box-sizing: border-box; - overflow: hidden; - display: flex; - img { - display: flex; - align-items: center; - } + .my-in-input { + flex-grow: 1; + } + } + .user-profile-info-list { + ul { + display: flex; + flex-direction: column; + justify-content: space-between; } } - ///////////////////////////////////////////////////////////////////////////////////// } diff --git a/projects/ui-organization/src/lib/components/profile-01.component.ts b/projects/ui-organization/src/lib/components/profile-01.component.ts index 6b143c8..2e6ea30 100644 --- a/projects/ui-organization/src/lib/components/profile-01.component.ts +++ b/projects/ui-organization/src/lib/components/profile-01.component.ts @@ -1,3 +1,5 @@ +import { Subject } from 'rxjs'; + import { Component, OnInit, @@ -10,10 +12,13 @@ import { ViewChild, ElementRef } from '@angular/core'; + +import { FileUploadItem } from '@ucap/api'; +import { VersionInfo2Response } from '@ucap/api-public'; + import { UserInfoSS, AuthResponse } from '@ucap/protocol-query'; import { OpenProfileOptions } from '@ucap/protocol-buddy'; -import { FileUploadItem } from '@ucap/api'; -import { Subject } from 'rxjs'; +import { StatusBulkInfo } from '@ucap/protocol-status'; @Component({ selector: 'ucap-organization-profile-01', @@ -25,12 +30,21 @@ export class Profile01Component implements OnInit, OnDestroy { @Input() authRes: AuthResponse; + @Input() + versionInfo2Res: VersionInfo2Response; + @Input() userInfo: UserInfoSS; + @Input() + nickName: string; + @Input() profileImageRoot: string; + @Input() + defaultProfileImage: string; + @Input() isMe: boolean; @@ -53,10 +67,10 @@ export class Profile01Component implements OnInit, OnDestroy { openProfileOptions: OpenProfileOptions; @Input() - presenceClass: string; + presenceInfo: StatusBulkInfo; @Input() - presenceMsg: string; + companyName: string; @Output() toggleFavorite: EventEmitter<{ @@ -86,7 +100,7 @@ export class Profile01Component implements OnInit, OnDestroy { sendMessage: EventEmitter = new EventEmitter(); @Output() - profileImageView: EventEmitter = new EventEmitter(); + profileImageView: EventEmitter = new EventEmitter(); @Output() uploadProfileImage: EventEmitter = new EventEmitter(); @@ -97,18 +111,19 @@ export class Profile01Component implements OnInit, OnDestroy { @Output() changeNickname: EventEmitter = new EventEmitter(); + @Output() + settings: EventEmitter = new EventEmitter(); + @ViewChild('profileImageFileInput', { static: false }) profileImageFileInput: ElementRef; profileImageFileUploadItem: FileUploadItem; - private ngOnDestroySubject: Subject; + private ngOnDestroySubject: Subject = new Subject(); constructor(private changeDetectorRef: ChangeDetectorRef) {} - ngOnInit(): void { - this.ngOnDestroySubject = new Subject(); - } + ngOnInit(): void {} ngOnDestroy(): void { if (!!this.ngOnDestroySubject) { @@ -179,8 +194,12 @@ export class Profile01Component implements OnInit, OnDestroy { this.createConference.emit(Number(this.userInfo.seq)); } - onProfileImageView() { - this.profileImageView.emit(); + onProfileImageView(userInfo: UserInfoSS) { + this.profileImageView.emit(userInfo); + } + + onSettings(event: MouseEvent) { + this.settings.emit(); } getShowBuddyToggleBtn(type: 'DEL' | 'ADD'): boolean { @@ -201,6 +220,17 @@ export class Profile01Component implements OnInit, OnDestroy { return rtn; } + getUserIntro(info: UserInfoSS): string { + if (!!info && !!info.intro) { + if (info.intro.trim().localeCompare('') === 0) { + return info.intro.trim(); + } else { + return info.intro; + } + } + + return ''; + } getDisabledBtn(type: string): boolean { if (!this.myMadn || this.myMadn.trim().length === 0) { if (type === 'LINE' || type === 'MOBILE') { @@ -239,4 +269,16 @@ export class Profile01Component implements OnInit, OnDestroy { return true; } + + getUserNameForEn(): string { + if ( + !!this.userInfo && + !!this.userInfo.nameEn && + this.userInfo.nameEn.trim() !== '' + ) { + return '(' + this.userInfo.nameEn + ')'; + } + + return ''; + } } diff --git a/projects/ui-organization/src/lib/components/profile-image-01.component.html b/projects/ui-organization/src/lib/components/profile-image-01.component.html index c4aa9f8..1993734 100644 --- a/projects/ui-organization/src/lib/components/profile-image-01.component.html +++ b/projects/ui-organization/src/lib/components/profile-image-01.component.html @@ -8,6 +8,7 @@ @@ -18,7 +19,7 @@
    diff --git a/projects/ui-organization/src/lib/components/profile-image-01.component.scss b/projects/ui-organization/src/lib/components/profile-image-01.component.scss index ad5c031..acfff9a 100644 --- a/projects/ui-organization/src/lib/components/profile-image-01.component.scss +++ b/projects/ui-organization/src/lib/components/profile-image-01.component.scss @@ -3,6 +3,7 @@ .ucap-organization-profile-image-01-container { width: 100%; height: 100%; + display: inline-flex; .ucap-organization-profile-image-01-image-container { } diff --git a/projects/ui-organization/src/lib/components/profile-image-01.component.ts b/projects/ui-organization/src/lib/components/profile-image-01.component.ts index a7c098e..d44cc43 100644 --- a/projects/ui-organization/src/lib/components/profile-image-01.component.ts +++ b/projects/ui-organization/src/lib/components/profile-image-01.component.ts @@ -15,6 +15,8 @@ import { PresenceType } from '@ucap/core'; import { UserInfoSS } from '@ucap/protocol-query'; import { StatusBulkInfo } from '@ucap/protocol-status'; +import { I18nService } from '@ucap/ng-i18n'; + import { PresenceUtil } from '../utils/presence.util'; @Component({ @@ -44,7 +46,10 @@ export class ProfileImage01Component implements OnInit, OnDestroy { private ngOnDestroySubject: Subject; - constructor(private changeDetectorRef: ChangeDetectorRef) {} + constructor( + private i18nService: I18nService, + private changeDetectorRef: ChangeDetectorRef + ) {} ngOnInit(): void { this.ngOnDestroySubject = new Subject(); @@ -63,4 +68,29 @@ export class ProfileImage01Component implements OnInit, OnDestroy { this.openProfile.emit(this.userInfo); } + + getProfileImageFile(userInfo: UserInfoSS): string | undefined { + if (!!userInfo) { + return userInfo.profileImageFile; + } else { + return undefined; + } + } + + getStatusMessage(presenceType: PresenceType): string { + if (PresenceUtil.isOnline(this.presenceInfo, presenceType)) { + return this.i18nService.t('organization:presence.online'); + } else if (PresenceUtil.isOffline(this.presenceInfo, presenceType)) { + return this.i18nService.t('organization:presence.offline'); + } else if ( + PresenceUtil.isAway(this.presenceInfo, presenceType) || + PresenceUtil.isAway2(this.presenceInfo, presenceType) + ) { + return this.i18nService.t('organization:presence.away'); + } else if (PresenceUtil.isBusy(this.presenceInfo, presenceType)) { + return this.presenceInfo.statusMessage; + } else { + return ''; + } + } } diff --git a/projects/ui-organization/src/lib/components/profile-list-item-01.component.html b/projects/ui-organization/src/lib/components/profile-list-item-01.component.html index 4db1c95..c28ac58 100644 --- a/projects/ui-organization/src/lib/components/profile-list-item-01.component.html +++ b/projects/ui-organization/src/lib/components/profile-list-item-01.component.html @@ -31,6 +31,10 @@ {{ userInfo.lineNumber | ucapPhoneNumber }}
    +
    +
    - -
    - +
    +
    +
    diff --git a/projects/ui-organization/src/lib/components/profile-list-item.component.scss b/projects/ui-organization/src/lib/components/profile-list-item.component.scss deleted file mode 100644 index e69de29..0000000 diff --git a/projects/ui-organization/src/lib/components/profile-list-item.component.stories.ts b/projects/ui-organization/src/lib/components/profile-list-item.component.stories.ts deleted file mode 100644 index 5811451..0000000 --- a/projects/ui-organization/src/lib/components/profile-list-item.component.stories.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { action } from '@storybook/addon-actions'; -import { linkTo } from '@storybook/addon-links'; - -import { ProfileListItemComponent } from './profile-list-item.component'; - -export default { - title: 'ProfileListItemComponent', - component: ProfileListItemComponent -}; - -export const Text = () => ({ - component: ProfileListItemComponent, - props: { - text: 'Hello ProfileListItemComponent' - } -}); diff --git a/projects/ui-organization/src/lib/components/profile-list-item.component.ts b/projects/ui-organization/src/lib/components/profile-list-item.component.ts deleted file mode 100644 index b623c70..0000000 --- a/projects/ui-organization/src/lib/components/profile-list-item.component.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { - Component, - OnInit, - OnDestroy, - ChangeDetectionStrategy, - ChangeDetectorRef -} from '@angular/core'; - -@Component({ - selector: 'ucap-organization-profile-list-item', - templateUrl: './profile-list-item.component.html', - styleUrls: ['./profile-list-item.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush -}) -export class ProfileListItemComponent implements OnInit, OnDestroy { - constructor(private changeDetectorRef: ChangeDetectorRef) {} - - ngOnInit(): void {} - - ngOnDestroy(): void {} -} diff --git a/projects/ui-organization/src/lib/components/profile-list.component.html b/projects/ui-organization/src/lib/components/profile-list.component.html index 228c62a..1ba666d 100644 --- a/projects/ui-organization/src/lib/components/profile-list.component.html +++ b/projects/ui-organization/src/lib/components/profile-list.component.html @@ -1,10 +1,19 @@ -
    - - - +
    + + +
    + +
    - +
    diff --git a/projects/ui-organization/src/lib/components/profile-list.component.scss b/projects/ui-organization/src/lib/components/profile-list.component.scss index 98fb68c..0c17a55 100644 --- a/projects/ui-organization/src/lib/components/profile-list.component.scss +++ b/projects/ui-organization/src/lib/components/profile-list.component.scss @@ -3,4 +3,9 @@ .ucap-organization-profile-list-container { width: 100%; height: 100%; + + ucap-virtual-scroll-viewport { + width: 100%; + height: 100%; + } } diff --git a/projects/ui-organization/src/lib/components/profile-list.component.ts b/projects/ui-organization/src/lib/components/profile-list.component.ts index b646f8c..5e6303f 100644 --- a/projects/ui-organization/src/lib/components/profile-list.component.ts +++ b/projects/ui-organization/src/lib/components/profile-list.component.ts @@ -9,12 +9,17 @@ import { Input, Directive, ContentChild, - TemplateRef + TemplateRef, + ViewChild, + ElementRef } from '@angular/core'; +import { ObjectUtil } from '@ucap/core'; import { UserInfo } from '@ucap/protocol-sync'; import { UserInfoSS, UserInfoF, UserInfoDN } from '@ucap/protocol-query'; +import { VirtualScrollViewportComponent } from '@ucap/ng-ui'; + export type UserInfoTypes = UserInfo | UserInfoSS | UserInfoF | UserInfoDN; @Directive({ @@ -30,7 +35,13 @@ export class ProfileListNodeDirective {} }) export class ProfileListComponent implements OnInit, OnDestroy { @Input() - userInfos: UserInfo[]; + userInfos: UserInfoTypes[]; + + @Input() + vsItemSize: number; + + @Input() + cacheSize = 20; @ContentChild(ProfileListNodeDirective, { read: TemplateRef, @@ -38,6 +49,14 @@ export class ProfileListComponent implements OnInit, OnDestroy { }) nodeTemplate: TemplateRef; + @ViewChild('container', { static: true }) + container: ElementRef; + + @ViewChild('cvsvList', { static: true }) + cvsvList: VirtualScrollViewportComponent; + + ObjectUtil = ObjectUtil; + private ngOnDestroySubject: Subject; constructor(private changeDetectorRef: ChangeDetectorRef) {} diff --git a/projects/ui-organization/src/lib/components/profile-menu-01.component.html b/projects/ui-organization/src/lib/components/profile-menu-01.component.html new file mode 100644 index 0000000..1d73c09 --- /dev/null +++ b/projects/ui-organization/src/lib/components/profile-menu-01.component.html @@ -0,0 +1,187 @@ +
    +
    + +
    +
    + +
    +
    + + {{ + _userIntro + }} + {{ placeholderForUserIntro }} + + + + +
    +
    + +
    +
    +
    + {{ 'organization:presence.online' | ucapI18n }} +
    +
    + {{ 'organization:presence.away' | ucapI18n }} + + + {{ idleTimePeriod }}{{ 'common:units.minute' | ucapI18n }} + + +
    +
    + + {{ user.statusMessage1 }} + + + + +
    +
    + + {{ user.statusMessage2 }} + + + + +
    +
    + + {{ user.statusMessage3 }} + + + + +
    +
    +
    + +
    +
    +
    + +
    +
    + +
    +
    +
    diff --git a/projects/ui-organization/src/lib/components/profile-menu-01.component.scss b/projects/ui-organization/src/lib/components/profile-menu-01.component.scss new file mode 100644 index 0000000..29e4165 --- /dev/null +++ b/projects/ui-organization/src/lib/components/profile-menu-01.component.scss @@ -0,0 +1,26 @@ +@import '~@ucap/lg-scss/mixins'; + +.ucap-organization-profile-menu-01-container { + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + .ucap-organization-profile-menu-01-profile { + //profile + } + .ucap-organization-profile-menu-01-intro { + //profile intro + } + .ucap-organization-profile-menu-01-intro-below { + display: flex; + flex-direction: row; + } + .ucap-organization-profile-menu-01-status-message { + display: flex; + flex-direction: column; + } + .ucap-organization-profile-menu-01-btn-area { + display: flex; + flex-direction: row; + } +} diff --git a/projects/ui-organization/src/lib/components/profile-menu-01.component.spec.ts b/projects/ui-organization/src/lib/components/profile-menu-01.component.spec.ts new file mode 100644 index 0000000..a636a40 --- /dev/null +++ b/projects/ui-organization/src/lib/components/profile-menu-01.component.spec.ts @@ -0,0 +1,12 @@ +import { TestBed, async } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { ProfileMenu01Component } from './profile-menu-01.component'; + +describe('ucap::organization::ProfileMenu01Component', () => { + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [RouterTestingModule], + declarations: [ProfileMenu01Component] + }).compileComponents(); + })); +}); diff --git a/projects/ui-organization/src/lib/components/profile-menu-01.component.stories.ts b/projects/ui-organization/src/lib/components/profile-menu-01.component.stories.ts new file mode 100644 index 0000000..8b42995 --- /dev/null +++ b/projects/ui-organization/src/lib/components/profile-menu-01.component.stories.ts @@ -0,0 +1,26 @@ +import { moduleMetadata } from '@storybook/angular'; +import { action } from '@storybook/addon-actions'; +import { linkTo } from '@storybook/addon-links'; + +import { BrowserModule } from '@angular/platform-browser'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; + +import { OrganizationUiModule } from '../organization-ui.module'; + +import { ProfileMenu01Component } from './profile-menu-01.component'; + +export default { + title: 'ui-organization::ProfileMenu01Component', + component: ProfileMenu01Component, + decorators: [ + moduleMetadata({ + imports: [BrowserModule, BrowserAnimationsModule, OrganizationUiModule], + providers: [] + }) + ] +}; + +export const Default = () => ({ + component: ProfileMenu01Component, + props: {} +}); diff --git a/projects/ui-organization/src/lib/components/profile-menu-01.component.theme.scss b/projects/ui-organization/src/lib/components/profile-menu-01.component.theme.scss new file mode 100644 index 0000000..88da8ad --- /dev/null +++ b/projects/ui-organization/src/lib/components/profile-menu-01.component.theme.scss @@ -0,0 +1,20 @@ +@import '~@ucap/ng-ui-material/material'; + +@mixin ucap-organization-profile-menu-01-theme($theme) { + $is-dark-theme: map-get($theme, is-dark); + $primary: map-get($theme, primary); + $accent: map-get($theme, accent); + $warn: map-get($theme, warn); + $background: map-get($theme, background); + $foreground: map-get($theme, foreground); + + .ucap-organization-profile-menu-01-container { + border-color: mat-color($foreground, secondary-text); + } +} + +@mixin ucap-organization-profile-menu-01-typography($config) { + .ucap-organization-profile-menu-01-container { + font-family: mat-font-family($config); + } +} diff --git a/projects/ui-organization/src/lib/components/profile-menu-01.component.ts b/projects/ui-organization/src/lib/components/profile-menu-01.component.ts new file mode 100644 index 0000000..521ee89 --- /dev/null +++ b/projects/ui-organization/src/lib/components/profile-menu-01.component.ts @@ -0,0 +1,179 @@ +import { Subject } from 'rxjs'; +import { takeUntil, take, map, withLatestFrom, tap } from 'rxjs/operators'; + +import { + Component, + OnInit, + OnDestroy, + ChangeDetectionStrategy, + ChangeDetectorRef, + Input, + HostBinding, + Output, + EventEmitter, + Directive, + ContentChild, + TemplateRef +} from '@angular/core'; + +import { MatRadioChange } from '@angular/material/radio'; + +import { VersionInfo2Response } from '@ucap/api-public'; +import { LoginResponse } from '@ucap/protocol-authentication'; +import { UserInfoSS } from '@ucap/protocol-query'; + +import { LogService } from '@ucap/ng-logger'; +import { User } from '@ucap/protocol-info'; + +@Directive({ + selector: '[ucapOrganizationProfileMenu01Profile]' +}) +export class ProfileMenu01ProfileDirective {} + +@Directive({ + selector: '[ucapOrganizationProfileMenu01ProfileBelow]' +}) +export class ProfileMenu01ProfileBelowDirective {} + +@Directive({ + selector: '[ucapOrganizationProfileMenu01IntroBelow]' +}) +export class ProfileMenu01IntroBelowDirective {} + +@Directive({ + selector: '[ucapOrganizationProfileMenu01StatusMessageBelow]' +}) +export class ProfileMenu01StatusMessageBelowDirective {} + +@Component({ + selector: 'ucap-organization-profile-menu-01', + templateUrl: './profile-menu-01.component.html', + styleUrls: ['./profile-menu-01.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class ProfileMenu01Component implements OnInit, OnDestroy { + @HostBinding('class') + cssClass = 'ucap-organization-profile-menu-01'; + + @Input() + set user(user: User) { + this._user = user; + this._userIntro = + !user || !user.info || !user.info.intro || '' === user.info.intro.trim() + ? '' + : user.info.intro.trim(); + } + get user() { + return this._user; + } + // tslint:disable-next-line: variable-name + _user: User; + + @Input() + versionInfo2Res: VersionInfo2Response; + + @Input() + idleCheckTime: number; + + @Input() + placeholderForUserIntro: string; + + @Input() + idleTimePeriods: number[]; + + @Output() + changedIntro: EventEmitter = new EventEmitter(); + + @Output() + changedStatusMessage: EventEmitter<{ + index: number; + message: string; + }> = new EventEmitter(); + + @Output() + selectedStatus: EventEmitter< + 'online' | 'away' | 'busy1' | 'busy2' | 'busy3' + > = new EventEmitter(); + + @Output() + changedIdleCheckTime: EventEmitter = new EventEmitter(); + + @Output() + logout: EventEmitter = new EventEmitter(); + + @Output() + exit: EventEmitter = new EventEmitter(); + + @ContentChild(ProfileMenu01ProfileDirective, { + read: TemplateRef, + static: false + }) + profileTemplate: TemplateRef; + + @ContentChild(ProfileMenu01ProfileBelowDirective, { + read: TemplateRef, + static: false + }) + profileBelowTemplate: TemplateRef; + + @ContentChild(ProfileMenu01IntroBelowDirective, { + read: TemplateRef, + static: false + }) + introBelowTemplate: TemplateRef; + + @ContentChild(ProfileMenu01StatusMessageBelowDirective, { + read: TemplateRef, + static: false + }) + statusMessageBelowTemplate: TemplateRef< + ProfileMenu01StatusMessageBelowDirective + >; + + // tslint:disable-next-line: variable-name + _userIntro: string; + + private ngOnDestroySubject: Subject = new Subject(); + + constructor( + private changeDetectorRef: ChangeDetectorRef, + private logService: LogService + ) {} + + ngOnInit(): void {} + + ngOnDestroy(): void { + if (!!this.ngOnDestroySubject) { + this.ngOnDestroySubject.next(); + this.ngOnDestroySubject.complete(); + } + } + + onApplyIntro(intro: string) { + this.changedIntro.emit(intro); + } + + onCancelIntro(event: Event) {} + + onApplyStatusMessage(index: number, message: string) { + this.changedStatusMessage.emit({ index, message }); + } + + onCancelStatusMessage(event: Event, index: number) {} + + onClickStatus(status: 'online' | 'away' | 'busy1' | 'busy2' | 'busy3') { + this.selectedStatus.emit(status); + } + + onChangeIdleCheckTime(event: MatRadioChange) { + this.changedIdleCheckTime.emit(Number(event.value)); + } + + onClickLogout(event: Event) { + this.logout.emit(); + } + + onClickExit(event: Event) { + this.exit.emit(); + } +} diff --git a/projects/ui-organization/src/lib/components/profile-selection-01.component.html b/projects/ui-organization/src/lib/components/profile-selection-01.component.html index fdcdd46..11b65a3 100644 --- a/projects/ui-organization/src/lib/components/profile-selection-01.component.html +++ b/projects/ui-organization/src/lib/components/profile-selection-01.component.html @@ -14,7 +14,7 @@ [removable]="_removableFor(userInfo)" (removed)="_onRemoved($event, userInfo)" > - {{ userInfo.name }} + {{ userInfo.name }} clear diff --git a/projects/ui-organization/src/lib/components/search-for-tenant.component.html b/projects/ui-organization/src/lib/components/search-for-tenant.component.html index 546e66b..ad784a8 100644 --- a/projects/ui-organization/src/lib/components/search-for-tenant.component.html +++ b/projects/ui-organization/src/lib/components/search-for-tenant.component.html @@ -2,7 +2,11 @@ class="ucap-organization-search-for-tenant-container ucap-mat-input-container" >
    - + + {{ placeholder }}
    diff --git a/projects/ui-organization/src/lib/components/search-for-tenant.component.ts b/projects/ui-organization/src/lib/components/search-for-tenant.component.ts index e115051..5f7ba48 100644 --- a/projects/ui-organization/src/lib/components/search-for-tenant.component.ts +++ b/projects/ui-organization/src/lib/components/search-for-tenant.component.ts @@ -53,6 +53,8 @@ export class SearchForTenantComponent implements OnInit, OnDestroy { @ViewChild('inputSearchWord', { static: true }) inputSearchWord: ElementRef; + showRemoveBtn = false; + private ngOnDestroySubject: Subject; constructor(private changeDetectorRef: ChangeDetectorRef) {} @@ -68,7 +70,7 @@ export class SearchForTenantComponent implements OnInit, OnDestroy { } onSelectionChangeTenant(event: MatSelectChange) { - this.changedSearchData(); + // this.changedSearchData(); } onKeyupEnter(event: Event) { @@ -80,7 +82,18 @@ export class SearchForTenantComponent implements OnInit, OnDestroy { } onClickCancel(event: Event) { + this.changeSearchword(''); this.canceled.emit(); + event.stopPropagation(); + } + + changeSearchword(search: string) { + if (search.localeCompare('') === 0) { + this.inputSearchWord.nativeElement.value = ''; + this.showRemoveBtn = false; + } else { + this.showRemoveBtn = true; + } } private changedSearchData() { diff --git a/projects/ui-organization/src/lib/components/tree.component.html b/projects/ui-organization/src/lib/components/tree.component.html index d663c02..03123a8 100644 --- a/projects/ui-organization/src/lib/components/tree.component.html +++ b/projects/ui-organization/src/lib/components/tree.component.html @@ -1,7 +1,12 @@
    - + -
  • -
    +
  • +
    {{ node?.data?.deptInfo | ucapOrganizationTranslate: 'name' }}
  • @@ -33,6 +41,7 @@ matTreeNodePadding matTreeNodePaddingIndent="20" class="tree-has-child" + [style.height.px]="vsItemSize" (click)="onClickNode($event, node)" >
  • @@ -64,5 +73,5 @@
  • -
    +
    diff --git a/projects/ui-organization/src/lib/components/tree.component.ts b/projects/ui-organization/src/lib/components/tree.component.ts index 92c9488..6092eaf 100644 --- a/projects/ui-organization/src/lib/components/tree.component.ts +++ b/projects/ui-organization/src/lib/components/tree.component.ts @@ -10,23 +10,25 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, EventEmitter, - Output + Output, + AfterViewInit } from '@angular/core'; import { trigger, transition, style, animate } from '@angular/animations'; -import { CdkVirtualScrollViewport } from '@angular/cdk/scrolling'; import { FlatTreeControl } from '@angular/cdk/tree'; import { MatTreeFlattener, MatTree } from '@angular/material/tree'; import { PerfectScrollbarDirective } from 'ngx-perfect-scrollbar'; -import { LoginResponse } from '@ucap/protocol-authentication'; import { DeptInfo } from '@ucap/protocol-query'; import { LogService } from '@ucap/ng-logger'; -import { VirtualScrollTreeFlatDataSource } from '@ucap/ng-ui'; +import { + VirtualScrollTreeFlatDataSource, + VirtualScrollViewportComponent +} from '@ucap/ng-ui'; export interface OrganizationNode { deptInfo: DeptInfo; @@ -57,10 +59,7 @@ export interface FlatNode { ], changeDetection: ChangeDetectionStrategy.OnPush }) -export class TreeComponent implements OnInit, OnDestroy { - @Input() - loginRes: LoginResponse; - +export class TreeComponent implements OnInit, OnDestroy, AfterViewInit { @Input() set deptInfoList(data: { deptInfoList: DeptInfo[]; @@ -127,7 +126,7 @@ export class TreeComponent implements OnInit, OnDestroy { this.expand(...data.expanded); } - this.changeDetectorRef.detectChanges(); + this.changeDetectorRef.markForCheck(); } @Output() currentDeptSeqChange: EventEmitter = new EventEmitter(); @@ -140,14 +139,17 @@ export class TreeComponent implements OnInit, OnDestroy { // tslint:disable-next-line: variable-name _currentDeptSeq: number; + @Input() + vsItemSize: number; + @Output() clickNode = new EventEmitter(); @ViewChild('treeList', { static: false }) treeList: MatTree; - @ViewChild('cvsvList', { static: false }) - cvsvList: CdkVirtualScrollViewport; + @ViewChild('vsvList', { static: false }) + vsvList: VirtualScrollViewportComponent; @ViewChild(PerfectScrollbarDirective, { static: false }) psDirectiveRef?: PerfectScrollbarDirective; @@ -157,7 +159,7 @@ export class TreeComponent implements OnInit, OnDestroy { dataSource: VirtualScrollTreeFlatDataSource; // tslint:disable-next-line: variable-name - private _ngOnDestroySubject: Subject; + private _ngOnDestroySubject: Subject = new Subject(); constructor( private changeDetectorRef: ChangeDetectorRef, @@ -188,14 +190,12 @@ export class TreeComponent implements OnInit, OnDestroy { } ngOnInit(): void { - this._ngOnDestroySubject = new Subject(); - - this.dataSource.cdkVirtualScrollViewport = this.cvsvList; - this.treeControl.expansionModel.changed + this.dataSource.expandedData$ .pipe(takeUntil(this._ngOnDestroySubject)) - .subscribe(() => { - this.cvsvList.checkViewportSize(); - this.psDirectiveRef.update(); + .subscribe((datas) => { + if (!!this.psDirectiveRef) { + this.psDirectiveRef.update(); + } }); } @@ -206,6 +206,10 @@ export class TreeComponent implements OnInit, OnDestroy { } } + ngAfterViewInit(): void { + this.dataSource.virtualScrollViewport = this.vsvList; + } + hasChild = (_: number, node: FlatNode) => node.expandable; trackBy = (_: number, node: FlatNode) => node.data.deptInfo.seq; @@ -235,6 +239,29 @@ export class TreeComponent implements OnInit, OnDestroy { if (0 < flatNodes.length) { this.treeControl.expansionModel.select(...flatNodes); } + + setTimeout( + () => { + const curIndex = this.dataSource.expandedDataSubject.value.findIndex( + (node) => node.data.deptInfo.seq === this.currentDeptSeq + ); + if (-1 < curIndex) { + if ('vertical' === this.vsvList.orientation) { + this.psDirectiveRef.scrollToY( + this.vsvList.offsetForIndex(curIndex) + ); + } else { + this.psDirectiveRef.scrollToX( + this.vsvList.offsetForIndex(curIndex) + ); + } + } + }, + 0 < flatNodes.length && + 0 === this.dataSource.expandedDataSubject.value.length + ? 100 + : 0 + ); } expandAll() { diff --git a/projects/ui-organization/src/lib/organization-ui.module.ts b/projects/ui-organization/src/lib/organization-ui.module.ts index 2bec493..9df40f6 100644 --- a/projects/ui-organization/src/lib/organization-ui.module.ts +++ b/projects/ui-organization/src/lib/organization-ui.module.ts @@ -3,14 +3,13 @@ import { CommonModule } from '@angular/common'; import { FlexLayoutModule } from '@angular/flex-layout'; -import { ScrollingModule } from '@angular/cdk/scrolling'; - import { MatButtonModule } from '@angular/material/button'; import { MatCheckboxModule } from '@angular/material/checkbox'; import { MatChipsModule } from '@angular/material/chips'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatIconModule } from '@angular/material/icon'; import { MatInputModule } from '@angular/material/input'; +import { MatRadioModule } from '@angular/material/radio'; import { MatRippleModule } from '@angular/material/core'; import { MatSelectModule } from '@angular/material/select'; import { MatTreeModule } from '@angular/material/tree'; @@ -18,6 +17,7 @@ import { MatTooltipModule } from '@angular/material/tooltip'; import { PerfectScrollbarModule } from 'ngx-perfect-scrollbar'; +import { UCAP_I18N_NAMESPACE, I18nModule } from '@ucap/ng-i18n'; import { UiModule } from '@ucap/ng-ui'; import { ModuleConfig } from './config/module-config'; @@ -31,6 +31,15 @@ import { ProfileListComponent, ProfileListNodeDirective } from './components/profile-list.component'; + +import { + ProfileMenu01Component, + ProfileMenu01IntroBelowDirective, + ProfileMenu01ProfileBelowDirective, + ProfileMenu01ProfileDirective, + ProfileMenu01StatusMessageBelowDirective +} from './components/profile-menu-01.component'; + import { ProfileSelection01Component, ProfileSelection01HeaderDirective, @@ -42,7 +51,6 @@ import { TreeComponent } from './components/tree.component'; import { TranslatePipe } from './pipes/translate.pipe'; import { TranslateService } from './services/translate.service'; -import { UCAP_I18N_NAMESPACE, I18nModule } from '@ucap/ng-i18n'; const COMPONENTS = [ Profile01Component, @@ -50,6 +58,7 @@ const COMPONENTS = [ ProfileListItem02Component, ProfileListComponent, ProfileImage01Component, + ProfileMenu01Component, ProfileSelection01Component, SearchForTenantComponent, TreeComponent @@ -58,6 +67,12 @@ const DIALOGS = []; const PIPES = [TranslatePipe]; const DIRECTIVES = [ ProfileListNodeDirective, + + ProfileMenu01IntroBelowDirective, + ProfileMenu01ProfileBelowDirective, + ProfileMenu01ProfileDirective, + ProfileMenu01StatusMessageBelowDirective, + ProfileSelection01HeaderDirective, ProfileSelection01ActionsDirective ]; @@ -74,7 +89,6 @@ export class OrganizationUiRootModule {} imports: [ CommonModule, FlexLayoutModule, - ScrollingModule, MatButtonModule, MatCheckboxModule, @@ -82,6 +96,7 @@ export class OrganizationUiRootModule {} MatFormFieldModule, MatIconModule, MatInputModule, + MatRadioModule, MatRippleModule, MatSelectModule, MatTreeModule, diff --git a/projects/ui-organization/src/lib/pipes/translate.pipe.ts b/projects/ui-organization/src/lib/pipes/translate.pipe.ts index f61cfb3..d862d7d 100644 --- a/projects/ui-organization/src/lib/pipes/translate.pipe.ts +++ b/projects/ui-organization/src/lib/pipes/translate.pipe.ts @@ -30,19 +30,29 @@ export class TranslatePipe implements PipeTransform, OnDestroy { private changeDetectorRef: ChangeDetectorRef ) {} - updateValue(target: any, key: string, separator?: string): void { + updateValue( + target: any, + key: string, + separator?: string, + defaultValue?: string + ): void { try { const value = this.translateService.get(target, key, separator); this.value = value; } catch (error) { - this.value = key; + this.value = !!defaultValue ? defaultValue : key; } this.lastTarget = target; this.lastKey = key; this.changeDetectorRef.markForCheck(); } - transform(target: any, key: string, separator?: string): any { + transform( + target: any, + key: string, + separator?: string, + defaultValue?: string + ): any { if ( !target || 0 === Object.keys(target).length || @@ -69,7 +79,7 @@ export class TranslatePipe implements PipeTransform, OnDestroy { this.lastSeparator = separator; // set the value - this.updateValue(target, key, separator); + this.updateValue(target, key, separator, defaultValue); // if there is a subscription to onLangChange, clean it this._dispose(); diff --git a/projects/ui-organization/src/lib/utils/presence.util.ts b/projects/ui-organization/src/lib/utils/presence.util.ts index 4a7581a..1b63e14 100644 --- a/projects/ui-organization/src/lib/utils/presence.util.ts +++ b/projects/ui-organization/src/lib/utils/presence.util.ts @@ -6,20 +6,17 @@ export class PresenceUtil { /** * Returns css class name of presence * Format - * ucap-organization-presence-none * ucap-organization-presence-pc-online - * ucap-organization-presence-pc-offline - * ucap-organization-presence-pc-busy * ucap-organization-presence-pc-away + * ucap-organization-presence-pc-busy + * ucap-organization-presence-pc-offline * ucap-organization-presence-pc-away2 - * ucap-organization-presence-pc-none * * ucap-organization-presence-mobile-online - * ucap-organization-presence-mobile-offline - * ucap-organization-presence-mobile-busy * ucap-organization-presence-mobile-away + * ucap-organization-presence-mobile-busy + * ucap-organization-presence-mobile-offline * ucap-organization-presence-mobile-away2 - * ucap-organization-presence-mobile-none * * @param statusBulkInfo status info. * @param presenceType presence type. @@ -30,7 +27,7 @@ export class PresenceUtil { ): string { const statusCode = PresenceUtil.getStatusCode(statusBulkInfo, presenceType); if (!statusCode) { - return 'ucap-organization-presence-none'; + return ''; } let cssClass = `ucap-organization-presence-${presenceType}`; @@ -51,9 +48,6 @@ export class PresenceUtil { case StatusCode.Away2: cssClass = `${cssClass}-away2`; break; - default: - cssClass = `${cssClass}-none`; - break; } return cssClass; @@ -163,7 +157,7 @@ export class PresenceUtil { return statusBulkInfo.phoneStatus; case PresenceType.CONFERENCE: return statusBulkInfo.conferenceStatus; - case PresenceType.IMESSENER: + case PresenceType.IMESSENGER: return statusBulkInfo.imessengerStatus; default: return undefined; diff --git a/projects/ui-organization/src/public-api.ts b/projects/ui-organization/src/public-api.ts index 00062c9..5233cc5 100644 --- a/projects/ui-organization/src/public-api.ts +++ b/projects/ui-organization/src/public-api.ts @@ -2,15 +2,18 @@ * Public API Surface of ui-organization */ -export * from './lib/config/module-config'; - export * from './lib/components/profile-01.component'; +export * from './lib/components/profile-image-01.component'; export * from './lib/components/profile-list-item-01.component'; +export * from './lib/components/profile-list-item-02.component'; export * from './lib/components/profile-list.component'; export * from './lib/components/profile-selection-01.component'; export * from './lib/components/search-for-tenant.component'; export * from './lib/components/tree.component'; +export * from './lib/config/module-config'; +export * from './lib/config/token'; + export * from './lib/pipes/translate.pipe'; export * from './lib/services/translate.service'; diff --git a/projects/ui/ng-package.json b/projects/ui/ng-package.json index 2cf2b0d..d858489 100644 --- a/projects/ui/ng-package.json +++ b/projects/ui/ng-package.json @@ -14,7 +14,9 @@ "@ucap/api": "@ucap/api", "@ucap/api-common": "@ucap/api-common", "@ucap/protocol-event": "@ucap/protocol-event", + "@ucap/protocol-file": "@ucap/protocol-file", "@ucap/native": "@ucap/native", + "@ucap/ng-logger": "@ucap/ng-logger", "@ucap/ng-i18n": "@ucap/ng-i18n" } } diff --git a/projects/ui/package.json b/projects/ui/package.json index 30c4708..9d2ead5 100644 --- a/projects/ui/package.json +++ b/projects/ui/package.json @@ -1,6 +1,6 @@ { "name": "@ucap/ng-ui", - "version": "0.0.24", + "version": "0.0.97", "publishConfig": { "registry": "https://nexus.loafle.net/repository/npm-ucap/" }, diff --git a/projects/ui/src/lib/components/breadcrumb.component.html b/projects/ui/src/lib/components/breadcrumb.component.html new file mode 100644 index 0000000..fe28fa7 --- /dev/null +++ b/projects/ui/src/lib/components/breadcrumb.component.html @@ -0,0 +1,44 @@ +
    + +
    +
    +
    + +
    +
    +
    + +
    diff --git a/projects/ui/src/lib/components/breadcrumb.component.scss b/projects/ui/src/lib/components/breadcrumb.component.scss new file mode 100644 index 0000000..6284023 --- /dev/null +++ b/projects/ui/src/lib/components/breadcrumb.component.scss @@ -0,0 +1,33 @@ +@import '~@ucap/ng-ui-material/material'; + +:host { + display: block; + + .ucap-breadcrumb-container { + width: 100%; + height: 100%; + + .ucap-breadcrumb-label-container { + display: flex; + flex-grow: 1; + overflow: hidden; + z-index: 1; + + .ucap-breadcrumb-list { + flex-grow: 1; + position: relative; + transition: transform 500ms cubic-bezier(0.35, 0, 0.25, 1); + + .ucap-breadcrumb-labels { + display: flex; + background-color: #f1f2f6; + .ucap-breadcrumb-node { + &.ucap-breadcrumb-disabled { + pointer-events: none; + } + } + } + } + } + } +} diff --git a/projects/ui/src/lib/components/breadcrumb.component.spec.ts b/projects/ui/src/lib/components/breadcrumb.component.spec.ts new file mode 100644 index 0000000..ecb60c7 --- /dev/null +++ b/projects/ui/src/lib/components/breadcrumb.component.spec.ts @@ -0,0 +1,18 @@ +import { TestBed, async } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { BreadcrumbComponent } from './breadcrumb.component'; + +describe('ucap::BreadcrumbComponent', () => { + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [RouterTestingModule], + declarations: [BreadcrumbComponent] + }).compileComponents(); + })); + + it('should create the breadcrumb', () => { + const fixture = TestBed.createComponent(BreadcrumbComponent); + const app = fixture.componentInstance; + expect(app).toBeTruthy(); + }); +}); diff --git a/projects/ui/src/lib/components/breadcrumb.component.stories.ts b/projects/ui/src/lib/components/breadcrumb.component.stories.ts new file mode 100644 index 0000000..b52fb64 --- /dev/null +++ b/projects/ui/src/lib/components/breadcrumb.component.stories.ts @@ -0,0 +1,26 @@ +import { moduleMetadata } from '@storybook/angular'; +import { action } from '@storybook/addon-actions'; +import { linkTo } from '@storybook/addon-links'; + +import { BrowserModule } from '@angular/platform-browser'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; + +import { UiModule } from '../ui.module'; + +import { BreadcrumbComponent } from './breadcrumb.component'; + +export default { + title: 'ui::BreadcrumbComponent', + component: BreadcrumbComponent, + decorators: [ + moduleMetadata({ + imports: [BrowserModule, BrowserAnimationsModule, UiModule], + providers: [] + }) + ] +}; + +export const Default = () => ({ + component: BreadcrumbComponent, + props: {} +}); diff --git a/projects/ui/src/lib/components/breadcrumb.component.theme.scss b/projects/ui/src/lib/components/breadcrumb.component.theme.scss new file mode 100644 index 0000000..36f0d4c --- /dev/null +++ b/projects/ui/src/lib/components/breadcrumb.component.theme.scss @@ -0,0 +1,20 @@ +@import '~@ucap/ng-ui-material/material'; + +@mixin ucap-breadcrumb-theme($theme) { + $is-dark-theme: map-get($theme, is-dark); + $primary: map-get($theme, primary); + $accent: map-get($theme, accent); + $warn: map-get($theme, warn); + $background: map-get($theme, background); + $foreground: map-get($theme, foreground); + + .ucap-breadcrumb-container { + border-color: mat-color($foreground, secondary-text); + } +} + +@mixin ucap-breadcrumb-typography($config) { + .ucap-breadcrumb-container { + font-family: mat-font-family($config); + } +} diff --git a/projects/ui/src/lib/components/breadcrumb.component.ts b/projects/ui/src/lib/components/breadcrumb.component.ts new file mode 100644 index 0000000..4b4d20d --- /dev/null +++ b/projects/ui/src/lib/components/breadcrumb.component.ts @@ -0,0 +1,285 @@ +import { takeUntil, startWith } from 'rxjs/operators'; + +import { + Component, + OnDestroy, + ChangeDetectionStrategy, + ChangeDetectorRef, + Input, + ElementRef, + ViewChild, + Directive, + ContentChildren, + QueryList, + Optional, + Inject, + Attribute, + NgZone, + AfterContentChecked, + AfterContentInit, + forwardRef, + HostBinding +} from '@angular/core'; + +import { + CanDisable, + HasTabIndex, + HasTabIndexCtor, + CanDisableCtor, + mixinDisabled, + mixinTabIndex, + ThemePalette +} from '@angular/material/core'; +import { FocusableOption, FocusMonitor } from '@angular/cdk/a11y'; +import { ANIMATION_MODULE_TYPE } from '@angular/platform-browser/animations'; +import { BooleanInput, coerceBooleanProperty } from '@angular/cdk/coercion'; +import { Platform } from '@angular/cdk/platform'; +import { Directionality } from '@angular/cdk/bidi'; +import { ViewportRuler } from '@angular/cdk/overlay'; + +import { + PaginatedHeaderDirective, + PaginatedHeaderItem +} from '../directives/paginated-header'; +import { DomService } from '../services/dom.service'; + +class BreadcrumbNodeMixinBase {} +// tslint:disable-next-line: variable-name +const _BreadcrumbNodeMixinBase: HasTabIndexCtor & + CanDisableCtor & + typeof BreadcrumbNodeMixinBase = mixinTabIndex( + mixinDisabled(BreadcrumbNodeMixinBase) +); + +@Directive() +export abstract class _BreadcrumbNodeBase extends _BreadcrumbNodeMixinBase + implements OnDestroy, CanDisable, HasTabIndex, FocusableOption { + static ngAcceptInputType_disabled: BooleanInput; + + protected _isActive = false; + + @Input() + get active(): boolean { + return this._isActive; + } + set active(value: boolean) { + if (value !== this._isActive) { + this._isActive = value; + this._breadcrumb.updateActiveLink(this.elementRef); + } + } + + constructor( + private _breadcrumb: _BreadcrumbBase, + public elementRef: ElementRef, + @Attribute('tabindex') tabIndex: string, + private _focusMonitor: FocusMonitor + ) { + super(); + + this.tabIndex = parseInt(tabIndex, 10) || 0; + + _focusMonitor.monitor(elementRef); + } + + focus() { + this.elementRef.nativeElement.focus(); + } + + ngOnDestroy() { + this._focusMonitor.stopMonitoring(this.elementRef); + } +} + +@Directive({ + selector: '[ucap-breadcrumb-node], [ucapBreadcrumbNode]', + exportAs: 'ucapBreadcrumbNode', + inputs: ['disabled', 'tabIndex'], + host: { + class: 'ucap-breadcrumb-node mat-focus-indicator', + '[attr.aria-current]': 'active ? "page" : null', + '[attr.aria-disabled]': 'disabled', + '[attr.tabIndex]': 'tabIndex', + '[class.ucap-breadcrumb-disabled]': 'disabled', + '[class.ucap-breadcrumb-label-active]': 'active' + } +}) +export class BreadcrumbNodeDirective extends _BreadcrumbNodeBase + implements OnDestroy { + constructor( + breadcrumb: BreadcrumbComponent, + elementRef: ElementRef, + @Attribute('tabindex') tabIndex: string, + focusMonitor: FocusMonitor + ) { + super(breadcrumb, elementRef, tabIndex, focusMonitor); + } + + ngOnDestroy() { + super.ngOnDestroy(); + } +} + +@Directive() +export abstract class _BreadcrumbBase extends PaginatedHeaderDirective + implements AfterContentChecked, AfterContentInit, OnDestroy { + abstract _items: QueryList; + + @Input() + get backgroundColor(): ThemePalette { + return this._backgroundColor; + } + set backgroundColor(value: ThemePalette) { + const classList = this._elementRef.nativeElement.classList; + classList.remove(`mat-background-${this.backgroundColor}`); + + if (value) { + classList.add(`mat-background-${value}`); + } + + this._backgroundColor = value; + } + private _backgroundColor: ThemePalette; + + @Input() + get startWithLast(): boolean | null { + return this._startWithLast; + } + set startWithLast(value: boolean | null) { + this._startWithLast = coerceBooleanProperty(value); + } + + @Input() + get disableRipple() { + return this._disableRipple; + } + set disableRipple(value: any) { + this._disableRipple = coerceBooleanProperty(value); + } + private _disableRipple = false; + + @Input() color: ThemePalette = 'primary'; + + private _indexToSelect: number | null = 0; + + constructor( + domService: DomService, + elementRef: ElementRef, + @Optional() dir: Directionality, + ngZone: NgZone, + changeDetectorRef: ChangeDetectorRef, + viewportRuler: ViewportRuler, + @Optional() platform?: Platform, + @Optional() @Inject(ANIMATION_MODULE_TYPE) animationMode?: string + ) { + super( + domService, + elementRef, + changeDetectorRef, + viewportRuler, + dir, + ngZone, + platform, + animationMode + ); + } + + protected _itemSelected() { + // noop + } + + ngAfterContentInit() { + this._items.changes + .pipe(startWith(null), takeUntil(this._destroyed)) + .subscribe(() => { + this.updateActiveLink(); + }); + + super.ngAfterContentInit(); + } + + updateActiveLink(_element?: ElementRef) { + if (!this._items) { + return; + } + + const items = this._items.toArray(); + + for (let i = 0; i < items.length; i++) { + if (items[i].active) { + this.selectedIndex = i; + this._changeDetectorRef.markForCheck(); + return; + } + } + + this.selectedIndex = -1; + } +} + +@Component({ + selector: 'ucap-breadcrumb', + exportAs: 'ucapBreadcrumb', + templateUrl: './breadcrumb.component.html', + styleUrls: ['./breadcrumb.component.scss'], + host: { + class: 'ucap-breadcrumb' + }, + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class BreadcrumbComponent extends _BreadcrumbBase { + static ngAcceptInputType_disableRipple: BooleanInput; + + @ContentChildren(forwardRef(() => BreadcrumbNodeDirective), { + descendants: true + }) + _items: QueryList; + + @ViewChild('breadcrumbListContainer', { static: true }) + _listContainer: ElementRef; + + @ViewChild('breadcrumbList', { static: true }) + _list: ElementRef; + + @ViewChild('nextPaginator') + _nextPaginator: ElementRef; + + @ViewChild('previousPaginator') + _previousPaginator: ElementRef; + + @HostBinding('class.mat-tab-header-pagination-controls-enabled') + get _paginationControlsEnabled() { + this.checkSizeChanged(); + return this._showPaginationControls; + } + + @HostBinding('class.mat-tab-header-rtl') + get _headerRtl() { + return this._getLayoutDirection() === 'rtl'; + } + + constructor( + domService: DomService, + elementRef: ElementRef, + @Optional() dir: Directionality, + ngZone: NgZone, + changeDetectorRef: ChangeDetectorRef, + viewportRuler: ViewportRuler, + /** + * @deprecated @breaking-change 9.0.0 `platform` parameter to become required. + */ + @Optional() platform?: Platform, + @Optional() @Inject(ANIMATION_MODULE_TYPE) animationMode?: string + ) { + super( + domService, + elementRef, + dir, + ngZone, + changeDetectorRef, + viewportRuler, + platform, + animationMode + ); + } +} diff --git a/projects/ui/src/lib/components/file-upload-queue.component.html b/projects/ui/src/lib/components/file-upload-queue.component.html index 10a6bea..086b52d 100644 --- a/projects/ui/src/lib/components/file-upload-queue.component.html +++ b/projects/ui/src/lib/components/file-upload-queue.component.html @@ -37,7 +37,7 @@
    {{ 'common.file.dropZoneForUpload' | ucapI18n }} + >{{ 'common:file.dropZoneForUpload' | ucapI18n }}
    diff --git a/projects/ui/src/lib/components/file-viewer.component.html b/projects/ui/src/lib/components/file-viewer.component.html index caced57..7918958 100644 --- a/projects/ui/src/lib/components/file-viewer.component.html +++ b/projects/ui/src/lib/components/file-viewer.component.html @@ -1,38 +1,18 @@
    - - - + > + f.sentMessageJson.attachmentSeq === v.selectFileInfo.attachmentSeq + ); + } + // tslint:disable-next-line: variable-name + _fileInfo: { + fileInfos: FileInfo[]; + selectFileInfo: SelectFileInfo; + }; @Input() - fileDownloadUrl: string; + fileDownloadUrl: (attachmentSeq: number) => string; @Output() - download = new EventEmitter(); + download: EventEmitter<{ + attachmentSeq?: number; + downloadUrl?: string; + fileName: string; + fileDownloadItem: FileDownloadItem; + }> = new EventEmitter(); @Output() closed = new EventEmitter(); + currentFileInfo: FileInfo; FileViewerType = FileViewerType; constructor() {} ngOnInit() {} - detectFileViewerType(fileInfo: FileEventJson): FileViewerType { - switch (fileInfo.fileType) { + detectFileViewerType(fileInfo: FileInfo): FileViewerType { + switch (fileInfo.type) { case FileType.Image: return FileViewerType.Image; case FileType.Sound: @@ -39,21 +58,21 @@ export class FileViewerComponent implements OnInit { case FileType.Video: return FileViewerType.Video; default: - if (this.isSoundFile(fileInfo.fileExt)) { + if (isSound(fileInfo)) { return FileViewerType.Sound; } return FileViewerType.Binary; } } onDownload(fileDownloadItem: FileDownloadItem): void { - this.download.emit(fileDownloadItem); + this.download.emit({ + attachmentSeq: this.currentFileInfo.sentMessageJson.attachmentSeq, + fileName: this.currentFileInfo.sentMessageJson.fileName, + fileDownloadItem + }); } onClosedViewer(): void { this.closed.emit(); } - - private isSoundFile(fileExt: string): boolean { - return -1 !== ['mp3'].indexOf(fileExt); - } } diff --git a/projects/ui/src/lib/components/file-viewer/binary-viewer.component.html b/projects/ui/src/lib/components/file-viewer/binary-viewer.component.html index b3ccab1..255d32f 100644 --- a/projects/ui/src/lib/components/file-viewer/binary-viewer.component.html +++ b/projects/ui/src/lib/components/file-viewer/binary-viewer.component.html @@ -17,13 +17,15 @@ d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48" > - {{ fileInfo.fileName }} + {{ + fileInfo.sentMessageJson.fileName + }}
    diff --git a/projects/ui/src/lib/components/file-viewer/binary-viewer.component.ts b/projects/ui/src/lib/components/file-viewer/binary-viewer.component.ts index 60fe96b..68318c5 100644 --- a/projects/ui/src/lib/components/file-viewer/binary-viewer.component.ts +++ b/projects/ui/src/lib/components/file-viewer/binary-viewer.component.ts @@ -3,6 +3,7 @@ import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core'; import { FileDownloadItem } from '@ucap/api'; import { FileEventJson } from '@ucap/protocol-event'; +import { FileInfo } from '@ucap/protocol-file'; @Component({ selector: 'ucap-binary-viewer', @@ -11,7 +12,7 @@ import { FileEventJson } from '@ucap/protocol-event'; }) export class BinaryViewerComponent implements OnInit { @Input() - fileInfo: FileEventJson; + fileInfo: FileInfo; @Input() fileDownloadUrl: string; diff --git a/projects/ui/src/lib/components/file-viewer/document-viewer.component.ts b/projects/ui/src/lib/components/file-viewer/document-viewer.component.ts index 78ea253..ae8d7b9 100644 --- a/projects/ui/src/lib/components/file-viewer/document-viewer.component.ts +++ b/projects/ui/src/lib/components/file-viewer/document-viewer.component.ts @@ -3,6 +3,7 @@ import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core'; import { FileDownloadItem } from '@ucap/api'; import { FileEventJson } from '@ucap/protocol-event'; +import { FileInfo } from '@ucap/protocol-file'; @Component({ selector: 'ucap-document-viewer', @@ -11,7 +12,7 @@ import { FileEventJson } from '@ucap/protocol-event'; }) export class DocumentViewerComponent implements OnInit { @Input() - fileInfo: FileEventJson; + fileInfo: FileInfo; @Input() fileDownloadUrl: string; diff --git a/projects/ui/src/lib/components/file-viewer/image-viewer.component.html b/projects/ui/src/lib/components/file-viewer/image-viewer.component.html index cdd0288..69ad4fb 100644 --- a/projects/ui/src/lib/components/file-viewer/image-viewer.component.html +++ b/projects/ui/src/lib/components/file-viewer/image-viewer.component.html @@ -17,13 +17,13 @@ - {{ fileInfo.fileName }} + {{ fileName }}
    @@ -143,7 +143,7 @@ mat-icon-button class="ucap-video-viewer-action" matTooltip="{{ - (playing ? 'common.player.stop' : 'common.player.play') | ucapI18n + (playing ? 'common:player.stop' : 'common:player.play') | ucapI18n }}" aria-label="" [disabled]="!playable" diff --git a/projects/ui/src/lib/components/file-viewer/video-viewer.component.ts b/projects/ui/src/lib/components/file-viewer/video-viewer.component.ts index 96f11dd..094c4b1 100644 --- a/projects/ui/src/lib/components/file-viewer/video-viewer.component.ts +++ b/projects/ui/src/lib/components/file-viewer/video-viewer.component.ts @@ -12,6 +12,7 @@ import { MatSlider, MatSliderChange } from '@angular/material/slider'; import { FileDownloadItem } from '@ucap/api'; import { FileEventJson } from '@ucap/protocol-event'; +import { FileInfo } from '@ucap/protocol-file'; @Component({ selector: 'ucap-video-viewer', @@ -20,7 +21,10 @@ import { FileEventJson } from '@ucap/protocol-event'; }) export class VideoViewerComponent implements OnInit { @Input() - fileInfo: FileEventJson; + fileInfo: FileInfo; + + @Input() + fileName: string; @Input() fileDownloadUrl: string; diff --git a/projects/ui/src/lib/components/float-action-button.component.html b/projects/ui/src/lib/components/float-action-button.component.html index ce3e51c..268e187 100644 --- a/projects/ui/src/lib/components/float-action-button.component.html +++ b/projects/ui/src/lib/components/float-action-button.component.html @@ -5,7 +5,13 @@ >
    diff --git a/projects/ui/src/lib/components/media-viewer.component.html b/projects/ui/src/lib/components/media-viewer.component.html new file mode 100644 index 0000000..462a363 --- /dev/null +++ b/projects/ui/src/lib/components/media-viewer.component.html @@ -0,0 +1,58 @@ +
    + + + + + + + + + + + + +
    diff --git a/projects/ui/src/lib/components/media-viewer.component.scss b/projects/ui/src/lib/components/media-viewer.component.scss new file mode 100644 index 0000000..0ff547a --- /dev/null +++ b/projects/ui/src/lib/components/media-viewer.component.scss @@ -0,0 +1,55 @@ +.ucap-media-viewer-container { + position: relative; + width: 100%; + height: 100%; + + .navigation-prev { + position: absolute; + width: 48px; + height: 48px; + top: calc(50% - 24px); + left: 10px; + color: white; + z-index: 99999; + .mat-icon-button { + width: 100%; + height: 100%; + line-height: unset; + justify-content: center; + align-items: center; + } + } + .navigation-next { + position: absolute; + width: 48px; + height: 48px; + top: calc(50% - 24px); + right: 10px; + color: white; + z-index: 99999; + } + .bundle-index { + position: absolute; + width: 100px; + height: 30px; + left: calc(50% - 50px); + bottom: 30px; + color: white; + z-index: 99999; + background-color: rgb(0, 0, 0, 0.6); + text-align: center; + line-height: 30px; + border-radius: 30px; + } +} +.mat-icon-button { + width: 100%; + height: 100%; + line-height: unset; + .mdi-48px { + width: 100%; + height: 100%; + align-items: center; + display: flex; + } +} diff --git a/projects/ui-group/src/lib/components/expansion-list.component.spec.ts b/projects/ui/src/lib/components/media-viewer.component.spec.ts similarity index 59% rename from projects/ui-group/src/lib/components/expansion-list.component.spec.ts rename to projects/ui/src/lib/components/media-viewer.component.spec.ts index 6ea8bfe..aa6713b 100644 --- a/projects/ui-group/src/lib/components/expansion-list.component.spec.ts +++ b/projects/ui/src/lib/components/media-viewer.component.spec.ts @@ -3,20 +3,20 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { DebugElement } from '@angular/core'; -import { ExpansionListComponent } from './expansion-list.component'; +import { MediaViewerComponent } from './media-viewer.component'; -describe('ucap::ui-group::ExpansionListComponent', () => { - let component: ExpansionListComponent; - let fixture: ComponentFixture; +describe('MediaViewerComponent', () => { + let component: MediaViewerComponent; + let fixture: ComponentFixture; beforeEach(async(() => { TestBed.configureTestingModule({ - declarations: [ExpansionListComponent] + declarations: [MediaViewerComponent] }).compileComponents(); })); beforeEach(() => { - fixture = TestBed.createComponent(ExpansionListComponent); + fixture = TestBed.createComponent(MediaViewerComponent); component = fixture.componentInstance; fixture.detectChanges(); }); diff --git a/projects/ui/src/lib/components/media-viewer.component.stories.ts b/projects/ui/src/lib/components/media-viewer.component.stories.ts new file mode 100644 index 0000000..1d16ccd --- /dev/null +++ b/projects/ui/src/lib/components/media-viewer.component.stories.ts @@ -0,0 +1,26 @@ +import { moduleMetadata } from '@storybook/angular'; +import { action } from '@storybook/addon-actions'; +import { linkTo } from '@storybook/addon-links'; + +import { BrowserModule } from '@angular/platform-browser'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; + +import { UiModule } from '../ui.module'; + +import { MediaViewerComponent } from './media-viewer.component'; + +export default { + title: 'ui::MediaViewerComponent', + component: MediaViewerComponent, + decorators: [ + moduleMetadata({ + imports: [BrowserModule, BrowserAnimationsModule, UiModule], + providers: [] + }) + ] +}; + +export const Default = () => ({ + component: MediaViewerComponent, + props: {} +}); diff --git a/projects/ui/src/lib/components/media-viewer.component.theme.scss b/projects/ui/src/lib/components/media-viewer.component.theme.scss new file mode 100644 index 0000000..4097cf1 --- /dev/null +++ b/projects/ui/src/lib/components/media-viewer.component.theme.scss @@ -0,0 +1,20 @@ +@import '~@ucap/ng-ui-material/material'; + +@mixin ucap-media-viewer-theme($theme) { + $is-dark-theme: map-get($theme, is-dark); + $primary: map-get($theme, primary); + $accent: map-get($theme, accent); + $warn: map-get($theme, warn); + $background: map-get($theme, background); + $foreground: map-get($theme, foreground); + + .ucap-media-viewer-container { + border-color: mat-color($foreground, secondary-text); + } +} + +@mixin ucap-media-viewer-typography($config) { + .ucap-media-viewer-container { + font-family: mat-font-family($config); + } +} diff --git a/projects/ui/src/lib/components/media-viewer.component.ts b/projects/ui/src/lib/components/media-viewer.component.ts new file mode 100644 index 0000000..ea95ada --- /dev/null +++ b/projects/ui/src/lib/components/media-viewer.component.ts @@ -0,0 +1,131 @@ +import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core'; +import { ucapAnimations } from '../animations'; + +import { FileViewerType } from '../types/file-viewer.type'; +import { FileInfo, isSound } from '@ucap/protocol-file'; +import { SelectFileInfo } from '../models/select-file-info'; +import { FileDownloadItem } from '@ucap/api'; +import { FileType } from '@ucap/protocol-event'; + +@Component({ + selector: 'ucap-media-viewer', + templateUrl: './media-viewer.component.html', + styleUrls: ['./media-viewer.component.scss'], + animations: ucapAnimations +}) +export class MediaViewerComponent implements OnInit { + @Input() + set fileInfo(v: { fileInfos: FileInfo[]; selectFileInfo: SelectFileInfo }) { + this.fileInfos = v.fileInfos.sort((a, b) => a.eventSeq - b.eventSeq); + + this.fileInfoIndex = this.fileInfos.findIndex( + (f) => f.sentMessageJson.attachmentSeq === v.selectFileInfo.attachmentSeq + ); + this.bundleIndex = v.selectFileInfo.index; + + this.setCuttentFileInfo(); + } + + @Input() + fileDownloadUrl: (attachmentSeq: number) => string; + + @Output() + download = new EventEmitter<{ + attachmentSeq?: number; + downloadUrl?: string; + fileName: string; + fileDownloadItem: FileDownloadItem; + roomSeq?: number; + eventSeq?: number; + }>(); + + @Output() + closed = new EventEmitter(); + + fileInfos: FileInfo[]; + currentFileInfo: FileInfo; + fileInfoIndex: number; + bundleIndex: number; + FileType = FileType; + FileViewerType = FileViewerType; + + get downloadUrl() { + return this.fileDownloadUrl(this.currentFileInfo.seq); + } + + get fileName() { + return this.currentFileInfo.sentMessageJson.fileName; + } + + constructor() {} + + ngOnInit() {} + + detectFileViewerType(fileInfo: FileInfo): FileViewerType { + switch (fileInfo.type) { + case FileType.Image: + return FileViewerType.Image; + case FileType.Sound: + return FileViewerType.Sound; + case FileType.Video: + return FileViewerType.Video; + default: + if (isSound(fileInfo)) { + return FileViewerType.Sound; + } + return FileViewerType.Binary; + } + } + + onDownload(fileDownloadItem: FileDownloadItem): void { + const fileJsonInfo = this.currentFileInfo.sentMessageJson; + this.download.emit({ + attachmentSeq: this.currentFileInfo.sentMessageJson.attachmentSeq, + fileName: this.currentFileInfo.sentMessageJson.fileName, + fileDownloadItem + }); + } + + onClosedViewer(): void { + this.closed.emit(); + } + + onClickPrev() { + if (0 === this.fileInfoIndex) { + return; + } + + this.fileInfoIndex--; + + this.setCuttentFileInfo(false); + } + + onClickNext() { + if (this.fileInfos.length - 1 === this.fileInfoIndex) { + return; + } + this.fileInfoIndex++; + this.setCuttentFileInfo(true); + } + + private setCuttentFileInfo(direction?: boolean) { + this.currentFileInfo = this.fileInfos[this.fileInfoIndex]; + // if ( + // FileType.Bundle === this.currentFileInfo.type && + // undefined === this.bundleIndex + // ) { + // if (undefined === direction) { + // return; + // } + // const fileJsonInfo = this.currentFileInfo + // .sentMessageJson as BundleImageEventJson; + // if (direction) { + // // Next + // this.bundleIndex = 0; + // } else { + // // Prev + // this.bundleIndex = fileJsonInfo.fileCount - 1; + // } + // } + } +} diff --git a/projects/ui/src/lib/components/title-bar.component.html b/projects/ui/src/lib/components/title-bar.component.html index 1047d06..d484965 100644 --- a/projects/ui/src/lib/components/title-bar.component.html +++ b/projects/ui/src/lib/components/title-bar.component.html @@ -1,6 +1,6 @@
    -
    +
    diff --git a/projects/ui/src/lib/components/title-bar.component.scss b/projects/ui/src/lib/components/title-bar.component.scss index dffdbf9..a283576 100644 --- a/projects/ui/src/lib/components/title-bar.component.scss +++ b/projects/ui/src/lib/components/title-bar.component.scss @@ -17,9 +17,6 @@ margin-left: auto; height: 100%; - .ucap-title-bar-controls-mac { - } - .ucap-title-bar-mac-button { width: 12px; height: 12px; @@ -53,6 +50,7 @@ background-color: #bf4943; } } + .ucap-title-bar-mac-button-minimize { border: 1px solid #e1a116; background-color: #ffbd2e; @@ -97,9 +95,6 @@ } } - .ucap-title-bar-controls-windows { - } - .ucap-title-bar-windows-button { -webkit-app-region: no-drag; display: inline-block; @@ -151,7 +146,7 @@ } } - .ucap-title-bar-content { + .ucap-title-bar-content-container { display: flex; } } diff --git a/projects/ui/src/lib/components/title-bar.component.stories.ts b/projects/ui/src/lib/components/title-bar.component.stories.ts index f74c3ef..7e27c7f 100644 --- a/projects/ui/src/lib/components/title-bar.component.stories.ts +++ b/projects/ui/src/lib/components/title-bar.component.stories.ts @@ -5,6 +5,8 @@ import { linkTo } from '@storybook/addon-links'; import { BrowserModule } from '@angular/platform-browser'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { WindowState } from '@ucap/native'; + import { UiModule } from '../ui.module'; import { TitleBarComponent } from './title-bar.component'; @@ -24,3 +26,35 @@ export const Default = () => ({ component: TitleBarComponent, props: {} }); + +export const Browser = () => ({ + component: TitleBarComponent, + props: { + platform: 'browser', + windowState: WindowState.Normal + } +}); + +export const Darwin = () => ({ + component: TitleBarComponent, + props: { + platform: 'darwin', + windowState: WindowState.Normal + } +}); + +export const Linux = () => ({ + component: TitleBarComponent, + props: { + platform: 'linux', + windowState: WindowState.Normal + } +}); + +export const Win32 = () => ({ + component: TitleBarComponent, + props: { + platform: 'win32', + windowState: WindowState.Normal + } +}); diff --git a/projects/ui/src/lib/components/title-bar.component.ts b/projects/ui/src/lib/components/title-bar.component.ts index e27ee46..cdddc3e 100644 --- a/projects/ui/src/lib/components/title-bar.component.ts +++ b/projects/ui/src/lib/components/title-bar.component.ts @@ -5,19 +5,21 @@ import { HostListener, Output, EventEmitter, - ViewChild + ViewChild, + OnDestroy, + ChangeDetectorRef } from '@angular/core'; import { WindowState } from '@ucap/native'; +import { Subject } from 'rxjs'; +import { DomService } from '../services/dom.service'; +import { debounceTime } from 'rxjs/operators'; @Component({ selector: 'ucap-title-bar', templateUrl: './title-bar.component.html', styleUrls: ['./title-bar.component.scss'] }) -export class TitleBarComponent implements OnInit { - @Input() - native = false; - +export class TitleBarComponent implements OnInit, OnDestroy { @Input() platform: 'darwin' | 'win32' | 'linux' | 'browser'; @@ -40,9 +42,32 @@ export class TitleBarComponent implements OnInit { WindowState = WindowState; - constructor() {} + private ngOnDestroySubject: Subject = new Subject(); - ngOnInit() {} + constructor( + private domService: DomService, + private changeDetectorRef: ChangeDetectorRef + ) {} + + ngOnInit() { + this.domService + .attach('resize') + .pipe(debounceTime(1000)) + .subscribe((event) => { + if (!!this.windowsControlsContainer) { + this.windowsControlsContainerOffsetHeight = this.windowsControlsContainer.offsetHeight; + } + }); + } + + ngOnDestroy(): void { + this.domService.detach('resize'); + + if (!!this.ngOnDestroySubject) { + this.ngOnDestroySubject.next(); + this.ngOnDestroySubject.complete(); + } + } @HostListener('window:keydown', ['$event']) onKeydown(event: KeyboardEvent) { @@ -64,11 +89,6 @@ export class TitleBarComponent implements OnInit { } } - @HostListener('window:resize') - public onResizeWindow(): void { - this.windowsControlsContainerOffsetHeight = this.windowsControlsContainer.offsetHeight; - } - onClickClose(event: Event) { this.closed.emit(); this.isKeyDownAlt = false; diff --git a/projects/ui/src/lib/data-source/virtual-scroll-tree-flat.data-source.ts b/projects/ui/src/lib/data-source/virtual-scroll-tree-flat.data-source.ts index 455cda6..6ae457c 100644 --- a/projects/ui/src/lib/data-source/virtual-scroll-tree-flat.data-source.ts +++ b/projects/ui/src/lib/data-source/virtual-scroll-tree-flat.data-source.ts @@ -1,15 +1,15 @@ import { BehaviorSubject, merge, Observable, Subject } from 'rxjs'; -import { map, share, takeUntil } from 'rxjs/operators'; +import { map, takeUntil } from 'rxjs/operators'; import { CollectionViewer, DataSource } from '@angular/cdk/collections'; -import { CdkVirtualScrollViewport } from '@angular/cdk/scrolling'; import { FlatTreeControl } from '@angular/cdk/tree'; import { MatTreeFlattener } from '@angular/material/tree'; +import { VirtualScrollViewportComponent } from '../scrolling/virtual-scroll-viewport'; + export class VirtualScrollTreeFlatDataSource extends DataSource { expandedDataSubject = new BehaviorSubject([]); - expandedData$: Observable; // tslint:disable-next-line: variable-name private _flattenedDataSubject = new BehaviorSubject([]); @@ -26,14 +26,14 @@ export class VirtualScrollTreeFlatDataSource extends DataSource { }>; // tslint:disable-next-line: variable-name - private _cdkVirtualScrollViewport: CdkVirtualScrollViewport; - set cdkVirtualScrollViewport( - cdkVirtualScrollViewport: CdkVirtualScrollViewport + private _virtualScrollViewport: VirtualScrollViewportComponent; + set virtualScrollViewport( + virtualScrollViewport: VirtualScrollViewportComponent ) { - if (!cdkVirtualScrollViewport) { + if (!virtualScrollViewport) { return; } - this._cdkVirtualScrollViewport = cdkVirtualScrollViewport; + this._virtualScrollViewport = virtualScrollViewport; this._rangeSubject = new BehaviorSubject<{ start: number; end: number; @@ -42,7 +42,7 @@ export class VirtualScrollTreeFlatDataSource extends DataSource { end: 1 }); - cdkVirtualScrollViewport.renderedRangeStream + virtualScrollViewport.renderedRangeStream .pipe(takeUntil(this._destroySubject)) .subscribe((range) => { this._rangeSubject.next({ @@ -82,12 +82,10 @@ export class VirtualScrollTreeFlatDataSource extends DataSource { ) { super(); this._data = new BehaviorSubject(initialData); - this.expandedData$ = this.expandedDataSubject.asObservable().pipe(share()); } connect(collectionViewer: CollectionViewer): Observable { this._destroySubject = new Subject(); - this._connectSubject = new Subject(); merge( @@ -115,8 +113,8 @@ export class VirtualScrollTreeFlatDataSource extends DataSource { ) .subscribe((datas) => { this._connectSubject.next(datas); - if (!!this._cdkVirtualScrollViewport) { - this._cdkVirtualScrollViewport.checkViewportSize(); + if (!!this._virtualScrollViewport) { + this._virtualScrollViewport.checkViewportSize(); } }); @@ -124,13 +122,33 @@ export class VirtualScrollTreeFlatDataSource extends DataSource { } disconnect() { + if (!!this.expandedDataSubject) { + this.expandedDataSubject.complete(); + } + + if (!!this._flattenedDataSubject) { + this._flattenedDataSubject.complete(); + } + + if (!!this._rangeSubject) { + this._rangeSubject.complete(); + } + + if (!!this._data) { + this._data.complete(); + } + if (!!this._connectSubject) { this._connectSubject.next(); - this._connectSubject.unsubscribe(); + this._connectSubject.complete(); } if (!!this._destroySubject) { this._destroySubject.complete(); } } + + get expandedData$(): Observable { + return this.expandedDataSubject; + } } diff --git a/projects/ui/src/lib/dialogs/alert.dialog.component.html b/projects/ui/src/lib/dialogs/alert.dialog.component.html index 12d2e09..0d8487b 100644 --- a/projects/ui/src/lib/dialogs/alert.dialog.component.html +++ b/projects/ui/src/lib/dialogs/alert.dialog.component.html @@ -12,7 +12,7 @@ diff --git a/projects/ui/src/lib/dialogs/confirm.dialog.component.html b/projects/ui/src/lib/dialogs/confirm.dialog.component.html index 76f8501..d59dc57 100644 --- a/projects/ui/src/lib/dialogs/confirm.dialog.component.html +++ b/projects/ui/src/lib/dialogs/confirm.dialog.component.html @@ -21,10 +21,10 @@ (click)="onClickChoice(false)" class="mat-primary" > - {{ 'common.messages.no' | ucapI18n }} + {{ 'common:messages.no' | ucapI18n }} diff --git a/projects/ui/src/lib/directives/cdk-virtual-scroll-viewport-patch.directive.ts b/projects/ui/src/lib/directives/cdk-virtual-scroll-viewport-patch.directive.ts deleted file mode 100644 index 62fd91b..0000000 --- a/projects/ui/src/lib/directives/cdk-virtual-scroll-viewport-patch.directive.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { Subject, fromEvent } from 'rxjs'; -import { debounceTime, takeUntil } from 'rxjs/operators'; - -import { Directive, OnInit, OnDestroy, Self, Inject } from '@angular/core'; - -import { CdkVirtualScrollViewport } from '@angular/cdk/scrolling'; - -@Directive({ - // tslint:disable-next-line: directive-selector - selector: 'cdk-virtual-scroll-viewport' -}) -export class CdkVirtualScrollViewportPatchDirective - implements OnInit, OnDestroy { - protected destroySubject: Subject; - - constructor( - @Self() - @Inject(CdkVirtualScrollViewport) - private readonly viewportComponent: CdkVirtualScrollViewport - ) {} - - ngOnInit() { - this.destroySubject = new Subject(); - - fromEvent(document.defaultView, 'resize') - .pipe(takeUntil(this.destroySubject), debounceTime(10)) - .subscribe(() => { - this.viewportComponent.checkViewportSize(); - }); - } - - ngOnDestroy() { - if (!!this.destroySubject) { - this.destroySubject.next(); - this.destroySubject.complete(); - } - } -} diff --git a/projects/ui/src/lib/directives/file-upload-for.directive.ts b/projects/ui/src/lib/directives/file-upload-for.directive.ts index 63a592d..e11fac4 100644 --- a/projects/ui/src/lib/directives/file-upload-for.directive.ts +++ b/projects/ui/src/lib/directives/file-upload-for.directive.ts @@ -1,3 +1,5 @@ +import { Subject } from 'rxjs'; + import { Directive, ElementRef, @@ -6,17 +8,21 @@ import { Output, Input, AfterViewInit, - NgZone + NgZone, + OnDestroy, + OnInit } from '@angular/core'; import { FileUploadItem } from '@ucap/api'; import { FileUploadQueueComponent } from '../components/file-upload-queue.component'; +import { DomService } from '../services/dom.service'; @Directive({ selector: 'input[ucapFileUploadFor], div[ucapFileUploadFor]' }) -export class FileUploadForDirective implements AfterViewInit { +export class FileUploadForDirective + implements OnInit, OnDestroy, AfterViewInit { @Input() fileUploadQueue: FileUploadQueueComponent; @@ -37,57 +43,109 @@ export class FileUploadForDirective implements AfterViewInit { dragOver = false; + private ngOnDestroySubject: Subject = new Subject(); + constructor( + private domService: DomService, private elementRef: ElementRef, private readonly ngZone: NgZone ) {} - ngAfterViewInit(): void {} - - @HostListener('window:dragenter', ['$event']) - public onDragEnter(event: DragEvent): any { - if (!this.isFileDrag(event.dataTransfer)) { - return; - } - - if (!this.dragOver) { - this.fileDragEnter.emit(event.dataTransfer.items); - this.dragOver = true; - if (!!this.fileUploadQueue) { - this.fileUploadQueue.onDragEnter(event.dataTransfer.items); + ngOnInit(): void { + this.domService.attach('dragenter').subscribe((event: DragEvent) => { + if (!this.isFileDrag(event.dataTransfer)) { + return; } - } - } - @HostListener('window:dragover', ['$event']) - public onDragOver(event: DragEvent): any { - if (!this.isFileDrag(event.dataTransfer)) { - return; - } + if (!this.dragOver) { + this.fileDragEnter.emit(event.dataTransfer.items); + this.dragOver = true; + if (!!this.fileUploadQueue) { + this.fileUploadQueue.onDragEnter(event.dataTransfer.items); + } + } + }); - // if (this.fileUploadQueue.isEventInElement(event)) { - // event.dataTransfer.dropEffect = 'copy'; - // } else { - // event.dataTransfer.dropEffect = 'none'; - // } - event.preventDefault(); - } + this.domService.attach('dragover').subscribe((event: DragEvent) => { + if (!this.isFileDrag(event.dataTransfer)) { + return; + } - @HostListener('window:dragleave', ['$event']) - public onDragLeave(event: DragEvent): any { - if (!this.isFileDrag(event.dataTransfer)) { - return; - } + // if (this.fileUploadQueue.isEventInElement(event)) { + // event.dataTransfer.dropEffect = 'copy'; + // } else { + // event.dataTransfer.dropEffect = 'none'; + // } + event.preventDefault(); + }); - if (event && event.pageX === 0 && event.pageY === 0) { - this.fileDragLeave.emit(); + this.domService.attach('dragleave').subscribe((event: DragEvent) => { + if (!this.isFileDrag(event.dataTransfer)) { + return; + } + + if (event && event.pageX === 0 && event.pageY === 0) { + this.fileDragLeave.emit(); + this.dragOver = false; + if (!!this.fileUploadQueue) { + this.fileUploadQueue.onDragLeave(); + } + } + }); + + this.domService.attach('drop').subscribe((event: any) => { + if (!this.isFileDrag(event.dataTransfer)) { + return; + } + + const self = this; + const files: FileList = event.dataTransfer.files; + + event.preventDefault(); + event.stopPropagation(); + this.elementRef.nativeElement.value = ''; this.dragOver = false; - if (!!this.fileUploadQueue) { - this.fileUploadQueue.onDragLeave(); + + if (!!this.validator) { + this.validator(files) + .then(async (result) => { + if (!result) { + if (!!this.fileUploadQueue) { + this.fileUploadQueue.onUploadComplete(); + } + return; + } else { + const fileUploadItems = FileUploadItem.fromFiles(files); + self.fileSelected.emit(fileUploadItems); + + if (!!self.fileUploadQueue) { + self.fileUploadQueue.onDrop(fileUploadItems); + } + } + }) + .catch((err) => { + if (!!this.fileUploadQueue) { + this.fileUploadQueue.onUploadComplete(); + } + }); } + }); + } + + ngOnDestroy(): void { + this.domService.detach('dragenter'); + this.domService.detach('dragover'); + this.domService.detach('dragleave'); + this.domService.detach('drop'); + + if (!!this.ngOnDestroySubject) { + this.ngOnDestroySubject.next(); + this.ngOnDestroySubject.complete(); } } + ngAfterViewInit(): void {} + @HostListener('change') public onChange(): any { const files = this.elementRef.nativeElement.files; @@ -95,45 +153,6 @@ export class FileUploadForDirective implements AfterViewInit { this.elementRef.nativeElement.value = ''; } - @HostListener('window:drop', ['$event']) - public onDrop(event: any): any { - if (!this.isFileDrag(event.dataTransfer)) { - return; - } - - const self = this; - const files: FileList = event.dataTransfer.files; - - event.preventDefault(); - event.stopPropagation(); - this.elementRef.nativeElement.value = ''; - this.dragOver = false; - - if (!!this.validator) { - this.validator(files) - .then(async result => { - if (!result) { - if (!!this.fileUploadQueue) { - this.fileUploadQueue.onUploadComplete(); - } - return; - } else { - const fileUploadItems = FileUploadItem.fromFiles(files); - self.fileSelected.emit(fileUploadItems); - - if (!!self.fileUploadQueue) { - self.fileUploadQueue.onDrop(fileUploadItems); - } - } - }) - .catch(err => { - if (!!this.fileUploadQueue) { - this.fileUploadQueue.onUploadComplete(); - } - }); - } - } - private isFileDrag(dataTransfer: DataTransfer): boolean { if (0 >= dataTransfer.items.length) { return false; diff --git a/projects/ui/src/lib/directives/image.directive.ts b/projects/ui/src/lib/directives/image.directive.ts index 214ba2a..d454928 100644 --- a/projects/ui/src/lib/directives/image.directive.ts +++ b/projects/ui/src/lib/directives/image.directive.ts @@ -10,6 +10,8 @@ import { SimpleChanges } from '@angular/core'; +import { LogService } from '@ucap/ng-logger'; + const PATH = 'path'; @Directive({ @@ -33,19 +35,13 @@ export class ImageDirective implements OnInit, AfterViewInit, OnChanges { imageSrc: string; - constructor(private elementRef: ElementRef) {} + constructor( + private elementRef: ElementRef, + private logService: LogService + ) {} ngOnInit(): void { - if (this.validation) { - if ( - // !this.base || - // '' === this.base.trim() || - !this.path || - '' === this.path.trim() - ) { - this.imageSrc = this.default; - } - } + this.imageSrc = this._validateForPath(); } ngAfterViewInit(): void { @@ -55,7 +51,13 @@ export class ImageDirective implements OnInit, AfterViewInit, OnChanges { ngOnChanges(changes: SimpleChanges): void { const pathChanges = changes[PATH]; - if (!!pathChanges && !pathChanges.firstChange) { + if ( + !!pathChanges && + !pathChanges.firstChange && + (pathChanges.previousValue as string) !== + (pathChanges.currentValue as string) + ) { + this.imageSrc = this._validateForPath(); this.loadImage(); } } @@ -83,11 +85,34 @@ export class ImageDirective implements OnInit, AfterViewInit, OnChanges { this.elementRef.nativeElement.src = image.src; this.loaded.emit(image); }; - image.onerror = () => { + image.onerror = ( + event: Event | string, + source?: string, + lineno?: number, + colno?: number, + error?: Error + ) => { + this.logService.error( + 'ucap::ImageDirective', + event, + source, + lineno, + colno, + error + ); this.elementRef.nativeElement.src = this.default; this.loaded.emit(this.elementRef.nativeElement); }; image.src = imageUrl; } } + + private _validateForPath(): string { + if (this.validation) { + if (!this.path || '' === this.path.trim()) { + return this.default; + } + } + return this.path; + } } diff --git a/projects/ui/src/lib/directives/paginated-header.ts b/projects/ui/src/lib/directives/paginated-header.ts new file mode 100644 index 0000000..6b70fbc --- /dev/null +++ b/projects/ui/src/lib/directives/paginated-header.ts @@ -0,0 +1,544 @@ +import { + merge, + of as observableOf, + Subject, + timer, + fromEvent, + Observable +} from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; + +import { + ChangeDetectorRef, + ElementRef, + NgZone, + Optional, + QueryList, + EventEmitter, + AfterContentChecked, + AfterContentInit, + AfterViewInit, + OnDestroy, + Directive, + Inject, + Input, + OnInit +} from '@angular/core'; + +import { Direction, Directionality } from '@angular/cdk/bidi'; +import { coerceNumberProperty, NumberInput } from '@angular/cdk/coercion'; +import { ViewportRuler } from '@angular/cdk/scrolling'; +import { FocusKeyManager, FocusableOption } from '@angular/cdk/a11y'; +import { END, ENTER, HOME, SPACE, hasModifierKey } from '@angular/cdk/keycodes'; +import { + Platform, + normalizePassiveListenerOptions +} from '@angular/cdk/platform'; + +import { ANIMATION_MODULE_TYPE } from '@angular/platform-browser/animations'; +import { DomService } from '../services/dom.service'; + +const passiveEventListenerOptions = normalizePassiveListenerOptions({ + passive: true +}) as EventListenerOptions; + +export type ScrollDirection = 'after' | 'before'; + +const EXAGGERATED_OVERSCROLL = 60; + +const HEADER_SCROLL_DELAY = 650; + +const HEADER_SCROLL_INTERVAL = 100; + +export type PaginatedHeaderItem = FocusableOption & { + elementRef: ElementRef; +}; + +@Directive() +export abstract class PaginatedHeaderDirective + implements + OnInit, + OnDestroy, + AfterContentChecked, + AfterContentInit, + AfterViewInit { + static ngAcceptInputType_selectedIndex: NumberInput; + + abstract _items: QueryList; + abstract _listContainer: ElementRef; + abstract _list: ElementRef; + abstract _nextPaginator: ElementRef; + abstract _previousPaginator: ElementRef; + + private _scrollDistance = 0; + + private _selectedIndexChanged = false; + + protected readonly _destroyed = new Subject(); + + _showPaginationControls = false; + + _disableScrollAfter = true; + + _disableScrollBefore = true; + + private _labelCount: number; + + private _scrollDistanceChanged: boolean; + + private _keyManager: FocusKeyManager; + + private _currentTextContent: string; + + private _stopScrolling = new Subject(); + + private _listSize: { width: number; height: number }; + private _listContainerSize: { width: number; height: number }; + + @Input() + disablePagination = false; + + get selectedIndex(): number { + return this._selectedIndex; + } + set selectedIndex(value: number) { + value = coerceNumberProperty(value); + + if (this._selectedIndex !== value) { + this._selectedIndexChanged = true; + this._selectedIndex = value; + + if (this._keyManager) { + this._keyManager.updateActiveItem(value); + } + } + } + private _selectedIndex = 0; + + protected _startWithLast = false; + + private _windowResize$: Observable; + + readonly selectFocusedIndex: EventEmitter = new EventEmitter< + number + >(); + + readonly indexFocused: EventEmitter = new EventEmitter(); + + constructor( + protected _domService: DomService, + protected _elementRef: ElementRef, + protected _changeDetectorRef: ChangeDetectorRef, + private _viewportRuler: ViewportRuler, + @Optional() private _dir: Directionality, + private _ngZone: NgZone, + private _platform?: Platform, + @Optional() @Inject(ANIMATION_MODULE_TYPE) public _animationMode?: string + ) { + _ngZone.runOutsideAngular(() => { + fromEvent(_elementRef.nativeElement, 'mouseleave') + .pipe(takeUntil(this._destroyed)) + .subscribe(() => { + this._stopInterval(); + }); + }); + + this._listSize = { + width: 0, + height: 0 + }; + this._listContainerSize = { + width: 0, + height: 0 + }; + } + + protected abstract _itemSelected(event: KeyboardEvent): void; + + ngOnInit(): void { + this._windowResize$ = this._domService.attach('resize'); + } + + ngOnDestroy() { + this._domService.detach('resize'); + this._windowResize$ = undefined; + + this._destroyed.next(); + this._destroyed.complete(); + this._stopScrolling.complete(); + } + + ngAfterViewInit() { + fromEvent( + this._previousPaginator.nativeElement, + 'touchstart', + passiveEventListenerOptions + ) + .pipe(takeUntil(this._destroyed)) + .subscribe(() => { + this._handlePaginatorPress('before'); + }); + + fromEvent( + this._nextPaginator.nativeElement, + 'touchstart', + passiveEventListenerOptions + ) + .pipe(takeUntil(this._destroyed)) + .subscribe(() => { + this._handlePaginatorPress('after'); + }); + } + + ngAfterContentInit() { + const dirChange = this._dir ? this._dir.change : observableOf(null); + const resize = this._viewportRuler.change(150); + const realign = () => { + this.updatePagination(); + }; + + this._keyManager = new FocusKeyManager(this._items) + .withHorizontalOrientation(this._getLayoutDirection()) + .withWrap(); + + this._keyManager.updateActiveItem(0); + + typeof requestAnimationFrame !== 'undefined' + ? requestAnimationFrame(realign) + : realign(); + + merge(dirChange, resize, this._items.changes, this._windowResize$) + .pipe(takeUntil(this._destroyed)) + .subscribe(() => { + realign(); + this._keyManager.withHorizontalOrientation(this._getLayoutDirection()); + this.checkSizeChanged(); + }); + + this._keyManager.change + .pipe(takeUntil(this._destroyed)) + .subscribe((newFocusIndex) => { + this.indexFocused.emit(newFocusIndex); + this._setFocus(newFocusIndex); + }); + } + + ngAfterContentChecked(): void { + if (this._labelCount !== this._items.length) { + this.updatePagination(); + this._labelCount = this._items.length; + this._changeDetectorRef.markForCheck(); + } + + if (this._selectedIndexChanged) { + this._scrollToLabel(this._selectedIndex); + this._checkScrollingControls(); + this._selectedIndexChanged = false; + this._changeDetectorRef.markForCheck(); + } + + if (this._scrollDistanceChanged) { + this._updateScrollPosition(); + this._scrollDistanceChanged = false; + this._changeDetectorRef.markForCheck(); + } + } + + _handleKeydown(event: KeyboardEvent) { + if (hasModifierKey(event)) { + return; + } + + switch (event.keyCode) { + case HOME: + this._keyManager.setFirstItemActive(); + event.preventDefault(); + break; + case END: + this._keyManager.setLastItemActive(); + event.preventDefault(); + break; + case ENTER: + case SPACE: + this.selectFocusedIndex.emit(this.focusIndex); + this._itemSelected(event); + break; + default: + this._keyManager.onKeydown(event); + } + } + + _onContentChanges() { + const textContent = this._elementRef.nativeElement.textContent; + + if (textContent !== this._currentTextContent) { + this._currentTextContent = textContent || ''; + + this._ngZone.run(() => { + this.updatePagination(); + this._changeDetectorRef.markForCheck(); + }); + } + } + + updatePagination() { + this._checkPaginationEnabled(); + this._checkScrollingControls(); + this._updateScrollPosition(); + } + + checkSizeChanged() { + const listContainerSize = { + width: this._listContainer.nativeElement.offsetWidth, + height: this._listContainer.nativeElement.offsetHeight + }; + + const listSize = { + width: this._list.nativeElement.offsetWidth, + height: this._list.nativeElement.offsetHeight + }; + + if ( + this._listSize.width !== listSize.width || + this._listSize.height !== listSize.height || + this._listContainerSize.width !== listContainerSize.width || + this._listContainerSize.height !== listContainerSize.height + ) { + this._listContainerSize = listContainerSize; + this._listSize = listSize; + + this.updatePagination(); + + if (this._startWithLast) { + let nextPaginatorWidth = this._nextPaginator?.nativeElement.offsetWidth; + let previousPaginatorWidth = this._previousPaginator?.nativeElement + .offsetWidth; + + const notRendered = !nextPaginatorWidth && !this.disablePagination; + + nextPaginatorWidth = notRendered ? 32 : nextPaginatorWidth; + previousPaginatorWidth = notRendered ? 32 : previousPaginatorWidth; + + const cWidth = this._listContainer.nativeElement.offsetWidth; + const lWidth = this._list.nativeElement.scrollWidth; + + if (cWidth === lWidth) { + this._scrollDistance = 0; + this._scrollDistanceChanged = true; + this._checkScrollingControls(); + } else { + const scrollDistance = Math.max( + 0, + lWidth - cWidth + (notRendered ? 64 : 0) + ); + + if (0 < scrollDistance) { + this._scrollDistance = scrollDistance; + this._scrollDistanceChanged = true; + this._checkScrollingControls(); + } + } + } + } + } + + get focusIndex(): number { + return this._keyManager ? this._keyManager.activeItemIndex! : 0; + } + + set focusIndex(value: number) { + if ( + !this._isValidIndex(value) || + this.focusIndex === value || + !this._keyManager + ) { + return; + } + + this._keyManager.setActiveItem(value); + } + + _isValidIndex(index: number): boolean { + if (!this._items) { + return true; + } + + const item = this._items ? this._items.toArray()[index] : null; + return !!item && !item.disabled; + } + + _setFocus(index: number) { + if (this._showPaginationControls) { + this._scrollToLabel(index); + } + + if (this._items && this._items.length) { + this._items.toArray()[index].focus(); + + const containerEl = this._listContainer.nativeElement; + const dir = this._getLayoutDirection(); + + if (dir === 'ltr') { + containerEl.scrollLeft = 0; + } else { + containerEl.scrollLeft = + containerEl.scrollWidth - containerEl.offsetWidth; + } + } + } + + _getLayoutDirection(): Direction { + return this._dir && this._dir.value === 'rtl' ? 'rtl' : 'ltr'; + } + + _updateScrollPosition() { + if (this.disablePagination) { + return; + } + + const scrollDistance = this.scrollDistance; + const platform = this._platform; + const translateX = + this._getLayoutDirection() === 'ltr' ? -scrollDistance : scrollDistance; + + this._list.nativeElement.style.transform = `translateX(${Math.round( + translateX + )}px)`; + + if (platform && (platform.TRIDENT || platform.EDGE)) { + this._listContainer.nativeElement.scrollLeft = 0; + } + } + + get scrollDistance(): number { + return this._scrollDistance; + } + set scrollDistance(value: number) { + this._scrollTo(value); + } + + _scrollHeader(direction: ScrollDirection) { + const viewLength = this._listContainer.nativeElement.offsetWidth; + + const scrollAmount = ((direction === 'before' ? -1 : 1) * viewLength) / 3; + + return this._scrollTo(this._scrollDistance + scrollAmount); + } + + _handlePaginatorClick(direction: ScrollDirection) { + this._stopInterval(); + this._scrollHeader(direction); + } + + _scrollToLabel(labelIndex: number) { + if (this.disablePagination) { + return; + } + + const selectedLabel = this._items + ? this._items.toArray()[labelIndex] + : null; + + if (!selectedLabel) { + return; + } + + const viewLength = this._listContainer.nativeElement.offsetWidth; + const { offsetLeft, offsetWidth } = selectedLabel.elementRef.nativeElement; + + let labelBeforePos: number; + let labelAfterPos: number; + + if (this._getLayoutDirection() === 'ltr') { + labelBeforePos = offsetLeft; + labelAfterPos = labelBeforePos + offsetWidth; + } else { + labelAfterPos = this._list.nativeElement.offsetWidth - offsetLeft; + labelBeforePos = labelAfterPos - offsetWidth; + } + + const beforeVisiblePos = this.scrollDistance; + const afterVisiblePos = this.scrollDistance + viewLength; + + if (labelBeforePos < beforeVisiblePos) { + this.scrollDistance -= + beforeVisiblePos - labelBeforePos + EXAGGERATED_OVERSCROLL; + } else if (labelAfterPos > afterVisiblePos) { + this.scrollDistance += + labelAfterPos - afterVisiblePos + EXAGGERATED_OVERSCROLL; + } + } + + _checkPaginationEnabled() { + if (this.disablePagination) { + this._showPaginationControls = false; + } else { + const isEnabled = + this._list.nativeElement.scrollWidth > + this._elementRef.nativeElement.offsetWidth; + + if (!isEnabled) { + this.scrollDistance = 0; + } + + if (isEnabled !== this._showPaginationControls) { + this._changeDetectorRef.markForCheck(); + } + + this._showPaginationControls = isEnabled; + } + } + + _checkScrollingControls() { + if (this.disablePagination) { + this._disableScrollAfter = this._disableScrollBefore = true; + } else { + this._disableScrollBefore = this.scrollDistance === 0; + this._disableScrollAfter = + this.scrollDistance === this._getMaxScrollDistance(); + this._changeDetectorRef.markForCheck(); + } + } + + _getMaxScrollDistance(): number { + const lengthOfList = this._list.nativeElement.scrollWidth; + const viewLength = this._listContainer.nativeElement.offsetWidth; + + return lengthOfList - viewLength || 0; + } + + _stopInterval() { + this._stopScrolling.next(); + } + + _handlePaginatorPress(direction: ScrollDirection, mouseEvent?: MouseEvent) { + if (mouseEvent && mouseEvent.button != null && mouseEvent.button !== 0) { + return; + } + + this._stopInterval(); + + timer(HEADER_SCROLL_DELAY, HEADER_SCROLL_INTERVAL) + .pipe(takeUntil(merge(this._stopScrolling, this._destroyed))) + .subscribe(() => { + const { maxScrollDistance, distance } = this._scrollHeader(direction); + + if (distance === 0 || distance >= maxScrollDistance) { + this._stopInterval(); + } + }); + } + private _scrollTo(position: number) { + if (this.disablePagination) { + return { maxScrollDistance: 0, distance: 0 }; + } + + const maxScrollDistance = this._getMaxScrollDistance(); + this._scrollDistance = Math.max(0, Math.min(maxScrollDistance, position)); + + this._scrollDistanceChanged = true; + this._checkScrollingControls(); + + return { maxScrollDistance, distance: this._scrollDistance }; + } +} diff --git a/projects/ui/src/lib/directives/var.directive.ts b/projects/ui/src/lib/directives/var.directive.ts new file mode 100644 index 0000000..4dab99b --- /dev/null +++ b/projects/ui/src/lib/directives/var.directive.ts @@ -0,0 +1,35 @@ +import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core'; + +@Directive({ + selector: '[ucapVar]' +}) +export class VarDirective { + private context?: Context; + + // https://angular.io/guide/structural-directives#typing-the-directives-context + static ngTemplateContextGuard( + dir: VarDirective, + ctx: any + ): ctx is Context { + return true; + } + + constructor( + private vcRef: ViewContainerRef, + private templateRef: TemplateRef> + ) {} + + @Input() + set ucapVar(value: T) { + if (this.context) { + this.context.ucapVar = value; + } else { + this.context = { ucapVar: value }; + this.vcRef.createEmbeddedView(this.templateRef, this.context); + } + } +} + +interface Context { + ucapVar: T; +} diff --git a/projects/ui/src/lib/models/select-file-info.ts b/projects/ui/src/lib/models/select-file-info.ts new file mode 100644 index 0000000..eea7598 --- /dev/null +++ b/projects/ui/src/lib/models/select-file-info.ts @@ -0,0 +1,4 @@ +export interface SelectFileInfo { + attachmentSeq: number; + index?: number; +} diff --git a/projects/ui/src/lib/scrolling/auto-size-virtual-scroll.ts b/projects/ui/src/lib/scrolling/auto-size-virtual-scroll.ts new file mode 100644 index 0000000..bc299b3 --- /dev/null +++ b/projects/ui/src/lib/scrolling/auto-size-virtual-scroll.ts @@ -0,0 +1,562 @@ +import { Observable } from 'rxjs'; + +import { coerceNumberProperty, NumberInput } from '@angular/cdk/coercion'; +import { ListRange } from '@angular/cdk/collections'; + +import { Directive, forwardRef, Input, OnChanges } from '@angular/core'; +import { + VirtualScrollStrategy, + VIRTUAL_SCROLL_STRATEGY, + VirtualScrollMeasureSize, + VirtualScrollToAlign +} from './virtual-scroll-strategy'; +import { VirtualScrollViewportComponent } from './virtual-scroll-viewport'; + +/** + * A class that tracks the size of items that have been seen and uses it to estimate the average + * item size. + */ +export class ItemSizeAverager { + /** The total amount of weight behind the current average. */ + private _totalWeight = 0; + + /** The current average item size. */ + private _averageItemSize: number; + + /** The default size to use for items when no data is available. */ + private _defaultItemSize: number; + + /** @param defaultItemSize The default size to use for items when no data is available. */ + constructor(defaultItemSize = 50) { + this._defaultItemSize = defaultItemSize; + this._averageItemSize = defaultItemSize; + } + + /** Returns the average item size. */ + getAverageItemSize(): number { + return this._averageItemSize; + } + + /** + * Adds a measurement sample for the estimator to consider. + * @param range The measured range. + * @param size The measured size of the given range in pixels. + */ + addSample(range: ListRange, size: number) { + const newTotalWeight = this._totalWeight + range.end - range.start; + if (newTotalWeight) { + const newAverageItemSize = + (size + this._averageItemSize * this._totalWeight) / newTotalWeight; + if (newAverageItemSize) { + this._averageItemSize = newAverageItemSize; + this._totalWeight = newTotalWeight; + } + } + } + + /** Resets the averager. */ + reset() { + this._averageItemSize = this._defaultItemSize; + this._totalWeight = 0; + } +} + +/** Virtual scrolling strategy for lists with items of unknown or dynamic size. */ +export class AutoSizeVirtualScrollStrategy implements VirtualScrollStrategy { + /** @docs-private Implemented as part of VirtualScrollStrategy. */ + scrolledIndexChange = new Observable(() => { + // TODO(mmalerba): Implement. + throw Error( + 'ucap-virtual-scroll: scrolledIndexChange is currently not supported for the' + + ' autosize scroll strategy' + ); + }); + + /** The attached viewport. */ + private _viewport: VirtualScrollViewportComponent | null = null; + + /** The minimum amount of buffer rendered beyond the viewport (in pixels). */ + private _minBufferPx: number; + + /** The number of buffer items to render beyond the edge of the viewport (in pixels). */ + private _maxBufferPx: number; + + /** The estimator used to estimate the size of unseen items. */ + private _averager: ItemSizeAverager; + + /** The last measured scroll offset of the viewport. */ + private _lastScrollOffset: number; + + /** The last measured size of the rendered content in the viewport. */ + private _lastRenderedContentSize: number; + + /** The last measured size of the rendered content in the viewport. */ + private _lastRenderedContentOffset: number; + + /** + * The number of consecutive cycles where removing extra items has failed. Failure here means that + * we estimated how many items we could safely remove, but our estimate turned out to be too much + * and it wasn't safe to remove that many elements. + */ + private _removalFailures = 0; + + /** + * @param minBufferPx The minimum amount of buffer rendered beyond the viewport (in pixels). + * If the amount of buffer dips below this number, more items will be rendered. + * @param maxBufferPx The number of pixels worth of buffer to shoot for when rendering new items. + * If the actual amount turns out to be less it will not necessarily trigger an additional + * rendering cycle (as long as the amount of buffer is still greater than `minBufferPx`). + * @param averager The averager used to estimate the size of unseen items. + */ + constructor( + minBufferPx: number, + maxBufferPx: number, + averager = new ItemSizeAverager() + ) { + this._minBufferPx = minBufferPx; + this._maxBufferPx = maxBufferPx; + this._averager = averager; + } + + /** + * Attaches this scroll strategy to a viewport. + * @param viewport The viewport to attach this strategy to. + */ + attach(viewport: VirtualScrollViewportComponent) { + this._averager.reset(); + this._viewport = viewport; + this._renderContentForCurrentOffset(); + } + + /** Detaches this scroll strategy from the currently attached viewport. */ + detach() { + this._viewport = null; + } + + useMeasureSize(): boolean { + return false; + } + + measureSize(sizes: VirtualScrollMeasureSize[]) {} + + measurePartitionCount(): number { + return 0; + } + + /** @docs-private Implemented as part of VirtualScrollStrategy. */ + onContentScrolled() { + if (this._viewport) { + this._updateRenderedContentAfterScroll(); + } + } + + /** @docs-private Implemented as part of VirtualScrollStrategy. */ + onDataModified() { + if (this._viewport) { + this._renderContentForCurrentOffset(); + this._checkRenderedContentSize(); + } + } + + onDataChanged() { + if (this._viewport) { + } + } + + /** @docs-private Implemented as part of VirtualScrollStrategy. */ + onContentRendered() { + if (this._viewport) { + this._checkRenderedContentSize(); + } + } + + /** @docs-private Implemented as part of VirtualScrollStrategy. */ + onRenderedOffsetChanged() { + if (this._viewport) { + this._checkRenderedContentOffset(); + } + } + + /** Scroll to the offset for the given index. */ + scrollToIndex( + index: number, + align: VirtualScrollToAlign = 'center', + behavior: ScrollBehavior + ): void { + // TODO(mmalerba): Implement. + throw Error( + 'ucap-virtual-scroll: scrollToIndex is currently not supported for the autosize' + + ' scroll strategy' + ); + } + + offsetForIndex( + index: number, + align: VirtualScrollToAlign = 'center' + ): number { + return 0; + } + + /** + * Update the buffer parameters. + * @param minBufferPx The minimum amount of buffer rendered beyond the viewport (in pixels). + * @param maxBufferPx The number of buffer items to render beyond the edge of the viewport (in + * pixels). + */ + updateBufferSize(minBufferPx: number, maxBufferPx: number) { + if (maxBufferPx < minBufferPx) { + throw 'CDK virtual scroll: maxBufferPx must be greater than or equal to minBufferPx'; + } + this._minBufferPx = minBufferPx; + this._maxBufferPx = maxBufferPx; + } + + /** Update the rendered content after the user scrolls. */ + private _updateRenderedContentAfterScroll() { + const viewport = this._viewport!; + + // The current scroll offset. + const scrollOffset = viewport.measureScrollOffset(); + // The delta between the current scroll offset and the previously recorded scroll offset. + let scrollDelta = scrollOffset - this._lastScrollOffset; + // The magnitude of the scroll delta. + let scrollMagnitude = Math.abs(scrollDelta); + + // The currently rendered range. + const renderedRange = viewport.getRenderedRange(); + + // If we're scrolling toward the top, we need to account for the fact that the predicted amount + // of content and the actual amount of scrollable space may differ. We address this by slowly + // correcting the difference on each scroll event. + let offsetCorrection = 0; + if (scrollDelta < 0) { + // The content offset we would expect based on the average item size. + const predictedOffset = + renderedRange.start * this._averager.getAverageItemSize(); + // The difference between the predicted size of the unrendered content at the beginning and + // the actual available space to scroll over. We need to reduce this to zero by the time the + // user scrolls to the top. + // - 0 indicates that the predicted size and available space are the same. + // - A negative number that the predicted size is smaller than the available space. + // - A positive number indicates the predicted size is larger than the available space + const offsetDifference = + predictedOffset - this._lastRenderedContentOffset; + // The amount of difference to correct during this scroll event. We calculate this as a + // percentage of the total difference based on the percentage of the distance toward the top + // that the user scrolled. + offsetCorrection = Math.round( + offsetDifference * + Math.max( + 0, + Math.min(1, scrollMagnitude / (scrollOffset + scrollMagnitude)) + ) + ); + + // Based on the offset correction above, we pretend that the scroll delta was bigger or + // smaller than it actually was, this way we can start to eliminate the difference. + scrollDelta = scrollDelta - offsetCorrection; + scrollMagnitude = Math.abs(scrollDelta); + } + + // The current amount of buffer past the start of the viewport. + const startBuffer = + this._lastScrollOffset - this._lastRenderedContentOffset; + // The current amount of buffer past the end of the viewport. + const endBuffer = + this._lastRenderedContentOffset + + this._lastRenderedContentSize - + (this._lastScrollOffset + viewport.getViewportSize()); + // The amount of unfilled space that should be filled on the side the user is scrolling toward + // in order to safely absorb the scroll delta. + const underscan = + scrollMagnitude + + this._minBufferPx - + (scrollDelta < 0 ? startBuffer : endBuffer); + + // Check if there's unfilled space that we need to render new elements to fill. + if (underscan > 0) { + // Check if the scroll magnitude was larger than the viewport size. In this case the user + // won't notice a discontinuity if we just jump to the new estimated position in the list. + // However, if the scroll magnitude is smaller than the viewport the user might notice some + // jitteriness if we just jump to the estimated position. Instead we make sure to scroll by + // the same number of pixels as the scroll magnitude. + if (scrollMagnitude >= viewport.getViewportSize()) { + this._renderContentForCurrentOffset(); + } else { + // The number of new items to render on the side the user is scrolling towards. Rather than + // just filling the underscan space, we actually fill enough to have a buffer size of + // `maxBufferPx`. This gives us a little wiggle room in case our item size estimate is off. + const addItems = Math.max( + 0, + Math.ceil( + (underscan - this._minBufferPx + this._maxBufferPx) / + this._averager.getAverageItemSize() + ) + ); + // The amount of filled space beyond what is necessary on the side the user is scrolling + // away from. + const overscan = + (scrollDelta < 0 ? endBuffer : startBuffer) - + this._minBufferPx + + scrollMagnitude; + // The number of currently rendered items to remove on the side the user is scrolling away + // from. If removal has failed in recent cycles we are less aggressive in how much we try to + // remove. + const unboundedRemoveItems = Math.floor( + overscan / + this._averager.getAverageItemSize() / + (this._removalFailures + 1) + ); + const removeItems = Math.min( + renderedRange.end - renderedRange.start, + Math.max(0, unboundedRemoveItems) + ); + + // The new range we will tell the viewport to render. We first expand it to include the new + // items we want rendered, we then contract the opposite side to remove items we no longer + // want rendered. + const range = this._expandRange( + renderedRange, + scrollDelta < 0 ? addItems : 0, + scrollDelta > 0 ? addItems : 0 + ); + if (scrollDelta < 0) { + range.end = Math.max(range.start + 1, range.end - removeItems); + } else { + range.start = Math.min(range.end - 1, range.start + removeItems); + } + + // The new offset we want to set on the rendered content. To determine this we measure the + // number of pixels we removed and then adjust the offset to the start of the rendered + // content or to the end of the rendered content accordingly (whichever one doesn't require + // that the newly added items to be rendered to calculate.) + let contentOffset: number; + let contentOffsetTo: 'to-start' | 'to-end'; + if (scrollDelta < 0) { + let removedSize = viewport.measureRangeSize({ + start: range.end, + end: renderedRange.end + }); + // Check that we're not removing too much. + if (removedSize <= overscan) { + contentOffset = + this._lastRenderedContentOffset + + this._lastRenderedContentSize - + removedSize; + this._removalFailures = 0; + } else { + // If the removal is more than the overscan can absorb just undo it and record the fact + // that the removal failed so we can be less aggressive next time. + range.end = renderedRange.end; + contentOffset = + this._lastRenderedContentOffset + this._lastRenderedContentSize; + this._removalFailures++; + } + contentOffsetTo = 'to-end'; + } else { + const removedSize = viewport.measureRangeSize({ + start: renderedRange.start, + end: range.start + }); + // Check that we're not removing too much. + if (removedSize <= overscan) { + contentOffset = this._lastRenderedContentOffset + removedSize; + this._removalFailures = 0; + } else { + // If the removal is more than the overscan can absorb just undo it and record the fact + // that the removal failed so we can be less aggressive next time. + range.start = renderedRange.start; + contentOffset = this._lastRenderedContentOffset; + this._removalFailures++; + } + contentOffsetTo = 'to-start'; + } + + // Set the range and offset we calculated above. + viewport.setRenderedRange(range); + viewport.setRenderedContentOffset( + contentOffset + offsetCorrection, + contentOffsetTo + ); + } + } else if (offsetCorrection) { + // Even if the rendered range didn't change, we may still need to adjust the content offset to + // simulate scrolling slightly slower or faster than the user actually scrolled. + viewport.setRenderedContentOffset( + this._lastRenderedContentOffset + offsetCorrection + ); + } + + // Save the scroll offset to be compared to the new value on the next scroll event. + this._lastScrollOffset = scrollOffset; + } + + /** + * Checks the size of the currently rendered content and uses it to update the estimated item size + * and estimated total content size. + */ + private _checkRenderedContentSize() { + const viewport = this._viewport!; + this._lastRenderedContentSize = viewport.measureRenderedContentSize(); + this._averager.addSample( + viewport.getRenderedRange(), + this._lastRenderedContentSize + ); + this._updateTotalContentSize(this._lastRenderedContentSize); + } + + /** Checks the currently rendered content offset and saves the value for later use. */ + private _checkRenderedContentOffset() { + const viewport = this._viewport!; + this._lastRenderedContentOffset = viewport.getOffsetToRenderedContentStart()!; + } + + /** + * Recalculates the rendered content based on our estimate of what should be shown at the current + * scroll offset. + */ + private _renderContentForCurrentOffset() { + const viewport = this._viewport!; + const scrollOffset = viewport.measureScrollOffset(); + this._lastScrollOffset = scrollOffset; + this._removalFailures = 0; + + const itemSize = this._averager.getAverageItemSize(); + const firstVisibleIndex = Math.min( + viewport.getDataLength() - 1, + Math.floor(scrollOffset / itemSize) + ); + const bufferSize = Math.ceil(this._maxBufferPx / itemSize); + const range = this._expandRange( + this._getVisibleRangeForIndex(firstVisibleIndex), + bufferSize, + bufferSize + ); + + viewport.setRenderedRange(range); + viewport.setRenderedContentOffset(itemSize * range.start); + } + + // TODO: maybe move to base class, can probably share with fixed size strategy. + /** + * Gets the visible range of data for the given start index. If the start index is too close to + * the end of the list it may be backed up to ensure the estimated size of the range is enough to + * fill the viewport. + * Note: must not be called if `this._viewport` is null + * @param startIndex The index to start the range at + * @return a range estimated to be large enough to fill the viewport when rendered. + */ + private _getVisibleRangeForIndex(startIndex: number): ListRange { + const viewport = this._viewport!; + const range: ListRange = { + start: startIndex, + end: + startIndex + + Math.ceil( + viewport.getViewportSize() / this._averager.getAverageItemSize() + ) + }; + const extra = range.end - viewport.getDataLength(); + if (extra > 0) { + range.start = Math.max(0, range.start - extra); + } + return range; + } + + // TODO: maybe move to base class, can probably share with fixed size strategy. + /** + * Expand the given range by the given amount in either direction. + * Note: must not be called if `this._viewport` is null + * @param range The range to expand + * @param expandStart The number of items to expand the start of the range by. + * @param expandEnd The number of items to expand the end of the range by. + * @return The expanded range. + */ + private _expandRange( + range: ListRange, + expandStart: number, + expandEnd: number + ): ListRange { + const viewport = this._viewport!; + const start = Math.max(0, range.start - expandStart); + const end = Math.min(viewport.getDataLength(), range.end + expandEnd); + return { start, end }; + } + + /** Update the viewport's total content size. */ + private _updateTotalContentSize(renderedContentSize: number) { + const viewport = this._viewport!; + const renderedRange = viewport.getRenderedRange(); + const totalSize = + renderedContentSize + + (viewport.getDataLength() - (renderedRange.end - renderedRange.start)) * + this._averager.getAverageItemSize(); + viewport.setTotalContentSize(totalSize); + } +} + +/** + * Provider factory for `AutoSizeVirtualScrollStrategy` that simply extracts the already created + * `AutoSizeVirtualScrollStrategy` from the given directive. + * @param autoSizeDir The instance of `CdkAutoSizeVirtualScroll` to extract the + * `AutoSizeVirtualScrollStrategy` from. + */ +export function _autoSizeVirtualScrollStrategyFactory( + autoSizeDir: AutoSizeVirtualScrollDirective +) { + return autoSizeDir._scrollStrategy; +} + +/** A virtual scroll strategy that supports unknown or dynamic size items. */ +@Directive({ + selector: 'ucap-virtual-scroll-viewport[autosize]', + providers: [ + { + provide: VIRTUAL_SCROLL_STRATEGY, + useFactory: _autoSizeVirtualScrollStrategyFactory, + deps: [forwardRef(() => AutoSizeVirtualScrollDirective)] + } + ] +}) +export class AutoSizeVirtualScrollDirective implements OnChanges { + /** + * The minimum amount of buffer rendered beyond the viewport (in pixels). + * If the amount of buffer dips below this number, more items will be rendered. Defaults to 100px. + */ + @Input() + get minBufferPx(): number { + return this._minBufferPx; + } + set minBufferPx(value: number) { + this._minBufferPx = coerceNumberProperty(value); + } + _minBufferPx = 100; + + /** + * The number of pixels worth of buffer to shoot for when rendering new items. + * If the actual amount turns out to be less it will not necessarily trigger an additional + * rendering cycle (as long as the amount of buffer is still greater than `minBufferPx`). + * Defaults to 200px. + */ + @Input() + get maxBufferPx(): number { + return this._maxBufferPx; + } + set maxBufferPx(value: number) { + this._maxBufferPx = coerceNumberProperty(value); + } + _maxBufferPx = 200; + + /** The scroll strategy used by this directive. */ + _scrollStrategy = new AutoSizeVirtualScrollStrategy( + this.minBufferPx, + this.maxBufferPx + ); + + ngOnChanges() { + this._scrollStrategy.updateBufferSize(this.minBufferPx, this.maxBufferPx); + } + + static ngAcceptInputType_minBufferPx: NumberInput; + static ngAcceptInputType_maxBufferPx: NumberInput; +} diff --git a/projects/ui/src/lib/scrolling/fixed-size-virtual-scroll.ts b/projects/ui/src/lib/scrolling/fixed-size-virtual-scroll.ts new file mode 100644 index 0000000..f09dfd3 --- /dev/null +++ b/projects/ui/src/lib/scrolling/fixed-size-virtual-scroll.ts @@ -0,0 +1,341 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import { Observable, Subject } from 'rxjs'; +import { distinctUntilChanged } from 'rxjs/operators'; + +import { + VirtualScrollStrategy, + VIRTUAL_SCROLL_STRATEGY, + VirtualScrollMeasureSize, + VirtualScrollToAlign +} from './virtual-scroll-strategy'; +import { VirtualScrollViewportComponent } from './virtual-scroll-viewport'; +import { Directive, forwardRef, OnChanges, Input } from '@angular/core'; +import { coerceNumberProperty, NumberInput } from '@angular/cdk/coercion'; + +/** Virtual scrolling strategy for lists with items of known fixed size. */ +export class FixedSizeVirtualScrollStrategy implements VirtualScrollStrategy { + private _scrolledIndexChange = new Subject(); + + /** @docs-private Implemented as part of VirtualScrollStrategy. */ + scrolledIndexChange: Observable = this._scrolledIndexChange.pipe( + distinctUntilChanged() + ); + + /** The attached viewport. */ + private _viewport: VirtualScrollViewportComponent | null = null; + + /** The size of the items in the virtually scrolling list. */ + private _itemSize: number; + + /** The minimum amount of buffer rendered beyond the viewport (in pixels). */ + private _minBufferPx: number; + + /** The number of buffer items to render beyond the edge of the viewport (in pixels). */ + private _maxBufferPx: number; + + /** + * @param itemSize The size of the items in the virtually scrolling list. + * @param minBufferPx The minimum amount of buffer (in pixels) before needing to render more + * @param maxBufferPx The amount of buffer (in pixels) to render when rendering more. + */ + constructor(itemSize: number, minBufferPx: number, maxBufferPx: number) { + this._itemSize = itemSize; + this._minBufferPx = minBufferPx; + this._maxBufferPx = maxBufferPx; + } + + /** + * Attaches this scroll strategy to a viewport. + * @param viewport The viewport to attach this strategy to. + */ + attach(viewport: VirtualScrollViewportComponent) { + this._viewport = viewport; + this._updateTotalContentSize(); + this._updateRenderedRange(); + } + + /** Detaches this scroll strategy from the currently attached viewport. */ + detach() { + this._scrolledIndexChange.complete(); + this._viewport = null; + } + + useMeasureSize(): boolean { + return false; + } + + measureSize(sizes: VirtualScrollMeasureSize[]) {} + + measurePartitionCount(): number { + return 0; + } + + /** + * Update the item size and buffer size. + * @param itemSize The size of the items in the virtually scrolling list. + * @param minBufferPx The minimum amount of buffer (in pixels) before needing to render more + * @param maxBufferPx The amount of buffer (in pixels) to render when rendering more. + */ + updateItemAndBufferSize( + itemSize: number, + minBufferPx: number, + maxBufferPx: number + ) { + if (maxBufferPx < minBufferPx) { + throw Error( + 'CDK virtual scroll: maxBufferPx must be greater than or equal to minBufferPx' + ); + } + this._itemSize = itemSize; + this._minBufferPx = minBufferPx; + this._maxBufferPx = maxBufferPx; + this._updateTotalContentSize(); + this._updateRenderedRange(); + } + + /** @docs-private Implemented as part of VirtualScrollStrategy. */ + onContentScrolled() { + this._updateRenderedRange(); + } + + onDataModified(): void {} + + /** @docs-private Implemented as part of VirtualScrollStrategy. */ + onDataChanged() { + this._updateTotalContentSize(); + this._updateRenderedRange(); + } + + /** @docs-private Implemented as part of VirtualScrollStrategy. */ + onContentRendered() { + /* no-op */ + } + + /** @docs-private Implemented as part of VirtualScrollStrategy. */ + onRenderedOffsetChanged() { + /* no-op */ + } + + /** + * Scroll to the offset for the given index. + * @param index The index of the element to scroll to. + * @param behavior The ScrollBehavior to use when scrolling. + */ + scrollToIndex( + index: number, + align: VirtualScrollToAlign = 'center', + behavior: ScrollBehavior + ): void { + if (this._viewport) { + this._viewport.scrollToOffset( + this.offsetForIndex(index, align), + behavior + ); + } + } + + offsetForIndex( + index: number, + align: VirtualScrollToAlign = 'center' + ): number { + const viewport = this._viewport!; + const viewportSize = viewport.getViewportSize(); + const dataLength = this._viewport.getDataLength(); + const scrollOffset = this._viewport.measureScrollOffset(); + let scrollToAlign = align; + + const lastItemOffset = Math.max( + 0, + dataLength * this._itemSize - viewportSize + ); + const maxOffset = Math.min(lastItemOffset, index * this._itemSize); + const minOffset = Math.max( + 0, + index * this._itemSize - viewportSize + this._itemSize + ); + + if ('smart' === scrollToAlign) { + if ( + scrollOffset >= minOffset - viewportSize && + scrollOffset <= maxOffset + viewportSize + ) { + scrollToAlign = 'auto'; + } else { + scrollToAlign = 'center'; + } + } + + switch (scrollToAlign) { + case 'start': + return maxOffset; + case 'end': + return minOffset; + case 'center': { + const middleOffset = Math.round( + minOffset + (maxOffset - minOffset) / 2 + ); + if (middleOffset < Math.ceil(viewportSize / 2)) { + return 0; + } else if ( + middleOffset > + lastItemOffset + Math.floor(viewportSize / 2) + ) { + return lastItemOffset; + } else { + return middleOffset; + } + } + case 'auto': + default: + if (scrollOffset >= minOffset && scrollOffset <= maxOffset) { + return scrollOffset; + } else if (scrollOffset < minOffset) { + return minOffset; + } else { + return maxOffset; + } + } + } + + /** Update the viewport's total content size. */ + private _updateTotalContentSize() { + if (!this._viewport) { + return; + } + + this._viewport.setTotalContentSize( + this._viewport.getDataLength() * this._itemSize + ); + } + + /** Update the viewport's rendered range. */ + private _updateRenderedRange() { + if (!this._viewport) { + return; + } + + const scrollOffset = this._viewport.measureScrollOffset(); + const firstVisibleIndex = scrollOffset / this._itemSize; + const renderedRange = this._viewport.getRenderedRange(); + const newRange = { + start: renderedRange.start, + end: renderedRange.end + }; + const viewportSize = this._viewport.getViewportSize(); + const dataLength = this._viewport.getDataLength(); + + const startBuffer = scrollOffset - newRange.start * this._itemSize; + if (startBuffer < this._minBufferPx && newRange.start !== 0) { + const expandStart = Math.ceil( + (this._maxBufferPx - startBuffer) / this._itemSize + ); + newRange.start = Math.max(0, newRange.start - expandStart); + newRange.end = Math.min( + dataLength, + Math.ceil( + firstVisibleIndex + + (viewportSize + this._minBufferPx) / this._itemSize + ) + ); + } else { + const endBuffer = + newRange.end * this._itemSize - (scrollOffset + viewportSize); + if (endBuffer < this._minBufferPx && newRange.end !== dataLength) { + const expandEnd = Math.ceil( + (this._maxBufferPx - endBuffer) / this._itemSize + ); + if (expandEnd > 0) { + newRange.end = Math.min(dataLength, newRange.end + expandEnd); + newRange.start = Math.max( + 0, + Math.floor(firstVisibleIndex - this._minBufferPx / this._itemSize) + ); + } + } + } + + this._viewport.setRenderedRange(newRange); + this._viewport.setRenderedContentOffset(this._itemSize * newRange.start); + this._scrolledIndexChange.next(Math.floor(firstVisibleIndex)); + } +} + +export function _fixedSizeVirtualScrollStrategyFactory( + fixedSizeDir: FixedSizeVirtualScrollDirective +) { + return fixedSizeDir._scrollStrategy; +} + +/** A virtual scroll strategy that supports fixed-size items. */ +@Directive({ + selector: 'ucap-virtual-scroll-viewport[itemSize]', + providers: [ + { + provide: VIRTUAL_SCROLL_STRATEGY, + useFactory: _fixedSizeVirtualScrollStrategyFactory, + deps: [forwardRef(() => FixedSizeVirtualScrollDirective)] + } + ] +}) +export class FixedSizeVirtualScrollDirective implements OnChanges { + /** The size of the items in the list (in pixels). */ + @Input() + get itemSize(): number { + return this._itemSize; + } + set itemSize(value: number) { + this._itemSize = coerceNumberProperty(value); + } + _itemSize = 20; + + /** + * The minimum amount of buffer rendered beyond the viewport (in pixels). + * If the amount of buffer dips below this number, more items will be rendered. Defaults to 100px. + */ + @Input() + get minBufferPx(): number { + return this._minBufferPx; + } + set minBufferPx(value: number) { + this._minBufferPx = coerceNumberProperty(value); + } + _minBufferPx = 100; + + /** + * The number of pixels worth of buffer to render for when rendering new items. Defaults to 200px. + */ + @Input() + get maxBufferPx(): number { + return this._maxBufferPx; + } + set maxBufferPx(value: number) { + this._maxBufferPx = coerceNumberProperty(value); + } + _maxBufferPx = 200; + + /** The scroll strategy used by this directive. */ + _scrollStrategy = new FixedSizeVirtualScrollStrategy( + this.itemSize, + this.minBufferPx, + this.maxBufferPx + ); + + ngOnChanges() { + this._scrollStrategy.updateItemAndBufferSize( + this.itemSize, + this.minBufferPx, + this.maxBufferPx + ); + } + + static ngAcceptInputType_itemSize: NumberInput; + static ngAcceptInputType_minBufferPx: NumberInput; + static ngAcceptInputType_maxBufferPx: NumberInput; +} diff --git a/projects/ui/src/lib/scrolling/measure-size-virtual-scroll.ts b/projects/ui/src/lib/scrolling/measure-size-virtual-scroll.ts new file mode 100644 index 0000000..b5c5528 --- /dev/null +++ b/projects/ui/src/lib/scrolling/measure-size-virtual-scroll.ts @@ -0,0 +1,601 @@ +import { Observable } from 'rxjs'; + +import { Directive, forwardRef, Input, OnChanges } from '@angular/core'; + +import { coerceNumberProperty, NumberInput } from '@angular/cdk/coercion'; +import { ListRange } from '@angular/cdk/collections'; +import { + VirtualScrollStrategy, + VIRTUAL_SCROLL_STRATEGY, + VirtualScrollKeyType, + VirtualScrollToAlign, + VirtualScrollMeasureSize +} from './virtual-scroll-strategy'; +import { VirtualScrollViewportComponent } from './virtual-scroll-viewport'; + +export interface MeasureSizeMetadata { + key: VirtualScrollKeyType; + offset: number; + size: number; +} + +/** Virtual scrolling strategy for lists with items of unknown or dynamic size. */ +export class MeasureSizeVirtualScrollStrategy implements VirtualScrollStrategy { + /** @docs-private Implemented as part of VirtualScrollStrategy. */ + scrolledIndexChange = new Observable(() => { + // TODO(mmalerba): Implement. + throw Error( + 'ucap-virtual-scroll: scrolledIndexChange is currently not supported for the' + + ' autosize scroll strategy' + ); + }); + + /** The attached viewport. */ + private _viewport: VirtualScrollViewportComponent | null = null; + + /** The minimum amount of buffer rendered beyond the viewport (in pixels). */ + private _minBufferPx: number; + + /** The number of buffer items to render beyond the edge of the viewport (in pixels). */ + private _maxBufferPx: number; + + private _partitionCount: number; + + /** The last measured scroll offset of the viewport. */ + private _lastScrollOffset: number; + + /** The last measured size of the rendered content in the viewport. */ + private _lastRenderedContentSize: number; + + /** The last measured size of the rendered content in the viewport. */ + private _lastRenderedContentOffset: number; + + private _removalFailures = 0; + + /** The estimator used to estimate the size of unseen items. */ + // private _averager: ItemSizeAverager; + + private _metadatas: MeasureSizeMetadata[] = []; + private _measuredTotalSize = 0; + + /** + * @param minBufferPx The minimum amount of buffer rendered beyond the viewport (in pixels). + * If the amount of buffer dips below this number, more items will be rendered. + * @param maxBufferPx The number of pixels worth of buffer to shoot for when rendering new items. + * If the actual amount turns out to be less it will not necessarily trigger an additional + * rendering cycle (as long as the amount of buffer is still greater than `minBufferPx`). + * @param averager The averager used to estimate the size of unseen items. + */ + constructor( + partitionCount: number, + minBufferPx: number, + maxBufferPx: number + ) { + this._partitionCount = partitionCount; + this._minBufferPx = minBufferPx; + this._maxBufferPx = maxBufferPx; + } + + /** + * Attaches this scroll strategy to a viewport. + * @param viewport The viewport to attach this strategy to. + */ + attach(viewport: VirtualScrollViewportComponent) { + // this._averager.reset(); + this._viewport = viewport; + } + + /** Detaches this scroll strategy from the currently attached viewport. */ + detach() { + this._viewport = null; + } + + useMeasureSize(): boolean { + return true; + } + + measureSize(sizes: VirtualScrollMeasureSize[]) { + if (!sizes || 0 === sizes.length) { + return; + } + + let offset = this._measuredTotalSize; + sizes.forEach((s, i) => { + this._metadatas[s.index] = { key: s.key, offset, size: s.size }; + + offset += s.size; + }); + this._measuredTotalSize = offset; + this._updateTotalContentSize(); + this._renderContentForCurrentOffset(); + + console.log('this._metadatas', this._metadatas); + } + + measurePartitionCount(): number { + return this._partitionCount; + } + + /** + * Update the buffer parameters. + * @param minBufferPx The minimum amount of buffer rendered beyond the viewport (in pixels). + * @param maxBufferPx The number of buffer items to render beyond the edge of the viewport (in + * pixels). + */ + updateBufferSize( + partitionCount: number, + minBufferPx: number, + maxBufferPx: number + ) { + if (maxBufferPx < minBufferPx) { + throw 'CDK virtual scroll: maxBufferPx must be greater than or equal to minBufferPx'; + } + this._minBufferPx = minBufferPx; + this._maxBufferPx = maxBufferPx; + this._partitionCount = partitionCount; + } + + /** @docs-private Implemented as part of VirtualScrollStrategy. */ + onContentScrolled() { + if (this._viewport) { + this._updateRenderedContentAfterScroll(); + } + } + + onDataModified(): void { + if (this._viewport) { + this._renderContentForCurrentOffset(); + } + } + + /** @docs-private Implemented as part of VirtualScrollStrategy. */ + onDataChanged() { + if (this._viewport) { + } + } + + /** @docs-private Implemented as part of VirtualScrollStrategy. */ + onContentRendered() { + if (this._viewport) { + } + } + + /** @docs-private Implemented as part of VirtualScrollStrategy. */ + onRenderedOffsetChanged() { + if (this._viewport) { + this._checkRenderedContentOffset(); + } + } + + /** Scroll to the offset for the given index. */ + scrollToIndex( + index: number, + align: VirtualScrollToAlign = 'center', + behavior: ScrollBehavior + ): void { + if (this._viewport) { + this._viewport.scrollToOffset( + this.offsetForIndex(index, align), + behavior + ); + } + } + + offsetForIndex(index: number, align: VirtualScrollToAlign = 'smart'): number { + const viewport = this._viewport!; + const viewportSize = viewport.getViewportSize(); + const metadata = this._metadatas[index]; + const scrollOffset = this._viewport.measureScrollOffset(); + let scrollToAlign = align; + + const maxOffset = Math.max( + 0, + Math.min(this._measuredTotalSize - viewportSize, metadata.offset) + ); + const minOffset = Math.max( + 0, + metadata.offset - viewportSize + metadata.size + ); + + if ('smart' === scrollToAlign) { + if ( + scrollOffset >= minOffset - viewportSize && + scrollOffset <= maxOffset + viewportSize + ) { + scrollToAlign = 'auto'; + } else { + scrollToAlign = 'center'; + } + } + + switch (scrollToAlign) { + case 'start': + return maxOffset; + case 'end': + return minOffset; + case 'center': + return Math.round(minOffset + (maxOffset - minOffset) / 2); + case 'auto': + default: + if (scrollOffset >= minOffset && scrollOffset <= maxOffset) { + return scrollOffset; + } else if (scrollOffset < minOffset) { + return minOffset; + } else { + return maxOffset; + } + } + } + + private get _averageItemSize() { + return this._measuredTotalSize / this._metadatas.length; + } + + /** Update the viewport's total content size. */ + private _updateTotalContentSize() { + if (!this._viewport) { + return; + } + this._viewport.setTotalContentSize(this._measuredTotalSize); + } + + private _updateRenderedContentAfterScroll() { + const viewport = this._viewport!; + + // The current scroll offset. + const scrollOffset = viewport.measureScrollOffset(); + // The delta between the current scroll offset and the previously recorded scroll offset. + let scrollDelta = scrollOffset - this._lastScrollOffset; // +: forward, -: backward + // The magnitude of the scroll delta. + let scrollMagnitude = Math.abs(scrollDelta); + + const md = this._findNearestItem(scrollOffset); + + console.log('_updateRenderedContentAfterScroll', scrollOffset, md); + + // The currently rendered range. + const renderedRange = viewport.getRenderedRange(); + + let offsetCorrection = 0; + if (scrollDelta < 0) { + // The content offset we would expect based on the average item size. + const predictedOffset = this._metadatas[renderedRange.start].offset; + // The difference between the predicted size of the unrendered content at the beginning and + // the actual available space to scroll over. We need to reduce this to zero by the time the + // user scrolls to the top. + // - 0 indicates that the predicted size and available space are the same. + // - A negative number that the predicted size is smaller than the available space. + // - A positive number indicates the predicted size is larger than the available space + const offsetDifference = + predictedOffset - this._lastRenderedContentOffset; + // The amount of difference to correct during this scroll event. We calculate this as a + // percentage of the total difference based on the percentage of the distance toward the top + // that the user scrolled. + offsetCorrection = Math.round( + offsetDifference * + Math.max( + 0, + Math.min(1, scrollMagnitude / (scrollOffset + scrollMagnitude)) + ) + ); + + // Based on the offset correction above, we pretend that the scroll delta was bigger or + // smaller than it actually was, this way we can start to eliminate the difference. + scrollDelta = scrollDelta - offsetCorrection; + scrollMagnitude = Math.abs(scrollDelta); + } + + // The current amount of buffer past the start of the viewport. + const startBuffer = + this._lastScrollOffset - this._lastRenderedContentOffset; + // The current amount of buffer past the end of the viewport. + const endBuffer = + this._lastRenderedContentOffset + + this._lastRenderedContentSize - + (this._lastScrollOffset + viewport.getViewportSize()); + // The amount of unfilled space that should be filled on the side the user is scrolling toward + // in order to safely absorb the scroll delta. + const underscan = + scrollMagnitude + + this._minBufferPx - + (scrollDelta < 0 ? startBuffer : endBuffer); + + if (underscan > 0) { + // Check if the scroll magnitude was larger than the viewport size. In this case the user + // won't notice a discontinuity if we just jump to the new estimated position in the list. + // However, if the scroll magnitude is smaller than the viewport the user might notice some + // jitteriness if we just jump to the estimated position. Instead we make sure to scroll by + // the same number of pixels as the scroll magnitude. + if (scrollMagnitude >= viewport.getViewportSize()) { + this._renderContentForCurrentOffset(); + } else { + // The number of new items to render on the side the user is scrolling towards. Rather than + // just filling the underscan space, we actually fill enough to have a buffer size of + // `maxBufferPx`. This gives us a little wiggle room in case our item size estimate is off. + const addItems = Math.max( + 0, + Math.ceil( + (underscan - this._minBufferPx + this._maxBufferPx) / + this._averageItemSize + ) + ); + // The amount of filled space beyond what is necessary on the side the user is scrolling + // away from. + const overscan = + (scrollDelta < 0 ? endBuffer : startBuffer) - + this._minBufferPx + + scrollMagnitude; + // The number of currently rendered items to remove on the side the user is scrolling away + // from. If removal has failed in recent cycles we are less aggressive in how much we try to + // remove. + const unboundedRemoveItems = Math.floor( + overscan / this._averageItemSize / (this._removalFailures + 1) + ); + const removeItems = Math.min( + renderedRange.end - renderedRange.start, + Math.max(0, unboundedRemoveItems) + ); + + // The new range we will tell the viewport to render. We first expand it to include the new + // items we want rendered, we then contract the opposite side to remove items we no longer + // want rendered. + const range = this._expandRange( + renderedRange, + scrollDelta < 0 ? addItems : 0, + scrollDelta > 0 ? addItems : 0 + ); + if (scrollDelta < 0) { + range.end = Math.max(range.start + 1, range.end - removeItems); + } else { + range.start = Math.min(range.end - 1, range.start + removeItems); + } + + // The new offset we want to set on the rendered content. To determine this we measure the + // number of pixels we removed and then adjust the offset to the start of the rendered + // content or to the end of the rendered content accordingly (whichever one doesn't require + // that the newly added items to be rendered to calculate.) + let contentOffset: number; + let contentOffsetTo: 'to-start' | 'to-end'; + if (scrollDelta < 0) { + const removedSize = viewport.measureRangeSize({ + start: range.end, + end: renderedRange.end + }); + // Check that we're not removing too much. + if (removedSize <= overscan) { + contentOffset = + this._lastRenderedContentOffset + + this._lastRenderedContentSize - + removedSize; + this._removalFailures = 0; + } else { + // If the removal is more than the overscan can absorb just undo it and record the fact + // that the removal failed so we can be less aggressive next time. + range.end = renderedRange.end; + contentOffset = + this._lastRenderedContentOffset + this._lastRenderedContentSize; + this._removalFailures++; + } + contentOffsetTo = 'to-end'; + } else { + const removedSize = viewport.measureRangeSize({ + start: renderedRange.start, + end: range.start + }); + // Check that we're not removing too much. + if (removedSize <= overscan) { + contentOffset = this._lastRenderedContentOffset + removedSize; + this._removalFailures = 0; + } else { + // If the removal is more than the overscan can absorb just undo it and record the fact + // that the removal failed so we can be less aggressive next time. + range.start = renderedRange.start; + contentOffset = this._lastRenderedContentOffset; + this._removalFailures++; + } + contentOffsetTo = 'to-start'; + } + + // Set the range and offset we calculated above. + viewport.setRenderedRange(range); + viewport.setRenderedContentOffset( + contentOffset + offsetCorrection, + contentOffsetTo + ); + } + } else if (offsetCorrection) { + // Even if the rendered range didn't change, we may still need to adjust the content offset to + // simulate scrolling slightly slower or faster than the user actually scrolled. + viewport.setRenderedContentOffset( + this._lastRenderedContentOffset + offsetCorrection + ); + } + + // Save the scroll offset to be compared to the new value on the next scroll event. + this._lastScrollOffset = scrollOffset; + } + + private _checkRenderedContentOffset() { + const viewport = this._viewport!; + this._lastRenderedContentSize = viewport.measureRenderedContentSize(); + this._lastRenderedContentOffset = viewport.getOffsetToRenderedContentStart()!; + } + + private _renderContentForCurrentOffset() { + if (!this._viewport) { + return; + } + const viewport = this._viewport!; + const scrollOffset = viewport.measureScrollOffset(); + this._lastScrollOffset = scrollOffset; + this._removalFailures = 0; + + const itemSize = this._averageItemSize; + const firstVisibleIndex = this._getFirstIndexForOffset(scrollOffset); + const lastVisibleIndex = this._getLastIndexForOffset( + firstVisibleIndex, + scrollOffset + ); + + const startBufferIndex = this._findNearestItem( + Math.max(0, this._metadatas[firstVisibleIndex].offset - this._maxBufferPx) + ); + const endBufferIndex = this._findNearestItem( + Math.min( + this._measuredTotalSize, + this._metadatas[lastVisibleIndex].offset + + this._metadatas[lastVisibleIndex].size + + this._maxBufferPx + ) + ); + + const range = this._expandRange( + { start: firstVisibleIndex, end: lastVisibleIndex }, + firstVisibleIndex - startBufferIndex, + endBufferIndex - lastVisibleIndex + ); + + viewport.setRenderedRange(range); + viewport.setRenderedContentOffset( + this._metadatas[firstVisibleIndex].offset + ); + } + + private _expandRange( + range: ListRange, + expandStart: number, + expandEnd: number + ): ListRange { + const viewport = this._viewport!; + const start = Math.max(0, range.start - expandStart); + const end = Math.min(viewport.getDataLength(), range.end + expandEnd); + return { start, end }; + } + + private _getFirstIndexForOffset(offset: number) { + return this._findNearestItem(offset); + } + + private _getLastIndexForOffset(firstIndex: number, scrollOffset: number) { + const viewport = this._viewport!; + const viewportSize = viewport.getViewportSize(); + const itemCount = this._metadatas.length; + const metadata = this._metadatas[firstIndex]; + const maxOffset = scrollOffset + viewportSize; + + let offset = metadata.offset + metadata.size; + let lastIndex = firstIndex; + + while (lastIndex < itemCount - 1 && offset < maxOffset) { + lastIndex++; + offset += this._metadatas[lastIndex].size; + } + + return lastIndex; + } + + private _findNearestItem(offset: number) { + let high = this._metadatas.length - 1; + let low = 0; + + while (low <= high) { + const middle = low + Math.floor((high - low) / 2); + const currentOffset = this._metadatas[middle].offset; + + if (currentOffset === offset) { + return middle; + } else if (currentOffset < offset) { + low = middle + 1; + } else if (currentOffset > offset) { + high = middle - 1; + } + } + + if (0 < low) { + return low - 1; + } else { + return 0; + } + } +} + +/** + * Provider factory for `AutoSizeVirtualScrollStrategy` that simply extracts the already created + * `AutoSizeVirtualScrollStrategy` from the given directive. + * @param autoSizeDir The instance of `CdkAutoSizeVirtualScroll` to extract the + * `AutoSizeVirtualScrollStrategy` from. + */ +export function _measureSizeVirtualScrollStrategyFactory( + measureSizeDir: MeasureSizeVirtualScrollDirective +) { + return measureSizeDir._scrollStrategy; +} + +/** A virtual scroll strategy that supports unknown or dynamic size items. */ +@Directive({ + selector: 'ucap-virtual-scroll-viewport[measureSize]', + providers: [ + { + provide: VIRTUAL_SCROLL_STRATEGY, + useFactory: _measureSizeVirtualScrollStrategyFactory, + deps: [forwardRef(() => MeasureSizeVirtualScrollDirective)] + } + ] +}) +export class MeasureSizeVirtualScrollDirective implements OnChanges { + /** + * The minimum amount of buffer rendered beyond the viewport (in pixels). + * If the amount of buffer dips below this number, more items will be rendered. Defaults to 100px. + */ + @Input() + get minBufferPx(): number { + return this._minBufferPx; + } + set minBufferPx(value: number) { + this._minBufferPx = coerceNumberProperty(value); + } + _minBufferPx = 100; + + /** + * The number of pixels worth of buffer to shoot for when rendering new items. + * If the actual amount turns out to be less it will not necessarily trigger an additional + * rendering cycle (as long as the amount of buffer is still greater than `minBufferPx`). + * Defaults to 200px. + */ + @Input() + get maxBufferPx(): number { + return this._maxBufferPx; + } + set maxBufferPx(value: number) { + this._maxBufferPx = coerceNumberProperty(value); + } + _maxBufferPx = 200; + + @Input() + get partitionCount(): number { + return this._partitionCount; + } + set partitionCount(value: number) { + this._partitionCount = coerceNumberProperty(value); + } + _partitionCount = 100; + + /** The scroll strategy used by this directive. */ + _scrollStrategy = new MeasureSizeVirtualScrollStrategy( + this.partitionCount, + this.minBufferPx, + this.maxBufferPx + ); + + ngOnChanges() { + this._scrollStrategy.updateBufferSize( + this.partitionCount, + this.minBufferPx, + this.maxBufferPx + ); + } + + static ngAcceptInputType_partitionCount: NumberInput; + static ngAcceptInputType_minBufferPx: NumberInput; + static ngAcceptInputType_maxBufferPx: NumberInput; +} diff --git a/projects/ui/src/lib/scrolling/scroll-dispatcher.ts b/projects/ui/src/lib/scrolling/scroll-dispatcher.ts new file mode 100644 index 0000000..4c0db0e --- /dev/null +++ b/projects/ui/src/lib/scrolling/scroll-dispatcher.ts @@ -0,0 +1,220 @@ +import { + fromEvent, + of as observableOf, + Subject, + Subscription, + Observable, + Observer +} from 'rxjs'; +import { auditTime, filter } from 'rxjs/operators'; + +import { + ElementRef, + Injectable, + NgZone, + OnDestroy, + Optional, + Inject +} from '@angular/core'; + +import { DOCUMENT } from '@angular/common'; + +import { Platform } from '@angular/cdk/platform'; + +import { ScrollableDirective } from './scrollable'; + +/** Time in ms to throttle the scrolling events by default. */ +export const DEFAULT_SCROLL_TIME = 20; + +/** + * Service contained all registered Scrollable references and emits an event when any one of the + * Scrollable references emit a scrolled event. + */ +@Injectable({ providedIn: 'root' }) +export class ScrollDispatcher implements OnDestroy { + /** Used to reference correct document/window */ + protected _document?: Document; + + constructor( + private _ngZone: NgZone, + private _platform: Platform, + /** @breaking-change 11.0.0 make document required */ + @Optional() @Inject(DOCUMENT) document?: any + ) { + this._document = document; + } + + /** Subject for notifying that a registered scrollable reference element has been scrolled. */ + private _scrolled = new Subject(); + + /** Keeps track of the global `scroll` and `resize` subscriptions. */ + _globalSubscription: Subscription | null = null; + + /** Keeps track of the amount of subscriptions to `scrolled`. Used for cleaning up afterwards. */ + private _scrolledCount = 0; + + /** + * Map of all the scrollable references that are registered with the service and their + * scroll event subscriptions. + */ + scrollContainers: Map = new Map(); + + /** + * Registers a scrollable instance with the service and listens for its scrolled events. When the + * scrollable is scrolled, the service emits the event to its scrolled observable. + * @param scrollable Scrollable instance to be registered. + */ + register(scrollable: ScrollableDirective): void { + if (!this.scrollContainers.has(scrollable)) { + this.scrollContainers.set( + scrollable, + scrollable + .elementScrolled() + .subscribe(() => this._scrolled.next(scrollable)) + ); + } + } + + /** + * Deregisters a Scrollable reference and unsubscribes from its scroll event observable. + * @param scrollable Scrollable instance to be deregistered. + */ + deregister(scrollable: ScrollableDirective): void { + const scrollableReference = this.scrollContainers.get(scrollable); + + if (scrollableReference) { + scrollableReference.unsubscribe(); + this.scrollContainers.delete(scrollable); + } + } + + /** + * Returns an observable that emits an event whenever any of the registered Scrollable + * references (or window, document, or body) fire a scrolled event. Can provide a time in ms + * to override the default "throttle" time. + * + * **Note:** in order to avoid hitting change detection for every scroll event, + * all of the events emitted from this stream will be run outside the Angular zone. + * If you need to update any data bindings as a result of a scroll event, you have + * to run the callback using `NgZone.run`. + */ + scrolled( + auditTimeInMs: number = DEFAULT_SCROLL_TIME + ): Observable { + if (!this._platform.isBrowser) { + return observableOf(); + } + + return new Observable((observer: Observer) => { + if (!this._globalSubscription) { + this._addGlobalListener(); + } + + // In the case of a 0ms delay, use an observable without auditTime + // since it does add a perceptible delay in processing overhead. + const subscription = + auditTimeInMs > 0 + ? this._scrolled.pipe(auditTime(auditTimeInMs)).subscribe(observer) + : this._scrolled.subscribe(observer); + + this._scrolledCount++; + + return () => { + subscription.unsubscribe(); + this._scrolledCount--; + + if (!this._scrolledCount) { + this._removeGlobalListener(); + } + }; + }); + } + + ngOnDestroy() { + this._removeGlobalListener(); + this.scrollContainers.forEach((_, container) => this.deregister(container)); + this._scrolled.complete(); + } + + /** + * Returns an observable that emits whenever any of the + * scrollable ancestors of an element are scrolled. + * @param elementRef Element whose ancestors to listen for. + * @param auditTimeInMs Time to throttle the scroll events. + */ + ancestorScrolled( + elementRef: ElementRef, + auditTimeInMs?: number + ): Observable { + const ancestors = this.getAncestorScrollContainers(elementRef); + + return this.scrolled(auditTimeInMs).pipe( + filter((target) => { + return !target || ancestors.indexOf(target) > -1; + }) + ); + } + + /** Returns all registered Scrollables that contain the provided element. */ + getAncestorScrollContainers(elementRef: ElementRef): ScrollableDirective[] { + const scrollingContainers: ScrollableDirective[] = []; + + this.scrollContainers.forEach( + (_subscription: Subscription, scrollable: ScrollableDirective) => { + if (this._scrollableContainsElement(scrollable, elementRef)) { + scrollingContainers.push(scrollable); + } + } + ); + + return scrollingContainers; + } + + /** Access injected document if available or fallback to global document reference */ + private _getDocument(): Document { + return this._document || document; + } + + /** Use defaultView of injected document if available or fallback to global window reference */ + private _getWindow(): Window { + const doc = this._getDocument(); + return doc.defaultView || window; + } + + /** Returns true if the element is contained within the provided Scrollable. */ + private _scrollableContainsElement( + scrollable: ScrollableDirective, + elementRef: ElementRef + ): boolean { + let element: HTMLElement | null = elementRef.nativeElement; + const scrollableElement = scrollable.getElementRef().nativeElement; + + // Traverse through the element parents until we reach null, checking if any of the elements + // are the scrollable's element. + do { + if (element === scrollableElement) { + return true; + } + } while ((element = element!.parentElement)); + + return false; + } + + /** Sets up the global scroll listeners. */ + private _addGlobalListener() { + this._globalSubscription = this._ngZone.runOutsideAngular(() => { + const window = this._getWindow(); + return fromEvent(window.document, 'scroll').subscribe(() => + this._scrolled.next() + ); + }); + } + + /** Cleans up the global scroll listener. */ + private _removeGlobalListener() { + if (this._globalSubscription) { + this._globalSubscription.unsubscribe(); + this._globalSubscription = null; + } + } +} diff --git a/projects/ui/src/lib/scrolling/scrollable.ts b/projects/ui/src/lib/scrolling/scrollable.ts new file mode 100644 index 0000000..2debe03 --- /dev/null +++ b/projects/ui/src/lib/scrolling/scrollable.ts @@ -0,0 +1,241 @@ +import { fromEvent, Observable, Subject, Observer } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; + +import { + getRtlScrollAxisType, + RtlScrollAxisType, + supportsScrollBehavior +} from '@angular/cdk/platform'; +import { + Directive, + ElementRef, + NgZone, + OnDestroy, + OnInit, + Optional +} from '@angular/core'; + +import { Directionality } from '@angular/cdk/bidi'; + +import { ScrollDispatcher } from './scroll-dispatcher'; + +export type _Without = { [P in keyof T]?: never }; +export type _XOR = (_Without & U) | (_Without & T); +export type _Top = { top?: number }; +export type _Bottom = { bottom?: number }; +export type _Left = { left?: number }; +export type _Right = { right?: number }; +export type _Start = { start?: number }; +export type _End = { end?: number }; +export type _XAxis = _XOR<_XOR<_Left, _Right>, _XOR<_Start, _End>>; +export type _YAxis = _XOR<_Top, _Bottom>; + +/** + * An extended version of ScrollToOptions that allows expressing scroll offsets relative to the + * top, bottom, left, right, start, or end of the viewport rather than just the top and left. + * Please note: the top and bottom properties are mutually exclusive, as are the left, right, + * start, and end properties. + */ +export type ExtendedScrollToOptions = _XAxis & _YAxis & ScrollOptions; + +/** + * Sends an event when the directive's element is scrolled. Registers itself with the + * ScrollDispatcher service to include itself as part of its collection of scrolling events that it + * can be listened to through the service. + */ +@Directive({ + selector: '[ucap-scrollable], [ucapScrollable]' +}) +export class ScrollableDirective implements OnInit, OnDestroy { + private _destroyed = new Subject(); + + private _elementScrolled: Observable< + Event + > = new Observable((observer: Observer) => + this.ngZone.runOutsideAngular(() => + fromEvent(this.elementRef.nativeElement, 'scroll') + .pipe(takeUntil(this._destroyed)) + .subscribe(observer) + ) + ); + + constructor( + protected elementRef: ElementRef, + protected scrollDispatcher: ScrollDispatcher, + protected ngZone: NgZone, + @Optional() protected dir?: Directionality + ) {} + + ngOnInit() { + this.scrollDispatcher.register(this); + } + + ngOnDestroy() { + this.scrollDispatcher.deregister(this); + this._destroyed.next(); + this._destroyed.complete(); + } + + /** Returns observable that emits when a scroll event is fired on the host element. */ + elementScrolled(): Observable { + return this._elementScrolled; + } + + /** Gets the ElementRef for the viewport. */ + getElementRef(): ElementRef { + return this.elementRef; + } + + /** + * Scrolls to the specified offsets. This is a normalized version of the browser's native scrollTo + * method, since browsers are not consistent about what scrollLeft means in RTL. For this method + * left and right always refer to the left and right side of the scrolling container irrespective + * of the layout direction. start and end refer to left and right in an LTR context and vice-versa + * in an RTL context. + * @param options specified the offsets to scroll to. + */ + scrollTo(options: ExtendedScrollToOptions): void { + const el = this.elementRef.nativeElement; + const isRtl = this.dir && this.dir.value === 'rtl'; + + // Rewrite start & end offsets as right or left offsets. + if (!!options.start || !!options.end) { + if (!options.left) { + options = { + ...options, + left: isRtl ? options.end : options.start + } as any; + // options.left = isRtl ? options.end : options.start; + } + + if (!options.right) { + options = { + ...options, + right: isRtl ? options.start : options.end + } as any; + // options.right = isRtl ? options.start : options.end; + } + } + + // Rewrite the bottom offset as a top offset. + if (!!options.bottom) { + options = { + ...options, + top: el.scrollHeight - el.clientHeight - options.bottom + } as any; + + // (options as _Without<_Bottom> & _Top).top = + // el.scrollHeight - el.clientHeight - options.bottom; + } + + // Rewrite the right offset as a left offset. + if (isRtl && getRtlScrollAxisType() !== RtlScrollAxisType.NORMAL) { + if (options.left !== null) { + options = { + ...options, + right: el.scrollWidth - el.clientWidth - options.left + } as any; + + // (options as _Without<_Left> & _Right).right = + // el.scrollWidth - el.clientWidth - options.left; + } + + if (getRtlScrollAxisType() === RtlScrollAxisType.INVERTED) { + options = { + ...options, + left: options.right + } as any; + // options.left = options.right; + } else if (getRtlScrollAxisType() === RtlScrollAxisType.NEGATED) { + options = { + ...options, + left: options.right ? -options.right : options.right + } as any; + // options.left = options.right ? -options.right : options.right; + } + } else { + if (!!options.right) { + options = { + ...options, + left: el.scrollWidth - el.clientWidth - options.right + } as any; + // (options as _Without<_Right> & _Left).left = + // el.scrollWidth - el.clientWidth - options.right; + } + } + + this._applyScrollToOptions(options); + } + + private _applyScrollToOptions(options: ScrollToOptions): void { + const el = this.elementRef.nativeElement; + + if (supportsScrollBehavior()) { + el.scrollTo(options); + } else { + if (!!options.top) { + el.scrollTop = options.top; + } + if (!!options.left) { + el.scrollLeft = options.left; + } + } + } + + /** + * Measures the scroll offset relative to the specified edge of the viewport. This method can be + * used instead of directly checking scrollLeft or scrollTop, since browsers are not consistent + * about what scrollLeft means in RTL. The values returned by this method are normalized such that + * left and right always refer to the left and right side of the scrolling container irrespective + * of the layout direction. start and end refer to left and right in an LTR context and vice-versa + * in an RTL context. + * @param from The edge to measure from. + */ + measureScrollOffset( + from: 'top' | 'left' | 'right' | 'bottom' | 'start' | 'end' + ): number { + const LEFT = 'left'; + const RIGHT = 'right'; + const el = this.elementRef.nativeElement; + if (from === 'top') { + return el.scrollTop; + } + if (from === 'bottom') { + return el.scrollHeight - el.clientHeight - el.scrollTop; + } + + // Rewrite start & end as left or right offsets. + const isRtl = this.dir && this.dir.value === 'rtl'; + if (from === 'start') { + from = isRtl ? RIGHT : LEFT; + } else if (from === 'end') { + from = isRtl ? LEFT : RIGHT; + } + + if (isRtl && getRtlScrollAxisType() === RtlScrollAxisType.INVERTED) { + // For INVERTED, scrollLeft is (scrollWidth - clientWidth) when scrolled all the way left and + // 0 when scrolled all the way right. + if (from === LEFT) { + return el.scrollWidth - el.clientWidth - el.scrollLeft; + } else { + return el.scrollLeft; + } + } else if (isRtl && getRtlScrollAxisType() === RtlScrollAxisType.NEGATED) { + // For NEGATED, scrollLeft is -(scrollWidth - clientWidth) when scrolled all the way left and + // 0 when scrolled all the way right. + if (from === LEFT) { + return el.scrollLeft + el.scrollWidth - el.clientWidth; + } else { + return -el.scrollLeft; + } + } else { + // For NORMAL, as well as non-RTL contexts, scrollLeft is 0 when scrolled all the way left and + // (scrollWidth - clientWidth) when scrolled all the way right. + if (from === LEFT) { + return el.scrollLeft; + } else { + return el.scrollWidth - el.clientWidth - el.scrollLeft; + } + } + } +} diff --git a/projects/ui/src/lib/scrolling/viewport-ruler.ts b/projects/ui/src/lib/scrolling/viewport-ruler.ts new file mode 100644 index 0000000..d350567 --- /dev/null +++ b/projects/ui/src/lib/scrolling/viewport-ruler.ts @@ -0,0 +1,179 @@ +import { + merge, + of as observableOf, + fromEvent, + Observable, + Subscription +} from 'rxjs'; +import { auditTime } from 'rxjs/operators'; + +import { Injectable, NgZone, OnDestroy, Optional, Inject } from '@angular/core'; +import { DOCUMENT } from '@angular/common'; + +import { Platform } from '@angular/cdk/platform'; + +/** Time in ms to throttle the resize events by default. */ +export const DEFAULT_RESIZE_TIME = 20; + +/** Object that holds the scroll position of the viewport in each direction. */ +export interface ViewportScrollPosition { + top: number; + left: number; +} + +/** + * Simple utility for getting the bounds of the browser viewport. + * @docs-private + */ +@Injectable({ providedIn: 'root' }) +export class ViewportRuler implements OnDestroy { + /** Cached viewport dimensions. */ + private _viewportSize: { width: number; height: number }; + + /** Stream of viewport change events. */ + private _change: Observable; + + /** Subscription to streams that invalidate the cached viewport dimensions. */ + private _invalidateCache: Subscription; + + /** Used to reference correct document/window */ + protected _document?: Document; + + constructor( + private _platform: Platform, + ngZone: NgZone, + /** @breaking-change 11.0.0 make document required */ + @Optional() @Inject(DOCUMENT) document?: any + ) { + this._document = document; + + ngZone.runOutsideAngular(() => { + const window = this._getWindow(); + + this._change = _platform.isBrowser + ? merge( + fromEvent(window, 'resize'), + fromEvent(window, 'orientationchange') + ) + : observableOf(); + + // Note that we need to do the subscription inside `runOutsideAngular` + // since subscribing is what causes the event listener to be added. + this._invalidateCache = this.change().subscribe(() => + this._updateViewportSize() + ); + }); + } + + ngOnDestroy() { + this._invalidateCache.unsubscribe(); + } + + /** Returns the viewport's width and height. */ + getViewportSize(): Readonly<{ width: number; height: number }> { + if (!this._viewportSize) { + this._updateViewportSize(); + } + + const output = { + width: this._viewportSize.width, + height: this._viewportSize.height + }; + + // If we're not on a browser, don't cache the size since it'll be mocked out anyway. + if (!this._platform.isBrowser) { + this._viewportSize = null!; + } + + return output; + } + + /** Gets a ClientRect for the viewport's bounds. */ + getViewportRect(): ClientRect { + // Use the document element's bounding rect rather than the window scroll properties + // (e.g. pageYOffset, scrollY) due to in issue in Chrome and IE where window scroll + // properties and client coordinates (boundingClientRect, clientX/Y, etc.) are in different + // conceptual viewports. Under most circumstances these viewports are equivalent, but they + // can disagree when the page is pinch-zoomed (on devices that support touch). + // See https://bugs.chromium.org/p/chromium/issues/detail?id=489206#c4 + // We use the documentElement instead of the body because, by default (without a css reset) + // browsers typically give the document body an 8px margin, which is not included in + // getBoundingClientRect(). + const scrollPosition = this.getViewportScrollPosition(); + const { width, height } = this.getViewportSize(); + + return { + top: scrollPosition.top, + left: scrollPosition.left, + bottom: scrollPosition.top + height, + right: scrollPosition.left + width, + height, + width + }; + } + + /** Gets the (top, left) scroll position of the viewport. */ + getViewportScrollPosition(): ViewportScrollPosition { + // While we can get a reference to the fake document + // during SSR, it doesn't have getBoundingClientRect. + if (!this._platform.isBrowser) { + return { top: 0, left: 0 }; + } + + // The top-left-corner of the viewport is determined by the scroll position of the document + // body, normally just (scrollLeft, scrollTop). However, Chrome and Firefox disagree about + // whether `document.body` or `document.documentElement` is the scrolled element, so reading + // `scrollTop` and `scrollLeft` is inconsistent. However, using the bounding rect of + // `document.documentElement` works consistently, where the `top` and `left` values will + // equal negative the scroll position. + const document = this._getDocument(); + const window = this._getWindow(); + const documentElement = document.documentElement!; + const documentRect = documentElement.getBoundingClientRect(); + + const top = + -documentRect.top || + document.body.scrollTop || + window.scrollY || + documentElement.scrollTop || + 0; + + const left = + -documentRect.left || + document.body.scrollLeft || + window.scrollX || + documentElement.scrollLeft || + 0; + + return { top, left }; + } + + /** + * Returns a stream that emits whenever the size of the viewport changes. + * @param throttleTime Time in milliseconds to throttle the stream. + */ + change(throttleTime: number = DEFAULT_RESIZE_TIME): Observable { + return throttleTime > 0 + ? this._change.pipe(auditTime(throttleTime)) + : this._change; + } + + /** Access injected document if available or fallback to global document reference */ + private _getDocument(): Document { + return this._document || document; + } + + /** Use defaultView of injected document if available or fallback to global window reference */ + private _getWindow(): Window { + const doc = this._getDocument(); + return doc.defaultView || window; + } + + /** Updates the cached viewport size. */ + private _updateViewportSize() { + const window = this._getWindow(); + this._viewportSize = this._platform.isBrowser + ? { width: window.innerWidth, height: window.innerHeight } + : { width: 0, height: 0 }; + } +} diff --git a/projects/ui/src/lib/scrolling/virtual-for-of.ts b/projects/ui/src/lib/scrolling/virtual-for-of.ts new file mode 100644 index 0000000..94e82b7 --- /dev/null +++ b/projects/ui/src/lib/scrolling/virtual-for-of.ts @@ -0,0 +1,523 @@ +import { + Observable, + Subject, + of as observableOf, + isObservable, + BehaviorSubject +} from 'rxjs'; +import { + pairwise, + shareReplay, + startWith, + switchMap, + takeUntil +} from 'rxjs/operators'; + +import { + Directive, + DoCheck, + EmbeddedViewRef, + Input, + IterableChangeRecord, + IterableChanges, + IterableDiffer, + IterableDiffers, + NgIterable, + NgZone, + OnDestroy, + SkipSelf, + TemplateRef, + TrackByFunction, + ViewContainerRef, + OnInit, + AfterViewChecked, + AfterViewInit, + ViewRef +} from '@angular/core'; + +import { + ArrayDataSource, + CollectionViewer, + DataSource, + ListRange, + isDataSource +} from '@angular/cdk/collections'; + +import { VirtualScrollViewportComponent } from './virtual-scroll-viewport'; +import { + VirtualScrollKeyOfFunction, + VirtualScrollMeasureSize, + VirtualScrollKeyType +} from './virtual-scroll-strategy'; + +/** The context for an item rendered by `VirtualForOf` */ +export type VirtualForOfContext = { + /** The item value. */ + $implicit: T; + /** The DataSource, Observable, or NgIterable that was passed to *ucapVirtualFor. */ + virtualForOf: DataSource | Observable | NgIterable; + /** The index of the item in the DataSource. */ + index: number; + /** The number of items in the DataSource. */ + count: number; + /** Whether this is the first item in the DataSource. */ + first: boolean; + /** Whether this is the last item in the DataSource. */ + last: boolean; + /** Whether the index is even. */ + even: boolean; + /** Whether the index is odd. */ + odd: boolean; +}; + +/** Helper to extract size from a DOM Node. */ +function getSize(orientation: 'horizontal' | 'vertical', node: Node): number { + const el = node as Element; + if (!el.getBoundingClientRect) { + return 0; + } + const rect = el.getBoundingClientRect(); + return orientation === 'horizontal' ? rect.width : rect.height; +} + +/** + * A directive similar to `ngForOf` to be used for rendering data inside a virtual scrolling + * container. + */ +@Directive({ + selector: '[ucapVirtualFor][ucapVirtualForOf]' +}) +export class VirtualForOfDirective + implements CollectionViewer, DoCheck, OnDestroy { + /** Emits when the rendered view of the data changes. */ + viewChange = new Subject(); + + /** Subject that emits when a new DataSource instance is given. */ + private _dataSourceChanges = new Subject>(); + + /** The DataSource to display. */ + @Input() + get ucapVirtualForOf(): + | DataSource + | Observable + | NgIterable + | null + | undefined { + return this._virtualForOf; + } + set ucapVirtualForOf( + value: DataSource | Observable | NgIterable | null | undefined + ) { + this._virtualForOf = value; + if (isDataSource(value)) { + this._dataSourceChanges.next(value); + } else { + // Slice the value if its an NgIterable to ensure we're working with an array. + this._dataSourceChanges.next( + new ArrayDataSource( + isObservable(value) ? value : Array.prototype.slice.call(value || []) + ) + ); + } + } + _virtualForOf: + | DataSource + | Observable + | NgIterable + | null + | undefined; + + /** + * The `TrackByFunction` to use for tracking changes. The `TrackByFunction` takes the index and + * the item and produces a value to be used as the item's identity when tracking changes. + */ + @Input() + get ucapVirtualForTrackBy(): TrackByFunction | undefined { + return this._virtualForTrackBy; + } + set ucapVirtualForTrackBy(fn: TrackByFunction | undefined) { + this._needsUpdate = true; + this._virtualForTrackBy = fn + ? (index, item) => + fn( + index + (this._renderedRange ? this._renderedRange.start : 0), + item + ) + : undefined; + } + private _virtualForTrackBy: TrackByFunction | undefined; + + /** The template used to stamp out new elements. */ + @Input() + set ucapVirtualForTemplate(value: TemplateRef>) { + if (value) { + this._needsUpdate = true; + this._template = value; + } + } + + /** + * The size of the cache used to store templates that are not being used for re-use later. + * Setting the cache size to `0` will disable caching. Defaults to 20 templates. + */ + @Input() + ucapVirtualForTemplateCacheSize = 20; + + @Input() + ucapVirtualForKeyOf: VirtualScrollKeyOfFunction; + + /** Emits whenever the data in the current DataSource changes. */ + dataStream: Observable> = this._dataSourceChanges.pipe( + // Start off with null `DataSource`. + startWith(null!), + // Bundle up the previous and current data sources so we can work with both. + pairwise(), + // Use `_changeDataSource` to disconnect from the previous data source and connect to the + // new one, passing back a stream of data changes which we run through `switchMap` to give + // us a data stream that emits the latest data from whatever the current `DataSource` is. + switchMap(([prev, cur]) => this._changeDataSource(prev, cur)), + // Replay the last emitted data when someone subscribes. + shareReplay(1) + ); + + measuredSize$: BehaviorSubject< + VirtualScrollMeasureSize[] + > = new BehaviorSubject([]); + + /** The differ used to calculate changes to the data. */ + private _differ: IterableDiffer | null = null; + + /** The most recent data emitted from the DataSource. */ + private _data: T[] | ReadonlyArray; + + /** The currently rendered items. */ + private _renderedItems: T[]; + + /** The currently rendered range of indices. */ + private _renderedRange: ListRange; + + /** + * The template cache used to hold on ot template instancess that have been stamped out, but don't + * currently need to be rendered. These instances will be reused in the future rather than + * stamping out brand new ones. + */ + private _templateCache: EmbeddedViewRef>[] = []; + + /** Whether the rendered data should be updated during the next ngDoCheck cycle. */ + private _needsUpdate = false; + + private _destroyed = new Subject(); + + constructor( + /** The view container to add items to. */ + private _viewContainerRef: ViewContainerRef, + /** The template to use when stamping out new items. */ + private _template: TemplateRef>, + /** The set of available differs. */ + private _differs: IterableDiffers, + /** The virtual scrolling viewport that these items are being rendered in. */ + @SkipSelf() private _viewport: VirtualScrollViewportComponent, + private ngZone: NgZone + ) { + this.dataStream.subscribe((data) => { + this._data = data; + this._onRenderedDataChange(); + }); + this._viewport.renderedRangeStream + .pipe(takeUntil(this._destroyed)) + .subscribe((range) => { + this._renderedRange = range; + ngZone.run(() => this.viewChange.next(this._renderedRange)); + this._onRenderedDataChange(); + }); + this._viewport.attach(this); + } + + /** + * Measures the combined size (width for horizontal orientation, height for vertical) of all items + * in the specified range. Throws an error if the range includes items that are not currently + * rendered. + */ + measureRangeSize( + range: ListRange, + orientation: 'horizontal' | 'vertical' + ): number { + if (range.start >= range.end) { + return 0; + } + if ( + range.start < this._renderedRange.start || + range.end > this._renderedRange.end + ) { + throw Error(`Error: attempted to measure an item that isn't rendered.`); + } + + // The index into the list of rendered views for the first item in the range. + const renderedStartIndex = range.start - this._renderedRange.start; + // The length of the range we're measuring. + const rangeLen = range.end - range.start; + + // Loop over all root nodes for all items in the range and sum up their size. + let totalSize = 0; + let i = rangeLen; + while (i--) { + const view = this._viewContainerRef.get( + i + renderedStartIndex + ) as EmbeddedViewRef> | null; + let j = view ? view.rootNodes.length : 0; + while (j--) { + totalSize += getSize(orientation, view!.rootNodes[j]); + } + } + + return totalSize; + } + + ngDoCheck() { + if (this._differ && this._needsUpdate) { + // TODO(mmalerba): We should differentiate needs update due to scrolling and a new portion of + // this list being rendered (can use simpler algorithm) vs needs update due to data actually + // changing (need to do this diff). + const changes = this._differ.diff(this._renderedItems); + if (!changes) { + this._updateContext(); + } else { + this._applyChanges(changes); + } + this._needsUpdate = false; + } + } + + ngOnDestroy() { + this._viewport.detach(); + + this._dataSourceChanges.next(); + this._dataSourceChanges.complete(); + this.viewChange.complete(); + + this._destroyed.next(); + this._destroyed.complete(); + + for (const view of this._templateCache) { + view.destroy(); + } + } + + measureSizeOf( + viewRef: ViewRef, + orientation: 'horizontal' | 'vertical' + ): { index: number; key: VirtualScrollKeyType; size: number } { + const view = viewRef as EmbeddedViewRef> | null; + const index = view.context.index; + const key = !!this.ucapVirtualForKeyOf + ? this.ucapVirtualForKeyOf(view.context.$implicit) + : index; + + let j = view ? view.rootNodes.length : 0; + let size = 0; + while (j--) { + size += getSize(orientation, view!.rootNodes[j]); + } + + return { + index, + key, + size + }; + } + + renderBufferWith( + viewContainerRef: ViewContainerRef, + dataIndex: number, + viewIndex: number + ) { + const d = this._data[dataIndex]; + + viewContainerRef.createEmbeddedView( + this._template, + { + $implicit: d, + virtualForOf: this._virtualForOf!, + index: dataIndex, + count: this._data.length, + first: 0 === dataIndex, + last: this._data.length - 1 === dataIndex, + odd: 0 !== dataIndex % 2, + even: 0 === dataIndex % 2 + }, + viewIndex + ); + } + + /** React to scroll state changes in the viewport. */ + private _onRenderedDataChange() { + if (!this._renderedRange) { + return; + } + this._renderedItems = this._data.slice( + this._renderedRange.start, + this._renderedRange.end + ); + if (!this._differ) { + this._differ = this._differs + .find(this._renderedItems) + .create(this.ucapVirtualForTrackBy); + } + this._needsUpdate = true; + } + + /** Swap out one `DataSource` for another. */ + private _changeDataSource( + oldDs: DataSource | null, + newDs: DataSource | null + ): Observable> { + if (oldDs) { + oldDs.disconnect(this); + } + + this._needsUpdate = true; + return newDs ? newDs.connect(this) : observableOf(); + } + + /** Update the `VirtualForOfContext` for all views. */ + private _updateContext() { + const count = this._data.length; + let i = this._viewContainerRef.length; + while (i--) { + const view = this._viewContainerRef.get(i) as EmbeddedViewRef< + VirtualForOfContext + >; + view.context.index = this._renderedRange.start + i; + view.context.count = count; + this._updateComputedContextProperties(view.context); + view.detectChanges(); + } + } + + /** Apply changes to the DOM. */ + private _applyChanges(changes: IterableChanges) { + // Rearrange the views to put them in the right location. + changes.forEachOperation( + ( + record: IterableChangeRecord, + adjustedPreviousIndex: number | null, + currentIndex: number | null + ) => { + if (record.previousIndex === null) { + // Item added. + const view = this._insertViewForNewItem(currentIndex!); + view.context.$implicit = record.item; + } else if (currentIndex === null) { + // Item removed. + this._cacheView(this._detachView(adjustedPreviousIndex!)); + } else { + // Item moved. + const view = this._viewContainerRef.get( + adjustedPreviousIndex! + ) as EmbeddedViewRef>; + this._viewContainerRef.move(view, currentIndex); + view.context.$implicit = record.item; + } + } + ); + + // Update $implicit for any items that had an identity change. + changes.forEachIdentityChange((record: IterableChangeRecord) => { + const view = this._viewContainerRef.get( + record.currentIndex! + ) as EmbeddedViewRef>; + view.context.$implicit = record.item; + }); + + // Update the context variables on all items. + const count = this._data.length; + let i = this._viewContainerRef.length; + while (i--) { + const view = this._viewContainerRef.get(i) as EmbeddedViewRef< + VirtualForOfContext + >; + view.context.index = this._renderedRange.start + i; + view.context.count = count; + this._updateComputedContextProperties(view.context); + } + } + + /** Cache the given detached view. */ + private _cacheView(view: EmbeddedViewRef>) { + if (this._templateCache.length < this.ucapVirtualForTemplateCacheSize) { + this._templateCache.push(view); + } else { + const index = this._viewContainerRef.indexOf(view); + + // It's very unlikely that the index will ever be -1, but just in case, + // destroy the view on its own, otherwise destroy it through the + // container to ensure that all the references are removed. + if (index === -1) { + view.destroy(); + } else { + this._viewContainerRef.remove(index); + } + } + } + + /** Inserts a view for a new item, either from the cache or by creating a new one. */ + private _insertViewForNewItem( + index: number + ): EmbeddedViewRef> { + return ( + this._insertViewFromCache(index) || this._createEmbeddedViewAt(index) + ); + } + + /** Update the computed properties on the `VirtualForOfContext`. */ + private _updateComputedContextProperties(context: VirtualForOfContext) { + context.first = context.index === 0; + context.last = context.index === context.count - 1; + context.even = context.index % 2 === 0; + context.odd = !context.even; + } + + /** Creates a new embedded view and moves it to the given index */ + private _createEmbeddedViewAt( + index: number + ): EmbeddedViewRef> { + // Note that it's important that we insert the item directly at the proper index, + // rather than inserting it and the moving it in place, because if there's a directive + // on the same node that injects the `ViewContainerRef`, Angular will insert another + // comment node which can throw off the move when it's being repeated for all items. + return this._viewContainerRef.createEmbeddedView( + this._template, + { + $implicit: null!, + // It's guaranteed that the iterable is not "undefined" or "null" because we only + // generate views for elements if the "ucapVirtualForOf" iterable has elements. + virtualForOf: this._virtualForOf!, + index: -1, + count: -1, + first: false, + last: false, + odd: false, + even: false + }, + index + ); + } + + /** Inserts a recycled view from the cache at the given index. */ + private _insertViewFromCache( + index: number + ): EmbeddedViewRef> | null { + const cachedView = this._templateCache.pop(); + if (cachedView) { + this._viewContainerRef.insert(cachedView, index); + } + return cachedView || null; + } + + /** Detaches the embedded view at the given index. */ + private _detachView(index: number): EmbeddedViewRef> { + return this._viewContainerRef.detach(index) as EmbeddedViewRef< + VirtualForOfContext + >; + } +} diff --git a/projects/ui/src/lib/scrolling/virtual-scroll-strategy.ts b/projects/ui/src/lib/scrolling/virtual-scroll-strategy.ts new file mode 100644 index 0000000..b72a9e9 --- /dev/null +++ b/projects/ui/src/lib/scrolling/virtual-scroll-strategy.ts @@ -0,0 +1,72 @@ +import { Observable } from 'rxjs'; + +import { InjectionToken } from '@angular/core'; + +import { VirtualScrollViewportComponent } from './virtual-scroll-viewport'; + +export type VirtualScrollToAlign = + | 'auto' + | 'smart' + | 'center' + | 'start' + | 'end'; +export type VirtualScrollKeyType = string | number; +export type VirtualScrollKeyOfFunction = (item: T) => VirtualScrollKeyType; +export interface VirtualScrollMeasureSize { + index: number; + key: VirtualScrollKeyType; + size: number; +} + +/** The injection token used to specify the virtual scrolling strategy. */ +export const VIRTUAL_SCROLL_STRATEGY = new InjectionToken< + VirtualScrollStrategy +>('UCAP_VIRTUAL_SCROLL_STRATEGY'); + +/** A strategy that dictates which items should be rendered in the viewport. */ +export interface VirtualScrollStrategy { + /** Emits when the index of the first element visible in the viewport changes. */ + scrolledIndexChange: Observable; + + /** + * Attaches this scroll strategy to a viewport. + * @param viewport The viewport to attach this strategy to. + */ + attach(viewport: VirtualScrollViewportComponent): void; + + /** Detaches this scroll strategy from the currently attached viewport. */ + detach(): void; + + useMeasureSize(): boolean; + + measureSize(sizes: VirtualScrollMeasureSize[]): void; + + measurePartitionCount(): number; + + /** Called when the viewport is scrolled (debounced using requestAnimationFrame). */ + onContentScrolled(): void; + + /** Called when the length of the data changes. */ + onDataModified(): void; + + onDataChanged(): void; + + /** Called when the range of items rendered in the DOM has changed. */ + onContentRendered(): void; + + /** Called when the offset of the rendered items changed. */ + onRenderedOffsetChanged(): void; + + /** + * Scroll to the offset for the given index. + * @param index The index of the element to scroll to. + * @param behavior The ScrollBehavior to use when scrolling. + */ + scrollToIndex( + index: number, + align: VirtualScrollToAlign, + behavior: ScrollBehavior + ): void; + + offsetForIndex(index: number, align: VirtualScrollToAlign): number; +} diff --git a/projects/ui/src/lib/scrolling/virtual-scroll-viewport.html b/projects/ui/src/lib/scrolling/virtual-scroll-viewport.html new file mode 100644 index 0000000..30dcf4e --- /dev/null +++ b/projects/ui/src/lib/scrolling/virtual-scroll-viewport.html @@ -0,0 +1,21 @@ + +
    + +
    + +
    + +
    + + +
    diff --git a/projects/ui/src/lib/scrolling/virtual-scroll-viewport.scss b/projects/ui/src/lib/scrolling/virtual-scroll-viewport.scss new file mode 100644 index 0000000..d0f0658 --- /dev/null +++ b/projects/ui/src/lib/scrolling/virtual-scroll-viewport.scss @@ -0,0 +1,87 @@ +// When elements such as `` or `
  • ` are repeated inside the ucap-virtual-scroll-viewport, +// their container element (e.g. ``, `
      `, etc.) needs to be placed in the viewport as +// well. We reset some properties here to prevent these container elements from introducing +// additional space that would throw off the scrolling calculations. +@mixin _ucap-virtual-scroll-clear-container-space($direction) { + $start: if($direction == horizontal, 'left', 'top'); + $end: if($direction == horizontal, 'right', 'bottom'); + + & > dl:not([ucapVirtualFor]), + & > ol:not([ucapVirtualFor]), + & > table:not([ucapVirtualFor]), + & > ul:not([ucapVirtualFor]) { + padding: { + #{$start}: 0; + #{$end}: 0; + } + margin: { + #{$start}: 0; + #{$end}: 0; + } + border: { + #{$start}-width: 0; + #{$end}-width: 0; + } + outline: none; + } +} + +// Scrolling container. +ucap-virtual-scroll-viewport { + display: block; + position: relative; + overflow: auto; + contain: strict; + transform: translateZ(0); + will-change: scroll-position; + -webkit-overflow-scrolling: touch; +} + +// Wrapper element for the rendered content. This element will be transformed to push the rendered +// content to its correct offset in the data set as a whole. +.ucap-virtual-scroll-content-wrapper { + position: absolute; + top: 0; + left: 0; + contain: content; + + // Note: We can't put `will-change: transform;` here because it causes Safari to not update the + // viewport's `scrollHeight` when the spacer's transform changes. + + [dir='rtl'] & { + right: 0; + left: auto; + } +} + +.ucap-virtual-scroll-orientation-horizontal + .ucap-virtual-scroll-content-wrapper { + min-height: 100%; + @include _ucap-virtual-scroll-clear-container-space(horizontal); +} + +.ucap-virtual-scroll-orientation-vertical .ucap-virtual-scroll-content-wrapper { + min-width: 100%; + @include _ucap-virtual-scroll-clear-container-space(vertical); +} + +// Spacer element that whose width or height will be adjusted to match the size of the entire data +// set if it were rendered all at once. This ensures that the scrollable content region is the +// correct size. +.ucap-virtual-scroll-spacer { + position: absolute; + top: 0; + left: 0; + height: 1px; + width: 1px; + transform-origin: 0 0; + + // Note: We can't put `will-change: transform;` here because it causes Safari to not update the + // viewport's `scrollHeight` when the spacer's transform changes. + + [dir='rtl'] & { + right: 0; + left: auto; + transform-origin: 100% 0; + } +} diff --git a/projects/ui/src/lib/scrolling/virtual-scroll-viewport.ts b/projects/ui/src/lib/scrolling/virtual-scroll-viewport.ts new file mode 100644 index 0000000..b0642ad --- /dev/null +++ b/projects/ui/src/lib/scrolling/virtual-scroll-viewport.ts @@ -0,0 +1,583 @@ +import { + animationFrameScheduler, + asapScheduler, + Observable, + Subject, + Observer, + Subscription, + fromEvent +} from 'rxjs'; +import { auditTime, startWith, takeUntil, debounceTime } from 'rxjs/operators'; + +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ElementRef, + Inject, + Input, + NgZone, + OnDestroy, + OnInit, + Optional, + Output, + ViewChild, + ViewEncapsulation, + EventEmitter, + ViewContainerRef, + AfterViewChecked +} from '@angular/core'; + +import { Directionality } from '@angular/cdk/bidi'; +import { ListRange } from '@angular/cdk/collections'; + +import { ScrollDispatcher } from './scroll-dispatcher'; +import { ScrollableDirective, ExtendedScrollToOptions } from './scrollable'; +import { VirtualForOfDirective } from './virtual-for-of'; +import { + VIRTUAL_SCROLL_STRATEGY, + VirtualScrollStrategy, + VirtualScrollMeasureSize, + VirtualScrollToAlign +} from './virtual-scroll-strategy'; +import { ViewportRuler } from './viewport-ruler'; + +/** Checks if the given ranges are equal. */ +function rangesEqual(r1: ListRange, r2: ListRange): boolean { + return r1.start === r2.start && r1.end === r2.end; +} + +/** + * Scheduler to be used for scroll events. Needs to fall back to + * something that doesn't rely on requestAnimationFrame on environments + * that don't support it (e.g. server-side rendering). + */ +const SCROLL_SCHEDULER = + typeof requestAnimationFrame !== 'undefined' + ? animationFrameScheduler + : asapScheduler; + +/** A viewport that virtualizes its scrolling with the help of `CdkVirtualForOf`. */ +@Component({ + selector: 'ucap-virtual-scroll-viewport', + templateUrl: 'virtual-scroll-viewport.html', + styleUrls: ['virtual-scroll-viewport.scss'], + host: { + class: 'ucap-virtual-scroll-viewport', + '[class.ucap-virtual-scroll-orientation-horizontal]': + 'orientation === "horizontal"', + '[class.ucap-virtual-scroll-orientation-vertical]': + 'orientation !== "horizontal"' + }, + encapsulation: ViewEncapsulation.None, + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [ + { + provide: ScrollableDirective, + useExisting: VirtualScrollViewportComponent + } + ] +}) +export class VirtualScrollViewportComponent extends ScrollableDirective + implements OnInit, OnDestroy, AfterViewChecked { + /** Emits when the viewport is detached from a CdkVirtualForOf. */ + private _detachedSubject = new Subject(); + + /** Emits when the rendered range changes. */ + private _renderedRangeSubject = new Subject(); + + /** The direction the viewport scrolls. */ + @Input() + get orientation() { + return this._orientation; + } + set orientation(orientation: 'horizontal' | 'vertical') { + if (this._orientation !== orientation) { + this._orientation = orientation; + this._calculateSpacerSize(); + } + } + private _orientation: 'horizontal' | 'vertical' = 'vertical'; + + // Note: we don't use the typical EventEmitter here because we need to subscribe to the scroll + // strategy lazily (i.e. only if the user is actually listening to the events). We do this because + // depending on how the strategy calculates the scrolled index, it may come at a cost to + // performance. + /** Emits when the index of the first element visible in the viewport changes. */ + @Output() scrolledIndexChange: Observable< + number + > = new Observable((observer: Observer) => + this._scrollStrategy.scrolledIndexChange.subscribe((index) => + Promise.resolve().then(() => this.ngZone.run(() => observer.next(index))) + ) + ); + + @Output() + contentSizeChanged: EventEmitter = new EventEmitter(); + + /** The element that wraps the rendered content. */ + @ViewChild('contentWrapper', { static: true }) + _contentWrapper: ElementRef; + + @ViewChild('contentBuffer', { static: true, read: ViewContainerRef }) + _bufferViewContainerRef: ViewContainerRef; + + /** A stream that emits whenever the rendered range changes. */ + renderedRangeStream: Observable< + ListRange + > = this._renderedRangeSubject.asObservable(); + + /** + * The total size of all content (in pixels), including content that is not currently rendered. + */ + private _totalContentSize = 0; + + /** A string representing the `style.width` property value to be used for the spacer element. */ + _totalContentWidth = ''; + + /** A string representing the `style.height` property value to be used for the spacer element. */ + _totalContentHeight = ''; + + /** + * The CSS transform applied to the rendered subset of items so that they appear within the bounds + * of the visible viewport. + */ + private _renderedContentTransform: string; + + /** The currently rendered range of indices. */ + private _renderedRange: ListRange = { start: 0, end: 0 }; + + /** The data bound to this viewport. */ + private _data: any[] | readonly any[]; + + /** The size of the viewport (in pixels). */ + private _viewportSize = 0; + + /** the currently attached CdkVirtualForOf. */ + private _forOf: VirtualForOfDirective | null; + + /** The last rendered content offset that was set. */ + private _renderedContentOffset = 0; + + /** + * Whether the last rendered content offset was to the end of the content (and therefore needs to + * be rewritten as an offset to the start of the content). + */ + private _renderedContentOffsetNeedsRewrite = false; + + /** Whether there is a pending change detection cycle. */ + private _isChangeDetectionPending = false; + + /** A list of functions to run after the next change detection cycle. */ + private _runAfterChangeDetection: Function[] = []; + + /** Subscription to changes in the viewport size. */ + private _viewportChanges = Subscription.EMPTY; + + private _ngDestroySubject: Subject = new Subject(); + + private _needsUpdateForMeasureSize = false; + private _startBufferIndex = -1; + private _endBufferIndex = -1; + + constructor( + public elementRef: ElementRef, + private _changeDetectorRef: ChangeDetectorRef, + ngZone: NgZone, + @Optional() + @Inject(VIRTUAL_SCROLL_STRATEGY) + private _scrollStrategy: VirtualScrollStrategy, + @Optional() dir: Directionality, + scrollDispatcher: ScrollDispatcher, + /** + * @deprecated `viewportRuler` parameter to become required. + * @breaking-change 11.0.0 + */ + @Optional() viewportRuler?: ViewportRuler + ) { + super(elementRef, scrollDispatcher, ngZone, dir); + + if (!_scrollStrategy) { + throw Error( + 'Error: ucap-virtual-scroll-viewport requires the "itemSize" property to be set.' + ); + } + + // @breaking-change 11.0.0 Remove null check for `viewportRuler`. + if (viewportRuler) { + this._viewportChanges = viewportRuler.change().subscribe(() => { + this.checkViewportSize(); + }); + } + } + + ngOnInit() { + super.ngOnInit(); + + // It's still too early to measure the viewport at this point. Deferring with a promise allows + // the Viewport to be rendered with the correct size before we measure. We run this outside the + // zone to avoid causing more change detection cycles. We handle the change detection loop + // ourselves instead. + this.ngZone.runOutsideAngular(() => + Promise.resolve().then(() => { + this._measureViewportSize(); + this._scrollStrategy.attach(this); + + fromEvent(document.defaultView, 'resize') + .pipe(debounceTime(100), takeUntil(this._ngDestroySubject)) + .subscribe(() => { + this.checkViewportSize(); + }); + + this.elementScrolled() + .pipe( + // Start off with a fake scroll event so we properly detect our initial position. + startWith(null!), + // Collect multiple events into one until the next animation frame. This way if + // there are multiple scroll events in the same frame we only need to recheck + // our layout once. + auditTime(0, SCROLL_SCHEDULER) + ) + .subscribe(() => this._scrollStrategy.onContentScrolled()); + + this._markChangeDetectionNeeded(); + }) + ); + } + + ngOnDestroy() { + this.detach(); + this._scrollStrategy.detach(); + + // Complete all subjects + this._renderedRangeSubject.complete(); + this._detachedSubject.complete(); + this._viewportChanges.unsubscribe(); + + if (!!this._ngDestroySubject) { + this._ngDestroySubject.next(); + this._ngDestroySubject.complete(); + } + super.ngOnDestroy(); + } + + ngAfterViewChecked(): void { + if ( + this._needsUpdateForMeasureSize && + !!this._data && + 0 < this._data.length && + !!this._bufferViewContainerRef && + 0 < this._bufferViewContainerRef.length + ) { + const sizes: VirtualScrollMeasureSize[] = []; + for (let i = 0; i < this._bufferViewContainerRef.length; i++) { + const view = this._bufferViewContainerRef.get(i); + const size = this._forOf.measureSizeOf(view, this.orientation); + + sizes.push(size); + } + this._scrollStrategy.measureSize(sizes); + this._needsUpdateForMeasureSize = false; + this._bufferViewContainerRef.clear(); + + if (this._data.length - 1 === this._endBufferIndex) { + } else { + this._renderToBuffer(); + } + } + } + + /** Attaches a `CdkVirtualForOf` to this viewport. */ + attach(forOf: VirtualForOfDirective) { + if (this._forOf) { + throw Error('VirtualScrollViewport is already attached.'); + } + + // Subscribe to the data stream of the CdkVirtualForOf to keep track of when the data length + // changes. Run outside the zone to avoid triggering change detection, since we're managing the + // change detection loop ourselves. + this.ngZone.runOutsideAngular(() => { + this._forOf = forOf; + + this._forOf.dataStream + .pipe(takeUntil(this._detachedSubject)) + .subscribe((data) => { + const newLength = data.length; + if (data !== this._data) { + this._data = data; + this._onDataChange(); + this._scrollStrategy.onDataModified(); + } + this._doChangeDetection(); + }); + }); + } + + /** Detaches the current `CdkVirtualForOf`. */ + detach() { + this._forOf = null; + this._detachedSubject.next(); + } + + /** Gets the length of the data bound to this viewport (in number of items). */ + getData(): any[] | readonly any[] { + return this._data; + } + + getDataLength(): number { + return !!this._data ? this._data.length : 0; + } + + /** Gets the size of the viewport (in pixels). */ + getViewportSize(): number { + return this._viewportSize; + } + + // TODO(mmalerba): This is technically out of sync with what's really rendered until a render + // cycle happens. I'm being careful to only call it after the render cycle is complete and before + // setting it to something else, but its error prone and should probably be split into + // `pendingRange` and `renderedRange`, the latter reflecting whats actually in the DOM. + + /** Get the current rendered range of items. */ + getRenderedRange(): ListRange { + return this._renderedRange; + } + + /** + * Sets the total size of all content (in pixels), including content that is not currently + * rendered. + */ + setTotalContentSize(size: number) { + if (this._totalContentSize !== size) { + this._totalContentSize = size; + this._calculateSpacerSize(); + this.contentSizeChanged.emit(size); + this._markChangeDetectionNeeded(); + } + } + + /** Sets the currently rendered range of indices. */ + setRenderedRange(range: ListRange) { + if (!rangesEqual(this._renderedRange, range)) { + this._renderedRangeSubject.next((this._renderedRange = range)); + this._markChangeDetectionNeeded(() => + this._scrollStrategy.onContentRendered() + ); + } + } + + /** + * Gets the offset from the start of the viewport to the start of the rendered data (in pixels). + */ + getOffsetToRenderedContentStart(): number | null { + return this._renderedContentOffsetNeedsRewrite + ? null + : this._renderedContentOffset; + } + + /** + * Sets the offset from the start of the viewport to either the start or end of the rendered data + * (in pixels). + */ + setRenderedContentOffset( + offset: number, + to: 'to-start' | 'to-end' = 'to-start' + ) { + // For a horizontal viewport in a right-to-left language we need to translate along the x-axis + // in the negative direction. + const isRtl = this.dir && this.dir.value === 'rtl'; + const isHorizontal = this.orientation === 'horizontal'; + const axis = isHorizontal ? 'X' : 'Y'; + const axisDirection = isHorizontal && isRtl ? -1 : 1; + let transform = `translate${axis}(${Number(axisDirection * offset)}px)`; + this._renderedContentOffset = offset; + if (to === 'to-end') { + transform += ` translate${axis}(-100%)`; + // The viewport should rewrite this as a `to-start` offset on the next render cycle. Otherwise + // elements will appear to expand in the wrong direction (e.g. `mat-expansion-panel` would + // expand upward). + this._renderedContentOffsetNeedsRewrite = true; + } + if (this._renderedContentTransform !== transform) { + // We know this value is safe because we parse `offset` with `Number()` before passing it + // into the string. + this._renderedContentTransform = transform; + this._markChangeDetectionNeeded(() => { + if (this._renderedContentOffsetNeedsRewrite) { + this._renderedContentOffset -= this.measureRenderedContentSize(); + this._renderedContentOffsetNeedsRewrite = false; + this.setRenderedContentOffset(this._renderedContentOffset); + } else { + this._scrollStrategy.onRenderedOffsetChanged(); + } + }); + } + } + + /** + * Scrolls to the given offset from the start of the viewport. Please note that this is not always + * the same as setting `scrollTop` or `scrollLeft`. In a horizontal viewport with right-to-left + * direction, this would be the equivalent of setting a fictional `scrollRight` property. + * @param offset The offset to scroll to. + * @param behavior The ScrollBehavior to use when scrolling. Default is behavior is `auto`. + */ + scrollToOffset(offset: number, behavior: ScrollBehavior = 'auto') { + let options: ExtendedScrollToOptions = { behavior }; + if (this.orientation === 'horizontal') { + options = { ...options, start: offset } as any; + } else { + options = { ...options, top: offset } as any; + } + this.scrollTo(options); + } + + /** + * Scrolls to the offset for the given index. + * @param index The index of the element to scroll to. + * @param behavior The ScrollBehavior to use when scrolling. Default is behavior is `auto`. + */ + scrollToIndex( + index: number, + align: VirtualScrollToAlign = 'center', + behavior: ScrollBehavior = 'auto' + ) { + this._scrollStrategy.scrollToIndex(index, align, behavior); + } + + offsetForIndex( + index: number, + align: VirtualScrollToAlign = 'center' + ): number { + return this._scrollStrategy.offsetForIndex(index, align); + } + + /** + * Gets the current scroll offset from the start of the viewport (in pixels). + * @param from The edge to measure the offset from. Defaults to 'top' in vertical mode and 'start' + * in horizontal mode. + */ + measureScrollOffset( + from?: 'top' | 'left' | 'right' | 'bottom' | 'start' | 'end' + ): number { + return from + ? super.measureScrollOffset(from) + : super.measureScrollOffset( + this.orientation === 'horizontal' ? 'start' : 'top' + ); + } + + /** Measure the combined size of all of the rendered items. */ + measureRenderedContentSize(): number { + const contentEl = this._contentWrapper.nativeElement; + return this.orientation === 'horizontal' + ? contentEl.offsetWidth + : contentEl.offsetHeight; + } + + /** + * Measure the total combined size of the given range. Throws if the range includes items that are + * not rendered. + */ + measureRangeSize(range: ListRange): number { + if (!this._forOf) { + return 0; + } + return this._forOf.measureRangeSize(range, this.orientation); + } + + /** Update the viewport dimensions and re-render. */ + checkViewportSize() { + // TODO: Cleanup later when add logic for handling content resize + this._measureViewportSize(); + this._scrollStrategy.onDataModified(); + } + + /** Measure the viewport size. */ + private _measureViewportSize() { + const viewportEl = this.elementRef.nativeElement; + this._viewportSize = + this.orientation === 'horizontal' + ? viewportEl.clientWidth + : viewportEl.clientHeight; + } + + /** Queue up change detection to run. */ + private _markChangeDetectionNeeded(runAfter?: Function) { + if (runAfter) { + this._runAfterChangeDetection.push(runAfter); + } + + // Use a Promise to batch together calls to `_doChangeDetection`. This way if we set a bunch of + // properties sequentially we only have to run `_doChangeDetection` once at the end. + if (!this._isChangeDetectionPending) { + this._isChangeDetectionPending = true; + this.ngZone.runOutsideAngular(() => + Promise.resolve().then(() => { + this._doChangeDetection(); + }) + ); + } + } + + /** Run change detection. */ + private _doChangeDetection() { + this._isChangeDetectionPending = false; + // Apply the content transform. The transform can't be set via an Angular binding because + // bypassSecurityTrustStyle is banned in Google. However the value is safe, it's composed of + // string literals, a variable that can only be 'X' or 'Y', and user input that is run through + // the `Number` function first to coerce it to a numeric value. + this._contentWrapper.nativeElement.style.transform = this._renderedContentTransform; + // Apply changes to Angular bindings. Note: We must call `markForCheck` to run change detection + // from the root, since the repeated items are content projected in. Calling `detectChanges` + // instead does not properly check the projected content. + this.ngZone.run(() => this._changeDetectorRef.markForCheck()); + + const runAfterChangeDetection = this._runAfterChangeDetection; + this._runAfterChangeDetection = []; + for (const fn of runAfterChangeDetection) { + fn(); + } + } + + /** Calculates the `style.width` and `style.height` for the spacer element. */ + private _calculateSpacerSize() { + this._totalContentHeight = + this.orientation === 'horizontal' ? '' : `${this._totalContentSize}px`; + this._totalContentWidth = + this.orientation === 'horizontal' ? `${this._totalContentSize}px` : ''; + } + + private _onDataChange() { + if (this._scrollStrategy.useMeasureSize()) { + this._renderToBuffer(); + } + } + + private _renderToBuffer() { + if ( + !!this._data && + 0 < this._data.length && + this._data.length - 1 > this._endBufferIndex && + !!this._bufferViewContainerRef + ) { + const startIndex = ++this._endBufferIndex; + const lastIndex = Math.min( + startIndex + this._scrollStrategy.measurePartitionCount() - 1, + this._data.length - 1 + ); + + for (let i = startIndex; i <= lastIndex; i++) { + this._forOf.renderBufferWith( + this._bufferViewContainerRef, + i, + i - startIndex + ); + } + + this._startBufferIndex = startIndex; + this._endBufferIndex = lastIndex; + + this._needsUpdateForMeasureSize = true; + } + } +} diff --git a/projects/ui/src/lib/services/clipboard.service.ts b/projects/ui/src/lib/services/clipboard.service.ts new file mode 100644 index 0000000..7377b95 --- /dev/null +++ b/projects/ui/src/lib/services/clipboard.service.ts @@ -0,0 +1,151 @@ +import { DOCUMENT } from '@angular/common'; +import { Inject, Injectable } from '@angular/core'; + +@Injectable({ providedIn: 'root' }) +export class ClipboardService { + private tempTextArea: HTMLTextAreaElement | undefined; + private window: any; + + constructor(@Inject(DOCUMENT) public document: any) { + this.window = window; + } + + public get isSupported(): boolean { + return ( + !!this.document.queryCommandSupported && + !!this.document.queryCommandSupported('copy') && + !!this.window + ); + } + + public isTargetValid( + element: HTMLInputElement | HTMLTextAreaElement + ): boolean { + if ( + element instanceof HTMLInputElement || + element instanceof HTMLTextAreaElement + ) { + if (element.hasAttribute('disabled')) { + throw new Error( + 'Invalid "target" attribute. Please use "readonly" instead of "disabled" attribute' + ); + } + return true; + } + throw new Error('Target should be input or textarea'); + } + + /** + * copyFromInputElement + */ + public copyFromInputElement( + targetElm: HTMLInputElement | HTMLTextAreaElement + ): boolean { + try { + this.selectTarget(targetElm); + const re = this.copyText(); + this.clearSelection(targetElm, this.window); + return re && this.isCopySuccessInIE11(); + } catch (error) { + return false; + } + } + + // this is for IE11 return true even if copy fail + isCopySuccessInIE11() { + // tslint:disable-next-line: no-string-literal + const clipboardData = this.window['clipboardData']; + if (clipboardData && clipboardData.getData) { + if (!clipboardData.getData('Text')) { + return false; + } + } + return true; + } + + /** + * Creates a fake textarea element, sets its value from `text` property, + * and makes a selection on it. + */ + public copyFromContent( + content: string, + container: HTMLElement = this.document.body + ) { + // check if the temp textarea still belongs to the current container. + // In case we have multiple places using ngx-clipboard, one is in a modal using container but the other one is not. + if (this.tempTextArea && !container.contains(this.tempTextArea)) { + this.destroy(this.tempTextArea.parentElement); + } + + if (!this.tempTextArea) { + this.tempTextArea = this.createTempTextArea(this.document, this.window); + try { + container.appendChild(this.tempTextArea); + } catch (error) { + throw new Error('Container should be a Dom element'); + } + } + this.tempTextArea.value = content; + + const toReturn = this.copyFromInputElement(this.tempTextArea); + + this.destroy(this.tempTextArea.parentElement); + + return toReturn; + } + + // remove temporary textarea if any + public destroy(container: HTMLElement = this.document.body) { + if (this.tempTextArea) { + container.removeChild(this.tempTextArea); + // removeChild doesn't remove the reference from memory + this.tempTextArea = undefined; + } + } + + // select the target html input element + private selectTarget( + inputElement: HTMLInputElement | HTMLTextAreaElement + ): number | undefined { + inputElement.select(); + inputElement.setSelectionRange(0, inputElement.value.length); + return inputElement.value.length; + } + + private copyText(): boolean { + return this.document.execCommand('copy'); + } + // Moves focus away from `target` and back to the trigger, removes current selection. + private clearSelection( + inputElement: HTMLInputElement | HTMLTextAreaElement, + window: Window + ) { + // tslint:disable-next-line:no-unused-expression + inputElement && inputElement.focus(); + window.getSelection().removeAllRanges(); + } + + // create a fake textarea for copy command + private createTempTextArea( + doc: Document, + window: Window + ): HTMLTextAreaElement { + const isRTL = doc.documentElement.getAttribute('dir') === 'rtl'; + let ta: HTMLTextAreaElement; + ta = doc.createElement('textarea'); + // Prevent zooming on iOS + ta.style.fontSize = '12pt'; + // Reset box model + ta.style.border = '0'; + ta.style.padding = '0'; + ta.style.margin = '0'; + // Move element out of screen horizontally + ta.style.position = 'absolute'; + ta.style[isRTL ? 'right' : 'left'] = '-9999px'; + // Move element to the same position vertically + const yPosition = window.pageYOffset || doc.documentElement.scrollTop; + ta.style.top = yPosition + 'px'; + ta.setAttribute('readonly', ''); + return ta; + } +} diff --git a/projects/ui/src/lib/services/dialog.service.ts b/projects/ui/src/lib/services/dialog.service.ts deleted file mode 100644 index 66a049c..0000000 --- a/projects/ui/src/lib/services/dialog.service.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { TemplateRef, Injectable } from '@angular/core'; -import { ComponentType } from '@angular/cdk/portal'; -import { - MatDialog, - MatDialogConfig, - MatDialogRef -} from '@angular/material/dialog'; - -import { of } from 'rxjs'; -import { take, map, catchError } from 'rxjs/operators'; - -@Injectable({ - providedIn: 'root' -}) -export class DialogService { - constructor(private matDialog: MatDialog) {} - - public open( - componentOrTemplateRef: ComponentType | TemplateRef, - config?: MatDialogConfig - ): Promise { - return new Promise((resolve, reject) => { - config = { ...config, autoFocus: false }; - - const dialogRef = this.matDialog.open( - componentOrTemplateRef, - config - ); - - dialogRef - .afterClosed() - .pipe( - take(1), - map(result => { - return resolve(result); - }), - catchError(err => { - return of(reject(err)); - }) - ) - .subscribe(); - }); - } - - getDialogById(id: string): MatDialogRef { - return this.matDialog.getDialogById(id); - } - - closeAll() { - this.matDialog.closeAll(); - } -} diff --git a/projects/ui/src/lib/services/dom.service.ts b/projects/ui/src/lib/services/dom.service.ts new file mode 100644 index 0000000..16f9539 --- /dev/null +++ b/projects/ui/src/lib/services/dom.service.ts @@ -0,0 +1,148 @@ +import { Subject, fromEventPattern, Observable } from 'rxjs'; +import { takeUntil, share } from 'rxjs/operators'; + +import { + Injectable, + OnDestroy, + RendererFactory2, + Renderer2 +} from '@angular/core'; + +@Injectable({ providedIn: 'root' }) +export class DomService implements OnDestroy { + readonly window: Window; + + // tslint:disable-next-line: variable-name + private readonly _renderer2: Renderer2; + + // tslint:disable-next-line: variable-name + private readonly _handlerMap: Map< + any, + Map< + string, + { destroySubject: Subject; $: Observable; refCount: number } + > + >; + + constructor(private rendererFactory2: RendererFactory2) { + this.window = window; + this._renderer2 = this.rendererFactory2.createRenderer(null, null); + this._handlerMap = new Map(); + } + + ngOnDestroy(): void { + this._handlerMap.forEach((handlers) => { + handlers.forEach((handler) => { + if (!!handler.destroySubject) { + handler.destroySubject.next(); + handler.destroySubject.complete(); + } + }); + handlers.clear(); + }); + + this._handlerMap.clear(); + } + + attach(eventName: string, toTarget: any = this.window): Observable { + // tslint:disable-next-line: variable-name + const _target = !!toTarget ? toTarget : this.window; + + const handler = this._getHandler(eventName, _target); + if (!!handler) { + handler.refCount++; + return handler.$; + } + + let removeHandler: () => void; + const addHandler = (callback: (e: Event) => boolean | void) => { + removeHandler = this._renderer2.listen(_target, eventName, callback); + }; + + const destroySubject: Subject = new Subject(); + const $ = fromEventPattern(addHandler, () => removeHandler()).pipe( + takeUntil(destroySubject), + share() + ); + + this._setHandler(eventName, _target, { destroySubject, $, refCount: 1 }); + + return $; + } + + detach(eventName: string, fromTarget: any = this.window) { + // tslint:disable-next-line: variable-name + const _target = !!fromTarget ? fromTarget : this.window; + + const handler = this._getHandler(eventName, _target); + if (!handler) { + return; + } + + handler.refCount--; + + if (0 < handler.refCount) { + return; + } + + this._removeHandler(eventName, _target); + } + + private _getHandler( + eventName: string, + fromTarget: any + ): + | { destroySubject: Subject; $: Observable; refCount: number } + | undefined { + const eventHandler = this._handlerMap.get(fromTarget); + if (!eventHandler) { + return undefined; + } + + return eventHandler.get(eventName); + } + + private _setHandler( + eventName: string, + toTarget: any, + handler: { + destroySubject: Subject; + $: Observable; + refCount: number; + } + ) { + let eventHandler = this._handlerMap.get(toTarget); + if (!eventHandler) { + eventHandler = new Map(); + this._handlerMap.set(toTarget, eventHandler); + } + + if (eventHandler.has(eventName)) { + throw new Error('Event handler exist already'); + } + + eventHandler.set(eventName, handler); + } + + private _removeHandler(eventName: string, fromTarget: any) { + const eventHandler = this._handlerMap.get(fromTarget); + if (!eventHandler) { + return; + } + + const handler = eventHandler.get(eventName); + if (!handler) { + return; + } + + if (!!handler.destroySubject) { + handler.destroySubject.next(); + handler.destroySubject.complete(); + } + eventHandler.delete(eventName); + + if (0 === eventHandler.size) { + this._handlerMap.delete(fromTarget); + } + } +} diff --git a/projects/ui/src/lib/ui.module.ts b/projects/ui/src/lib/ui.module.ts index a7a322a..f24374b 100644 --- a/projects/ui/src/lib/ui.module.ts +++ b/projects/ui/src/lib/ui.module.ts @@ -3,6 +3,9 @@ import { CommonModule } from '@angular/common'; import { FlexLayoutModule } from '@angular/flex-layout'; +import { BidiModule } from '@angular/cdk/bidi'; +import { PlatformModule } from '@angular/cdk/platform'; + import { MatButtonModule } from '@angular/material/button'; import { MatButtonToggleModule } from '@angular/material/button-toggle'; import { MatCardModule } from '@angular/material/card'; @@ -10,6 +13,7 @@ import { MatDatepickerModule } from '@angular/material/datepicker'; import { MatIconModule } from '@angular/material/icon'; import { MatMenuModule } from '@angular/material/menu'; import { MatProgressBarModule } from '@angular/material/progress-bar'; +import { MatRippleModule } from '@angular/material/core'; import { MatSliderModule } from '@angular/material/slider'; import { MatToolbarModule } from '@angular/material/toolbar'; import { MatTooltipModule } from '@angular/material/tooltip'; @@ -26,6 +30,9 @@ import { ImageViewerComponent } from './components/file-viewer/image-viewer.comp import { SoundViewerComponent } from './components/file-viewer/sound-viewer.component'; import { VideoViewerComponent } from './components/file-viewer/video-viewer.component'; import { FileViewerComponent } from './components/file-viewer.component'; +import { MediaViewerComponent } from './components/media-viewer.component'; + +import { BreadcrumbComponent } from './components/breadcrumb.component'; import { FloatActionButtonComponent } from './components/float-action-button.component'; import { InlineEditInputComponent } from './components/inline-edit-input.component'; import { PickDateComponent } from './components/pick-date.component'; @@ -37,11 +44,19 @@ import { TitleBarComponent } from './components/title-bar.component'; import { AlertDialogComponent } from './dialogs/alert.dialog.component'; import { ConfirmDialogComponent } from './dialogs/confirm.dialog.component'; -import { CdkVirtualScrollViewportPatchDirective } from './directives/cdk-virtual-scroll-viewport-patch.directive'; import { ClickDebounceDirective } from './directives/click-debounce.directive'; import { ClickOutsideDirective } from './directives/click-outside.directive'; import { FileUploadForDirective } from './directives/file-upload-for.directive'; import { ImageDirective } from './directives/image.directive'; +import { VarDirective } from './directives/var.directive'; +import { BreadcrumbNodeDirective } from './components/breadcrumb.component'; + +import { VirtualScrollViewportComponent } from './scrolling/virtual-scroll-viewport'; +import { ScrollableDirective } from './scrolling/scrollable'; +import { VirtualForOfDirective } from './scrolling/virtual-for-of'; +import { AutoSizeVirtualScrollDirective } from './scrolling/auto-size-virtual-scroll'; +import { FixedSizeVirtualScrollDirective } from './scrolling/fixed-size-virtual-scroll'; +import { MeasureSizeVirtualScrollDirective } from './scrolling/measure-size-virtual-scroll'; import { BytesPipe } from './pipes/bytes.pipe'; import { DatePipe } from './pipes/date.pipe'; @@ -53,8 +68,11 @@ import { SafeHtmlPipe } from './pipes/safe-html.pipe'; import { SecondsToMinutesPipe } from './pipes/seconds-to-minutes.pipe'; import { DateService } from './services/date.service'; +import { ClipboardService } from './services/clipboard.service'; +import { DomService } from './services/dom.service'; const COMPONENTS = [ + BreadcrumbComponent, FileUploadQueueComponent, BinaryViewerComponent, @@ -63,6 +81,7 @@ const COMPONENTS = [ SoundViewerComponent, VideoViewerComponent, FileViewerComponent, + MediaViewerComponent, FloatActionButtonComponent, @@ -72,7 +91,9 @@ const COMPONENTS = [ PickTimeComponent, SplitButtonComponent, StepInputComponent, - TitleBarComponent + TitleBarComponent, + + VirtualScrollViewportComponent ]; const DIALOGS = [AlertDialogComponent, ConfirmDialogComponent]; const PIPES = [ @@ -87,13 +108,22 @@ const PIPES = [ SecondsToMinutesPipe ]; const DIRECTIVES = [ - CdkVirtualScrollViewportPatchDirective, + BreadcrumbNodeDirective, + ClickDebounceDirective, ClickOutsideDirective, FileUploadForDirective, - ImageDirective + ImageDirective, + VarDirective, + + ScrollableDirective, + VirtualForOfDirective, + + AutoSizeVirtualScrollDirective, + FixedSizeVirtualScrollDirective, + MeasureSizeVirtualScrollDirective ]; -const SERVICES = [DateService]; +const SERVICES = [DateService, ClipboardService, DomService]; @NgModule({ declarations: [], @@ -108,6 +138,9 @@ export class UiRootModule {} FlexLayoutModule, + BidiModule, + PlatformModule, + MatButtonModule, MatButtonToggleModule, MatCardModule, @@ -115,13 +148,14 @@ export class UiRootModule {} MatIconModule, MatMenuModule, MatProgressBarModule, + MatRippleModule, MatSliderModule, MatToolbarModule, MatTooltipModule, I18nModule ], - exports: [...COMPONENTS, ...DIRECTIVES, ...PIPES], + exports: [...COMPONENTS, ...DIRECTIVES, ...PIPES, BidiModule], declarations: [...COMPONENTS, ...DIALOGS, ...DIRECTIVES, ...PIPES], entryComponents: [...DIALOGS] }) diff --git a/projects/ui/src/public-api.ts b/projects/ui/src/public-api.ts index 3007522..aca185b 100644 --- a/projects/ui/src/public-api.ts +++ b/projects/ui/src/public-api.ts @@ -2,37 +2,42 @@ * Public API Surface of ui */ -export * from './lib/config/module-config'; - export * from './lib/animations'; -export * from './lib/components/file-upload-queue.component'; - export * from './lib/components/file-viewer/binary-viewer.component'; export * from './lib/components/file-viewer/document-viewer.component'; export * from './lib/components/file-viewer/image-viewer.component'; export * from './lib/components/file-viewer/sound-viewer.component'; export * from './lib/components/file-viewer/video-viewer.component'; +export * from './lib/components/breadcrumb.component'; +export * from './lib/components/file-upload-queue.component'; export * from './lib/components/file-viewer.component'; - export * from './lib/components/float-action-button.component'; export * from './lib/components/inline-edit-input.component'; +export * from './lib/components/media-viewer.component'; export * from './lib/components/pick-date.component'; export * from './lib/components/pick-time.component'; export * from './lib/components/split-button.component'; export * from './lib/components/step-input.component'; +export * from './lib/components/title-bar.component'; + +export * from './lib/config/module-config'; +export * from './lib/config/token'; export * from './lib/data-source/virtual-scroll-tree-flat.data-source'; export * from './lib/dialogs/alert.dialog.component'; export * from './lib/dialogs/confirm.dialog.component'; -export * from './lib/directives/cdk-virtual-scroll-viewport-patch.directive'; export * from './lib/directives/click-debounce.directive'; export * from './lib/directives/click-outside.directive'; export * from './lib/directives/file-upload-for.directive'; export * from './lib/directives/image.directive'; +export * from './lib/directives/paginated-header'; +export * from './lib/directives/var.directive'; + +export * from './lib/models/select-file-info'; export * from './lib/pipes/bytes.pipe'; export * from './lib/pipes/date.pipe'; @@ -43,6 +48,18 @@ export * from './lib/pipes/phone-number.pipe'; export * from './lib/pipes/safe-html.pipe'; export * from './lib/pipes/seconds-to-minutes.pipe'; +export * from './lib/scrolling/auto-size-virtual-scroll'; +export * from './lib/scrolling/fixed-size-virtual-scroll'; +export * from './lib/scrolling/measure-size-virtual-scroll'; +export * from './lib/scrolling/scroll-dispatcher'; +export * from './lib/scrolling/scrollable'; +export * from './lib/scrolling/viewport-ruler'; +export * from './lib/scrolling/virtual-for-of'; +export * from './lib/scrolling/virtual-scroll-strategy'; +export * from './lib/scrolling/virtual-scroll-viewport'; + export * from './lib/services/date.service'; +export * from './lib/services/clipboard.service'; +export * from './lib/services/dom.service'; export * from './lib/ui.module';