Feature/selectable card component (#1703)

* added selectable card

* creating new card type
This commit is contained in:
Leila Lali
2018-06-22 14:25:21 -07:00
committed by GitHub
parent 322847469d
commit a627285a4c
12 changed files with 327 additions and 86 deletions

View File

@@ -10,21 +10,16 @@ import {
import * as sqlops from 'sqlops';
import { ComponentBase } from 'sql/parts/modelComponents/componentBase';
import { ComponentWithIconBase } from 'sql/parts/modelComponents/componentWithIconBase';
import { IComponent, IComponentDescriptor, IModelStore, ComponentEventType } from 'sql/parts/modelComponents/interfaces';
import { attachButtonStyler } from 'sql/common/theme/styler';
import { Button } from 'sql/base/browser/ui/button/button';
import { SIDE_BAR_BACKGROUND, SIDE_BAR_TITLE_FOREGROUND } from 'vs/workbench/common/theme';
import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService';
import { attachListStyler } from 'vs/platform/theme/common/styler';
import URI from 'vs/base/common/uri';
import { IdGenerator } from 'vs/base/common/idGenerator';
import { createCSSRule, removeCSSRulesContainingSelector } from 'vs/base/browser/dom';
import { focusBorder, foreground } from 'vs/platform/theme/common/colorRegistry';
import { Color } from 'vs/base/common/color';
type IUserFriendlyIcon = string | URI | { light: string | URI; dark: string | URI };
@Component({
selector: 'modelview-button',
@@ -32,12 +27,10 @@ type IUserFriendlyIcon = string | URI | { light: string | URI; dark: string | UR
<div #input style="width: 100%"></div>
`
})
export default class ButtonComponent extends ComponentBase implements IComponent, OnDestroy, AfterViewInit {
export default class ButtonComponent extends ComponentWithIconBase implements IComponent, OnDestroy, AfterViewInit {
@Input() descriptor: IComponentDescriptor;
@Input() modelStore: IModelStore;
private _button: Button;
private _iconClass: string;
private _iconPath: IUserFriendlyIcon;
@ViewChild('input', { read: ElementRef }) private _inputContainer: ElementRef;
constructor(
@@ -71,9 +64,6 @@ export default class ButtonComponent extends ComponentBase implements IComponent
}
ngOnDestroy(): void {
if (this._iconClass) {
removeCSSRulesContainingSelector(this._iconClass);
}
this.baseDestroy();
}
@@ -101,14 +91,11 @@ export default class ButtonComponent extends ComponentBase implements IComponent
this.updateIcon();
}
private updateIcon() {
if (this.iconPath && this.iconPath !== this._iconPath) {
this._iconPath = this.iconPath;
protected updateIcon() {
if (this.iconPath) {
if (!this._iconClass) {
const ids = new IdGenerator('button-component-icon-' + Math.round(Math.random() * 1000));
this._iconClass = ids.nextId();
super.updateIcon();
this._button.icon = this._iconClass + ' icon';
// Styling for icon button
this._register(attachButtonStyler(this._button, this.themeService, {
buttonBackground: Color.transparent.toString(),
@@ -117,36 +104,6 @@ export default class ButtonComponent extends ComponentBase implements IComponent
buttonForeground: foreground
}));
}
removeCSSRulesContainingSelector(this._iconClass);
const icon = this.getLightIconPath(this.iconPath);
const iconDark = this.getDarkIconPath(this.iconPath) || icon;
createCSSRule(`.icon.${this._iconClass}`, `background-image: url("${icon}")`);
createCSSRule(`.vs-dark .icon.${this._iconClass}, .hc-black .icon.${this._iconClass}`, `background-image: url("${iconDark}")`);
}
}
private getLightIconPath(iconPath: IUserFriendlyIcon): string {
if (iconPath && iconPath['light']) {
return this.getIconPath(iconPath['light']);
} else {
return this.getIconPath(<string | URI>iconPath);
}
}
private getDarkIconPath(iconPath: IUserFriendlyIcon): string {
if (iconPath && iconPath['dark']) {
return this.getIconPath(iconPath['dark']);
}
return null;
}
private getIconPath(iconPath: string | URI): string {
if (typeof iconPath === 'string') {
return URI.file(iconPath).toString();
} else {
let uri = URI.revive(iconPath);
return uri.toString();
}
}
@@ -160,13 +117,7 @@ export default class ButtonComponent extends ComponentBase implements IComponent
this.setPropertyFromUI<sqlops.ButtonProperties, string>(this.setValueProperties, newValue);
}
public get iconPath(): string | URI | { light: string | URI; dark: string | URI } {
return this.getPropertyOrDefault<sqlops.ButtonProperties, IUserFriendlyIcon>((props) => props.iconPath, undefined);
}
public set iconPath(newValue: string | URI | { light: string | URI; dark: string | URI }) {
this.setPropertyFromUI<sqlops.ButtonProperties, IUserFriendlyIcon>((properties, iconPath) => { properties.iconPath = iconPath; }, newValue);
}
private setValueProperties(properties: sqlops.ButtonProperties, label: string): void {
properties.label = label;

View File

@@ -1,19 +1,32 @@
<div *ngIf="label" class="model-card">
<div *ngIf="label" [class]="getClass()" (click)="onCardClick()" (mouseover)="onCardHoverChanged($event)" (mouseout)="onCardHoverChanged($event)">
<span *ngIf="hasStatus" class="card-status">
<div class="status-content" [style.backgroundColor]="statusColor"></div>
</span>
<div class="card-content">
<h4 class="card-label">{{label}}</h4>
<p class="card-value">{{value}}</p>
<span *ngIf="actions">
<table class="model-table">
<tr *ngFor="let action of actions">
<td class="table-row">{{action.label}}</td>
<td *ngIf="action.actionTitle" class="table-row">
<a class="pointer prominent" (click)="onDidActionClick(action)">{{action.actionTitle}}</a>
</td>
</tr>
</table>
</span>
</div>
<ng-container *ngIf="isVerticalButton">
<div class="card-vertical-button">
<div *ngIf="iconPath" class="iconContainer"><div [class]="iconClass" [style.width]="iconWidth" [style.height]="iconHeight"></div>
<hr/>
<h4 class="card-label">{{label}}</h4>
</div>
</div>
</ng-container>
<ng-container *ngIf="isDetailsCard">
<div class="card-content">
<h4 class="card-label">{{label}}</h4>
<p class="card-value">{{value}}</p>
<span *ngIf="actions">
<table class="model-table">
<tr *ngFor="let action of actions">
<td class="table-row">{{action.label}}</td>
<td *ngIf="action.actionTitle" class="table-row">
<a class="pointer prominent" (click)="onDidActionClick(action)">{{action.actionTitle}}</a>
</td>
</tr>
</table>
</span>
</div>
</ng-container>
</div>

View File

@@ -15,14 +15,14 @@ import * as colors from 'vs/platform/theme/common/colorRegistry';
import { IColorTheme, IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService';
import { DashboardServiceInterface } from 'sql/parts/dashboard/services/dashboardServiceInterface.service';
import { ComponentBase } from 'sql/parts/modelComponents/componentBase';
import { ComponentWithIconBase } from 'sql/parts/modelComponents/componentWithIconBase';
import { IComponent, IComponentDescriptor, IModelStore, ComponentEventType } from 'sql/parts/modelComponents/interfaces';
import { StatusIndicator, CardProperties, ActionDescriptor } from 'sql/workbench/api/common/sqlExtHostTypes';
@Component({
templateUrl: decodeURI(require.toUrl('sql/parts/modelComponents/card.component.html'))
})
export default class CardComponent extends ComponentBase implements IComponent, OnDestroy {
export default class CardComponent extends ComponentWithIconBase implements IComponent, OnDestroy {
@Input() descriptor: IComponentDescriptor;
@Input() modelStore: IModelStore;
@@ -30,7 +30,7 @@ export default class CardComponent extends ComponentBase implements IComponent,
constructor(@Inject(forwardRef(() => ChangeDetectorRef)) changeRef: ChangeDetectorRef,
@Inject(forwardRef(() => ElementRef)) private _el: ElementRef,
@Inject(IWorkbenchThemeService) private themeService: IWorkbenchThemeService
@Inject(IWorkbenchThemeService) private themeService: IWorkbenchThemeService,
) {
super(changeRef);
}
@@ -46,6 +46,39 @@ export default class CardComponent extends ComponentBase implements IComponent,
this.baseDestroy();
}
private _defaultBorderColor = 'rgb(214, 214, 214)';
private _hasFocus: boolean;
public onCardClick() {
if (this.selectable) {
this.selected = !this.selected;
this._changeRef.detectChanges();
this._onEventEmitter.fire({
eventType: ComponentEventType.onDidClick,
args: this.selected
});
}
}
public getBorderColor() {
if (this.selectable && this.selected || this._hasFocus) {
return 'Blue';
} else {
return this._defaultBorderColor;
}
}
public getClass(): string {
return (this.selectable && this.selected || this._hasFocus) ? 'model-card selected' :
'model-card unselected';
}
public onCardHoverChanged(event: any) {
if (this.selectable) {
this._hasFocus = event.type === 'mouseover';
this._changeRef.detectChanges();
}
}
/// IComponent implementation
public layout(): void {
@@ -57,6 +90,19 @@ export default class CardComponent extends ComponentBase implements IComponent,
this.layout();
}
public setProperties(properties: { [key: string]: any; }): void {
super.setProperties(properties);
this.updateIcon();
}
public get iconClass(): string {
return this._iconClass + ' icon' + ' cardIcon';
}
private get selectable(): boolean {
return this.cardType === 'VerticalButton';
}
// CSS-bound properties
public get label(): string {
@@ -67,6 +113,27 @@ export default class CardComponent extends ComponentBase implements IComponent,
return this.getPropertyOrDefault<CardProperties, string>((props) => props.value, '');
}
public get cardType(): string {
return this.getPropertyOrDefault<CardProperties, string>((props) => props.cardType, 'Details');
}
public get selected(): boolean {
return this.getPropertyOrDefault<sqlops.CardProperties, boolean>((props) => props.selected, false);
}
public set selected(newValue: boolean) {
this.setPropertyFromUI<sqlops.CardProperties, boolean>((props, value) => props.selected = value, newValue);
}
public get isDetailsCard(): boolean {
return !this.cardType || this.cardType === 'Details';
}
public get isVerticalButton(): boolean {
return this.cardType === 'VerticalButton';
}
public get actions(): ActionDescriptor[] {
return this.getPropertyOrDefault<CardProperties, ActionDescriptor[]>((props) => props.actions, []);
}

View File

@@ -7,12 +7,26 @@
margin: 15px;
border-width: 1px;
border-style: solid;
border-color: rgb(214, 214, 214);
text-align: left;
vertical-align: top;
box-shadow: rgba(120, 120, 120, 0.75) 0px 0px 6px;
}
.model-card.selected {
border-color: darkblue
}
.vs-dark .monaco-workbench .model-card.selected,
.hc-black .monaco-workbench .model-card.selected {
border-color: darkblue
}
.model-card.unselected {
border-color: rgb(214, 214, 214);
}
.model-card .card-content {
position: relative;
display: inline-block;
@@ -23,6 +37,16 @@
min-width: 30px;
}
.model-card .card-vertical-button {
position: relative;
display: inline-block;
height: auto;
width: auto;
padding: 5px 5px 5px 5px;
min-height: 130px;
min-width: 130px;
}
.model-card .card-label {
font-size: 12px;
font-weight: bold;
@@ -33,6 +57,19 @@
line-height: 18px;
}
.model-card .iconContainer {
width: 100%;
height: 50px;
text-align: center;
padding: 10px 0px 10px 0px;
}
.model-card .cardIcon {
display: inline-block;
width: 40px;
height: 40px;
}
.model-card .card-status {
position: absolute;
top: 7px;

View File

@@ -18,6 +18,12 @@ import { DashboardServiceInterface } from 'sql/parts/dashboard/services/dashboar
import { Event, Emitter } from 'vs/base/common/event';
import { IDisposable, Disposable } from 'vs/base/common/lifecycle';
import { ModelComponentWrapper } from 'sql/parts/modelComponents/modelComponentWrapper.component';
import URI from 'vs/base/common/uri';
import { IdGenerator } from 'vs/base/common/idGenerator';
import { createCSSRule, removeCSSRulesContainingSelector } from 'vs/base/browser/dom';
export type IUserFriendlyIcon = string | URI | { light: string | URI; dark: string | URI };
export class ItemDescriptor<T> {
constructor(public descriptor: IComponentDescriptor, public config: T) { }

View File

@@ -0,0 +1,107 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import {
Component, Input, Inject, ChangeDetectorRef, forwardRef, ComponentFactoryResolver,
ViewChild, ViewChildren, ElementRef, Injector, OnDestroy, OnInit, QueryList
} from '@angular/core';
import { IComponent, IComponentDescriptor, IModelStore, IComponentEventArgs, ComponentEventType } from 'sql/parts/modelComponents/interfaces';
import * as sqlops from 'sqlops';
import URI from 'vs/base/common/uri';
import { IdGenerator } from 'vs/base/common/idGenerator';
import { createCSSRule, removeCSSRulesContainingSelector } from 'vs/base/browser/dom';
import { ComponentBase } from 'sql/parts/modelComponents/componentBase';
export type IUserFriendlyIcon = string | URI | { light: string | URI; dark: string | URI };
export class ItemDescriptor<T> {
constructor(public descriptor: IComponentDescriptor, public config: T) { }
}
export abstract class ComponentWithIconBase extends ComponentBase {
protected _iconClass: string;
protected _iconPath: IUserFriendlyIcon;
constructor(
changeRef: ChangeDetectorRef) {
super(changeRef);
}
/// IComponent implementation
public get iconClass(): string {
return this._iconClass + ' icon';
}
protected updateIcon() {
if (this.iconPath && this.iconPath !== this._iconPath) {
this._iconPath = this.iconPath;
if (!this._iconClass) {
const ids = new IdGenerator('model-view-component-icon-' + Math.round(Math.random() * 1000));
this._iconClass = ids.nextId();
}
removeCSSRulesContainingSelector(this._iconClass);
const icon = this.getLightIconPath(this.iconPath);
const iconDark = this.getDarkIconPath(this.iconPath) || icon;
createCSSRule(`.icon.${this._iconClass}`, `background-image: url("${icon}")`);
createCSSRule(`.vs-dark .icon.${this._iconClass}, .hc-black .icon.${this._iconClass}`, `background-image: url("${iconDark}")`);
}
}
private getLightIconPath(iconPath: IUserFriendlyIcon): string {
if (iconPath && iconPath['light']) {
return this.getIconPath(iconPath['light']);
} else {
return this.getIconPath(<string | URI>iconPath);
}
}
private getDarkIconPath(iconPath: IUserFriendlyIcon): string {
if (iconPath && iconPath['dark']) {
return this.getIconPath(iconPath['dark']);
}
return null;
}
private getIconPath(iconPath: string | URI): string {
if (typeof iconPath === 'string') {
return URI.file(iconPath).toString();
} else {
let uri = URI.revive(iconPath);
return uri.toString();
}
}
public getIconWidth(): string {
return this.convertSize(this.iconWidth, '40px');
}
public getIconHeight(): string {
return this.convertSize(this.iconHeight, '40px');
}
public get iconPath(): string | URI | { light: string | URI; dark: string | URI } {
return this.getPropertyOrDefault<sqlops.ComponentWithIcon, IUserFriendlyIcon>((props) => props.iconPath, undefined);
}
public get iconHeight(): number | string {
return this.getPropertyOrDefault<sqlops.ComponentWithIcon, number | string>((props) => props.iconHeight, '40px');
}
public get iconWidth(): number | string {
return this.getPropertyOrDefault<sqlops.ComponentWithIcon, number | string>((props) => props.iconWidth, '40px');
}
ngOnDestroy(): void {
if (this._iconClass) {
removeCSSRulesContainingSelector(this._iconClass);
}
super.ngOnDestroy();
}
}

View File

@@ -26,7 +26,7 @@ export interface TitledFormItemLayout {
horizontal: boolean;
componentWidth?: number | string;
componentHeight?: number | string;
titleFontSize?: number;
titleFontSize?: number | string;
required?: boolean;
info?: string;
}
@@ -48,7 +48,7 @@ class FormItem {
<ng-container *ngIf="isHorizontal(item)">
<div class="form-cell" [style.font-size]="getItemTitleFontSize(item)">
{{getItemTitle(item)}}<span class="form-required" *ngIf="isItemRequired(item)">*</span>
<span class="icon info" *ngIf="itemHasInfo(item)" [title]="getItemInfo(item)"></span>
<span class="icon info form-info" *ngIf="itemHasInfo(item)" [title]="getItemInfo(item)"></span>
</div>
<div class="form-cell">
<div class="form-component-container">
@@ -68,7 +68,7 @@ class FormItem {
<div class="form-vertical-container" *ngIf="isVertical(item)" [style.height]="getRowHeight(item)">
<div class="form-item-row" [style.font-size]="getItemTitleFontSize(item)">
{{getItemTitle(item)}}<span class="form-required" *ngIf="isItemRequired(item)">*</span>
<span class="icon info" *ngIf="itemHasInfo(item)" [title]="getItemInfo(item)"></span>
<span class="icon info form-info" *ngIf="itemHasInfo(item)" [title]="getItemInfo(item)"></span>
</div>
<div class="form-item-row" [style.width]="getComponentWidth(item)" [style.height]="getRowHeight(item)">
<model-component-wrapper [descriptor]="item.descriptor" [modelStore]="modelStore" [style.width]="getComponentWidth(item)" [style.height]="getRowHeight(item)">

View File

@@ -42,6 +42,11 @@
padding-left: 5px;
}
.form-info {
width: 15px;
height: 15px;
}
.form-component-actions {
padding-left: 5px;
}