mirror of https://github.com/Squidex/squidex.git
Browse Source
* Fix URLS for read singleton. * Release notes for 6.10.0 * Better table fields (#903) * Better table fields. * Fix radio table. * Radio groups. * Just some cleanup.pull/908/head
committed by
GitHub
43 changed files with 770 additions and 460 deletions
@ -0,0 +1,91 @@ |
|||||
|
/* |
||||
|
* Squidex Headless CMS |
||||
|
* |
||||
|
* @license |
||||
|
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
||||
|
*/ |
||||
|
|
||||
|
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; |
||||
|
import { moduleMetadata } from '@storybook/angular'; |
||||
|
import { Meta, Story } from '@storybook/angular/types-6-0'; |
||||
|
import { CheckboxGroupComponent, SqxFrameworkModule } from '@app/framework'; |
||||
|
|
||||
|
export default { |
||||
|
title: 'Framework/CheckboxGroup', |
||||
|
component: CheckboxGroupComponent, |
||||
|
argTypes: { |
||||
|
disabled: { |
||||
|
control: 'boolean', |
||||
|
}, |
||||
|
unsorted: { |
||||
|
control: 'boolean', |
||||
|
}, |
||||
|
}, |
||||
|
decorators: [ |
||||
|
moduleMetadata({ |
||||
|
imports: [ |
||||
|
BrowserAnimationsModule, |
||||
|
SqxFrameworkModule, |
||||
|
SqxFrameworkModule.forRoot(), |
||||
|
], |
||||
|
}), |
||||
|
], |
||||
|
} as Meta; |
||||
|
|
||||
|
const Template: Story<CheckboxGroupComponent & { model: any }> = (args: CheckboxGroupComponent) => ({ |
||||
|
props: args, |
||||
|
template: ` |
||||
|
<div style="padding: 2rem; max-width: 400px"> |
||||
|
<sqx-checkbox-group |
||||
|
[disabled]="disabled" |
||||
|
[layout]="layout" |
||||
|
(ngModelChange)="ngModelChange" |
||||
|
[ngModel]="model" |
||||
|
[unsorted]="unsorted" |
||||
|
[values]="values"> |
||||
|
</sqx-checkbox-group> |
||||
|
</div> |
||||
|
`,
|
||||
|
}); |
||||
|
|
||||
|
export const Default = Template.bind({}); |
||||
|
|
||||
|
Default.args = { |
||||
|
values: ['Lorem', 'ipsum', 'dolor', 'sit', 'amet', 'consectetur', 'adipiscing'], |
||||
|
model: [], |
||||
|
}; |
||||
|
|
||||
|
export const Unsorted = Template.bind({}); |
||||
|
|
||||
|
Unsorted.args = { |
||||
|
values: ['Lorem', 'ipsum', 'dolor', 'sit', 'amet', 'consectetur', 'adipiscing'], |
||||
|
unsorted: true, |
||||
|
}; |
||||
|
|
||||
|
export const Small = Template.bind({}); |
||||
|
|
||||
|
Small.args = { |
||||
|
values: ['Lorem', 'ipsum', 'dolor'], |
||||
|
layout: 'Auto', |
||||
|
}; |
||||
|
|
||||
|
export const SmallMultiline = Template.bind({}); |
||||
|
|
||||
|
SmallMultiline.args = { |
||||
|
values: ['Lorem', 'ipsum', 'dolor'], |
||||
|
layout: 'Multiline', |
||||
|
}; |
||||
|
|
||||
|
export const Disabled = Template.bind({}); |
||||
|
|
||||
|
Disabled.args = { |
||||
|
values: ['Lorem', 'ipsum', 'dolor', 'sit', 'amet', 'consectetur', 'adipiscing'], |
||||
|
disabled: true, |
||||
|
}; |
||||
|
|
||||
|
export const Checked = Template.bind({}); |
||||
|
|
||||
|
Checked.args = { |
||||
|
values: ['Lorem', 'ipsum', 'dolor', 'sit', 'amet', 'consectetur', 'adipiscing'], |
||||
|
model: ['Lorem', 'ipsum'], |
||||
|
}; |
||||
@ -0,0 +1,14 @@ |
|||||
|
<div #container (sqxResized)="updateContainerWidth($event.width)"> |
||||
|
<div class="form-check" *ngFor="let value of tagValues; trackBy: trackByValue" |
||||
|
[class.form-check-block]="!snapshot.isSingleline" |
||||
|
[class.form-check-inline]="snapshot.isSingleline"> |
||||
|
<input class="form-check-input" type="radio" id="{{controlId}}_{{value}}" |
||||
|
[disabled]="snapshot.isDisabled" |
||||
|
[ngModel]="valueModel" |
||||
|
(ngModelChange)="callChange($event)" |
||||
|
[value]="value.value" |
||||
|
/> |
||||
|
|
||||
|
<label class="form-check-label" for="{{controlId}}_{{value}}">{{value.name}}</label> |
||||
|
</div> |
||||
|
</div> |
||||
@ -0,0 +1,11 @@ |
|||||
|
@import 'mixins'; |
||||
|
@import 'vars'; |
||||
|
|
||||
|
.form-check-block { |
||||
|
margin-bottom: .5rem; |
||||
|
margin-left: 0; |
||||
|
|
||||
|
.form-check-input { |
||||
|
margin-top: .3rem; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,135 @@ |
|||||
|
/* |
||||
|
* Squidex Headless CMS |
||||
|
* |
||||
|
* @license |
||||
|
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
||||
|
*/ |
||||
|
|
||||
|
import { AfterViewChecked, AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, forwardRef, Input, OnChanges, ViewChild } from '@angular/core'; |
||||
|
import { NG_VALUE_ACCESSOR } from '@angular/forms'; |
||||
|
import { getTagValues, MathHelper, StatefulControlComponent, TagValue, TextMeasurer } from '@app/framework/internal'; |
||||
|
|
||||
|
export const SQX_RADIO_GROUP_CONTROL_VALUE_ACCESSOR: any = { |
||||
|
provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => RadioGroupComponent), multi: true, |
||||
|
}; |
||||
|
|
||||
|
interface State { |
||||
|
// True when all checkboxes can be shown as single line.
|
||||
|
isSingleline?: boolean; |
||||
|
} |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'sqx-radio-group', |
||||
|
styleUrls: ['./radio-group.component.scss'], |
||||
|
templateUrl: './radio-group.component.html', |
||||
|
providers: [ |
||||
|
SQX_RADIO_GROUP_CONTROL_VALUE_ACCESSOR, |
||||
|
], |
||||
|
changeDetection: ChangeDetectionStrategy.OnPush, |
||||
|
}) |
||||
|
export class RadioGroupComponent extends StatefulControlComponent<State, string> implements AfterViewInit, AfterViewChecked, OnChanges { |
||||
|
private readonly textMeasurer: TextMeasurer; |
||||
|
private childrenWidth = 0; |
||||
|
private containerWidth = 0; |
||||
|
private labelsMeasured = false; |
||||
|
|
||||
|
public readonly controlId = MathHelper.guid(); |
||||
|
|
||||
|
@ViewChild('container', { static: false }) |
||||
|
public containerElement!: ElementRef<HTMLDivElement>; |
||||
|
|
||||
|
@Input() |
||||
|
public layout: 'Auto' | 'Singleline' | 'Multiline' = 'Auto'; |
||||
|
|
||||
|
@Input() |
||||
|
public unsorted = true; |
||||
|
|
||||
|
@Input() |
||||
|
public set disabled(value: boolean | undefined | null) { |
||||
|
this.setDisabledState(value === true); |
||||
|
} |
||||
|
|
||||
|
@Input() |
||||
|
public set values(value: ReadonlyArray<string | TagValue>) { |
||||
|
this.tagValuesUnsorted = getTagValues(value, false); |
||||
|
this.tagValuesSorted = this.tagValuesUnsorted.sortedByString(x => x.lowerCaseName); |
||||
|
} |
||||
|
|
||||
|
public get tagValues() { |
||||
|
return !this.unsorted ? this.tagValuesSorted : this.tagValuesUnsorted; |
||||
|
} |
||||
|
|
||||
|
public tagValuesSorted: ReadonlyArray<TagValue> = []; |
||||
|
public tagValuesUnsorted: ReadonlyArray<TagValue> = []; |
||||
|
|
||||
|
public valueModel: any; |
||||
|
|
||||
|
constructor(changeDetector: ChangeDetectorRef) { |
||||
|
super(changeDetector, {}); |
||||
|
|
||||
|
this.textMeasurer = new TextMeasurer(() => this.containerElement); |
||||
|
} |
||||
|
|
||||
|
public ngAfterViewInit() { |
||||
|
this.calculateWidth(); |
||||
|
} |
||||
|
|
||||
|
public ngAfterViewChecked() { |
||||
|
this.calculateWidth(); |
||||
|
} |
||||
|
|
||||
|
public ngOnChanges() { |
||||
|
this.labelsMeasured = false; |
||||
|
|
||||
|
this.calculateWidth(); |
||||
|
} |
||||
|
|
||||
|
public updateContainerWidth(width: number) { |
||||
|
this.containerWidth = width; |
||||
|
|
||||
|
this.calculateSingleLine(); |
||||
|
} |
||||
|
|
||||
|
private calculateWidth() { |
||||
|
if (this.labelsMeasured) { |
||||
|
this.calculateSingleLine(); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
let width = 0; |
||||
|
|
||||
|
for (const value of this.tagValuesUnsorted) { |
||||
|
width += 40; |
||||
|
width += this.textMeasurer.getTextSize(value.name); |
||||
|
} |
||||
|
|
||||
|
if (width < 0) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
this.childrenWidth = width; |
||||
|
this.calculateSingleLine(); |
||||
|
|
||||
|
this.labelsMeasured = true; |
||||
|
} |
||||
|
|
||||
|
private calculateSingleLine() { |
||||
|
let isSingleline = false; |
||||
|
|
||||
|
if (this.layout !== 'Auto') { |
||||
|
isSingleline = this.layout === 'Singleline'; |
||||
|
} else { |
||||
|
isSingleline = this.childrenWidth < this.containerWidth; |
||||
|
} |
||||
|
|
||||
|
this.next({ isSingleline }); |
||||
|
} |
||||
|
|
||||
|
public writeValue(obj: any) { |
||||
|
this.valueModel = obj; |
||||
|
} |
||||
|
|
||||
|
public trackByValue(_index: number, tag: TagValue) { |
||||
|
return tag.id; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,91 @@ |
|||||
|
/* |
||||
|
* Squidex Headless CMS |
||||
|
* |
||||
|
* @license |
||||
|
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
||||
|
*/ |
||||
|
|
||||
|
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; |
||||
|
import { moduleMetadata } from '@storybook/angular'; |
||||
|
import { Meta, Story } from '@storybook/angular/types-6-0'; |
||||
|
import { RadioGroupComponent, SqxFrameworkModule } from '@app/framework'; |
||||
|
|
||||
|
export default { |
||||
|
title: 'Framework/RadioGroup', |
||||
|
component: RadioGroupComponent, |
||||
|
argTypes: { |
||||
|
disabled: { |
||||
|
control: 'boolean', |
||||
|
}, |
||||
|
unsorted: { |
||||
|
control: 'boolean', |
||||
|
}, |
||||
|
}, |
||||
|
decorators: [ |
||||
|
moduleMetadata({ |
||||
|
imports: [ |
||||
|
BrowserAnimationsModule, |
||||
|
SqxFrameworkModule, |
||||
|
SqxFrameworkModule.forRoot(), |
||||
|
], |
||||
|
}), |
||||
|
], |
||||
|
} as Meta; |
||||
|
|
||||
|
const Template: Story<RadioGroupComponent & { model: any }> = (args: RadioGroupComponent) => ({ |
||||
|
props: args, |
||||
|
template: ` |
||||
|
<div style="padding: 2rem; max-width: 400px"> |
||||
|
<sqx-radio-group |
||||
|
[disabled]="disabled" |
||||
|
[layout]="layout" |
||||
|
(ngModelChange)="ngModelChange" |
||||
|
[ngModel]="model" |
||||
|
[unsorted]="unsorted" |
||||
|
[values]="values"> |
||||
|
</sqx-radio-group> |
||||
|
</div> |
||||
|
`,
|
||||
|
}); |
||||
|
|
||||
|
export const Default = Template.bind({}); |
||||
|
|
||||
|
Default.args = { |
||||
|
values: ['Lorem', 'ipsum', 'dolor', 'sit', 'amet', 'consectetur', 'adipiscing'], |
||||
|
model: [], |
||||
|
}; |
||||
|
|
||||
|
export const Unsorted = Template.bind({}); |
||||
|
|
||||
|
Unsorted.args = { |
||||
|
values: ['Lorem', 'ipsum', 'dolor', 'sit', 'amet', 'consectetur', 'adipiscing'], |
||||
|
unsorted: false, |
||||
|
}; |
||||
|
|
||||
|
export const Small = Template.bind({}); |
||||
|
|
||||
|
Small.args = { |
||||
|
values: ['Lorem', 'ipsum', 'dolor'], |
||||
|
layout: 'Auto', |
||||
|
}; |
||||
|
|
||||
|
export const SmallMultiline = Template.bind({}); |
||||
|
|
||||
|
SmallMultiline.args = { |
||||
|
values: ['Lorem', 'ipsum', 'dolor'], |
||||
|
layout: 'Multiline', |
||||
|
}; |
||||
|
|
||||
|
export const Disabled = Template.bind({}); |
||||
|
|
||||
|
Disabled.args = { |
||||
|
values: ['Lorem', 'ipsum', 'dolor', 'sit', 'amet', 'consectetur', 'adipiscing'], |
||||
|
disabled: true, |
||||
|
}; |
||||
|
|
||||
|
export const Checked = Template.bind({}); |
||||
|
|
||||
|
Checked.args = { |
||||
|
values: ['Lorem', 'ipsum', 'dolor', 'sit', 'amet', 'consectetur', 'adipiscing'], |
||||
|
model: 'ipsum', |
||||
|
}; |
||||
@ -0,0 +1,63 @@ |
|||||
|
/* |
||||
|
* Squidex Headless CMS |
||||
|
* |
||||
|
* @license |
||||
|
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
||||
|
*/ |
||||
|
|
||||
|
import { ElementRef } from '@angular/core'; |
||||
|
import { Types } from '../internal'; |
||||
|
|
||||
|
let CANVAS: HTMLCanvasElement | null = null; |
||||
|
|
||||
|
export class TextMeasurer { |
||||
|
private font?: string; |
||||
|
|
||||
|
constructor( |
||||
|
private readonly element: () => any | ElementRef<any>, |
||||
|
) { |
||||
|
} |
||||
|
|
||||
|
public getTextSize(text: string) { |
||||
|
if (!CANVAS) { |
||||
|
CANVAS = document.createElement('canvas'); |
||||
|
} |
||||
|
|
||||
|
if (!this.font) { |
||||
|
let currentElement = this.element(); |
||||
|
|
||||
|
if (Types.is(currentElement, ElementRef)) { |
||||
|
currentElement = currentElement.nativeElement; |
||||
|
} |
||||
|
|
||||
|
if (!currentElement) { |
||||
|
return -1000; |
||||
|
} |
||||
|
|
||||
|
const style = window.getComputedStyle(currentElement); |
||||
|
|
||||
|
const fontSize = style.getPropertyValue('font-size'); |
||||
|
const fontFamily = style.getPropertyValue('font-family'); |
||||
|
|
||||
|
if (!fontSize || !fontFamily) { |
||||
|
return -1000; |
||||
|
} |
||||
|
|
||||
|
this.font = `${fontSize} ${fontFamily}`; |
||||
|
} |
||||
|
|
||||
|
if (!this.font) { |
||||
|
return -1000; |
||||
|
} |
||||
|
|
||||
|
const ctx = CANVAS.getContext('2d'); |
||||
|
|
||||
|
if (!ctx) { |
||||
|
return -1000; |
||||
|
} |
||||
|
|
||||
|
ctx.font = this.font; |
||||
|
|
||||
|
return ctx.measureText(text).width; |
||||
|
} |
||||
|
} |
||||
@ -1,56 +1,9 @@ |
|||||
<ng-container [ngSwitch]="fieldName"> |
<sqx-table-header |
||||
<ng-container *ngSwitchCase="metaFields.id"> |
[language]="language" |
||||
<sqx-table-header text="i18n:contents.tableHeaders.id"></sqx-table-header> |
|
||||
</ng-container> |
|
||||
<ng-container *ngSwitchCase="metaFields.created"> |
|
||||
<sqx-table-header text="i18n:contents.tableHeaders.created" |
|
||||
[sortable]="true" |
|
||||
[fieldPath]="'created'" |
|
||||
[query]="query" |
[query]="query" |
||||
(queryChange)="queryChange.emit($event)" |
(queryChange)="queryChange.emit($event)" |
||||
[language]="language"> |
[sortable]="!!sortPath" |
||||
|
[sortDefault]="sortDefault" |
||||
|
[sortPath]="sortPath" |
||||
|
[text]="field.label"> |
||||
</sqx-table-header> |
</sqx-table-header> |
||||
</ng-container> |
|
||||
<ng-container *ngSwitchCase="metaFields.createdByAvatar"> |
|
||||
<sqx-table-header text="i18n:contents.tableHeaders.createdByShort"></sqx-table-header> |
|
||||
</ng-container> |
|
||||
<ng-container *ngSwitchCase="metaFields.createdByName"> |
|
||||
<sqx-table-header text="i18n:contents.tableHeaders.createdBy"></sqx-table-header> |
|
||||
</ng-container> |
|
||||
<ng-container *ngSwitchCase="metaFields.lastModified"> |
|
||||
<sqx-table-header text="i18n:contents.tableHeaders.lastModified" defaultOrder="ascending" |
|
||||
[sortable]="true" |
|
||||
[fieldPath]="'lastModified'" |
|
||||
[query]="query" |
|
||||
(queryChange)="queryChange.emit($event)" |
|
||||
[language]="language"> |
|
||||
</sqx-table-header> |
|
||||
</ng-container> |
|
||||
<ng-container *ngSwitchCase="metaFields.lastModifiedByAvatar"> |
|
||||
<sqx-table-header text="i18n:contents.tableHeaders.lastModifiedByShort"></sqx-table-header> |
|
||||
</ng-container> |
|
||||
<ng-container *ngSwitchCase="metaFields.lastModifiedByName"> |
|
||||
<sqx-table-header text="i18n:contents.tableHeaders.lastModifiedBy"></sqx-table-header> |
|
||||
</ng-container> |
|
||||
<ng-container *ngSwitchCase="metaFields.status"> |
|
||||
<sqx-table-header text="i18n:contents.tableHeaders.status"></sqx-table-header> |
|
||||
</ng-container> |
|
||||
<ng-container *ngSwitchCase="metaFields.statusNext"> |
|
||||
<sqx-table-header text="i18n:contents.tableHeaders.nextStatus"></sqx-table-header> |
|
||||
</ng-container> |
|
||||
<ng-container *ngSwitchCase="metaFields.statusColor"> |
|
||||
<sqx-table-header text="i18n:contents.tableHeaders.status"></sqx-table-header> |
|
||||
</ng-container> |
|
||||
<ng-container *ngSwitchCase="metaFields.version"> |
|
||||
<sqx-table-header text="i18n:contents.tableHeaders.version"></sqx-table-header> |
|
||||
</ng-container> |
|
||||
<ng-container *ngSwitchDefault> |
|
||||
<sqx-table-header [text]="fieldDisplayName" |
|
||||
[sortable]="isSortable" |
|
||||
[fieldPath]="fieldPath" |
|
||||
[query]="query" |
|
||||
(queryChange)="queryChange.emit($event)" |
|
||||
[language]="language"> |
|
||||
</sqx-table-header> |
|
||||
</ng-container> |
|
||||
</ng-container> |
|
||||
Loading…
Reference in new issue