55 changed files with 2764 additions and 377 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -0,0 +1,71 @@ |
|||
///
|
|||
/// Copyright © 2016-2021 The Thingsboard Authors
|
|||
///
|
|||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|||
/// you may not use this file except in compliance with the License.
|
|||
/// You may obtain a copy of the License at
|
|||
///
|
|||
/// http://www.apache.org/licenses/LICENSE-2.0
|
|||
///
|
|||
/// Unless required by applicable law or agreed to in writing, software
|
|||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
/// See the License for the specific language governing permissions and
|
|||
/// limitations under the License.
|
|||
///
|
|||
|
|||
import { Injectable } from '@angular/core'; |
|||
import { HttpClient } from '@angular/common/http'; |
|||
import { TranslateService } from '@ngx-translate/core'; |
|||
import { Observable, of } from 'rxjs'; |
|||
import { catchError, tap } from 'rxjs/operators'; |
|||
|
|||
const NOT_FOUND_CONTENT = '## Not found'; |
|||
|
|||
@Injectable({ |
|||
providedIn: 'root' |
|||
}) |
|||
export class HelpService { |
|||
|
|||
private helpCache: {[lang: string]: {[key: string]: string}} = {}; |
|||
|
|||
constructor( |
|||
private translate: TranslateService, |
|||
private http: HttpClient |
|||
) {} |
|||
|
|||
getHelpContent(key: string): Observable<string> { |
|||
const lang = this.translate.currentLang; |
|||
if (this.helpCache[lang] && this.helpCache[lang][key]) { |
|||
return of(this.helpCache[lang][key]); |
|||
} else { |
|||
return this.loadHelpContent(lang, key).pipe( |
|||
catchError(() => { |
|||
const defaultLang = this.translate.getDefaultLang(); |
|||
if (lang !== defaultLang) { |
|||
return this.loadHelpContent(defaultLang, key).pipe( |
|||
catchError(() => { |
|||
return of(NOT_FOUND_CONTENT); |
|||
}) |
|||
); |
|||
} else { |
|||
return of(NOT_FOUND_CONTENT); |
|||
} |
|||
}), |
|||
tap((content) => { |
|||
let langContent = this.helpCache[lang]; |
|||
if (!langContent) { |
|||
langContent = {}; |
|||
this.helpCache[lang] = langContent; |
|||
} |
|||
langContent[key] = content; |
|||
}) |
|||
); |
|||
} |
|||
} |
|||
|
|||
private loadHelpContent(lang: string, key: string): Observable<string> { |
|||
return this.http.get(`/assets/help/${lang}/${key}.md`, {responseType: 'text'} ); |
|||
} |
|||
|
|||
} |
|||
@ -1,131 +0,0 @@ |
|||
/** |
|||
* Copyright © 2016-2021 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0 |
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
|
|||
.tb-markdown-view { |
|||
display: block; |
|||
height: 100%; |
|||
::ng-deep { |
|||
h1 { |
|||
font-size: 32px; |
|||
padding-right: 60px; |
|||
} |
|||
|
|||
h1, h2, h3, h4, h5, h6 { |
|||
line-height: normal; |
|||
font-weight: 500; |
|||
margin-bottom: 30px; |
|||
padding-bottom: 10px; |
|||
border-bottom: 1px solid #ccc; |
|||
} |
|||
|
|||
p { |
|||
font-size: 16px; |
|||
font-weight: 400; |
|||
line-height: 1.25em; |
|||
margin: 0; |
|||
} |
|||
|
|||
p+p { |
|||
margin-top: 10px; |
|||
} |
|||
|
|||
table { |
|||
width: 100%; |
|||
border: 1px solid #ccc; |
|||
border-spacing: 0; |
|||
margin-top: 30px; |
|||
margin-bottom: 30px; |
|||
} |
|||
|
|||
thead { |
|||
background-color: #555; |
|||
color: #fff; |
|||
} |
|||
|
|||
th, td { |
|||
font-size: .85em; |
|||
padding: 8px; |
|||
margin: 0; |
|||
text-align: left; |
|||
} |
|||
|
|||
td[align=center], th[align=center] { |
|||
text-align: center; |
|||
} |
|||
|
|||
td[align=right], th[align=right] { |
|||
text-align: right; |
|||
} |
|||
|
|||
tr:nth-child(even) { |
|||
background-color: #f7f7f7; |
|||
} |
|||
|
|||
code:not([class*=language-]) { |
|||
background: #f5f5f5; |
|||
border-radius: 2px; |
|||
color: #dd4a68; |
|||
padding: 2px 4px; |
|||
} |
|||
|
|||
div.code-wrapper { |
|||
position: relative; |
|||
button.clipboard-btn { |
|||
cursor: pointer; |
|||
margin: 0; |
|||
border: 0; |
|||
outline: none; |
|||
position: absolute; |
|||
top: 5px; |
|||
right: 5px; |
|||
background: #fff; |
|||
box-shadow: 0 1px 8px 0 rgba(0,0,0,0.2), 0 3px 4px 0 rgba(0,0,0,0.14), 0 3px 3px -2px rgba(0,0,0,0.12); |
|||
border-radius: 5px; |
|||
opacity: 0; |
|||
transition: opacity .3s; |
|||
padding: 3px 6px; |
|||
line-height: 16px; |
|||
img { |
|||
width: 18px; |
|||
} |
|||
&:hover { |
|||
background: #f9f9f9; |
|||
} |
|||
&:active { |
|||
background-color: #ececec; |
|||
box-shadow: inset 1px -1px 4px 0px rgba(0,0,0,0.4); |
|||
} |
|||
} |
|||
&:hover { |
|||
button.clipboard-btn { |
|||
opacity: .85; |
|||
} |
|||
} |
|||
} |
|||
|
|||
th, td { |
|||
div.code-wrapper { |
|||
display: inline-block; |
|||
button.clipboard-btn { |
|||
top: -5px; |
|||
right: -30px; |
|||
padding: 0 3px; |
|||
} |
|||
} |
|||
} |
|||
|
|||
} |
|||
} |
|||
@ -0,0 +1,127 @@ |
|||
///
|
|||
/// Copyright © 2016-2021 The Thingsboard Authors
|
|||
///
|
|||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|||
/// you may not use this file except in compliance with the License.
|
|||
/// You may obtain a copy of the License at
|
|||
///
|
|||
/// http://www.apache.org/licenses/LICENSE-2.0
|
|||
///
|
|||
/// Unless required by applicable law or agreed to in writing, software
|
|||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
/// See the License for the specific language governing permissions and
|
|||
/// limitations under the License.
|
|||
///
|
|||
|
|||
import { |
|||
ComponentFactory, ComponentRef, |
|||
Directive, EventEmitter, Injector, |
|||
Input, |
|||
OnChanges, Output, Renderer2, |
|||
SimpleChange, |
|||
SimpleChanges, |
|||
TemplateRef, |
|||
ViewContainerRef |
|||
} from '@angular/core'; |
|||
|
|||
@Directive({ |
|||
// tslint:disable-next-line:directive-selector
|
|||
selector: '[tbComponentOutlet]', |
|||
exportAs: 'tbComponentOutlet' |
|||
}) |
|||
export class TbComponentOutletDirective<_T = unknown> implements OnChanges { |
|||
private componentRef: ComponentRef<any> | null = null; |
|||
private context = new TbComponentOutletContext(); |
|||
@Input() tbComponentOutletContext: any | null = null; |
|||
@Input() tbComponentStyle: { [klass: string]: any } | null = null; |
|||
@Input() tbComponentInjector: Injector | null = null; |
|||
@Input() tbComponentOutlet: ComponentFactory<any> = null; |
|||
@Output() componentChange = new EventEmitter<ComponentRef<any>>(); |
|||
|
|||
static ngTemplateContextGuard<T>( |
|||
// tslint:disable-next-line:variable-name
|
|||
_dir: TbComponentOutletDirective<T>, |
|||
// tslint:disable-next-line:variable-name
|
|||
_ctx: any |
|||
): _ctx is TbComponentOutletContext { |
|||
return true; |
|||
} |
|||
|
|||
private recreateComponent(): void { |
|||
this.viewContainer.clear(); |
|||
this.componentRef = this.viewContainer.createComponent(this.tbComponentOutlet, 0, this.tbComponentInjector); |
|||
this.componentChange.next(this.componentRef); |
|||
if (this.tbComponentOutletContext) { |
|||
for (const propName of Object.keys(this.tbComponentOutletContext)) { |
|||
this.componentRef.instance[propName] = this.tbComponentOutletContext[propName]; |
|||
} |
|||
} |
|||
if (this.tbComponentStyle) { |
|||
for (const propName of Object.keys(this.tbComponentStyle)) { |
|||
this.renderer.setStyle(this.componentRef.location.nativeElement, propName, this.tbComponentStyle[propName]); |
|||
} |
|||
} |
|||
} |
|||
|
|||
private updateContext(): void { |
|||
const newCtx = this.tbComponentOutletContext; |
|||
const oldCtx = this.componentRef.instance as any; |
|||
if (newCtx) { |
|||
for (const propName of Object.keys(newCtx)) { |
|||
oldCtx[propName] = newCtx[propName]; |
|||
} |
|||
} |
|||
} |
|||
|
|||
constructor(private viewContainer: ViewContainerRef, |
|||
private renderer: Renderer2) {} |
|||
|
|||
ngOnChanges(changes: SimpleChanges): void { |
|||
const { tbComponentOutletContext, tbComponentOutlet } = changes; |
|||
const shouldRecreateComponent = (): boolean => { |
|||
let shouldOutletRecreate = false; |
|||
if (tbComponentOutlet) { |
|||
if (tbComponentOutlet.firstChange) { |
|||
shouldOutletRecreate = true; |
|||
} else { |
|||
const isPreviousOutletTemplate = tbComponentOutlet.previousValue instanceof ComponentFactory; |
|||
const isCurrentOutletTemplate = tbComponentOutlet.currentValue instanceof ComponentFactory; |
|||
shouldOutletRecreate = isPreviousOutletTemplate || isCurrentOutletTemplate; |
|||
} |
|||
} |
|||
const hasContextShapeChanged = (ctxChange: SimpleChange): boolean => { |
|||
const prevCtxKeys = Object.keys(ctxChange.previousValue || {}); |
|||
const currCtxKeys = Object.keys(ctxChange.currentValue || {}); |
|||
if (prevCtxKeys.length === currCtxKeys.length) { |
|||
for (const propName of currCtxKeys) { |
|||
if (prevCtxKeys.indexOf(propName) === -1) { |
|||
return true; |
|||
} |
|||
} |
|||
return false; |
|||
} else { |
|||
return true; |
|||
} |
|||
}; |
|||
const shouldContextRecreate = |
|||
tbComponentOutletContext && hasContextShapeChanged(tbComponentOutletContext); |
|||
return shouldContextRecreate || shouldOutletRecreate; |
|||
}; |
|||
|
|||
if (tbComponentOutlet) { |
|||
this.context.$implicit = tbComponentOutlet.currentValue; |
|||
} |
|||
|
|||
const recreateComponent = shouldRecreateComponent(); |
|||
if (recreateComponent) { |
|||
this.recreateComponent(); |
|||
} else { |
|||
this.updateContext(); |
|||
} |
|||
} |
|||
} |
|||
|
|||
export class TbComponentOutletContext { |
|||
public $implicit: any; |
|||
} |
|||
@ -0,0 +1,118 @@ |
|||
///
|
|||
/// Copyright © 2016-2021 The Thingsboard Authors
|
|||
///
|
|||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|||
/// you may not use this file except in compliance with the License.
|
|||
/// You may obtain a copy of the License at
|
|||
///
|
|||
/// http://www.apache.org/licenses/LICENSE-2.0
|
|||
///
|
|||
/// Unless required by applicable law or agreed to in writing, software
|
|||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
/// See the License for the specific language governing permissions and
|
|||
/// limitations under the License.
|
|||
///
|
|||
|
|||
import { |
|||
Directive, |
|||
EmbeddedViewRef, |
|||
Input, |
|||
OnChanges, |
|||
SimpleChange, |
|||
SimpleChanges, |
|||
TemplateRef, |
|||
ViewContainerRef |
|||
} from '@angular/core'; |
|||
|
|||
@Directive({ |
|||
// tslint:disable-next-line:directive-selector
|
|||
selector: '[tbStringTemplateOutlet]', |
|||
exportAs: 'tbStringTemplateOutlet' |
|||
}) |
|||
export class TbStringTemplateOutletDirective<_T = unknown> implements OnChanges { |
|||
private embeddedViewRef: EmbeddedViewRef<any> | null = null; |
|||
private context = new TbStringTemplateOutletContext(); |
|||
@Input() tbStringTemplateOutletContext: any | null = null; |
|||
@Input() tbStringTemplateOutlet: any | TemplateRef<any> = null; |
|||
|
|||
static ngTemplateContextGuard<T>( |
|||
// tslint:disable-next-line:variable-name
|
|||
_dir: TbStringTemplateOutletDirective<T>, |
|||
// tslint:disable-next-line:variable-name
|
|||
_ctx: any |
|||
): _ctx is TbStringTemplateOutletContext { |
|||
return true; |
|||
} |
|||
|
|||
private recreateView(): void { |
|||
this.viewContainer.clear(); |
|||
const isTemplateRef = this.tbStringTemplateOutlet instanceof TemplateRef; |
|||
const templateRef = (isTemplateRef ? this.tbStringTemplateOutlet : this.templateRef) as any; |
|||
this.embeddedViewRef = this.viewContainer.createEmbeddedView( |
|||
templateRef, |
|||
isTemplateRef ? this.tbStringTemplateOutletContext : this.context |
|||
); |
|||
} |
|||
|
|||
private updateContext(): void { |
|||
const isTemplateRef = this.tbStringTemplateOutlet instanceof TemplateRef; |
|||
const newCtx = isTemplateRef ? this.tbStringTemplateOutletContext : this.context; |
|||
const oldCtx = this.embeddedViewRef.context as any; |
|||
if (newCtx) { |
|||
for (const propName of Object.keys(newCtx)) { |
|||
oldCtx[propName] = newCtx[propName]; |
|||
} |
|||
} |
|||
} |
|||
|
|||
constructor(private viewContainer: ViewContainerRef, private templateRef: TemplateRef<any>) {} |
|||
|
|||
ngOnChanges(changes: SimpleChanges): void { |
|||
const { tbStringTemplateOutletContext, tbStringTemplateOutlet } = changes; |
|||
const shouldRecreateView = (): boolean => { |
|||
let shouldOutletRecreate = false; |
|||
if (tbStringTemplateOutlet) { |
|||
if (tbStringTemplateOutlet.firstChange) { |
|||
shouldOutletRecreate = true; |
|||
} else { |
|||
const isPreviousOutletTemplate = tbStringTemplateOutlet.previousValue instanceof TemplateRef; |
|||
const isCurrentOutletTemplate = tbStringTemplateOutlet.currentValue instanceof TemplateRef; |
|||
shouldOutletRecreate = isPreviousOutletTemplate || isCurrentOutletTemplate; |
|||
} |
|||
} |
|||
const hasContextShapeChanged = (ctxChange: SimpleChange): boolean => { |
|||
const prevCtxKeys = Object.keys(ctxChange.previousValue || {}); |
|||
const currCtxKeys = Object.keys(ctxChange.currentValue || {}); |
|||
if (prevCtxKeys.length === currCtxKeys.length) { |
|||
for (const propName of currCtxKeys) { |
|||
if (prevCtxKeys.indexOf(propName) === -1) { |
|||
return true; |
|||
} |
|||
} |
|||
return false; |
|||
} else { |
|||
return true; |
|||
} |
|||
}; |
|||
const shouldContextRecreate = |
|||
tbStringTemplateOutletContext && hasContextShapeChanged(tbStringTemplateOutletContext); |
|||
return shouldContextRecreate || shouldOutletRecreate; |
|||
}; |
|||
|
|||
if (tbStringTemplateOutlet) { |
|||
this.context.$implicit = tbStringTemplateOutlet.currentValue; |
|||
} |
|||
|
|||
const recreateView = shouldRecreateView(); |
|||
if (recreateView) { |
|||
this.recreateView(); |
|||
} else { |
|||
this.updateContext(); |
|||
} |
|||
} |
|||
} |
|||
|
|||
export class TbStringTemplateOutletContext { |
|||
public $implicit: any; |
|||
} |
|||
@ -0,0 +1,20 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2021 The Thingsboard Authors |
|||
|
|||
Licensed under the Apache License, Version 2.0 (the "License"); |
|||
you may not use this file except in compliance with the License. |
|||
You may obtain a copy of the License at |
|||
|
|||
http://www.apache.org/licenses/LICENSE-2.0 |
|||
|
|||
Unless required by applicable law or agreed to in writing, software |
|||
distributed under the License is distributed on an "AS IS" BASIS, |
|||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
See the License for the specific language governing permissions and |
|||
limitations under the License. |
|||
|
|||
--> |
|||
<ng-container *ngIf="markdownText$ | async as text;"> |
|||
<markdown [data]="text" lineNumbers (ready)="onMarkdownReady()" class="tb-help-markdown tb-markdown-view" (click)="markdownClick($event)"></markdown> |
|||
</ng-container> |
|||
@ -0,0 +1,33 @@ |
|||
/** |
|||
* Copyright © 2016-2021 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0 |
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
:host { |
|||
.tb-help-markdown { |
|||
overflow: auto; |
|||
max-width: 50vw; |
|||
max-height: 50vh; |
|||
margin-top: 30px; |
|||
} |
|||
} |
|||
|
|||
:host ::ng-deep { |
|||
.tb-help-markdown.tb-markdown-view { |
|||
h1, h2, h3, h4, h5, h6 { |
|||
&:first-child { |
|||
padding-top: 0; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,104 @@ |
|||
///
|
|||
/// Copyright © 2016-2021 The Thingsboard Authors
|
|||
///
|
|||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|||
/// you may not use this file except in compliance with the License.
|
|||
/// You may obtain a copy of the License at
|
|||
///
|
|||
/// http://www.apache.org/licenses/LICENSE-2.0
|
|||
///
|
|||
/// Unless required by applicable law or agreed to in writing, software
|
|||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
/// See the License for the specific language governing permissions and
|
|||
/// limitations under the License.
|
|||
///
|
|||
|
|||
import { |
|||
Component, |
|||
EventEmitter, |
|||
Input, OnChanges, |
|||
OnDestroy, OnInit, |
|||
Output, SimpleChanges |
|||
} from '@angular/core'; |
|||
import { BehaviorSubject } from 'rxjs'; |
|||
import { delay, share } from 'rxjs/operators'; |
|||
import { HelpService } from '@core/services/help.service'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-help-markdown', |
|||
templateUrl: './help-markdown.component.html', |
|||
styleUrls: ['./help-markdown.component.scss'] |
|||
}) |
|||
export class HelpMarkdownComponent implements OnDestroy, OnInit, OnChanges { |
|||
|
|||
@Input() helpId: string; |
|||
|
|||
@Input() visible: boolean; |
|||
|
|||
@Output() markdownReady = new EventEmitter<void>(); |
|||
|
|||
markdownText = new BehaviorSubject<string>(null); |
|||
|
|||
markdownText$ = this.markdownText.pipe( |
|||
share() |
|||
); |
|||
|
|||
isMarkdownReady = false; |
|||
|
|||
private loadHelpPending = false; |
|||
|
|||
constructor(private help: HelpService) {} |
|||
|
|||
ngOnInit(): void { |
|||
this.loadHelpWhenVisible(); |
|||
} |
|||
|
|||
ngOnDestroy(): void { |
|||
this.markdownText.complete(); |
|||
} |
|||
|
|||
ngOnChanges(changes: SimpleChanges): void { |
|||
for (const propName of Object.keys(changes)) { |
|||
const change = changes[propName]; |
|||
if (!change.firstChange && change.currentValue !== change.previousValue) { |
|||
if (propName === 'visible') { |
|||
if (this.loadHelpPending) { |
|||
this.loadHelpPending = false; |
|||
this.loadHelp(); |
|||
} |
|||
} |
|||
if (propName === 'helpId') { |
|||
this.markdownText.next(null); |
|||
this.loadHelpWhenVisible(); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
private loadHelpWhenVisible() { |
|||
if (this.visible) { |
|||
this.loadHelp(); |
|||
} else { |
|||
this.loadHelpPending = true; |
|||
} |
|||
} |
|||
|
|||
private loadHelp() { |
|||
if (this.helpId) { |
|||
this.help.getHelpContent(this.helpId).subscribe((content) => { |
|||
this.markdownText.next(content); |
|||
}); |
|||
} |
|||
} |
|||
|
|||
onMarkdownReady() { |
|||
this.isMarkdownReady = true; |
|||
this.markdownReady.next(); |
|||
} |
|||
|
|||
markdownClick($event: MouseEvent) { |
|||
|
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,30 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2021 The Thingsboard Authors |
|||
|
|||
Licensed under the Apache License, Version 2.0 (the "License"); |
|||
you may not use this file except in compliance with the License. |
|||
You may obtain a copy of the License at |
|||
|
|||
http://www.apache.org/licenses/LICENSE-2.0 |
|||
|
|||
Unless required by applicable law or agreed to in writing, software |
|||
distributed under the License is distributed on an "AS IS" BASIS, |
|||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
See the License for the specific language governing permissions and |
|||
limitations under the License. |
|||
|
|||
--> |
|||
<div style="position: relative;"> |
|||
<button color="primary" mat-button mat-icon-button |
|||
class="tb-help-popup-button tb-mat-32" |
|||
type="button" |
|||
(click)="toggleHelp()" |
|||
matTooltip="{{'help.show-help' | translate}}" |
|||
matTooltipPosition="above"> |
|||
<mat-icon class="material-icons">{{popoverVisible ? 'help' : 'help_outline'}}</mat-icon> |
|||
</button> |
|||
<div *ngIf="popoverVisible && !popoverReady" fxFlex fxLayoutAlign="center center" class="tb-absolute-fill tb-help-popup-button-loading"> |
|||
<mat-spinner mode="indeterminate" diameter="20" strokeWidth="2"></mat-spinner> |
|||
</div> |
|||
</div> |
|||
@ -0,0 +1,21 @@ |
|||
/** |
|||
* Copyright © 2016-2021 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0 |
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
.tb-help-popup-button { |
|||
} |
|||
.tb-help-popup-button-loading { |
|||
background: #fff; |
|||
border-radius: 50%; |
|||
} |
|||
@ -0,0 +1,53 @@ |
|||
///
|
|||
/// Copyright © 2016-2021 The Thingsboard Authors
|
|||
///
|
|||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|||
/// you may not use this file except in compliance with the License.
|
|||
/// You may obtain a copy of the License at
|
|||
///
|
|||
/// http://www.apache.org/licenses/LICENSE-2.0
|
|||
///
|
|||
/// Unless required by applicable law or agreed to in writing, software
|
|||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
/// See the License for the specific language governing permissions and
|
|||
/// limitations under the License.
|
|||
///
|
|||
|
|||
import { Component, ElementRef, Input, OnDestroy, Renderer2, ViewContainerRef, ViewEncapsulation } from '@angular/core'; |
|||
import { TbPopoverService } from '@shared/components/popover.component'; |
|||
|
|||
@Component({ |
|||
// tslint:disable-next-line:component-selector
|
|||
selector: '[tb-help-popup]', |
|||
templateUrl: './help-popup.component.html', |
|||
styleUrls: ['./help-popup.component.scss'], |
|||
encapsulation: ViewEncapsulation.None |
|||
}) |
|||
export class HelpPopupComponent implements OnDestroy { |
|||
|
|||
// tslint:disable-next-line:no-input-rename
|
|||
@Input('tb-help-popup') helpId: string; |
|||
|
|||
popoverVisible = false; |
|||
popoverReady = true; |
|||
|
|||
constructor(private elementRef: ElementRef, |
|||
private viewContainerRef: ViewContainerRef, |
|||
private renderer: Renderer2, |
|||
private popoverService: TbPopoverService) {} |
|||
|
|||
toggleHelp() { |
|||
this.popoverService.toggleHelpPopover(this.elementRef.nativeElement, this.renderer, this.viewContainerRef, |
|||
this.helpId, |
|||
(visible) => { |
|||
this.popoverVisible = visible; |
|||
}, (ready => { |
|||
this.popoverReady = ready; |
|||
})); |
|||
} |
|||
|
|||
ngOnDestroy(): void { |
|||
} |
|||
|
|||
} |
|||
@ -1,99 +0,0 @@ |
|||
///
|
|||
/// Copyright © 2016-2021 The Thingsboard Authors
|
|||
///
|
|||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|||
/// you may not use this file except in compliance with the License.
|
|||
/// You may obtain a copy of the License at
|
|||
///
|
|||
/// http://www.apache.org/licenses/LICENSE-2.0
|
|||
///
|
|||
/// Unless required by applicable law or agreed to in writing, software
|
|||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
/// See the License for the specific language governing permissions and
|
|||
/// limitations under the License.
|
|||
///
|
|||
|
|||
|
|||
import { MarkedOptions, MarkedRenderer } from 'ngx-markdown'; |
|||
|
|||
export function markedOptionsFactory(): MarkedOptions { |
|||
const renderer = new MarkedRenderer(); |
|||
const renderer2 = new MarkedRenderer(); |
|||
|
|||
const copyCodeBlock = '{:copy-code}'; |
|||
|
|||
let id = 1; |
|||
|
|||
renderer.code = (code: string, language: string | undefined, isEscaped: boolean) => { |
|||
if (code.endsWith(copyCodeBlock)) { |
|||
code = code.substring(0, code.length - copyCodeBlock.length); |
|||
const content = renderer2.code(code, language, isEscaped); |
|||
id++; |
|||
return wrapCopyCode(id, content, code); |
|||
} else { |
|||
return renderer2.code(code, language, isEscaped); |
|||
} |
|||
}; |
|||
|
|||
renderer.tablecell = (content: string, flags: { |
|||
header: boolean; |
|||
align: 'center' | 'left' | 'right' | null; |
|||
}) => { |
|||
if (content.endsWith(copyCodeBlock)) { |
|||
content = content.substring(0, content.length - copyCodeBlock.length); |
|||
id++; |
|||
content = wrapCopyCode(id, content, content); |
|||
} |
|||
return renderer2.tablecell(content, flags); |
|||
}; |
|||
|
|||
return { |
|||
renderer, |
|||
headerIds: true, |
|||
gfm: true, |
|||
breaks: false, |
|||
pedantic: false, |
|||
smartLists: true, |
|||
smartypants: false, |
|||
}; |
|||
} |
|||
|
|||
function wrapCopyCode(id: number, content: string, code: string): string { |
|||
return '<div class="code-wrapper">' + content + '<span id="copyCodeId' + id + '" style="display: none;">' + code + '</span>' + |
|||
'<button id="copyCodeBtn' + id + '" onClick="markdownCopyCode(' + id + ')" ' + |
|||
'class="clipboard-btn"><img src="https://clipboardjs.com/assets/images/clippy.svg" alt="Copy to clipboard">' + |
|||
'</button></div>'; |
|||
} |
|||
|
|||
(window as any).markdownCopyCode = (id: number) => { |
|||
const text = $('#copyCodeId' + id).text(); |
|||
navigator.clipboard.writeText(text).then(() => { |
|||
import('tooltipster').then( |
|||
() => { |
|||
const copyBtn = $('#copyCodeBtn' + id); |
|||
if (!copyBtn.hasClass('tooltipstered')) { |
|||
copyBtn.tooltipster( |
|||
{ |
|||
content: 'Copied!', |
|||
theme: 'tooltipster-shadow', |
|||
delay: 0, |
|||
trigger: 'custom', |
|||
triggerClose: { |
|||
click: true, |
|||
tap: true, |
|||
scroll: true, |
|||
mouseleave: true |
|||
}, |
|||
side: 'bottom', |
|||
distance: 12, |
|||
trackOrigin: true |
|||
} |
|||
); |
|||
} |
|||
const tooltip = copyBtn.tooltipster('instance'); |
|||
tooltip.open(); |
|||
} |
|||
); |
|||
}); |
|||
}; |
|||
@ -0,0 +1,143 @@ |
|||
///
|
|||
/// Copyright © 2016-2021 The Thingsboard Authors
|
|||
///
|
|||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|||
/// you may not use this file except in compliance with the License.
|
|||
/// You may obtain a copy of the License at
|
|||
///
|
|||
/// http://www.apache.org/licenses/LICENSE-2.0
|
|||
///
|
|||
/// Unless required by applicable law or agreed to in writing, software
|
|||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
/// See the License for the specific language governing permissions and
|
|||
/// limitations under the License.
|
|||
///
|
|||
|
|||
import { MarkedOptions, MarkedRenderer } from 'ngx-markdown'; |
|||
import { Inject, Injectable } from '@angular/core'; |
|||
import { TranslateService } from '@ngx-translate/core'; |
|||
import { DOCUMENT } from '@angular/common'; |
|||
import { WINDOW } from '@core/services/window.service'; |
|||
|
|||
const copyCodeBlock = '{:copy-code}'; |
|||
|
|||
@Injectable() |
|||
export class MarkedOptionsService extends MarkedOptions { |
|||
|
|||
renderer = new MarkedRenderer(); |
|||
headerIds = true; |
|||
gfm = true; |
|||
breaks = false; |
|||
pedantic = false; |
|||
smartLists = true; |
|||
smartypants = false; |
|||
|
|||
private renderer2 = new MarkedRenderer(); |
|||
|
|||
private id = 1; |
|||
|
|||
constructor(private translate: TranslateService, |
|||
@Inject(WINDOW) private readonly window: Window, |
|||
@Inject(DOCUMENT) private readonly document: Document) { |
|||
super(); |
|||
this.renderer.code = (code: string, language: string | undefined, isEscaped: boolean) => { |
|||
if (code.endsWith(copyCodeBlock)) { |
|||
code = code.substring(0, code.length - copyCodeBlock.length); |
|||
const content = checkLineNumbers(this.renderer2.code(code, language, isEscaped), code); |
|||
this.id++; |
|||
return this.wrapCopyCode(this.id, content, code); |
|||
} else { |
|||
return checkLineNumbers(this.renderer2.code(code, language, isEscaped), code); |
|||
} |
|||
}; |
|||
this.renderer.tablecell = (content: string, flags: { |
|||
header: boolean; |
|||
align: 'center' | 'left' | 'right' | null; |
|||
}) => { |
|||
if (content.endsWith(copyCodeBlock)) { |
|||
content = content.substring(0, content.length - copyCodeBlock.length); |
|||
this.id++; |
|||
content = this.wrapCopyCode(this.id, content, content); |
|||
} |
|||
return this.renderer2.tablecell(content, flags); |
|||
}; |
|||
this.document.addEventListener('selectionchange', this.onSelectionChange.bind(this)); |
|||
(this.window as any).markdownCopyCode = this.markdownCopyCode.bind(this); |
|||
} |
|||
|
|||
private wrapCopyCode(id: number, content: string, code: string): string { |
|||
return `<div class="code-wrapper noChars" id="codeWrapper${id}" onClick="markdownCopyCode(${id})">${content}` + |
|||
`<span id="copyCodeId${id}" style="display: none;">${code}</span>` + |
|||
`<button class="clipboard-btn">\n` + |
|||
` <p>${this.translate.instant('markdown.copy-code')}</p>\n` + |
|||
` <div>\n` + |
|||
` <img src="/assets/copy-code-icon.svg" alt="${this.translate.instant('markdown.copy-code')}">\n` + |
|||
` </div>\n` + |
|||
`</button>` + |
|||
`</div>`; |
|||
} |
|||
|
|||
private onSelectionChange() { |
|||
const codeWrappers = $('.code-wrapper'); |
|||
codeWrappers.removeClass('noChars'); |
|||
const selectedChars = this.getSelectedText(); |
|||
if (!selectedChars) { |
|||
codeWrappers.addClass('noChars'); |
|||
} |
|||
} |
|||
|
|||
private getSelectedText(): string { |
|||
let text; |
|||
if (this.window.getSelection) { |
|||
text = this.window.getSelection().toString(); |
|||
} else if (this.document.getSelection) { |
|||
text = this.document.getSelection(); |
|||
} else if ((this.document as any).selection) { |
|||
text = (this.document as any).selection.createRange().text; |
|||
} |
|||
return text; |
|||
} |
|||
|
|||
private markdownCopyCode(id: number) { |
|||
const copyWrapper = $('#codeWrapper' + id); |
|||
if (copyWrapper.hasClass('noChars')) { |
|||
const text = $('#copyCodeId' + id).text(); |
|||
this.window.navigator.clipboard.writeText(text).then(() => { |
|||
import('tooltipster').then( |
|||
() => { |
|||
if (!copyWrapper.hasClass('tooltipstered')) { |
|||
copyWrapper.tooltipster( |
|||
{ |
|||
content: this.translate.instant('markdown.copied'), |
|||
// theme: 'tooltipster-shadow',
|
|||
delay: 0, |
|||
trigger: 'custom', |
|||
triggerClose: { |
|||
click: true, |
|||
tap: true, |
|||
scroll: true, |
|||
mouseleave: true |
|||
}, |
|||
side: 'top', |
|||
distance: 12, |
|||
trackOrigin: true |
|||
} |
|||
); |
|||
} |
|||
const tooltip = copyWrapper.tooltipster('instance'); |
|||
tooltip.open(); |
|||
} |
|||
); |
|||
}); |
|||
} |
|||
} |
|||
} |
|||
|
|||
function checkLineNumbers(content: string, code: string): string { |
|||
const lineCount = code.trim().split('\n').length; |
|||
if (lineCount < 2) { |
|||
content = content.replace('<pre>', '<pre class="no-line-numbers">'); |
|||
} |
|||
return content; |
|||
} |
|||
@ -0,0 +1,203 @@ |
|||
/** |
|||
* Copyright © 2016-2021 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0 |
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
@import '../scss/mixins'; |
|||
|
|||
$popover-arrow-width: 6; |
|||
$popover-distance: $popover-arrow-width + 4; |
|||
$zindex-popover: 1030; |
|||
$popover-bg: #fff; |
|||
$border-radius-base: 6px; |
|||
$popover-close-button-size: 30px; |
|||
$box-shadow-base: 0 3px 6px -4px rgba(0, 0, 0, 0.12), 0 6px 16px 0 rgba(0, 0, 0, 0.08), |
|||
0 9px 28px 8px rgba(0, 0, 0, 0.05), 0 0 8px rgba(0, 0, 0, 0.15); |
|||
|
|||
.tb-popover { |
|||
z-index: $zindex-popover; |
|||
font-weight: normal; |
|||
white-space: normal; |
|||
text-align: left; |
|||
cursor: auto; |
|||
user-select: text; |
|||
|
|||
&::after { |
|||
position: absolute; |
|||
background: rgba(255, 255, 255, 0.01); |
|||
content: ''; |
|||
} |
|||
|
|||
&-hidden { |
|||
display: none; |
|||
} |
|||
|
|||
// Offset the popover to account for the popover arrow |
|||
&-placement-top, |
|||
&-placement-topLeft, |
|||
&-placement-topRight { |
|||
padding-bottom: $popover-distance + px; |
|||
} |
|||
|
|||
&-placement-right, |
|||
&-placement-rightTop, |
|||
&-placement-rightBottom { |
|||
padding-left: $popover-distance + px; |
|||
} |
|||
|
|||
&-placement-bottom, |
|||
&-placement-bottomLeft, |
|||
&-placement-bottomRight { |
|||
padding-top: $popover-distance + px; |
|||
} |
|||
|
|||
&-placement-left, |
|||
&-placement-leftTop, |
|||
&-placement-leftBottom { |
|||
padding-right: $popover-distance + px; |
|||
} |
|||
|
|||
&-inner { |
|||
background-color: $popover-bg; |
|||
background-clip: padding-box; |
|||
border-radius: $border-radius-base; |
|||
box-shadow: $box-shadow-base; |
|||
overflow: hidden; |
|||
position: relative; |
|||
} |
|||
|
|||
&-close-button { |
|||
cursor: pointer; |
|||
position: absolute; |
|||
top: 6px; |
|||
right: 6px; |
|||
padding: 0; |
|||
border: none; |
|||
text-align: center; |
|||
width: $popover-close-button-size; |
|||
height: $popover-close-button-size; |
|||
font-size: $popover-close-button-size; |
|||
color: #8e8e8e; |
|||
text-decoration: none; |
|||
font-weight: bold; |
|||
background: transparent; |
|||
z-index: 10; |
|||
&:hover { |
|||
color: #313131; |
|||
} |
|||
} |
|||
|
|||
&-inner-content { |
|||
padding: 12px 16px; |
|||
color: rgba(0, 0, 0, 0.85); |
|||
} |
|||
|
|||
// Arrows |
|||
// .popover-arrow is outer, .popover-arrow:after is inner |
|||
|
|||
&-arrow { |
|||
position: absolute; |
|||
display: block; |
|||
width: sqrt($popover-arrow-width * $popover-arrow-width * 2) + px; |
|||
height: sqrt($popover-arrow-width * $popover-arrow-width * 2) + px; |
|||
background: transparent; |
|||
border-style: solid; |
|||
border-width: (sqrt($popover-arrow-width * $popover-arrow-width * 2) / 2) + px; |
|||
transform: rotate(45deg); |
|||
z-index: 10; |
|||
} |
|||
|
|||
&-placement-top > &-content > &-arrow, |
|||
&-placement-topLeft > &-content > &-arrow, |
|||
&-placement-topRight > &-content > &-arrow { |
|||
bottom: $popover-distance - $popover-arrow-width + 2.2 + px; |
|||
border-top-color: transparent; |
|||
border-right-color: $popover-bg; |
|||
border-bottom-color: $popover-bg; |
|||
border-left-color: transparent; |
|||
box-shadow: 3px 3px 7px rgba(0, 0, 0, 0.07); |
|||
} |
|||
&-placement-top > &-content > &-arrow { |
|||
left: 50%; |
|||
transform: translateX(-50%) rotate(45deg); |
|||
} |
|||
&-placement-topLeft > &-content > &-arrow { |
|||
left: 16px; |
|||
} |
|||
&-placement-topRight > &-content > &-arrow { |
|||
right: 16px; |
|||
} |
|||
|
|||
&-placement-right > &-content > &-arrow, |
|||
&-placement-rightTop > &-content > &-arrow, |
|||
&-placement-rightBottom > &-content > &-arrow { |
|||
left: $popover-distance - $popover-arrow-width + 2 + px; |
|||
border-top-color: transparent; |
|||
border-right-color: transparent; |
|||
border-bottom-color: $popover-bg; |
|||
border-left-color: $popover-bg; |
|||
box-shadow: -3px 3px 7px rgba(0, 0, 0, 0.07); |
|||
} |
|||
&-placement-right > &-content > &-arrow { |
|||
top: 50%; |
|||
transform: translateY(-50%) rotate(45deg); |
|||
} |
|||
&-placement-rightTop > &-content > &-arrow { |
|||
top: 12px; |
|||
} |
|||
&-placement-rightBottom > &-content > &-arrow { |
|||
bottom: 12px; |
|||
} |
|||
|
|||
&-placement-bottom > &-content > &-arrow, |
|||
&-placement-bottomLeft > &-content > &-arrow, |
|||
&-placement-bottomRight > &-content > &-arrow { |
|||
top: $popover-distance - $popover-arrow-width + 2 + px; |
|||
border-top-color: $popover-bg; |
|||
border-right-color: transparent; |
|||
border-bottom-color: transparent; |
|||
border-left-color: $popover-bg; |
|||
box-shadow: -2px -2px 5px rgba(0, 0, 0, 0.06); |
|||
} |
|||
&-placement-bottom > &-content > &-arrow { |
|||
left: 50%; |
|||
transform: translateX(-50%) rotate(45deg); |
|||
} |
|||
&-placement-bottomLeft > &-content > &-arrow { |
|||
left: 16px; |
|||
} |
|||
&-placement-bottomRight > &-content > &-arrow { |
|||
right: 16px; |
|||
} |
|||
|
|||
&-placement-left > &-content > &-arrow, |
|||
&-placement-leftTop > &-content > &-arrow, |
|||
&-placement-leftBottom > &-content > &-arrow { |
|||
right: $popover-distance - $popover-arrow-width + 2 + px; |
|||
border-top-color: $popover-bg; |
|||
border-right-color: $popover-bg; |
|||
border-bottom-color: transparent; |
|||
border-left-color: transparent; |
|||
box-shadow: 3px -3px 7px rgba(0, 0, 0, 0.07); |
|||
} |
|||
&-placement-left > &-content > &-arrow { |
|||
top: 50%; |
|||
transform: translateY(-50%) rotate(45deg); |
|||
} |
|||
&-placement-leftTop > &-content > &-arrow { |
|||
top: 12px; |
|||
} |
|||
&-placement-leftBottom > &-content > &-arrow { |
|||
bottom: 12px; |
|||
} |
|||
} |
|||
@ -0,0 +1,725 @@ |
|||
///
|
|||
/// Copyright © 2016-2021 The Thingsboard Authors
|
|||
///
|
|||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|||
/// you may not use this file except in compliance with the License.
|
|||
/// You may obtain a copy of the License at
|
|||
///
|
|||
/// http://www.apache.org/licenses/LICENSE-2.0
|
|||
///
|
|||
/// Unless required by applicable law or agreed to in writing, software
|
|||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
/// See the License for the specific language governing permissions and
|
|||
/// limitations under the License.
|
|||
///
|
|||
|
|||
import { |
|||
AfterViewInit, |
|||
ChangeDetectionStrategy, ChangeDetectorRef, |
|||
Component, ComponentFactory, ComponentFactoryResolver, ComponentRef, |
|||
Directive, |
|||
ElementRef, |
|||
EventEmitter, Injectable, Injector, |
|||
Input, OnChanges, OnDestroy, OnInit, Optional, |
|||
Output, Renderer2, SimpleChanges, |
|||
TemplateRef, Type, ViewChild, ViewContainerRef, ViewEncapsulation |
|||
} from '@angular/core'; |
|||
import { Direction, Directionality } from '@angular/cdk/bidi'; |
|||
import { |
|||
CdkConnectedOverlay, |
|||
CdkOverlayOrigin, |
|||
ConnectedOverlayPositionChange, |
|||
ConnectionPositionPair |
|||
} from '@angular/cdk/overlay'; |
|||
import { Subject, Subscription } from 'rxjs'; |
|||
import { |
|||
DEFAULT_POPOVER_POSITIONS, |
|||
getPlacementName, |
|||
popoverMotion, PopoverPlacement, PopoverWithTrigger, |
|||
POSITION_MAP, PropertyMapping |
|||
} from '@shared/components/popover.models'; |
|||
import { distinctUntilChanged, takeUntil } from 'rxjs/operators'; |
|||
import { isDefinedAndNotNull, isNotEmptyStr, onParentScrollOrWindowResize } from '@core/utils'; |
|||
import { ScrollStrategyOptions } from '@angular/cdk/overlay/scroll/scroll-strategy-options'; |
|||
import { ScrollStrategy } from '@angular/cdk/overlay/scroll/scroll-strategy'; |
|||
import { taggedTemplate } from '@angular/compiler/src/output/output_ast'; |
|||
import { HelpMarkdownComponent } from '@shared/components/help-markdown.component'; |
|||
|
|||
export type TbPopoverTrigger = 'click' | 'focus' | 'hover' | null; |
|||
|
|||
@Directive({ |
|||
selector: '[tb-popover]', |
|||
exportAs: 'tbPopover', |
|||
host: { |
|||
'[class.tb-popover-open]': 'visible' |
|||
} |
|||
}) |
|||
export class TbPopoverDirective implements OnChanges, OnDestroy, AfterViewInit { |
|||
|
|||
// tslint:disable:no-input-rename
|
|||
@Input('tbPopoverContent') content?: string | TemplateRef<void>; |
|||
@Input('tbPopoverTrigger') trigger?: TbPopoverTrigger = 'hover'; |
|||
@Input('tbPopoverPlacement') placement?: string | string[] = 'top'; |
|||
@Input('tbPopoverOrigin') origin?: ElementRef<HTMLElement>; |
|||
@Input('tbPopoverVisible') visible?: boolean; |
|||
@Input('tbPopoverMouseEnterDelay') mouseEnterDelay?: number; |
|||
@Input('tbPopoverMouseLeaveDelay') mouseLeaveDelay?: number; |
|||
@Input('tbPopoverOverlayClassName') overlayClassName?: string; |
|||
@Input('tbPopoverOverlayStyle') overlayStyle?: { [klass: string]: any }; |
|||
@Input() tbPopoverBackdrop = false; |
|||
|
|||
// tslint:disable-next-line:no-output-rename
|
|||
@Output('tbPopoverVisibleChange') readonly visibleChange = new EventEmitter<boolean>(); |
|||
|
|||
componentFactory: ComponentFactory<TbPopoverComponent> = this.resolver.resolveComponentFactory(TbPopoverComponent); |
|||
component?: TbPopoverComponent; |
|||
|
|||
private readonly destroy$ = new Subject<void>(); |
|||
private readonly triggerDisposables: Array<() => void> = []; |
|||
private delayTimer?; |
|||
private internalVisible = false; |
|||
|
|||
constructor( |
|||
private elementRef: ElementRef, |
|||
private hostView: ViewContainerRef, |
|||
private resolver: ComponentFactoryResolver, |
|||
private renderer: Renderer2 |
|||
) {} |
|||
|
|||
ngOnChanges(changes: SimpleChanges): void { |
|||
const { trigger } = changes; |
|||
|
|||
if (trigger && !trigger.isFirstChange()) { |
|||
this.registerTriggers(); |
|||
} |
|||
|
|||
if (this.component) { |
|||
this.updatePropertiesByChanges(changes); |
|||
} |
|||
} |
|||
|
|||
ngAfterViewInit(): void { |
|||
this.registerTriggers(); |
|||
} |
|||
|
|||
ngOnDestroy(): void { |
|||
this.destroy$.next(); |
|||
this.destroy$.complete(); |
|||
this.clearTogglingTimer(); |
|||
this.removeTriggerListeners(); |
|||
} |
|||
|
|||
show(): void { |
|||
if (!this.component) { |
|||
this.createComponent(); |
|||
} |
|||
this.component?.show(); |
|||
} |
|||
|
|||
hide(): void { |
|||
this.component?.hide(); |
|||
} |
|||
|
|||
updatePosition(): void { |
|||
if (this.component) { |
|||
this.component.updatePosition(); |
|||
} |
|||
} |
|||
|
|||
private createComponent(): void { |
|||
const componentRef = this.hostView.createComponent(this.componentFactory); |
|||
|
|||
this.component = componentRef.instance; |
|||
|
|||
this.renderer.removeChild( |
|||
this.renderer.parentNode(this.elementRef.nativeElement), |
|||
componentRef.location.nativeElement |
|||
); |
|||
this.component.setOverlayOrigin({ elementRef: this.origin || this.elementRef }); |
|||
|
|||
this.initProperties(); |
|||
|
|||
this.component.tbVisibleChange |
|||
.pipe(distinctUntilChanged(), takeUntil(this.destroy$)) |
|||
.subscribe((visible: boolean) => { |
|||
this.internalVisible = visible; |
|||
this.visibleChange.emit(visible); |
|||
}); |
|||
} |
|||
|
|||
private registerTriggers(): void { |
|||
// When the method gets invoked, all properties has been synced to the dynamic component.
|
|||
// After removing the old API, we can just check the directive's own `nzTrigger`.
|
|||
const el = this.elementRef.nativeElement; |
|||
const trigger = this.trigger; |
|||
|
|||
this.removeTriggerListeners(); |
|||
|
|||
if (trigger === 'hover') { |
|||
let overlayElement: HTMLElement; |
|||
this.triggerDisposables.push( |
|||
this.renderer.listen(el, 'mouseenter', () => { |
|||
this.delayEnterLeave(true, true, this.mouseEnterDelay); |
|||
}) |
|||
); |
|||
this.triggerDisposables.push( |
|||
this.renderer.listen(el, 'mouseleave', () => { |
|||
this.delayEnterLeave(true, false, this.mouseLeaveDelay); |
|||
if (this.component?.overlay.overlayRef && !overlayElement) { |
|||
overlayElement = this.component.overlay.overlayRef.overlayElement; |
|||
this.triggerDisposables.push( |
|||
this.renderer.listen(overlayElement, 'mouseenter', () => { |
|||
this.delayEnterLeave(false, true, this.mouseEnterDelay); |
|||
}) |
|||
); |
|||
this.triggerDisposables.push( |
|||
this.renderer.listen(overlayElement, 'mouseleave', () => { |
|||
this.delayEnterLeave(false, false, this.mouseLeaveDelay); |
|||
}) |
|||
); |
|||
} |
|||
}) |
|||
); |
|||
} else if (trigger === 'focus') { |
|||
this.triggerDisposables.push(this.renderer.listen(el, 'focusin', () => this.show())); |
|||
this.triggerDisposables.push(this.renderer.listen(el, 'focusout', () => this.hide())); |
|||
} else if (trigger === 'click') { |
|||
this.triggerDisposables.push( |
|||
this.renderer.listen(el, 'click', (e: MouseEvent) => { |
|||
e.preventDefault(); |
|||
if (this.component?.visible) { |
|||
this.hide(); |
|||
} else { |
|||
this.show(); |
|||
} |
|||
}) |
|||
); |
|||
} |
|||
// Else do nothing because user wants to control the visibility programmatically.
|
|||
} |
|||
|
|||
private updatePropertiesByChanges(changes: SimpleChanges): void { |
|||
this.updatePropertiesByKeys(Object.keys(changes)); |
|||
} |
|||
|
|||
private updatePropertiesByKeys(keys?: string[]): void { |
|||
const mappingProperties: PropertyMapping = { |
|||
// common mappings
|
|||
content: ['tbContent', () => this.content], |
|||
trigger: ['tbTrigger', () => this.trigger], |
|||
placement: ['tbPlacement', () => this.placement], |
|||
visible: ['tbVisible', () => this.visible], |
|||
mouseEnterDelay: ['tbMouseEnterDelay', () => this.mouseEnterDelay], |
|||
mouseLeaveDelay: ['tbMouseLeaveDelay', () => this.mouseLeaveDelay], |
|||
overlayClassName: ['tbOverlayClassName', () => this.overlayClassName], |
|||
overlayStyle: ['tbOverlayStyle', () => this.overlayStyle], |
|||
tbPopoverBackdrop: ['tbBackdrop', () => this.tbPopoverBackdrop] |
|||
}; |
|||
|
|||
(keys || Object.keys(mappingProperties).filter(key => !key.startsWith('directive'))).forEach( |
|||
(property: any) => { |
|||
if (mappingProperties[property]) { |
|||
const [name, valueFn] = mappingProperties[property]; |
|||
this.updateComponentValue(name, valueFn()); |
|||
} |
|||
} |
|||
); |
|||
|
|||
this.component?.updateByDirective(); |
|||
} |
|||
|
|||
|
|||
private initProperties(): void { |
|||
this.updatePropertiesByKeys(); |
|||
} |
|||
|
|||
private updateComponentValue(key: string, value: any): void { |
|||
if (typeof value !== 'undefined') { |
|||
// @ts-ignore
|
|||
this.component[key] = value; |
|||
} |
|||
} |
|||
|
|||
private delayEnterLeave(isOrigin: boolean, isEnter: boolean, delay: number = -1): void { |
|||
if (this.delayTimer) { |
|||
this.clearTogglingTimer(); |
|||
} else if (delay > 0) { |
|||
this.delayTimer = setTimeout(() => { |
|||
this.delayTimer = undefined; |
|||
isEnter ? this.show() : this.hide(); |
|||
}, delay * 1000); |
|||
} else { |
|||
// `isOrigin` is used due to the tooltip will not hide immediately
|
|||
// (may caused by the fade-out animation).
|
|||
isEnter && isOrigin ? this.show() : this.hide(); |
|||
} |
|||
} |
|||
|
|||
private removeTriggerListeners(): void { |
|||
this.triggerDisposables.forEach(dispose => dispose()); |
|||
this.triggerDisposables.length = 0; |
|||
} |
|||
|
|||
private clearTogglingTimer(): void { |
|||
if (this.delayTimer) { |
|||
clearTimeout(this.delayTimer); |
|||
this.delayTimer = undefined; |
|||
} |
|||
} |
|||
} |
|||
|
|||
@Injectable() |
|||
export class TbPopoverService { |
|||
|
|||
private popoverWithTriggers: PopoverWithTrigger[] = []; |
|||
|
|||
componentFactory: ComponentFactory<TbPopoverComponent> = this.resolver.resolveComponentFactory(TbPopoverComponent); |
|||
|
|||
constructor(private resolver: ComponentFactoryResolver) { |
|||
} |
|||
|
|||
hasPopover(trigger: Element): boolean { |
|||
const res = this.findPopoverByTrigger(trigger); |
|||
return res !== null; |
|||
} |
|||
|
|||
hidePopover(trigger: Element): boolean { |
|||
const component: TbPopoverComponent = this.findPopoverByTrigger(trigger); |
|||
if (component && component.tbVisible) { |
|||
component.hide(); |
|||
return true; |
|||
} else { |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
displayPopover<T>(trigger: Element, renderer: Renderer2, hostView: ViewContainerRef, |
|||
componentType: Type<T>, preferredPlacement: PopoverPlacement = 'top', hideOnClickOutside = true, |
|||
injector?: Injector, context?: any, popoverStyle: any = {}, style?: any): TbPopoverComponent { |
|||
const componentRef = hostView.createComponent(this.componentFactory); |
|||
const component = componentRef.instance; |
|||
this.popoverWithTriggers.push({ |
|||
trigger, |
|||
popoverComponent: component |
|||
}); |
|||
renderer.removeChild( |
|||
renderer.parentNode(trigger), |
|||
componentRef.location.nativeElement |
|||
); |
|||
const originElementRef = new ElementRef(trigger); |
|||
component.setOverlayOrigin({ elementRef: originElementRef }); |
|||
component.tbPlacement = preferredPlacement; |
|||
component.tbComponentFactory = this.resolver.resolveComponentFactory(componentType); |
|||
component.tbComponentInjector = injector; |
|||
component.tbComponentContext = context; |
|||
component.tbPopoverInnerStyle = popoverStyle; |
|||
component.tbComponentStyle = style; |
|||
component.tbHideOnClickOutside = hideOnClickOutside; |
|||
component.tbVisibleChange.subscribe((visible: boolean) => { |
|||
if (!visible) { |
|||
component.tbAnimationDone.subscribe(() => { |
|||
componentRef.destroy(); |
|||
}); |
|||
} |
|||
}); |
|||
component.tbDestroy.subscribe(() => { |
|||
this.removePopoverByComponent(component); |
|||
}); |
|||
component.show(); |
|||
return component; |
|||
} |
|||
|
|||
toggleHelpPopover(trigger: Element, renderer: Renderer2, hostView: ViewContainerRef, helpId: string, |
|||
visibleFn: (visible: boolean) => void, readyFn: (ready: boolean) => void) { |
|||
if (this.hasPopover(trigger)) { |
|||
this.hidePopover(trigger); |
|||
} else { |
|||
readyFn(false); |
|||
const injector = Injector.create({ |
|||
parent: hostView.injector, providers: [] |
|||
}); |
|||
const componentRef = hostView.createComponent(this.componentFactory); |
|||
const component = componentRef.instance; |
|||
this.popoverWithTriggers.push({ |
|||
trigger, |
|||
popoverComponent: component |
|||
}); |
|||
renderer.removeChild( |
|||
renderer.parentNode(trigger), |
|||
componentRef.location.nativeElement |
|||
); |
|||
const originElementRef = new ElementRef(trigger); |
|||
component.tbAnimationState = 'void'; |
|||
component.tbOverlayStyle = { opacity: '0' }; |
|||
component.setOverlayOrigin({ elementRef: originElementRef }); |
|||
component.tbPlacement = 'bottom'; |
|||
component.tbComponentFactory = this.resolver.resolveComponentFactory(HelpMarkdownComponent); |
|||
component.tbComponentInjector = injector; |
|||
component.tbComponentContext = { |
|||
helpId, |
|||
visible: true |
|||
}; |
|||
component.tbHideOnClickOutside = true; |
|||
component.tbVisibleChange.subscribe((visible: boolean) => { |
|||
if (!visible) { |
|||
visibleFn(false); |
|||
component.tbAnimationDone.subscribe(() => { |
|||
componentRef.destroy(); |
|||
}); |
|||
} |
|||
}); |
|||
component.tbDestroy.subscribe(() => { |
|||
this.removePopoverByComponent(component); |
|||
}); |
|||
const showHelpMarkdownComponent = () => { |
|||
component.tbOverlayStyle = { opacity: '1' }; |
|||
component.tbAnimationState = 'active'; |
|||
component.updatePosition(); |
|||
readyFn(true); |
|||
setTimeout(() => { |
|||
component.updatePosition(); |
|||
}); |
|||
}; |
|||
const setupHelpMarkdownComponent = (helpMarkdownComponent: HelpMarkdownComponent) => { |
|||
if (helpMarkdownComponent.isMarkdownReady) { |
|||
showHelpMarkdownComponent(); |
|||
} else { |
|||
helpMarkdownComponent.markdownReady.subscribe(() => { |
|||
showHelpMarkdownComponent(); |
|||
}); |
|||
} |
|||
}; |
|||
if (component.tbComponentRef) { |
|||
setupHelpMarkdownComponent(component.tbComponentRef.instance); |
|||
} else { |
|||
component.tbComponentChange.subscribe((helpMarkdownComponentRef) => { |
|||
setupHelpMarkdownComponent(helpMarkdownComponentRef.instance); |
|||
}); |
|||
} |
|||
component.show(); |
|||
visibleFn(true); |
|||
} |
|||
} |
|||
|
|||
private findPopoverByTrigger(trigger: Element): TbPopoverComponent | null { |
|||
const res = this.popoverWithTriggers.find(val => this.elementsAreEqualOrDescendant(trigger, val.trigger)); |
|||
if (res) { |
|||
return res.popoverComponent; |
|||
} else { |
|||
return null; |
|||
} |
|||
} |
|||
|
|||
private removePopoverByComponent(component: TbPopoverComponent): void { |
|||
const index = this.popoverWithTriggers.findIndex(val => val.popoverComponent === component); |
|||
if (index > -1) { |
|||
this.popoverWithTriggers.splice(index, 1); |
|||
} |
|||
} |
|||
|
|||
private elementsAreEqualOrDescendant(element1: Element, element2: Element): boolean { |
|||
return element1 === element2 || element1.contains(element2) || element2.contains(element1); |
|||
} |
|||
} |
|||
|
|||
|
|||
@Component({ |
|||
selector: 'tb-popover', |
|||
exportAs: 'tbPopoverComponent', |
|||
animations: [popoverMotion], |
|||
changeDetection: ChangeDetectionStrategy.OnPush, |
|||
encapsulation: ViewEncapsulation.None, |
|||
styleUrls: ['./popover.component.scss'], |
|||
template: ` |
|||
<ng-template |
|||
#overlay="cdkConnectedOverlay" |
|||
cdkConnectedOverlay |
|||
[cdkConnectedOverlayHasBackdrop]="hasBackdrop" |
|||
[cdkConnectedOverlayOrigin]="origin" |
|||
[cdkConnectedOverlayPositions]="positions" |
|||
[cdkConnectedOverlayOpen]="visible" |
|||
[cdkConnectedOverlayPush]="true" |
|||
(overlayOutsideClick)="onClickOutside($event)" |
|||
(detach)="hide()" |
|||
(positionChange)="onPositionChange($event)" |
|||
> |
|||
<div #popoverRoot [@popoverMotion]="tbAnimationState" |
|||
(@popoverMotion.done)="animationDone()"> |
|||
<div |
|||
class="tb-popover" |
|||
[class.tb-popover-rtl]="dir === 'rtl'" |
|||
[ngClass]="classMap" |
|||
[ngStyle]="tbOverlayStyle" |
|||
> |
|||
<div class="tb-popover-content"> |
|||
<div class="tb-popover-arrow"> |
|||
<span class="tb-popover-arrow-content"></span> |
|||
</div> |
|||
<div class="tb-popover-inner" [ngStyle]="tbPopoverInnerStyle" role="tooltip"> |
|||
<div class="tb-popover-close-button" (click)="closeButtonClick($event)">×</div> |
|||
<div> |
|||
<div class="tb-popover-inner-content"> |
|||
<ng-container *ngIf="tbContent"> |
|||
<ng-container *tbStringTemplateOutlet="tbContent">{{ tbContent }}</ng-container> |
|||
</ng-container> |
|||
<ng-container *ngIf="tbComponentFactory" |
|||
[tbComponentOutlet]="tbComponentFactory" |
|||
[tbComponentInjector]="tbComponentInjector" |
|||
[tbComponentOutletContext]="tbComponentContext" |
|||
(componentChange)="onComponentChange($event)" |
|||
[tbComponentStyle]="tbComponentStyle"> |
|||
</ng-container> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</ng-template> |
|||
` |
|||
}) |
|||
export class TbPopoverComponent implements OnDestroy, OnInit { |
|||
|
|||
@ViewChild('overlay', { static: false }) overlay!: CdkConnectedOverlay; |
|||
@ViewChild('popoverRoot', { static: false }) popoverRoot!: ElementRef<HTMLElement>; |
|||
|
|||
tbContent: string | TemplateRef<void> | null = null; |
|||
tbComponentFactory: ComponentFactory<any> | null = null; |
|||
tbComponentRef: ComponentRef<any> | null = null; |
|||
tbComponentContext: any; |
|||
tbComponentInjector: Injector | null = null; |
|||
tbComponentStyle: { [klass: string]: any } = {}; |
|||
tbOverlayClassName!: string; |
|||
tbOverlayStyle: { [klass: string]: any } = {}; |
|||
tbPopoverInnerStyle: { [klass: string]: any } = {}; |
|||
tbBackdrop = false; |
|||
tbMouseEnterDelay?: number; |
|||
tbMouseLeaveDelay?: number; |
|||
tbHideOnClickOutside = true; |
|||
|
|||
tbAnimationState = 'active'; |
|||
|
|||
tbVisibleChange = new Subject<boolean>(); |
|||
tbAnimationDone = new Subject<void>(); |
|||
tbComponentChange = new Subject<ComponentRef<any>>(); |
|||
tbDestroy = new Subject<void>(); |
|||
|
|||
set tbVisible(value: boolean) { |
|||
const visible = value; |
|||
if (this.visible !== visible) { |
|||
this.visible = visible; |
|||
this.tbVisibleChange.next(visible); |
|||
} |
|||
} |
|||
|
|||
get tbVisible(): boolean { |
|||
return this.visible; |
|||
} |
|||
|
|||
visible = false; |
|||
|
|||
set tbHidden(value: boolean) { |
|||
const hidden = value; |
|||
if (this.hidden !== hidden) { |
|||
this.hidden = hidden; |
|||
if (this.hidden) { |
|||
this.renderer.setStyle(this.popoverRoot.nativeElement, 'width', this.popoverRoot.nativeElement.offsetWidth + 'px'); |
|||
this.renderer.setStyle(this.popoverRoot.nativeElement, 'height', this.popoverRoot.nativeElement.offsetHeight + 'px'); |
|||
} else { |
|||
setTimeout(() => { |
|||
this.renderer.removeStyle(this.popoverRoot.nativeElement, 'width'); |
|||
this.renderer.removeStyle(this.popoverRoot.nativeElement, 'height'); |
|||
}); |
|||
} |
|||
this.updateStyles(); |
|||
this.cdr.markForCheck(); |
|||
} |
|||
} |
|||
|
|||
get tbHidden(): boolean { |
|||
return this.hidden; |
|||
} |
|||
|
|||
hidden = false; |
|||
lastIsIntersecting = true; |
|||
|
|||
set tbTrigger(value: TbPopoverTrigger) { |
|||
this.trigger = value; |
|||
} |
|||
|
|||
get tbTrigger(): TbPopoverTrigger { |
|||
return this.trigger; |
|||
} |
|||
|
|||
protected trigger: TbPopoverTrigger = 'hover'; |
|||
|
|||
set tbPlacement(value: PopoverPlacement | PopoverPlacement[]) { |
|||
if (typeof value === 'string') { |
|||
this.positions = [POSITION_MAP[value], ...DEFAULT_POPOVER_POSITIONS]; |
|||
} else { |
|||
const preferredPosition = value.map(placement => POSITION_MAP[placement]); |
|||
this.positions = [...preferredPosition, ...DEFAULT_POPOVER_POSITIONS]; |
|||
} |
|||
} |
|||
|
|||
get hasBackdrop(): boolean { |
|||
return this.tbTrigger === 'click' ? this.tbBackdrop : false; |
|||
} |
|||
|
|||
preferredPlacement: PopoverPlacement = 'top'; |
|||
origin!: CdkOverlayOrigin; |
|||
public dir: Direction = 'ltr'; |
|||
classMap: { [klass: string]: any } = {}; |
|||
positions: ConnectionPositionPair[] = [...DEFAULT_POPOVER_POSITIONS]; |
|||
private parentScrollSubscription: Subscription = null; |
|||
private intersectionObserver = new IntersectionObserver((entries) => { |
|||
if (this.lastIsIntersecting !== entries[0].isIntersecting) { |
|||
this.lastIsIntersecting = entries[0].isIntersecting; |
|||
this.updateStyles(); |
|||
this.cdr.markForCheck(); |
|||
} |
|||
}, {threshold: [0.5]}); |
|||
|
|||
constructor( |
|||
public cdr: ChangeDetectorRef, |
|||
private renderer: Renderer2, |
|||
@Optional() private directionality: Directionality |
|||
) {} |
|||
|
|||
ngOnInit(): void { |
|||
this.directionality.change?.pipe(takeUntil(this.tbDestroy)).subscribe((direction: Direction) => { |
|||
this.dir = direction; |
|||
this.cdr.detectChanges(); |
|||
}); |
|||
|
|||
this.dir = this.directionality.value; |
|||
} |
|||
|
|||
ngOnDestroy(): void { |
|||
if (this.parentScrollSubscription) { |
|||
this.parentScrollSubscription.unsubscribe(); |
|||
this.parentScrollSubscription = null; |
|||
} |
|||
if (this.origin) { |
|||
const el = this.origin.elementRef.nativeElement; |
|||
this.intersectionObserver.unobserve(el); |
|||
} |
|||
this.intersectionObserver.disconnect(); |
|||
this.intersectionObserver = null; |
|||
this.tbVisibleChange.complete(); |
|||
this.tbAnimationDone.complete(); |
|||
this.tbDestroy.next(); |
|||
this.tbDestroy.complete(); |
|||
} |
|||
|
|||
closeButtonClick($event: Event) { |
|||
if ($event) { |
|||
$event.preventDefault(); |
|||
$event.stopPropagation(); |
|||
} |
|||
this.hide(); |
|||
} |
|||
|
|||
show(): void { |
|||
if (this.tbVisible) { |
|||
return; |
|||
} |
|||
|
|||
if (!this.isEmpty()) { |
|||
this.tbVisible = true; |
|||
this.tbVisibleChange.next(true); |
|||
this.cdr.detectChanges(); |
|||
} |
|||
|
|||
if (this.origin && this.overlay && this.overlay.overlayRef) { |
|||
if (this.overlay.overlayRef.getDirection() === 'rtl') { |
|||
this.overlay.overlayRef.setDirection('ltr'); |
|||
} |
|||
const el = this.origin.elementRef.nativeElement; |
|||
this.parentScrollSubscription = onParentScrollOrWindowResize(el).subscribe(() => { |
|||
this.overlay.overlayRef.updatePosition(); |
|||
}); |
|||
this.intersectionObserver.observe(el); |
|||
} |
|||
} |
|||
|
|||
hide(): void { |
|||
if (!this.tbVisible) { |
|||
return; |
|||
} |
|||
if (this.parentScrollSubscription) { |
|||
this.parentScrollSubscription.unsubscribe(); |
|||
this.parentScrollSubscription = null; |
|||
} |
|||
if (this.origin) { |
|||
const el = this.origin.elementRef.nativeElement; |
|||
this.intersectionObserver.unobserve(el); |
|||
} |
|||
|
|||
this.tbVisible = false; |
|||
this.tbVisibleChange.next(false); |
|||
this.cdr.detectChanges(); |
|||
} |
|||
|
|||
updateByDirective(): void { |
|||
this.updateStyles(); |
|||
this.cdr.detectChanges(); |
|||
|
|||
Promise.resolve().then(() => { |
|||
this.updatePosition(); |
|||
this.updateVisibilityByContent(); |
|||
}); |
|||
} |
|||
|
|||
updatePosition(): void { |
|||
if (this.origin && this.overlay && this.overlay.overlayRef) { |
|||
this.overlay.overlayRef.updatePosition(); |
|||
} |
|||
} |
|||
|
|||
onPositionChange(position: ConnectedOverlayPositionChange): void { |
|||
this.preferredPlacement = getPlacementName(position); |
|||
this.updateStyles(); |
|||
this.cdr.detectChanges(); |
|||
} |
|||
|
|||
updateStyles(): void { |
|||
this.classMap = { |
|||
[this.tbOverlayClassName]: true, |
|||
[`tb-popover-placement-${this.preferredPlacement}`]: true, |
|||
['tb-popover-hidden']: this.tbHidden || !this.lastIsIntersecting |
|||
}; |
|||
} |
|||
|
|||
setOverlayOrigin(origin: CdkOverlayOrigin): void { |
|||
this.origin = origin; |
|||
this.cdr.markForCheck(); |
|||
} |
|||
|
|||
onClickOutside(event: MouseEvent): void { |
|||
if (this.tbHideOnClickOutside && !this.origin.elementRef.nativeElement.contains(event.target) && this.tbTrigger !== null) { |
|||
this.hide(); |
|||
} |
|||
} |
|||
|
|||
onComponentChange(component: ComponentRef<any>) { |
|||
this.tbComponentRef = component; |
|||
this.tbComponentChange.next(component); |
|||
} |
|||
|
|||
animationDone() { |
|||
this.tbAnimationDone.next(); |
|||
} |
|||
|
|||
private updateVisibilityByContent(): void { |
|||
if (this.isEmpty()) { |
|||
this.hide(); |
|||
} |
|||
} |
|||
|
|||
private isEmpty(): boolean { |
|||
return (this.tbComponentFactory instanceof ComponentFactory || this.tbContent instanceof TemplateRef) |
|||
? false : !isNotEmptyStr(this.tbContent); |
|||
} |
|||
} |
|||
@ -0,0 +1,95 @@ |
|||
///
|
|||
/// Copyright © 2016-2021 The Thingsboard Authors
|
|||
///
|
|||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|||
/// you may not use this file except in compliance with the License.
|
|||
/// You may obtain a copy of the License at
|
|||
///
|
|||
/// http://www.apache.org/licenses/LICENSE-2.0
|
|||
///
|
|||
/// Unless required by applicable law or agreed to in writing, software
|
|||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
/// See the License for the specific language governing permissions and
|
|||
/// limitations under the License.
|
|||
///
|
|||
|
|||
import { animate, AnimationTriggerMetadata, style, transition, trigger } from '@angular/animations'; |
|||
import { ConnectedOverlayPositionChange, ConnectionPositionPair } from '@angular/cdk/overlay'; |
|||
import { TbPopoverComponent } from '@shared/components/popover.component'; |
|||
|
|||
export const popoverMotion: AnimationTriggerMetadata = trigger('popoverMotion', [ |
|||
transition('void => active', [ |
|||
style({ opacity: 0, transform: 'scale(0.8)' }), |
|||
animate( |
|||
'0.2s cubic-bezier(0.08, 0.82, 0.17, 1)', |
|||
style({ |
|||
opacity: 1, |
|||
transform: 'scale(1)' |
|||
}) |
|||
) |
|||
]), |
|||
transition('active => void', [ |
|||
style({ opacity: 1, transform: 'scale(1)' }), |
|||
animate( |
|||
'0.2s cubic-bezier(0.78, 0.14, 0.15, 0.86)', |
|||
style({ |
|||
opacity: 0, |
|||
transform: 'scale(0.8)' |
|||
}) |
|||
) |
|||
]) |
|||
]); |
|||
|
|||
export const PopoverPlacements = ['top', 'topLeft', 'topRight', 'right', 'rightTop', 'rightBottom', 'bottom', 'bottomLeft', 'bottomRight', 'left', 'leftTop', 'leftBottom'] as const; |
|||
type PopoverPlacementTuple = typeof PopoverPlacements; |
|||
export type PopoverPlacement = PopoverPlacementTuple[number]; |
|||
|
|||
export const POSITION_MAP: { [key: string]: ConnectionPositionPair } = { |
|||
top: new ConnectionPositionPair({ originX: 'center', originY: 'top' }, { overlayX: 'center', overlayY: 'bottom' }), |
|||
topLeft: new ConnectionPositionPair({ originX: 'start', originY: 'top' }, { overlayX: 'start', overlayY: 'bottom' }), |
|||
topRight: new ConnectionPositionPair({ originX: 'end', originY: 'top' }, { overlayX: 'end', overlayY: 'bottom' }), |
|||
right: new ConnectionPositionPair({ originX: 'end', originY: 'center' }, { overlayX: 'start', overlayY: 'center' }), |
|||
rightTop: new ConnectionPositionPair({ originX: 'end', originY: 'top' }, { overlayX: 'start', overlayY: 'top' }), |
|||
rightBottom: new ConnectionPositionPair( |
|||
{ originX: 'end', originY: 'bottom' }, |
|||
{ overlayX: 'start', overlayY: 'bottom' } |
|||
), |
|||
bottom: new ConnectionPositionPair({ originX: 'center', originY: 'bottom' }, { overlayX: 'center', overlayY: 'top' }), |
|||
bottomLeft: new ConnectionPositionPair( |
|||
{ originX: 'start', originY: 'bottom' }, |
|||
{ overlayX: 'start', overlayY: 'top' } |
|||
), |
|||
bottomRight: new ConnectionPositionPair({ originX: 'end', originY: 'bottom' }, { overlayX: 'end', overlayY: 'top' }), |
|||
left: new ConnectionPositionPair({ originX: 'start', originY: 'center' }, { overlayX: 'end', overlayY: 'center' }), |
|||
leftTop: new ConnectionPositionPair({ originX: 'start', originY: 'top' }, { overlayX: 'end', overlayY: 'top' }), |
|||
leftBottom: new ConnectionPositionPair( |
|||
{ originX: 'start', originY: 'bottom' }, |
|||
{ overlayX: 'end', overlayY: 'bottom' } |
|||
) |
|||
}; |
|||
|
|||
export const DEFAULT_POPOVER_POSITIONS = [POSITION_MAP.top, POSITION_MAP.right, POSITION_MAP.bottom, POSITION_MAP.left]; |
|||
|
|||
export function getPlacementName(position: ConnectedOverlayPositionChange): PopoverPlacement | undefined { |
|||
for (const placement in POSITION_MAP) { |
|||
if ( |
|||
position.connectionPair.originX === POSITION_MAP[placement].originX && |
|||
position.connectionPair.originY === POSITION_MAP[placement].originY && |
|||
position.connectionPair.overlayX === POSITION_MAP[placement].overlayX && |
|||
position.connectionPair.overlayY === POSITION_MAP[placement].overlayY |
|||
) { |
|||
return placement as PopoverPlacement; |
|||
} |
|||
} |
|||
return undefined; |
|||
} |
|||
|
|||
export interface PropertyMapping { |
|||
[key: string]: [string, () => unknown]; |
|||
} |
|||
|
|||
export interface PopoverWithTrigger { |
|||
trigger: Element; |
|||
popoverComponent: TbPopoverComponent; |
|||
} |
|||
|
After Width: | Height: | Size: 500 B |
@ -0,0 +1,5 @@ |
|||
#### Show cell button action JavaScript Function |
|||
|
|||
```javascript |
|||
return data.entityName === 'Test device'; {:copy-code} |
|||
``` |
|||
@ -0,0 +1,41 @@ |
|||
#### Show widget header action function |
|||
|
|||
<div class="divider"></div> |
|||
<br/> |
|||
|
|||
*function (widgetContext, data): boolean* |
|||
|
|||
JavaScript function evaluating whether to display particular widget header action. |
|||
|
|||
**Parameters:** |
|||
|
|||
<ul> |
|||
<li><b>widgetContext</b> - A reference to <a href="https://github.com/thingsboard/thingsboard/blob/5bb6403407aa4898084832d6698aa9ea6d484889/ui-ngx/src/app/modules/home/models/widget-component.models.ts#L107" target="_blank">WidgetContext</a> that has all necessary API<br/> |
|||
and data used by widget instance. |
|||
</li> |
|||
<li><b>data</b> - An array of <a href="https://github.com/thingsboard/thingsboard/blob/5bb6403407aa4898084832d6698aa9ea6d484889/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-models.ts#L108" target="_blank">FormattedData</a> objects.<br/> |
|||
Each object represents basic entity properties (ex. <code>entityId</code>, <code>entityName</code>)<br/>and provides access to other entity attributes/timeseries declared in widget datasource configuration. |
|||
</li> |
|||
</ul> |
|||
|
|||
**Returns:** |
|||
|
|||
`true` if header action should be displayed, `false` otherwise. |
|||
|
|||
<div class="divider"></div> |
|||
|
|||
##### Examples |
|||
|
|||
* Display action only for customer users: |
|||
|
|||
```javascript |
|||
return widgetContext.currentUser.authority === 'CUSTOMER_USER'; |
|||
{:copy-code} |
|||
``` |
|||
|
|||
* Display action only if the first entity is device and has type `thermostat`: |
|||
|
|||
```javascript |
|||
return data[0] && data[0].entityType === 'DEVICE' && data[0].Type === 'thermostat'; |
|||
{:copy-code} |
|||
``` |
|||
@ -0,0 +1,12 @@ |
|||
#### Tooltip value format JavaScript Function |
|||
|
|||
##### Input arguments |
|||
|
|||
- value - value to format |
|||
|
|||
```javascript |
|||
return value; |
|||
{:copy-code} |
|||
``` |
|||
|
|||
##### Examples |
|||
Loading…
Reference in new issue