From a935fb413b78314e528ad53426c23fc7a64b9a35 Mon Sep 17 00:00:00 2001 From: Dan Percic Date: Sun, 22 Aug 2021 19:04:29 +0300 Subject: [PATCH] add help mode --- .eslintrc.json | 1 + src/assets/styles/_dialogs.scss | 40 ++++++++ src/assets/styles/_texts.scss | 12 +++ src/assets/styles/common.scss | 1 + src/index.ts | 6 ++ src/lib/common-ui.module.ts | 12 ++- .../help-mode-dialog.component.html | 8 ++ .../help-mode-dialog.component.scss | 16 ++++ .../help-mode-dialog.component.ts | 7 ++ src/lib/help-mode/help-mode.directive.ts | 34 +++++++ src/lib/help-mode/help-mode.service.ts | 91 +++++++++++++++++++ .../help-mode/help-mode.component.html | 20 ++++ .../help-mode/help-mode.component.scss | 72 +++++++++++++++ .../help-mode/help-mode.component.ts | 29 ++++++ .../input-with-action.component.ts | 2 +- src/lib/utils/injection-tokens.ts | 3 + src/lib/utils/types/events.type.ts | 3 + 17 files changed, 353 insertions(+), 4 deletions(-) create mode 100644 src/assets/styles/_dialogs.scss create mode 100644 src/lib/help-mode/help-mode-dialog/help-mode-dialog.component.html create mode 100644 src/lib/help-mode/help-mode-dialog/help-mode-dialog.component.scss create mode 100644 src/lib/help-mode/help-mode-dialog/help-mode-dialog.component.ts create mode 100644 src/lib/help-mode/help-mode.directive.ts create mode 100644 src/lib/help-mode/help-mode.service.ts create mode 100644 src/lib/help-mode/help-mode/help-mode.component.html create mode 100644 src/lib/help-mode/help-mode/help-mode.component.scss create mode 100644 src/lib/help-mode/help-mode/help-mode.component.ts create mode 100644 src/lib/utils/injection-tokens.ts create mode 100644 src/lib/utils/types/events.type.ts diff --git a/.eslintrc.json b/.eslintrc.json index 6b93cff..f4c2934 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -44,6 +44,7 @@ "@angular-eslint/no-output-rename": "error", "@angular-eslint/prefer-output-readonly": "error", "@typescript-eslint/unbound-method": "error", + "@typescript-eslint/no-floating-promises": "off", "@typescript-eslint/lines-between-class-members": "off", "@typescript-eslint/naming-convention": [ "error", diff --git a/src/assets/styles/_dialogs.scss b/src/assets/styles/_dialogs.scss new file mode 100644 index 0000000..5a68806 --- /dev/null +++ b/src/assets/styles/_dialogs.scss @@ -0,0 +1,40 @@ +@import 'apps/red-ui/src/assets/styles/variables'; + +.mat-dialog-container { + color: $accent; + padding: 0 !important; + border-radius: 8px !important; +} + +.dialog { + position: relative; + min-height: 80px; + + .dialog-close { + position: absolute; + top: 16px; + right: 16px; + } + + .dialog-header { + padding: 32px 60px 0 32px; + } + + .dialog-content { + padding: 24px 32px 40px; + } + + .dialog-actions { + height: 81px; + box-sizing: border-box; + border-top: 1px solid $separator; + padding: 0 32px; + align-items: center; + + display: flex; + + > * { + margin-right: 16px; + } + } +} diff --git a/src/assets/styles/_texts.scss b/src/assets/styles/_texts.scss index 4455d95..bc2aa1c 100644 --- a/src/assets/styles/_texts.scss +++ b/src/assets/styles/_texts.scss @@ -27,3 +27,15 @@ opacity: 1; } } + +.heading { + font-size: 16px; + line-height: 20px; + font-weight: 600; +} + +.heading-l { + font-size: 20px; + font-weight: 600; + line-height: 24px; +} diff --git a/src/assets/styles/common.scss b/src/assets/styles/common.scss index c241880..b4cc8b0 100644 --- a/src/assets/styles/common.scss +++ b/src/assets/styles/common.scss @@ -3,3 +3,4 @@ @import 'texts'; @import 'tables'; @import 'layout'; +@import 'dialogs'; diff --git a/src/index.ts b/src/index.ts index b972240..02f1a25 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,10 +1,12 @@ export * from './lib/common-ui.module'; export * from './lib/buttons/icon-button/icon-button.type'; export * from './lib/buttons/icon-button/icon-button.component'; +export * from './lib/utils/injection-tokens'; export * from './lib/utils/functions'; export * from './lib/utils/operators'; export * from './lib/utils/auto-unsubscribe.directive'; export * from './lib/utils/pipes/humanize.pipe'; +export * from './lib/utils/types/events.type'; export * from './lib/utils/types/utility-types'; export * from './lib/utils/types/tooltip-positions.type'; export * from './lib/utils/decorators/bind.decorator'; @@ -36,3 +38,7 @@ export * from './lib/misc/status-bar/status-bar-config.model'; export * from './lib/inputs/round-checkbox/round-checkbox.component'; export * from './lib/inputs/editable-input/editable-input.component'; export * from './lib/inputs/input-with-action/input-with-action.component'; +export * from './lib/help-mode/help-mode.service'; +export * from './lib/help-mode/help-mode.directive'; +export * from './lib/help-mode/help-mode/help-mode.component'; +export * from './lib/help-mode/help-mode-dialog/help-mode-dialog.component'; diff --git a/src/lib/common-ui.module.ts b/src/lib/common-ui.module.ts index 9ff2c29..c0bbebe 100644 --- a/src/lib/common-ui.module.ts +++ b/src/lib/common-ui.module.ts @@ -8,6 +8,7 @@ import { TranslateModule } from '@ngx-translate/core'; import { FormsModule } from '@angular/forms'; import { MatMenuModule } from '@angular/material/menu'; import { MatCheckboxModule } from '@angular/material/checkbox'; +import { MatDialogModule } from '@angular/material/dialog'; import { IconButtonComponent } from './buttons/icon-button/icon-button.component'; import { ChevronButtonComponent } from './buttons/chevron-button/chevron-button.component'; import { CircleButtonComponent } from './buttons/circle-button/circle-button.component'; @@ -22,12 +23,15 @@ import { StatusBarComponent } from './misc/status-bar/status-bar.component'; import { EditableInputComponent } from './inputs/editable-input/editable-input.component'; import { PopupFilterComponent } from './filtering/popup-filter/popup-filter.component'; import { InputWithActionComponent } from './inputs/input-with-action/input-with-action.component'; +import { HelpModeDirective } from './help-mode/help-mode.directive'; +import { HelpModeComponent } from './help-mode/help-mode/help-mode.component'; +import { HelpModeDialogComponent } from './help-mode/help-mode-dialog/help-mode-dialog.component'; const buttons = [IconButtonComponent, ChevronButtonComponent, CircleButtonComponent]; const inputs = [RoundCheckboxComponent, EditableInputComponent, InputWithActionComponent]; -const matModules = [MatIconModule, MatButtonModule, MatTooltipModule, MatMenuModule, MatCheckboxModule]; +const matModules = [MatIconModule, MatButtonModule, MatTooltipModule, MatMenuModule, MatCheckboxModule, MatDialogModule]; const modules = [...matModules, FormsModule, TranslateModule]; @@ -38,10 +42,12 @@ const components = [ QuickFiltersComponent, PopupFilterComponent, TableHeaderComponent, - StatusBarComponent + StatusBarComponent, + HelpModeComponent, + HelpModeDialogComponent ]; -const utils = [SortByPipe, HumanizePipe, SyncWidthDirective]; +const utils = [SortByPipe, HumanizePipe, SyncWidthDirective, HelpModeDirective]; @NgModule({ declarations: [...components, ...utils], diff --git a/src/lib/help-mode/help-mode-dialog/help-mode-dialog.component.html b/src/lib/help-mode/help-mode-dialog/help-mode-dialog.component.html new file mode 100644 index 0000000..42884bb --- /dev/null +++ b/src/lib/help-mode/help-mode-dialog/help-mode-dialog.component.html @@ -0,0 +1,8 @@ +
+
+

+ +

+
+ +
diff --git a/src/lib/help-mode/help-mode-dialog/help-mode-dialog.component.scss b/src/lib/help-mode/help-mode-dialog/help-mode-dialog.component.scss new file mode 100644 index 0000000..17dd08c --- /dev/null +++ b/src/lib/help-mode/help-mode-dialog/help-mode-dialog.component.scss @@ -0,0 +1,16 @@ +section { + background: #ecedf0; + display: flex; + justify-content: center; +} + +.content { + width: 440px; + display: flex; + flex-direction: column; + align-items: center; + text-align: center; + padding-top: 20px; + padding-bottom: 30px; + line-height: 18px; +} diff --git a/src/lib/help-mode/help-mode-dialog/help-mode-dialog.component.ts b/src/lib/help-mode/help-mode-dialog/help-mode-dialog.component.ts new file mode 100644 index 0000000..38933ad --- /dev/null +++ b/src/lib/help-mode/help-mode-dialog/help-mode-dialog.component.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './help-mode-dialog.component.html', + styleUrls: ['./help-mode-dialog.component.scss'] +}) +export class HelpModeDialogComponent {} diff --git a/src/lib/help-mode/help-mode.directive.ts b/src/lib/help-mode/help-mode.directive.ts new file mode 100644 index 0000000..c2110b2 --- /dev/null +++ b/src/lib/help-mode/help-mode.directive.ts @@ -0,0 +1,34 @@ +import { Directive, ElementRef, HostListener, Input, OnInit, Renderer2 } from '@angular/core'; +import { HelpModeService } from './help-mode.service'; + +@Directive({ + selector: '[iqserHelpMode]', + exportAs: 'iqserHelpMode' +}) +export class HelpModeDirective implements OnInit { + @Input('iqserHelpMode') elementName!: string; + + constructor( + private readonly _elementRef: ElementRef, + private readonly _renderer: Renderer2, + private readonly _helpModeService: HelpModeService + ) {} + + ngOnInit(): void { + this._createHelperElement(); + } + + private _createHelperElement() { + const element = this._elementRef.nativeElement as HTMLElement; + + const helperElement = this._renderer.createElement('div') as HTMLElement; + this._renderer.addClass(helperElement, 'help-mode-on-mouse-over'); + this._renderer.addClass(helperElement, `help-mode-on-mouse-over-${this.elementName}`); + + this._helpModeService.addElement(this.elementName, element, helperElement); + } + + @HostListener('click') onClick(): void { + this._helpModeService.openDocsFor(this.elementName); + } +} diff --git a/src/lib/help-mode/help-mode.service.ts b/src/lib/help-mode/help-mode.service.ts new file mode 100644 index 0000000..e471839 --- /dev/null +++ b/src/lib/help-mode/help-mode.service.ts @@ -0,0 +1,91 @@ +import { Inject, Injectable, Renderer2, RendererFactory2 } from '@angular/core'; +import { MatDialog, MatDialogRef } from '@angular/material/dialog'; +import { TranslateService } from '@ngx-translate/core'; +import { HelpModeDialogComponent } from './help-mode-dialog/help-mode-dialog.component'; +import { HELP_DOCS } from '../utils/injection-tokens'; + +interface Helper { + readonly element: HTMLElement; + readonly helperElement: HTMLElement; +} + +@Injectable({ + providedIn: 'root' +}) +export class HelpModeService { + isHelpModeActive = false; + helpModeDialogIsOpened = false; + + private readonly _elements: Record = {}; + private readonly _renderer: Renderer2; + + constructor( + @Inject(HELP_DOCS) private readonly _docs: Record>, + private readonly _dialog: MatDialog, + private readonly _rendererFactory: RendererFactory2, + private readonly _translateService: TranslateService + ) { + this._renderer = this._rendererFactory.createRenderer(null, null); + } + + openHelpModeDialog(): MatDialogRef { + this.helpModeDialogIsOpened = true; + + const ref = this._dialog.open(HelpModeDialogComponent, { + width: '600px' + }); + + ref.afterClosed() + .toPromise() + .then(() => { + this.helpModeDialogIsOpened = false; + }); + return ref; + } + + openDocsFor(elementName: string): void { + if (this.isHelpModeActive) { + window.open(this._docs[elementName][this._translateService.currentLang]); + } + } + + activateHelpMode(): void { + this.isHelpModeActive = true; + this.openHelpModeDialog(); + this._enableHelperElements(); + } + + deactivateHelpMode(): void { + this.isHelpModeActive = false; + this._disableHelperElements(); + } + + highlightHelperElements(): void { + if (!this.isHelpModeActive) return; + + Object.values(this._elements).forEach(({ helperElement }) => { + this._renderer.addClass(helperElement, 'highlight'); + setTimeout(() => { + this._renderer.removeClass(helperElement, 'highlight'); + }, 500); + }); + } + + addElement(elementName: string, element: HTMLElement, helperElement: HTMLElement): void { + this._elements[elementName] = { element, helperElement }; + } + + private _enableHelperElements() { + Object.values(this._elements).forEach(({ element, helperElement }) => { + this._renderer.setStyle(element, 'position', 'relative'); + this._renderer.appendChild(element, helperElement); + }); + } + + private _disableHelperElements() { + Object.values(this._elements).forEach(({ element, helperElement }) => { + this._renderer.removeStyle(element, 'position'); + this._renderer.removeChild(element, helperElement); + }); + } +} diff --git a/src/lib/help-mode/help-mode/help-mode.component.html b/src/lib/help-mode/help-mode/help-mode.component.html new file mode 100644 index 0000000..2e86423 --- /dev/null +++ b/src/lib/help-mode/help-mode/help-mode.component.html @@ -0,0 +1,20 @@ +
+ +
{{ 'help-mode.button-text' | translate }}
+
+
+
+

{{ 'help-mode.text' | translate }}

+ + {{ 'help-mode.instructions' | translate }} + +
+ (esc) + +
+
+
diff --git a/src/lib/help-mode/help-mode/help-mode.component.scss b/src/lib/help-mode/help-mode/help-mode.component.scss new file mode 100644 index 0000000..fb99479 --- /dev/null +++ b/src/lib/help-mode/help-mode/help-mode.component.scss @@ -0,0 +1,72 @@ +@import '../../../../../../apps/red-ui/src/assets/styles/variables'; + +.help-button { + width: 44px; + height: 40px; + position: absolute; + bottom: 20px; + right: 0; + z-index: 1; + background: $green-2; + border-top-left-radius: 8px; + border-bottom-left-radius: 8px; + box-shadow: -1px 1px 5px 0 rgba(40, 50, 65, 0.25); + display: flex; + align-items: center; + justify-content: center; + transition: all 0.25s; +} + +.help-button:hover { + cursor: pointer; + width: fit-content; + padding-left: 10px; + padding-right: 10px; + + .text { + display: block; + } + + mat-icon { + padding-right: 8px; + } +} + +.text { + display: none; +} + +.help-mode-border { + box-sizing: border-box; + height: 100%; + width: 100%; + border-left: 8px solid $green-2; + border-right: 8px solid $green-2; + border-top: 8px solid $green-2; + border-bottom: 60px solid $green-2; + z-index: 10; + position: absolute; + display: flex; + justify-content: center; + + .bottom { + position: fixed; + height: 60px; + width: 95%; + bottom: 0; + display: flex; + justify-content: space-between; + align-items: center; + pointer-events: visiblePainted; + + a { + color: black; + text-decoration: underline; + } + + .close { + display: flex; + align-items: center; + } + } +} diff --git a/src/lib/help-mode/help-mode/help-mode.component.ts b/src/lib/help-mode/help-mode/help-mode.component.ts new file mode 100644 index 0000000..6293193 --- /dev/null +++ b/src/lib/help-mode/help-mode/help-mode.component.ts @@ -0,0 +1,29 @@ +import { Component, HostListener } from '@angular/core'; +import { HelpModeService } from '../help-mode.service'; +import { IqserEventTarget } from '../../utils/types/events.type'; + +@Component({ + selector: 'iqser-help-mode', + templateUrl: './help-mode.component.html', + styleUrls: ['./help-mode.component.scss'] +}) +export class HelpModeComponent { + constructor(readonly helpModeService: HelpModeService) {} + + @HostListener('document:keydown.escape') onEscKeydownHandler(): void { + if (!this.helpModeService.helpModeDialogIsOpened) { + this.helpModeService.deactivateHelpMode(); + } + } + + @HostListener('document:keydown.h', ['$event']) onHKeydownHandler(event: KeyboardEvent): void { + const node = (event.target as IqserEventTarget).localName; + if (!this.helpModeService.isHelpModeActive && node !== 'input' && node !== 'textarea') { + this.helpModeService.activateHelpMode(); + } + } + + @HostListener('click') onClick(): void { + this.helpModeService.highlightHelperElements(); + } +} diff --git a/src/lib/inputs/input-with-action/input-with-action.component.ts b/src/lib/inputs/input-with-action/input-with-action.component.ts index 8863b91..809eccc 100644 --- a/src/lib/inputs/input-with-action/input-with-action.component.ts +++ b/src/lib/inputs/input-with-action/input-with-action.component.ts @@ -17,7 +17,7 @@ export class InputWithActionComponent { @Output() readonly valueChange = new EventEmitter(); get hasContent(): boolean { - return !!this.value.length; + return !!this.value?.length; } get computedWidth(): string { diff --git a/src/lib/utils/injection-tokens.ts b/src/lib/utils/injection-tokens.ts new file mode 100644 index 0000000..3bb8bd9 --- /dev/null +++ b/src/lib/utils/injection-tokens.ts @@ -0,0 +1,3 @@ +import { InjectionToken } from '@angular/core'; + +export const HELP_DOCS = new InjectionToken>>('Links to user manual or help docs'); diff --git a/src/lib/utils/types/events.type.ts b/src/lib/utils/types/events.type.ts new file mode 100644 index 0000000..2d5c97d --- /dev/null +++ b/src/lib/utils/types/events.type.ts @@ -0,0 +1,3 @@ +export interface IqserEventTarget extends EventTarget { + localName: string; +}