[3.0] Add inputs widgets (#2526)
* Add location widget * Fix translate, clear code * Fix translate * Add date input widgets * Add image input widgets * Init web camera input widget * Add functional web camera input widget * Add styles to webcamera iputs widget * Add link code
This commit is contained in:
parent
adc7194669
commit
1af55e8400
File diff suppressed because one or more lines are too long
@ -0,0 +1,74 @@
|
|||||||
|
<!--
|
||||||
|
|
||||||
|
Copyright © 2016-2020 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 fxLayout="column" fxLayoutAlign="center center" class="tb-web-camera" tb-fullscreen [fullscreen]="isShowCamera">
|
||||||
|
<div [fxShow]="isEntityDetected && dataKeyDetected && isCameraSupport && isDeviceDetect" fxFlexFill>
|
||||||
|
<div [fxShow]="!isShowCamera" fxLayout="column" fxLayoutAlign="space-between center" fxFlexFill>
|
||||||
|
<div class="tb-web-camera__last-photo" fxFlex>
|
||||||
|
<span [fxShow]="!lastPhoto" class="tb-web-camera__last-photo_text" translate>widgets.input-widgets.no-image</span>
|
||||||
|
<img [fxShow]="lastPhoto" class="tb-web-camera__last-photo_img" [src]="lastPhoto" alt="last photo"/>
|
||||||
|
</div>
|
||||||
|
<button mat-raised-button color="primary" (click)="takePhoto()">
|
||||||
|
{{ "widgets.input-widgets.take-photo" | translate }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div [fxShow]="isShowCamera" fxLayout="column" fxLayoutAlign="center center" class="camera-container">
|
||||||
|
<div class="camera" [fxShow]="!isPreviewPhoto">
|
||||||
|
<video autoplay muted playsinline class="camera-stream" #videoStream></video>
|
||||||
|
<div class="camera-controls" fxLayout="row wrap" fxLayoutAlign="space-between end">
|
||||||
|
<div fxFlex></div>
|
||||||
|
<button mat-mini-fab color="primary" (click)="switchWebCamera()" [disabled]="singleDevice">
|
||||||
|
<mat-icon>switch_camera</mat-icon>
|
||||||
|
</button>
|
||||||
|
<button mat-fab color="accent" (click)="createPhoto()">
|
||||||
|
<mat-icon>photo_camera</mat-icon>
|
||||||
|
</button>
|
||||||
|
<button mat-mini-fab color="primary" (click)="closeCamera()">
|
||||||
|
<mat-icon>close</mat-icon>
|
||||||
|
</button>
|
||||||
|
<div fxFlex></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="camera" [fxShow]="isPreviewPhoto">
|
||||||
|
<img alt="preview photo" class="camera-stream" [src]="previewPhoto">
|
||||||
|
<canvas #canvas style="display:none;"></canvas>
|
||||||
|
<div class="camera-controls" fxLayout="row" fxLayoutAlign="space-between end">
|
||||||
|
<div fxFlex></div>
|
||||||
|
<button mat-fab color="primary" [disabled]="updatePhoto" (click)="cancelPhoto()">
|
||||||
|
<mat-icon>close</mat-icon>
|
||||||
|
</button>
|
||||||
|
<button mat-fab color="accent" [disabled]="updatePhoto" (click)="savePhoto()">
|
||||||
|
<mat-icon>check</mat-icon>
|
||||||
|
</button>
|
||||||
|
<div fxFlex></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="message-text" [fxHide]="isEntityDetected">
|
||||||
|
{{ 'widgets.input-widgets.no-entity-selected' | translate }}
|
||||||
|
</div>
|
||||||
|
<div class="message-text" [fxShow]="isEntityDetected && !dataKeyDetected">
|
||||||
|
{{ 'widgets.input-widgets.no-datakey-selected' | translate }}
|
||||||
|
</div>
|
||||||
|
<div class="message-text" [fxShow]="isEntityDetected && dataKeyDetected && !isCameraSupport">
|
||||||
|
{{ 'widgets.input-widgets.no-support-web-camera' | translate }}
|
||||||
|
</div>
|
||||||
|
<div class="message-text" [fxShow]="isEntityDetected && dataKeyDetected && isCameraSupport && !isDeviceDetect">
|
||||||
|
{{ 'widgets.input-widgets.no-support-web-camera' | translate }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@ -0,0 +1,74 @@
|
|||||||
|
/**
|
||||||
|
* Copyright © 2016-2020 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-web-camera {
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
&__last-photo {
|
||||||
|
width: 100%;
|
||||||
|
margin: 5px 0;
|
||||||
|
text-align: center;
|
||||||
|
border: solid 1px;
|
||||||
|
|
||||||
|
&_text {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
margin-top: -.625em;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
&_img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.camera-container{
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.camera {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.camera-stream {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.camera-controls {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0 5px 5px;
|
||||||
|
|
||||||
|
.mat-button-base{
|
||||||
|
margin: 6px 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-text {
|
||||||
|
font-size: 18px;
|
||||||
|
color: #a0a0a0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,274 @@
|
|||||||
|
///
|
||||||
|
/// Copyright © 2016-2020 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,
|
||||||
|
Inject,
|
||||||
|
Input,
|
||||||
|
NgZone,
|
||||||
|
OnDestroy,
|
||||||
|
OnInit,
|
||||||
|
ViewChild,
|
||||||
|
ViewEncapsulation
|
||||||
|
} from '@angular/core';
|
||||||
|
import { PageComponent } from '@shared/components/page.component';
|
||||||
|
import { WidgetContext } from '@home/models/widget-component.models';
|
||||||
|
import { Store } from '@ngrx/store';
|
||||||
|
import { AppState } from '@core/core.state';
|
||||||
|
import { Overlay } from '@angular/cdk/overlay';
|
||||||
|
import { UtilsService } from '@core/services/utils.service';
|
||||||
|
import { Datasource, DatasourceData, DatasourceType } from '@shared/models/widget.models';
|
||||||
|
import { WINDOW } from '@core/services/window.service';
|
||||||
|
import { AttributeService } from '@core/http/attribute.service';
|
||||||
|
import { EntityId } from '@shared/models/id/entity-id';
|
||||||
|
import { AttributeScope, DataKeyType } from '@shared/models/telemetry/telemetry.models';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
|
||||||
|
interface WebCameraInputWidgetSettings {
|
||||||
|
widgetTitle: string;
|
||||||
|
imageQuality: number;
|
||||||
|
imageFormat: string;
|
||||||
|
maxWidth: number;
|
||||||
|
maxHeight: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'tb-web-camera-widget',
|
||||||
|
templateUrl: './web-camera-input.component.html',
|
||||||
|
styleUrls: ['./web-camera-input.component.scss'],
|
||||||
|
encapsulation: ViewEncapsulation.None
|
||||||
|
})
|
||||||
|
export class WebCameraInputWidgetComponent extends PageComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
|
constructor(@Inject(WINDOW) private window: Window,
|
||||||
|
protected store: Store<AppState>,
|
||||||
|
private elementRef: ElementRef,
|
||||||
|
private ngZone: NgZone,
|
||||||
|
private overlay: Overlay,
|
||||||
|
private utils: UtilsService,
|
||||||
|
private attributeService: AttributeService,
|
||||||
|
) {
|
||||||
|
super(store);
|
||||||
|
}
|
||||||
|
|
||||||
|
public get videoElement() {
|
||||||
|
return this.videoStreamRef.nativeElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get canvasElement() {
|
||||||
|
return this.canvasRef.nativeElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get videoWidth() {
|
||||||
|
const videoRatio = this.getVideoAspectRatio();
|
||||||
|
return Math.min(this.width, this.height * videoRatio);
|
||||||
|
}
|
||||||
|
|
||||||
|
public get videoHeight() {
|
||||||
|
const videoRatio = this.getVideoAspectRatio();
|
||||||
|
return Math.min(this.height, this.width / videoRatio);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DEFAULT_IMAGE_TYPE = 'image/jpeg';
|
||||||
|
private static DEFAULT_IMAGE_QUALITY = 0.92;
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
ctx: WidgetContext;
|
||||||
|
|
||||||
|
@ViewChild('videoStream', {static: true}) videoStreamRef: ElementRef<HTMLVideoElement>;
|
||||||
|
@ViewChild('canvas', {static: true}) canvasRef: ElementRef<HTMLCanvasElement>;
|
||||||
|
|
||||||
|
private videoInputsIndex = 0;
|
||||||
|
private settings: WebCameraInputWidgetSettings;
|
||||||
|
private datasource: Datasource;
|
||||||
|
private width = 640;
|
||||||
|
private height = 480;
|
||||||
|
private availableVideoInputs: MediaDeviceInfo[];
|
||||||
|
private mediaStream: MediaStream;
|
||||||
|
|
||||||
|
isEntityDetected = false;
|
||||||
|
dataKeyDetected = false;
|
||||||
|
isCameraSupport = false;
|
||||||
|
isDeviceDetect = false;
|
||||||
|
isShowCamera = false;
|
||||||
|
isPreviewPhoto = false;
|
||||||
|
singleDevice = true;
|
||||||
|
updatePhoto = false;
|
||||||
|
previewPhoto: any;
|
||||||
|
lastPhoto: any;
|
||||||
|
|
||||||
|
private static hasGetUserMedia(): boolean {
|
||||||
|
return !!(window.navigator.mediaDevices && window.navigator.mediaDevices.getUserMedia);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static getAvailableVideoInputs(): Promise<MediaDeviceInfo[]> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
navigator.mediaDevices.enumerateDevices()
|
||||||
|
.then((devices: MediaDeviceInfo[]) => {
|
||||||
|
resolve(devices.filter((device: MediaDeviceInfo) => device.kind === 'videoinput'));
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
reject(err.message || err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.ctx.$scope.webCameraInputWidget = this;
|
||||||
|
this.settings = this.ctx.settings;
|
||||||
|
this.datasource = this.ctx.datasources[0];
|
||||||
|
|
||||||
|
if (this.settings.widgetTitle && this.settings.widgetTitle.length) {
|
||||||
|
this.ctx.widgetTitle = this.utils.customTranslation(this.settings.widgetTitle, this.settings.widgetTitle);
|
||||||
|
} else {
|
||||||
|
this.ctx.widgetTitle = this.ctx.widgetConfig.title;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.width = this.settings.maxWidth ? this.settings.maxWidth : 640;
|
||||||
|
this.height = this.settings.maxHeight ? this.settings.maxWidth : 480;
|
||||||
|
|
||||||
|
if (this.datasource.type === DatasourceType.entity) {
|
||||||
|
if (this.datasource.entityType && this.datasource.entityId) {
|
||||||
|
this.isEntityDetected = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (this.datasource.dataKeys.length) {
|
||||||
|
this.dataKeyDetected = true;
|
||||||
|
}
|
||||||
|
this.detectAvailableDevices();
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.stopMediaTracks();
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateWidgetData(data: Array<DatasourceData>) {
|
||||||
|
const keyData = data[0].data;
|
||||||
|
if (keyData && keyData.length) {
|
||||||
|
this.lastPhoto = keyData[0][1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public onDataUpdated() {
|
||||||
|
this.ngZone.run(() => {
|
||||||
|
this.updateWidgetData(this.ctx.defaultSubscription.data);
|
||||||
|
this.ctx.detectChanges();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private detectAvailableDevices(): void {
|
||||||
|
if (WebCameraInputWidgetComponent.hasGetUserMedia()) {
|
||||||
|
this.isCameraSupport = true;
|
||||||
|
WebCameraInputWidgetComponent.getAvailableVideoInputs().then((devices) => {
|
||||||
|
this.isDeviceDetect = !!devices.length;
|
||||||
|
this.singleDevice = devices.length < 2;
|
||||||
|
this.availableVideoInputs = devices;
|
||||||
|
this.ctx.detectChanges();
|
||||||
|
}, () => {
|
||||||
|
this.availableVideoInputs = [];
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private getVideoAspectRatio(): number {
|
||||||
|
if (this.videoElement.videoWidth && this.videoElement.videoWidth > 0 &&
|
||||||
|
this.videoElement.videoHeight && this.videoElement.videoHeight > 0) {
|
||||||
|
return this.videoElement.videoWidth / this.videoElement.videoHeight;
|
||||||
|
}
|
||||||
|
return this.width / this.height;
|
||||||
|
}
|
||||||
|
|
||||||
|
private stopMediaTracks() {
|
||||||
|
if (this.mediaStream && this.mediaStream.getTracks) {
|
||||||
|
this.mediaStream.getTracks()
|
||||||
|
.forEach((track: MediaStreamTrack) => track.stop());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
takePhoto() {
|
||||||
|
this.isShowCamera = true;
|
||||||
|
this.initWebCamera(this.availableVideoInputs[this.videoInputsIndex].deviceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
closeCamera() {
|
||||||
|
this.stopMediaTracks();
|
||||||
|
this.videoElement.srcObject = null;
|
||||||
|
this.isShowCamera = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
cancelPhoto() {
|
||||||
|
this.isPreviewPhoto = false;
|
||||||
|
this.previewPhoto = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
savePhoto() {
|
||||||
|
this.updatePhoto = true;
|
||||||
|
let task: Observable<any>;
|
||||||
|
const entityId: EntityId = {
|
||||||
|
entityType: this.datasource.entityType,
|
||||||
|
id: this.datasource.entityId
|
||||||
|
};
|
||||||
|
const saveData = [{
|
||||||
|
key: this.datasource.dataKeys[0].name,
|
||||||
|
value: this.previewPhoto
|
||||||
|
}];
|
||||||
|
if (this.datasource.dataKeys[0].type === DataKeyType.attribute) {
|
||||||
|
task = this.attributeService.saveEntityAttributes(entityId, AttributeScope.SERVER_SCOPE, saveData);
|
||||||
|
} else if (this.datasource.dataKeys[0].type === DataKeyType.timeseries) {
|
||||||
|
task = this.attributeService.saveEntityTimeseries(entityId, 'scope', saveData);
|
||||||
|
}
|
||||||
|
task.subscribe(() => {
|
||||||
|
this.isPreviewPhoto = false;
|
||||||
|
this.updatePhoto = false;
|
||||||
|
this.closeCamera();
|
||||||
|
}, () => {
|
||||||
|
this.updatePhoto = false;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
switchWebCamera() {
|
||||||
|
this.videoInputsIndex = (this.videoInputsIndex + 1) % this.availableVideoInputs.length;
|
||||||
|
this.stopMediaTracks();
|
||||||
|
this.initWebCamera(this.availableVideoInputs[this.videoInputsIndex].deviceId)
|
||||||
|
}
|
||||||
|
|
||||||
|
createPhoto() {
|
||||||
|
this.canvasElement.width = this.videoWidth;
|
||||||
|
this.canvasElement.height = this.videoHeight;
|
||||||
|
this.canvasElement.getContext('2d').drawImage(this.videoElement, 0, 0, this.videoWidth, this.videoHeight);
|
||||||
|
|
||||||
|
const mimeType: string = this.settings.imageFormat ? this.settings.imageFormat : WebCameraInputWidgetComponent.DEFAULT_IMAGE_TYPE;
|
||||||
|
const quality: number = this.settings.imageQuality ? this.settings.imageQuality : WebCameraInputWidgetComponent.DEFAULT_IMAGE_QUALITY;
|
||||||
|
this.previewPhoto = this.canvasElement.toDataURL(mimeType, quality);
|
||||||
|
this.isPreviewPhoto = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private initWebCamera(deviceId?: string) {
|
||||||
|
if (window.navigator.mediaDevices && window.navigator.mediaDevices.getUserMedia) {
|
||||||
|
const videoTrackConstraints = {
|
||||||
|
video: {deviceId: deviceId !== '' ? {exact: deviceId} : undefined}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.navigator.mediaDevices.getUserMedia(videoTrackConstraints).then((stream: MediaStream) => {
|
||||||
|
this.mediaStream = stream;
|
||||||
|
this.videoElement.srcObject = stream;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -31,6 +31,7 @@ import {
|
|||||||
DateRangeNavigatorWidgetComponent
|
DateRangeNavigatorWidgetComponent
|
||||||
} from '@home/components/widget/lib/date-range-navigator/date-range-navigator.component';
|
} from '@home/components/widget/lib/date-range-navigator/date-range-navigator.component';
|
||||||
import { MultipleInputWidgetComponent } from './lib/multiple-input-widget.component';
|
import { MultipleInputWidgetComponent } from './lib/multiple-input-widget.component';
|
||||||
|
import { WebCameraInputWidgetComponent } from './lib/web-camera-input.component';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations:
|
declarations:
|
||||||
@ -43,7 +44,8 @@ import { MultipleInputWidgetComponent } from './lib/multiple-input-widget.compon
|
|||||||
EntitiesHierarchyWidgetComponent,
|
EntitiesHierarchyWidgetComponent,
|
||||||
DateRangeNavigatorWidgetComponent,
|
DateRangeNavigatorWidgetComponent,
|
||||||
DateRangeNavigatorPanelComponent,
|
DateRangeNavigatorPanelComponent,
|
||||||
MultipleInputWidgetComponent
|
MultipleInputWidgetComponent,
|
||||||
|
WebCameraInputWidgetComponent
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
@ -58,7 +60,8 @@ import { MultipleInputWidgetComponent } from './lib/multiple-input-widget.compon
|
|||||||
EntitiesHierarchyWidgetComponent,
|
EntitiesHierarchyWidgetComponent,
|
||||||
RpcWidgetsModule,
|
RpcWidgetsModule,
|
||||||
DateRangeNavigatorWidgetComponent,
|
DateRangeNavigatorWidgetComponent,
|
||||||
MultipleInputWidgetComponent
|
MultipleInputWidgetComponent,
|
||||||
|
WebCameraInputWidgetComponent
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
CustomDialogService
|
CustomDialogService
|
||||||
|
|||||||
@ -16,15 +16,15 @@
|
|||||||
|
|
||||||
-->
|
-->
|
||||||
<div class="tb-container">
|
<div class="tb-container">
|
||||||
<label class="tb-title">{{label}}</label>
|
<label class="tb-title" *ngIf="label">{{label}}</label>
|
||||||
<ng-container #flow="flow"
|
<ng-container #flow="flow"
|
||||||
[flowConfig]="{singleFile: true, allowDuplicateUploads: true}">
|
[flowConfig]="{singleFile: true, allowDuplicateUploads: true}">
|
||||||
<div class="tb-image-select-container">
|
<div class="tb-image-select-container">
|
||||||
<div class="tb-image-preview-container">
|
<div *ngIf="showPreview" class="tb-image-preview-container">
|
||||||
<div *ngIf="!safeImageUrl" translate>dashboard.no-image</div>
|
<div *ngIf="!safeImageUrl;else elseBlock" translate>dashboard.no-image</div>
|
||||||
<img *ngIf="safeImageUrl" class="tb-image-preview" [src]="safeImageUrl" />
|
<ng-template #elseBlock><img class="tb-image-preview" [src]="safeImageUrl" /></ng-template>
|
||||||
</div>
|
</div>
|
||||||
<div class="tb-image-clear-container">
|
<div *ngIf="showClearButton" class="tb-image-clear-container">
|
||||||
<button mat-button mat-icon-button color="primary"
|
<button mat-button mat-icon-button color="primary"
|
||||||
type="button"
|
type="button"
|
||||||
(click)="clearImage()"
|
(click)="clearImage()"
|
||||||
@ -37,8 +37,8 @@
|
|||||||
<div class="drop-area tb-flow-drop"
|
<div class="drop-area tb-flow-drop"
|
||||||
flowDrop
|
flowDrop
|
||||||
[flow]="flow.flowJs">
|
[flow]="flow.flowJs">
|
||||||
<label for="select" translate>dashboard.drop-image</label>
|
<label for="{{inputId}}" translate>dashboard.drop-image</label>
|
||||||
<input class="file-input" flowButton type="file" [flow]="flow.flowJs" [flowAttributes]="{accept: 'image/*'}" id="select">
|
<input class="file-input" flowButton type="file" [flow]="flow.flowJs" [flowAttributes]="{accept: 'image/*'}" id="{{inputId}}">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|||||||
@ -35,9 +35,9 @@ $previewSize: 100px !default;
|
|||||||
|
|
||||||
.tb-image-preview {
|
.tb-image-preview {
|
||||||
width: auto;
|
width: auto;
|
||||||
max-width: $previewSize;
|
max-width: $previewSize - 2;
|
||||||
height: auto;
|
height: auto;
|
||||||
max-height: $previewSize;
|
max-height: $previewSize - 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tb-image-preview-container {
|
.tb-image-preview-container {
|
||||||
|
|||||||
@ -14,34 +14,16 @@
|
|||||||
/// limitations under the License.
|
/// limitations under the License.
|
||||||
///
|
///
|
||||||
|
|
||||||
import { Component, ElementRef, forwardRef, Input, OnInit, ViewChild, AfterViewInit, OnDestroy } from '@angular/core';
|
import { AfterViewInit, Component, forwardRef, Input, OnDestroy, ViewChild } from '@angular/core';
|
||||||
import { PageComponent } from '@shared/components/page.component';
|
import { PageComponent } from '@shared/components/page.component';
|
||||||
import { Store } from '@ngrx/store';
|
import { Store } from '@ngrx/store';
|
||||||
import { AppState } from '@core/core.state';
|
import { AppState } from '@core/core.state';
|
||||||
import { DataKey, DatasourceType } from '@shared/models/widget.models';
|
import { ControlValueAccessor, NG_VALUE_ACCESSOR, } from '@angular/forms';
|
||||||
import {
|
import { Subscription } from 'rxjs';
|
||||||
ControlValueAccessor,
|
|
||||||
FormBuilder,
|
|
||||||
FormControl,
|
|
||||||
FormGroup,
|
|
||||||
NG_VALIDATORS,
|
|
||||||
NG_VALUE_ACCESSOR,
|
|
||||||
Validator,
|
|
||||||
Validators
|
|
||||||
} from '@angular/forms';
|
|
||||||
import { UtilsService } from '@core/services/utils.service';
|
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
|
||||||
import { MatDialog } from '@angular/material/dialog';
|
|
||||||
import { EntityService } from '@core/http/entity.service';
|
|
||||||
import { DataKeysCallbacks } from '@home/components/widget/data-keys.component.models';
|
|
||||||
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
|
|
||||||
import { Observable, of, Subscription } from 'rxjs';
|
|
||||||
import { map, mergeMap, tap } from 'rxjs/operators';
|
|
||||||
import { alarmFields } from '@shared/models/alarm.models';
|
|
||||||
import { coerceBooleanProperty } from '@angular/cdk/coercion';
|
import { coerceBooleanProperty } from '@angular/cdk/coercion';
|
||||||
import { DialogService } from '@core/services/dialog.service';
|
|
||||||
import { FlowDirective } from '@flowjs/ngx-flow';
|
import { FlowDirective } from '@flowjs/ngx-flow';
|
||||||
import { DomSanitizer, SafeUrl } from '@angular/platform-browser';
|
import { DomSanitizer, SafeUrl } from '@angular/platform-browser';
|
||||||
|
import { UtilsService } from '@core/services/utils.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'tb-image-input',
|
selector: 'tb-image-input',
|
||||||
@ -61,9 +43,11 @@ export class ImageInputComponent extends PageComponent implements AfterViewInit,
|
|||||||
label: string;
|
label: string;
|
||||||
|
|
||||||
private requiredValue: boolean;
|
private requiredValue: boolean;
|
||||||
|
|
||||||
get required(): boolean {
|
get required(): boolean {
|
||||||
return this.requiredValue;
|
return this.requiredValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Input()
|
@Input()
|
||||||
set required(value: boolean) {
|
set required(value: boolean) {
|
||||||
const newVal = coerceBooleanProperty(value);
|
const newVal = coerceBooleanProperty(value);
|
||||||
@ -75,6 +59,15 @@ export class ImageInputComponent extends PageComponent implements AfterViewInit,
|
|||||||
@Input()
|
@Input()
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
showClearButton = true;
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
showPreview = true;
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
inputId = this.utils.guid();
|
||||||
|
|
||||||
imageUrl: string;
|
imageUrl: string;
|
||||||
safeImageUrl: SafeUrl;
|
safeImageUrl: SafeUrl;
|
||||||
|
|
||||||
@ -86,6 +79,7 @@ export class ImageInputComponent extends PageComponent implements AfterViewInit,
|
|||||||
private propagateChange = null;
|
private propagateChange = null;
|
||||||
|
|
||||||
constructor(protected store: Store<AppState>,
|
constructor(protected store: Store<AppState>,
|
||||||
|
private utils: UtilsService,
|
||||||
private sanitizer: DomSanitizer) {
|
private sanitizer: DomSanitizer) {
|
||||||
super(store);
|
super(store);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -75,6 +75,7 @@
|
|||||||
import './zone-flags';
|
import './zone-flags';
|
||||||
import 'zone.js/dist/zone'; // Included with Angular CLI.
|
import 'zone.js/dist/zone'; // Included with Angular CLI.
|
||||||
import 'core-js/es/array';
|
import 'core-js/es/array';
|
||||||
|
import moment from 'moment';
|
||||||
|
|
||||||
/***************************************************************************************************
|
/***************************************************************************************************
|
||||||
* APPLICATION IMPORTS
|
* APPLICATION IMPORTS
|
||||||
@ -99,6 +100,7 @@ const tinycolor = tinycolor_;
|
|||||||
|
|
||||||
(window as any).tinycolor = tinycolor;
|
(window as any).tinycolor = tinycolor;
|
||||||
(window as any).cssjs = cssjs;
|
(window as any).cssjs = cssjs;
|
||||||
|
(window as any).moment = moment;
|
||||||
(window as any).TbFlot = TbFlot;
|
(window as any).TbFlot = TbFlot;
|
||||||
(window as any).TbAnalogueCompass = TbAnalogueCompass;
|
(window as any).TbAnalogueCompass = TbAnalogueCompass;
|
||||||
(window as any).TbAnalogueRadialGauge = TbAnalogueRadialGauge;
|
(window as any).TbAnalogueRadialGauge = TbAnalogueRadialGauge;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user