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"> |
|||
<ng-container *ngSwitchCase="metaFields.id"> |
|||
<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'" |
|||
<sqx-table-header |
|||
[language]="language" |
|||
[query]="query" |
|||
(queryChange)="queryChange.emit($event)" |
|||
[language]="language"> |
|||
</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> |
|||
[sortable]="!!sortPath" |
|||
[sortDefault]="sortDefault" |
|||
[sortPath]="sortPath" |
|||
[text]="field.label"> |
|||
</sqx-table-header> |
|||
Loading…
Reference in new issue