adding a medical sources card - using it for medical-source-connected and medical-sources pages/component.
make sure additional fields/metadata (like aliases and category) are sent.
This commit is contained in:
parent
2db04a15da
commit
2d570850f0
|
@ -0,0 +1,16 @@
|
|||
<div class="card h-100 d-flex align-items-center justify-content-center mt-3 mt-3 rounded-0 cursor-pointer">
|
||||
<div (click)="onCardClick()" class="card-body">
|
||||
|
||||
<div class="h-100 d-flex align-items-center">
|
||||
<img [src]="'assets/sources/'+(sourceInfo?.metadata.brand_logo ? sourceInfo?.metadata?.brand_logo : sourceInfo?.metadata?.source_type+'.png')" [alt]="sourceInfo?.metadata?.display" class="img-fluid">
|
||||
</div>
|
||||
<div *ngIf="status" class="progress">
|
||||
<div [style.width]="status == 'authorize' ? '33%' : '66%'" class="bg-indigo progress-bar progress-bar-striped progress-bar-animated" role="progressbar"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer text-center p-1" style="width:100%">
|
||||
<small class="tx-gray-700">
|
||||
{{sourceInfo?.metadata.display}}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,23 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { MedicalSourcesCardComponent } from './medical-sources-card.component';
|
||||
|
||||
describe('MedicalSourcesCardComponent', () => {
|
||||
let component: MedicalSourcesCardComponent;
|
||||
let fixture: ComponentFixture<MedicalSourcesCardComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ MedicalSourcesCardComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(MedicalSourcesCardComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,25 @@
|
|||
import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core';
|
||||
import {SourceListItem} from '../../pages/medical-sources/medical-sources.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-medical-sources-card',
|
||||
templateUrl: './medical-sources-card.component.html',
|
||||
styleUrls: ['./medical-sources-card.component.scss']
|
||||
})
|
||||
export class MedicalSourcesCardComponent implements OnInit {
|
||||
|
||||
@Input() sourceInfo: SourceListItem;
|
||||
@Input() status: undefined | "token" | "authorize";
|
||||
|
||||
@Output() onClick = new EventEmitter<SourceListItem>()
|
||||
|
||||
constructor() { }
|
||||
|
||||
ngOnInit(): void {
|
||||
}
|
||||
|
||||
onCardClick(){
|
||||
this.onClick.emit(this.sourceInfo)
|
||||
}
|
||||
|
||||
}
|
|
@ -1,24 +1,13 @@
|
|||
<h2 class="az-content-title">Connected Sources</h2>
|
||||
|
||||
<div *ngIf="!loading else isLoadingTemplate" class="row">
|
||||
<div *ngFor="let sourceInfo of connectedSourceList" class="col-sm-3 mg-b-20 px-3">
|
||||
<div class="card h-100 d-flex align-items-center justify-content-center mt-3 mt-3 rounded-0 cursor-pointer">
|
||||
<div (click)="openModal(contentModalRef, sourceInfo)" class="card-body">
|
||||
|
||||
<div class="h-100 d-flex align-items-center">
|
||||
<img [src]="'assets/sources/'+(sourceInfo.metadata.brand_logo ? sourceInfo.metadata.brand_logo : sourceInfo.metadata.source_type+'.png')" [alt]="sourceInfo?.metadata.display" class="img-fluid">
|
||||
</div>
|
||||
<div *ngIf="status[sourceInfo.metadata?.source_type]" class="progress">
|
||||
<div [style.width]="status[sourceInfo?.metadata?.source_type] == 'authorize' ? '33%' : '66%'" class="bg-indigo progress-bar progress-bar-striped progress-bar-animated" role="progressbar"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer text-center p-1" style="width:100%">
|
||||
<small class="tx-gray-700">
|
||||
{{sourceInfo?.metadata.display}}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<app-medical-sources-card class="col-sm-3 mg-b-20 px-3"
|
||||
*ngFor="let sourceData of connectedSourceList"
|
||||
[sourceInfo]="sourceData"
|
||||
[status]="status[sourceData.metadata.source_type]"
|
||||
(onClick)="openModal(contentModalRef, $event)"
|
||||
></app-medical-sources-card>
|
||||
</div>
|
||||
|
||||
|
||||
|
@ -43,7 +32,7 @@
|
|||
<div class="modal-footer">
|
||||
<button (click)="sourceSyncHandler(modalSelectedSourceListItem.source)" type="button" class="btn btn-indigo">Sync</button>
|
||||
<!-- <button (click)="connectHandler($event, modalSelectedSourceListItem.source['source_type'])" type="button" class="btn btn-outline-light">Reconnect</button>-->
|
||||
<button type="button" class="btn btn-outline-danger">Delete</button>
|
||||
<button type="button" class="btn disabled btn-outline-danger">Delete</button>
|
||||
<button (click)="modal.dismiss('Close click')" type="button" class="btn btn-outline-light">Close</button>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
|
|
@ -13,7 +13,7 @@ import {LighthouseService} from '../../services/lighthouse.service';
|
|||
})
|
||||
export class MedicalSourcesConnectedComponent implements OnInit {
|
||||
loading: boolean = false
|
||||
status: { [name: string]: string } = {}
|
||||
status: { [name: string]: undefined | "token" | "authorize" } = {}
|
||||
|
||||
modalSelectedSourceListItem:SourceListItem = null;
|
||||
modalCloseResult = '';
|
||||
|
|
|
@ -74,6 +74,7 @@ import {GridstackItemComponent} from './gridstack/gridstack-item.component';
|
|||
import { MedicalSourcesFilterComponent } from './medical-sources-filter/medical-sources-filter.component';
|
||||
import { MedicalSourcesConnectedComponent } from './medical-sources-connected/medical-sources-connected.component';
|
||||
import { MedicalSourcesCategoryLookupPipe } from './medical-sources-filter/medical-sources-category-lookup.pipe';
|
||||
import { MedicalSourcesCardComponent } from './medical-sources-card/medical-sources-card.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
|
@ -160,6 +161,7 @@ import { MedicalSourcesCategoryLookupPipe } from './medical-sources-filter/medic
|
|||
MedicalSourcesFilterComponent,
|
||||
MedicalSourcesConnectedComponent,
|
||||
MedicalSourcesCategoryLookupPipe,
|
||||
MedicalSourcesCardComponent,
|
||||
],
|
||||
exports: [
|
||||
BinaryComponent,
|
||||
|
@ -208,6 +210,8 @@ import { MedicalSourcesCategoryLookupPipe } from './medical-sources-filter/medic
|
|||
ResourceListOutletDirective,
|
||||
ToastComponent,
|
||||
UtilitiesSidebarComponent,
|
||||
MedicalSourcesCardComponent,
|
||||
MedicalSourcesConnectedComponent,
|
||||
|
||||
//standalone components
|
||||
BadgeComponent,
|
||||
|
@ -224,7 +228,7 @@ import { MedicalSourcesCategoryLookupPipe } from './medical-sources-filter/medic
|
|||
BinaryComponent,
|
||||
GridstackComponent,
|
||||
GridstackItemComponent,
|
||||
MedicalSourcesConnectedComponent,
|
||||
MedicalSourcesCategoryLookupPipe,
|
||||
|
||||
]
|
||||
})
|
||||
|
|
|
@ -1,8 +1,12 @@
|
|||
export class MetadataSource {
|
||||
platform_type: string
|
||||
aliases?: string[]
|
||||
brand_logo?: string
|
||||
source_type: string
|
||||
display: string
|
||||
category: string[]
|
||||
display: string
|
||||
hidden: boolean
|
||||
identifiers?: {[name:string]: string}
|
||||
patient_access_description?: string
|
||||
patient_access_url?: string
|
||||
platform_type: string
|
||||
source_type: string
|
||||
}
|
||||
|
|
|
@ -71,21 +71,13 @@
|
|||
[infiniteScrollThrottle]="50"
|
||||
(scrolled)="onScroll()"
|
||||
>
|
||||
<div *ngFor="let sourceData of availableSourceList" (click)="connectHandler($event, sourceData.metadata.source_type)" class="col-sm-3 mg-b-20 px-3">
|
||||
<div class="card h-100 d-flex align-items-center justify-content-center mt-3 mt-3 rounded-0 cursor-pointer" [ngClass]="{'card-disable': sourceData.metadata.hidden}">
|
||||
<div class="card-body d-flex align-items-center">
|
||||
<img style="max-height: 130px;" [src]="'assets/sources/'+(sourceData.metadata.brand_logo ? sourceData.metadata.brand_logo : sourceData.metadata.source_type+'.png')" [alt]="sourceData.metadata.display" class="img-fluid">
|
||||
<div *ngIf="status[sourceData.metadata.source_type]" class="progress">
|
||||
<div [style.width]="status[sourceData.metadata.source_type] == 'authorize' ? '33%' : '66%'" class="bg-indigo progress-bar progress-bar-striped progress-bar-animated" role="progressbar"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer text-center p-1" style="width:100%">
|
||||
<small class="tx-gray-700">
|
||||
{{sourceData.metadata.display}}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<app-medical-sources-card class="col-sm-3 mg-b-20 px-3"
|
||||
*ngFor="let sourceData of availableSourceList"
|
||||
[sourceInfo]="sourceData"
|
||||
[status]="status[sourceData.metadata.source_type]"
|
||||
(onClick)="connectModalHandler(contentModalRef, $event)"
|
||||
></app-medical-sources-card>
|
||||
|
||||
</div><!-- row -->
|
||||
|
||||
|
@ -95,6 +87,61 @@
|
|||
</div><!-- container -->
|
||||
</div><!-- az-content -->
|
||||
|
||||
|
||||
<ng-template #contentModalRef let-modal>
|
||||
|
||||
<div class="modal-header">
|
||||
<div class="media">
|
||||
<img class="modal-header-media-image mg-sm-r-20 mg-b-20 mg-sm-b-0" [src]="'assets/sources/'+(modalSelectedSourceListItem?.metadata.brand_logo ? modalSelectedSourceListItem?.metadata?.brand_logo : modalSelectedSourceListItem?.metadata?.source_type+'.png')" [alt]="modalSelectedSourceListItem?.metadata?.display">
|
||||
<div class="media-body">
|
||||
<h6>{{modalSelectedSourceListItem?.metadata.display}}</h6>
|
||||
<a *ngIf="modalSelectedSourceListItem?.metadata.patient_access_url"
|
||||
[href]="modalSelectedSourceListItem.metadata?.patient_access_url"
|
||||
target="_blank"
|
||||
class="mg-b-0">{{modalSelectedSourceListItem?.metadata.patient_access_url}}</a>
|
||||
</div><!-- media-body -->
|
||||
</div><!-- media -->
|
||||
<button type="button" class="btn btn-close" aria-label="Close" (click)="modal.dismiss('Cross click')">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
<h6>Connect Source</h6>
|
||||
<p>Would you like to connect this healthcare institution with Fasten Health? You will be redirected to their patient portal,
|
||||
where you can authenticate and select data to import into Fasten Health.
|
||||
</p>
|
||||
<p>
|
||||
If the data about this institution is missing or incorrect, you can <a class="link" href="https://docs.google.com/spreadsheets/d/1ZSgwfd7kwxSnimk4yofIFcR8ZMUls0zi9SZpRiOJBx0/edit?usp=sharing">click here</a> to submit a correction.
|
||||
</p>
|
||||
|
||||
<ng-container *ngIf="modalSelectedSourceListItem?.metadata?.category?.length > 0 || modalSelectedSourceListItem?.metadata?.patient_access_description">
|
||||
<hr/>
|
||||
<ng-container *ngIf="modalSelectedSourceListItem?.metadata?.patient_access_description">
|
||||
<h6>About this Source</h6>
|
||||
<p >{{modalSelectedSourceListItem?.metadata?.patient_access_description}}</p>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="modalSelectedSourceListItem?.metadata?.category?.length > 0">
|
||||
<h6>Categories</h6>
|
||||
<ul>
|
||||
<li *ngFor="let cat of modalSelectedSourceListItem?.metadata?.category">{{cat | medicalSourcesCategoryLookup}}</li>
|
||||
</ul>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<!-- <button (click)="sourceSyncHandler(modalSelectedSourceListItem.source)" type="button" class="btn btn-indigo">Sync</button>-->
|
||||
<!-- <button (click)="connectHandler($event, modalSelectedSourceListItem.source['source_type'])" type="button" class="btn btn-outline-light">Reconnect</button>-->
|
||||
<button type="button" (click)="connectHandler($event, modalSelectedSourceListItem)" class="btn btn-indigo">
|
||||
<span *ngIf="status[modalSelectedSourceListItem?.metadata?.source_type] == 'authorize'" class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
|
||||
Connect
|
||||
</button>
|
||||
<button (click)="modal.dismiss('Close click')" type="button" class="btn btn-outline-light">Close</button>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
|
||||
<ng-template #isLoadingTemplate>
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
|
|
|
@ -43,7 +43,7 @@ export class MedicalSourcesComponent implements OnInit {
|
|||
|
||||
availableSourceList: SourceListItem[] = []
|
||||
searchTermUpdate = new BehaviorSubject<string>("");
|
||||
status: { [name: string]: string } = {}
|
||||
status: { [name: string]: undefined | "token" | "authorize" } = {}
|
||||
|
||||
//aggregation/filter data & limits
|
||||
globalLimits: {
|
||||
|
@ -68,8 +68,13 @@ export class MedicalSourcesComponent implements OnInit {
|
|||
|
||||
|
||||
//source of truth for current state
|
||||
//TODO: see if we can remove this without breaking search/filtering
|
||||
filterForm = this.filterService.filterForm;
|
||||
|
||||
//modal
|
||||
modalSelectedSourceListItem:SourceListItem = null;
|
||||
modalCloseResult = '';
|
||||
|
||||
constructor(
|
||||
private lighthouseApi: LighthouseService,
|
||||
private fastenApi: FastenApiService,
|
||||
|
@ -77,7 +82,7 @@ export class MedicalSourcesComponent implements OnInit {
|
|||
private location: Location,
|
||||
private toastService: ToastService,
|
||||
private filterService: MedicalSourcesFilterService,
|
||||
|
||||
private modalService: NgbModal,
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
|
@ -210,7 +215,7 @@ export class MedicalSourcesComponent implements OnInit {
|
|||
}))
|
||||
|
||||
//change the current Page (but don't cause a new query)
|
||||
if(wrapper.hits.hits.length == 0){
|
||||
if(!wrapper?.hits || !wrapper?.hits || wrapper?.hits?.hits?.length == 0){
|
||||
console.log("SCROLL_COMPLETE!@@@@@@@@")
|
||||
this.resultLimits.scrollComplete = true;
|
||||
} else {
|
||||
|
@ -223,33 +228,33 @@ export class MedicalSourcesComponent implements OnInit {
|
|||
// }))
|
||||
|
||||
|
||||
if(wrapper.aggregations){
|
||||
this.resultLimits.platformTypesBuckets = wrapper.aggregations.by_platform_type;
|
||||
this.resultLimits.categoryBuckets = wrapper.aggregations.by_category;
|
||||
var currentCategories = this.filterForm.get('categories').value;
|
||||
this.resultLimits.categoryBuckets.buckets.forEach((bucketData) => {
|
||||
if(!currentCategories.hasOwnProperty(bucketData.key)){
|
||||
(this.filterForm.get('categories') as FormGroup).addControl(bucketData.key, new FormControl(false))
|
||||
}
|
||||
})
|
||||
|
||||
this.resultLimits.platformTypesBuckets = wrapper.aggregations.by_platform_type;
|
||||
this.resultLimits.categoryBuckets = wrapper.aggregations.by_category;
|
||||
//
|
||||
// this.resultLimits.categoryBuckets.forEach((bucketData) => {
|
||||
// if(!this.globalLimits.categories.some((category) => { return category.id === bucketData.key})){
|
||||
// this.globalLimits.categories.push({
|
||||
// id: bucketData.key,
|
||||
// name: bucketData.key,
|
||||
// group: 'custom'
|
||||
// })
|
||||
// }
|
||||
// })
|
||||
|
||||
// const fileTypes = <FormGroup>this.filterForm.get('fileTypes');
|
||||
// fileTypes.forEach((option: any) => {
|
||||
// checkboxes.addControl(option.title, new FormControl(true));
|
||||
// });
|
||||
}
|
||||
|
||||
|
||||
var currentCategories = this.filterForm.get('categories').value;
|
||||
this.resultLimits.categoryBuckets.buckets.forEach((bucketData) => {
|
||||
if(!currentCategories.hasOwnProperty(bucketData.key)){
|
||||
(this.filterForm.get('categories') as FormGroup).addControl(bucketData.key, new FormControl(false))
|
||||
}
|
||||
})
|
||||
//
|
||||
// this.resultLimits.categoryBuckets.forEach((bucketData) => {
|
||||
// if(!this.globalLimits.categories.some((category) => { return category.id === bucketData.key})){
|
||||
// this.globalLimits.categories.push({
|
||||
// id: bucketData.key,
|
||||
// name: bucketData.key,
|
||||
// group: 'custom'
|
||||
// })
|
||||
// }
|
||||
// })
|
||||
|
||||
// const fileTypes = <FormGroup>this.filterForm.get('fileTypes');
|
||||
// fileTypes.forEach((option: any) => {
|
||||
// checkboxes.addControl(option.title, new FormControl(true));
|
||||
// });
|
||||
this.loading = false
|
||||
},
|
||||
error => {
|
||||
|
@ -265,6 +270,10 @@ export class MedicalSourcesComponent implements OnInit {
|
|||
}
|
||||
|
||||
|
||||
public onScroll(): void {
|
||||
this.querySources()
|
||||
}
|
||||
|
||||
//OLD FUNCTIONS
|
||||
//
|
||||
//
|
||||
|
@ -284,32 +293,49 @@ export class MedicalSourcesComponent implements OnInit {
|
|||
// }))
|
||||
// }
|
||||
//
|
||||
public onScroll(): void {
|
||||
console.log("TODO: SCROLL, TRIGGER update")
|
||||
this.querySources()
|
||||
}
|
||||
|
||||
|
||||
// /**
|
||||
// * after pressing the logo (connectHandler button), this function will generate an authorize url for this source, and redirec the user.
|
||||
// * after pressing the logo (connectModalHandler button), this function will display a modal with information about the source
|
||||
// * @param $event
|
||||
// * @param sourceType
|
||||
// */
|
||||
public connectHandler($event: MouseEvent, sourceType: string):void {
|
||||
public connectModalHandler(contentModalRef, sourceListItem: SourceListItem) :void {
|
||||
console.log("TODO: connect Handler")
|
||||
// ($event.currentTarget as HTMLButtonElement).disabled = true;
|
||||
// this.status[sourceType] = "authorize"
|
||||
//
|
||||
// this.lighthouseApi.getLighthouseSource(sourceType)
|
||||
// .then(async (sourceMetadata: LighthouseSourceMetadata) => {
|
||||
// console.log(sourceMetadata);
|
||||
// let authorizationUrl = await this.lighthouseApi.generateSourceAuthorizeUrl(sourceType, sourceMetadata)
|
||||
//
|
||||
// console.log('authorize url:', authorizationUrl.toString());
|
||||
// // redirect to lighthouse with uri's
|
||||
// this.lighthouseApi.redirectWithOriginAndDestination(authorizationUrl.toString(), sourceType, sourceMetadata.redirect_uri)
|
||||
//
|
||||
// });
|
||||
|
||||
|
||||
this.modalSelectedSourceListItem = sourceListItem
|
||||
this.modalService.open(contentModalRef, {ariaLabelledBy: 'modal-basic-title'}).result.then((result) => {
|
||||
this.modalSelectedSourceListItem = null
|
||||
this.modalCloseResult = `Closed with: ${result}`;
|
||||
}, (reason) => {
|
||||
this.modalSelectedSourceListItem = null
|
||||
});
|
||||
}
|
||||
|
||||
// /**
|
||||
// * after pressing the connect button in the Modal, this function will generate an authorize url for this source, and redirec the user.
|
||||
// * @param $event
|
||||
// * @param sourceType
|
||||
// */
|
||||
public connectHandler($event, sourceListItem: SourceListItem): void {
|
||||
|
||||
($event.currentTarget as HTMLButtonElement).disabled = true;
|
||||
this.status[sourceListItem.metadata.source_type] = "authorize"
|
||||
|
||||
let sourceType = sourceListItem.metadata.source_type
|
||||
this.lighthouseApi.getLighthouseSource(sourceType)
|
||||
.then(async (sourceMetadata: LighthouseSourceMetadata) => {
|
||||
console.log(sourceMetadata);
|
||||
let authorizationUrl = await this.lighthouseApi.generateSourceAuthorizeUrl(sourceType, sourceMetadata)
|
||||
|
||||
console.log('authorize url:', authorizationUrl.toString());
|
||||
// redirect to lighthouse with uri's
|
||||
this.lighthouseApi.redirectWithOriginAndDestination(authorizationUrl.toString(), sourceType, sourceMetadata.redirect_uri)
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// /**
|
||||
// * if the user is redirected to this page from the lighthouse, we'll need to process the "code" to retrieve the access token & refresh token.
|
||||
|
|
|
@ -105,11 +105,11 @@ export class LighthouseService {
|
|||
}
|
||||
|
||||
//this is for providers that support CORS and PKCE (public client auth)
|
||||
if(!lighthouseSource.confidential || lighthouseSource.code_challenge_methods_supported.length > 0){
|
||||
if(!lighthouseSource.confidential || (lighthouseSource.code_challenge_methods_supported || []).length > 0){
|
||||
// https://github.com/panva/oauth4webapi/blob/8eba19eac408bdec5c1fe8abac2710c50bfadcc3/examples/public.ts
|
||||
const codeVerifier = Oauth.generateRandomCodeVerifier();
|
||||
const codeChallenge = await Oauth.calculatePKCECodeChallenge(codeVerifier);
|
||||
const codeChallengeMethod = lighthouseSource.code_challenge_methods_supported[0] || 'S256'
|
||||
const codeChallengeMethod = lighthouseSource.code_challenge_methods_supported?.[0] || 'S256'
|
||||
|
||||
sourceStateInfo.code_verifier = codeVerifier
|
||||
sourceStateInfo.code_challenge = codeChallenge
|
||||
|
|
|
@ -215,13 +215,20 @@ app-nlm-typeahead {
|
|||
}
|
||||
}
|
||||
|
||||
//Medical Source Page
|
||||
.modal-header-media-image {
|
||||
max-height: 50px;
|
||||
max-width: 100px;
|
||||
}
|
||||
|
||||
|
||||
//Medical Source Filter
|
||||
.category-label {
|
||||
font-weight: 400
|
||||
}
|
||||
|
||||
app-medical-sources-filter > .az-content-left-components {
|
||||
overflow: hidden;
|
||||
overflow-x: hidden;
|
||||
-webkit-transition: width 0.2s ease-in-out;
|
||||
-moz-transition: width 0.2s ease-in-out;
|
||||
-o-transition: width 0.2s ease-in-out;
|
||||
|
|
Loading…
Reference in New Issue