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;
}

View File

@@ -239,7 +239,7 @@ declare module 'sqlops' {
horizontal?: boolean;
componentWidth?: number | string;
componentHeight?: number | string;
titleFontSize?: number;
titleFontSize?: number | string;
required?: boolean;
info?: string;
}
@@ -300,15 +300,22 @@ declare module 'sqlops' {
Error = 3
}
export enum CardType {
VerticalButton = 'VerticalButton',
Details = 'Details'
}
/**
* Properties representing the card component, can be used
* when using ModelBuilder to create the component
*/
export interface CardProperties {
export interface CardProperties extends ComponentWithIcon {
label: string;
value?: string;
actions?: ActionDescriptor[];
status?: StatusIndicator;
selected?: boolean;
cardType: CardType;
}
export type InputBoxInputType = 'color' | 'date' | 'datetime-local' | 'email' | 'month' | 'number' | 'password' | 'range' | 'search' | 'text' | 'time' | 'url' | 'week';
@@ -318,6 +325,12 @@ declare module 'sqlops' {
width?: number | string;
}
export interface ComponentWithIcon {
iconPath?: string | vscode.Uri | { light: string | vscode.Uri; dark: string | vscode.Uri };
iconHeight?: number | string;
iconWidth?: number | string;
}
export interface InputBoxProperties extends ComponentProperties {
value?: string;
ariaLabel?: string;
@@ -393,20 +406,17 @@ declare module 'sqlops' {
html?: string;
}
export interface ButtonProperties extends ComponentProperties {
export interface ButtonProperties extends ComponentProperties, ComponentWithIcon {
label?: string;
iconPath?: string | vscode.Uri | { light: string | vscode.Uri; dark: string | vscode.Uri };
}
export interface LoadingComponentProperties {
loading?: boolean;
}
export interface CardComponent extends Component {
label: string;
value: string;
actions?: ActionDescriptor[];
export interface CardComponent extends Component, CardProperties {
onDidActionClick: vscode.Event<ActionDescriptor>;
onCardSelectedChanged: vscode.Event<any>;
}
export interface TextComponent extends Component {

View File

@@ -208,6 +208,8 @@ export interface CardProperties {
value?: string;
actions?: ActionDescriptor[];
status?: StatusIndicator;
selected?: boolean;
cardType: CardType;
}
export interface ActionDescriptor {
@@ -236,4 +238,9 @@ export enum DeclarativeDataType {
string = 'string',
category = 'category',
boolean = 'boolean'
}
export enum CardType {
VerticalButton = 'VerticalButton',
Details = 'Details'
}

View File

@@ -15,7 +15,7 @@ import * as vscode from 'vscode';
import * as sqlops from 'sqlops';
import { SqlMainContext, ExtHostModelViewShape, MainThreadModelViewShape } from 'sql/workbench/api/node/sqlExtHost.protocol';
import { IItemConfig, ModelComponentTypes, IComponentShape, IComponentEventArgs, ComponentEventType } from 'sql/workbench/api/common/sqlExtHostTypes';
import { IItemConfig, ModelComponentTypes, IComponentShape, IComponentEventArgs, ComponentEventType, CardType } from 'sql/workbench/api/common/sqlExtHostTypes';
class ModelBuilderImpl implements sqlops.ModelBuilder {
private nextComponentId: number;
@@ -523,6 +523,7 @@ class CardWrapper extends ComponentWrapper implements sqlops.CardComponent {
super(proxy, handle, ModelComponentTypes.Card, id);
this.properties = {};
this._emitterMap.set(ComponentEventType.onDidClick, new Emitter<any>());
this._emitterMap.set(ComponentEventType.onDidClick, new Emitter<any>());
}
public get label(): string {
@@ -537,17 +538,53 @@ class CardWrapper extends ComponentWrapper implements sqlops.CardComponent {
public set value(v: string) {
this.setProperty('value', v);
}
public get selected(): boolean {
return this.properties['selected'];
}
public set selected(v: boolean) {
this.setProperty('selected', v);
}
public get cardType(): sqlops.CardType {
return this.properties['cardType'];
}
public set cardType(v: sqlops.CardType) {
this.setProperty('cardType', v);
}
public get actions(): sqlops.ActionDescriptor[] {
return this.properties['actions'];
}
public set actions(a: sqlops.ActionDescriptor[]) {
this.setProperty('actions', a);
}
public get iconPath(): string | URI | { light: string | URI; dark: string | URI } {
return this.properties['iconPath'];
}
public set iconPath(v: string | URI | { light: string | URI; dark: string | URI }) {
this.setProperty('iconPath', v);
}
public get iconHeight(): number | string {
return this.properties['iconHeight'];
}
public set iconHeight(v: number | string) {
this.setProperty('iconHeight', v);
}
public get iconWidth(): number | string {
return this.properties['iconWidth'];
}
public set iconWidth(v: number | string) {
this.setProperty('iconWidth', v);
}
public get onDidActionClick(): vscode.Event<sqlops.ActionDescriptor> {
let emitter = this._emitterMap.get(ComponentEventType.onDidClick);
return emitter && emitter.event;
}
public get onCardSelectedChanged(): vscode.Event<any> {
let emitter = this._emitterMap.get(ComponentEventType.onDidClick);
return emitter && emitter.event;
}
}
class InputBoxWrapper extends ComponentWrapper implements sqlops.InputBoxComponent {

View File

@@ -391,7 +391,8 @@ export function createApiFactory(
workspace,
queryeditor: queryEditor,
ui: ui,
StatusIndicator: sqlExtHostTypes.StatusIndicator
StatusIndicator: sqlExtHostTypes.StatusIndicator,
CardType: sqlExtHostTypes.CardType
};
}
};