Browse Source

Custom autocompleter

pull/1/head
Sebastian 9 years ago
parent
commit
17ed7c9f5b
  1. 20
      src/Squidex.Store.MongoDb/Apps/MongoAppRepository.cs
  2. 2
      src/Squidex/app/app.routes.ts
  3. 1
      src/Squidex/app/components/internal/app/settings/clients-page.component.ts
  4. 17
      src/Squidex/app/components/internal/app/settings/contributors-page.component.html
  5. 58
      src/Squidex/app/components/internal/app/settings/contributors-page.component.ts
  6. 1
      src/Squidex/app/components/internal/app/settings/languages-page.component.ts
  7. 18
      src/Squidex/app/framework/angular/autocomplete.component.html
  8. 51
      src/Squidex/app/framework/angular/autocomplete.component.scss
  9. 208
      src/Squidex/app/framework/angular/autocomplete.component.ts
  10. 54
      src/Squidex/app/framework/angular/scroll-active.directive.ts
  11. 2
      src/Squidex/app/framework/declarations.ts
  12. 6
      src/Squidex/app/framework/module.ts
  13. 2
      src/Squidex/app/shared/services/auth.service.ts

20
src/Squidex.Store.MongoDb/Apps/MongoAppRepository.cs

@ -62,6 +62,16 @@ namespace Squidex.Store.MongoDb.Apps
});
}
protected Task On(AppContributorAssigned @event, EnvelopeHeaders headers)
{
return Collection.UpdateAsync(headers, a =>
{
var contributor = a.Contributors.GetOrAddNew(@event.ContributorId);
SimpleMapper.Map(@event, contributor);
});
}
protected Task On(AppContributorRemoved @event, EnvelopeHeaders headers)
{
return Collection.UpdateAsync(headers, a =>
@ -118,16 +128,6 @@ namespace Squidex.Store.MongoDb.Apps
});
}
protected Task On(AppContributorAssigned @event, EnvelopeHeaders headers)
{
return Collection.UpdateAsync(headers, a =>
{
var contributor = a.Contributors.GetOrAddNew(@event.ContributorId);
SimpleMapper.Map(@event, contributor);
});
}
public Task On(Envelope<IEvent> @event)
{
return this.DispatchActionAsync(@event.Payload, @event.Headers);

2
src/Squidex/app/app.routes.ts

@ -86,4 +86,4 @@ export const routes: Ng2Router.Routes = [
}
];
export const routing: Ng2.ModuleWithProviders = Ng2Router.RouterModule.forRoot(routes, { useHash: false, enableTracing: true });
export const routing: Ng2.ModuleWithProviders = Ng2Router.RouterModule.forRoot(routes, { useHash: false });

1
src/Squidex/app/components/internal/app/settings/clients-page.component.ts

@ -59,7 +59,6 @@ export class ClientsPageComponent implements Ng2.OnInit {
this.appName = app.name;
this.titles.setTitle('{appName} | Settings | Clients', { appName: app.name });
this.load();
}
});

17
src/Squidex/app/components/internal/app/settings/contributors-page.component.html

@ -49,19 +49,12 @@
<div class="table-items-footer">
<form class="form-inline" (submit)="assignContributor()" >
<div class="form-group">
<ng2-completer
[autoMatch]="true"
[dataService]="usersDataSource"
[minSearchLength]="3"
[placeholder]="'Search user by e-mail'"
[pause]="300"
[clearSelected]="false"
[textSearching]="'Searching...'"
[inputName]="contributor"
[ngModel]="selectedUserName"
<sqx-autocomplete [source]="usersDataSource"
(ngModelChange)="selectUser($event.model)"
[ngModel]="selectedUser"
[ngModelOptions]="{standalone: true}"
(selected)="selectUser($event)">
</ng2-completer>
[inputName]="contributor">
</sqx-autocomplete>
</div>
<button type="submit" class="btn btn-success" [disabled]="!selectedUser">Add Contributor</button>

58
src/Squidex/app/components/internal/app/settings/contributors-page.component.ts

@ -9,13 +9,13 @@ import * as Ng2 from '@angular/core';
import { Observable, Subscription } from 'rxjs';
import { CompleterBaseData, CompleterItem } from 'ng2-completer';
import {
AppContributorDto,
AppContributorsService,
AppsStoreService,
AuthService,
AutocompleteItem,
AutocompleteSource,
Notification,
NotificationService,
TitleService,
@ -24,45 +24,30 @@ import {
UsersProviderService
} from 'shared';
class UsersDataSource extends CompleterBaseData {
private remoteSearch: Subscription;
class UsersDataSource implements AutocompleteSource {
constructor(
private readonly usersService: UsersService,
private readonly component: ContributorsPageComponent
) {
super();
}
public search(term: string): void {
this.cancel();
this.remoteSearch =
this.usersService.getUsers(term)
.map(users => {
const results: CompleterItem[] = [];
for (let u of users) {
if (!this.component.appContributors || !this.component.appContributors.find(t => t.contributorId === u.id)) {
results.push({ title: u.displayName, image: u.pictureUrl, originalObject: u, description: u.email });
}
public find(query: string): Observable<AutocompleteItem[]> {
return this.usersService.getUsers(query)
.map(users => {
const results: AutocompleteItem[] = [];
for (let user of users) {
if (!this.component.appContributors || !this.component.appContributors.find(t => t.contributorId === user.id)) {
results.push(
new AutocompleteItem(
user.displayName,
user.email,
user.pictureUrl,
user));
}
this.next(results);
return results;
})
.catch(err => {
this.error(err);
return null;
}).subscribe();
}
public cancel() {
if (this.remoteSearch) {
this.remoteSearch.unsubscribe();
}
}
return results;
});
}
}
@ -114,7 +99,6 @@ export class ContributorsPageComponent implements Ng2.OnInit {
this.appName = app.name;
this.titles.setTitle('{appName} | Settings | Contributors', { appName: app.name });
this.load();
}
});
@ -165,8 +149,8 @@ export class ContributorsPageComponent implements Ng2.OnInit {
}).subscribe();
}
public selectUser(selection: CompleterItem | null) {
this.selectedUser = selection ? selection.originalObject : null;
public selectUser(selection: UserDto) {
this.selectedUser = selection;
}
public email(contributor: AppContributorDto): Observable<string> {

1
src/Squidex/app/components/internal/app/settings/languages-page.component.ts

@ -63,7 +63,6 @@ export class LanguagesPageComponent implements Ng2.OnInit {
this.appName = app.name;
this.titles.setTitle('{appName} | Settings | Languages', { appName: app.name });
this.load();
}
});

18
src/Squidex/app/framework/angular/autocomplete.component.html

@ -0,0 +1,18 @@
<span>
<input type="text" class="form-control" (blur)="blur()" [attr.name]="inputName" (keydown)="keyDown($event)"
[formControl]="queryInput"
autocomplete="off"
autocorrect="off"
autocapitalize="off">
<div class="items-container" *ngIf="items.length > 0">
<div class="items" #container>
<div *ngFor="let item of items; let i = index;" class="item" [class.active]="i === itemSelection" (mousedown)="chooseItem(item)" (mouseover)="selectIndex(i)" [sqxScrollActive]="i === itemSelection" [container]="container">
<img class="item-image" [attr.src]="item.image" />
<span class="item-title">{{item.title}}</span>
<span class="item-description">{{item.description}}</span>
</div>
</div>
</div>
</span>

51
src/Squidex/app/framework/angular/autocomplete.component.scss

@ -0,0 +1,51 @@
@import '_mixins';
@import '_vars';
$color-input-border: rgba(0, 0, 0, .15);
.items {
&-container {
position: relative;
}
& {
@include absolute(2px, auto, auto, 0);
@include border-radius(.25em);
@include box-shadow;
width: 300px;
max-height: 200px;
border: 1px solid $color-input-border;
background: $color-accent-dark;
padding: .3rem 0;
overflow-y: auto;
}
}
.item {
& {
padding: .3rem .8rem;
background: transparent;
cursor: pointer;
}
&.active {
color: $color-accent-dark;
background: $color-theme-blue;
}
&-image {
@include circle(40px);
float: left;
}
&-title,
&-description {
@include truncate;
margin-left: 45px;
}
&-description {
font-size: .8rem;
font-style: italic;
}
}

208
src/Squidex/app/framework/angular/autocomplete.component.ts

@ -0,0 +1,208 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Sebastian Stehle. All rights reserved
*/
import * as Ng2 from '@angular/core';
import * as Ng2Forms from '@angular/forms';
import { Observable, Subscription } from 'rxjs';
export interface AutocompleteSource {
find(query: string): Observable<AutocompleteItem[]>;
}
export class AutocompleteItem {
constructor(
public readonly title: string,
public readonly description: string,
public readonly image: string,
public readonly model: any
) {
}
}
const KEY_ENTER = 13;
const KEY_UP = 38;
const KEY_DOWN = 40;
const NOOP = () => { };
@Ng2.Component({
selector: 'sqx-autocomplete',
styles,
template,
providers: [{
provide: Ng2Forms.NG_VALUE_ACCESSOR,
useExisting: Ng2.forwardRef(() => {
return AutocompleteComponent;
}),
multi: true
}]
})
export class AutocompleteComponent implements Ng2Forms.ControlValueAccessor {
private subscription: Subscription | null = null;
private lastQuery: string | null;
private changeCallback: (value: any) => void = NOOP;
private touchedCallback: () => void = NOOP;
@Ng2.Input()
public source: AutocompleteSource;
@Ng2.Input()
public inputName: string;
public items: AutocompleteItem[] = [];
public itemSelection = -1;
public queryInput = new Ng2Forms.FormControl();
constructor() {
this.queryInput.valueChanges.delay(100).subscribe(query => this.loadItems(query));
}
public writeValue(value: any) {
if (!value) {
this.queryInput.setValue('');
} else {
let item: AutocompleteItem | null = null;
if (value instanceof AutocompleteItem) {
item = value;
} else {
item = this.items.find(i => i.model === value);
}
if (item) {
this.queryInput.setValue(value.title || '');
}
}
this.reset();
}
public registerOnChange(fn: any) {
this.changeCallback = fn;
}
public registerOnTouched(fn: any) {
this.touchedCallback = fn;
}
public setDisabledState(isDisabled: boolean): void {
if (isDisabled) {
this.reset();
this.queryInput.disable();
} else {
this.queryInput.enable();
}
}
private loadItems(query: string) {
const source = this.source;
if (this.subscription != null) {
this.subscription.unsubscribe();
this.subscription = null;
}
if (!source) {
return;
}
let isInvalidQuery = this.lastQuery === query || !query || query.trim() === '';
this.lastQuery = query;
if (isInvalidQuery) {
this.reset();
return;
}
this.lastQuery = query;
this.subscription = source.find(query)
.catch(error => {
return Observable.of([]);
})
.subscribe(result => {
this.reset();
this.items = result || [];
});
}
public keyDown(event: KeyboardEvent) {
switch (event.keyCode) {
case KEY_UP:
this.up();
event.stopPropagation();
event.preventDefault();
break;
case KEY_DOWN:
this.down();
event.stopPropagation();
event.preventDefault();
break;
case KEY_ENTER:
if (this.items.length > 0) {
this.chooseItem();
event.stopPropagation();
event.preventDefault();
}
break;
}
}
private reset() {
this.items = [];
this.itemSelection = -1;
}
public blur() {
this.reset();
}
public up() {
this.selectIndex(this.itemSelection - 1);
}
public down() {
this.selectIndex(this.itemSelection + 1);
}
public chooseItem(selection: AutocompleteItem = null) {
if (!selection) {
selection = this.items[this.itemSelection];
}
if (!selection && this.items.length === 1) {
selection = this.items[0];
}
if (selection) {
try {
this.queryInput.setValue(selection.title);
this.changeCallback(selection);
} finally {
this.reset();
}
}
}
public selectIndex(selection: number) {
if (selection < 0) {
selection = 0;
}
if (selection >= this.items.length) {
selection = this.items.length - 1;
}
this.itemSelection = selection;
}
}

54
src/Squidex/app/framework/angular/scroll-active.directive.ts

@ -0,0 +1,54 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Sebastian Stehle. All rights reserved
*/
import * as Ng2 from '@angular/core';
@Ng2.Directive({
selector: '[sqxScrollActive]'
})
export class ScrollActiveDirective implements Ng2.OnChanges {
@Ng2.Input('sqxScrollActive')
public isActive = false;
@Ng2.Input()
public container: HTMLElement;
constructor(
private readonly element: Ng2.ElementRef
) {
}
public ngOnChanges() {
if (this.isActive && this.container) {
this.scrollInView(this.container, this.element.nativeElement);
}
}
private scrollInView(parent: HTMLElement, target: HTMLElement) {
if (!parent.getBoundingClientRect) {
return;
}
const parentRect = parent.getBoundingClientRect();
const targetRect = target.getBoundingClientRect();
const offset = (targetRect.top + document.body.scrollTop) - (parentRect.top + document.body.scrollTop);
const scroll = parent.scrollTop;
if (offset < 0) {
parent.scrollTop = scroll + offset;
} else {
const targetHeight = targetRect.height;
const parentHeight = parentRect.height;
if ((offset + targetHeight) > parentHeight) {
parent.scrollTop = scroll + offset - parentHeight + targetHeight;
}
}
}
}

2
src/Squidex/app/framework/declarations.ts

@ -7,6 +7,7 @@
export * from './angular/action';
export * from './angular/animations';
export * from './angular/autocomplete.component';
export * from './angular/validators';
export * from './angular/cloak.directive';
export * from './angular/color-picker.component';
@ -17,6 +18,7 @@ export * from './angular/focus-on-init.directive';
export * from './angular/image-drop.directive';
export * from './angular/modal-view.directive';
export * from './angular/money.pipe';
export * from './angular/scroll-active.directive';
export * from './angular/shortcut.component';
export * from './angular/slider.component';
export * from './angular/spinner.component';

6
src/Squidex/app/framework/module.ts

@ -12,6 +12,7 @@ import * as Ng2Common from '@angular/common';
import * as Ng2Router from '@angular/router';
import {
AutocompleteComponent,
CloakDirective,
ColorPickerComponent,
DayOfWeekPipe,
@ -24,6 +25,7 @@ import {
ModalViewDirective,
MoneyPipe,
MonthPipe,
ScrollActiveDirective,
ShortcutComponent,
ShortDatePipe,
ShortTimePipe,
@ -41,6 +43,7 @@ import {
Ng2Router.RouterModule
],
declarations: [
AutocompleteComponent,
CloakDirective,
ColorPickerComponent,
DayOfWeekPipe,
@ -53,6 +56,7 @@ import {
ModalViewDirective,
MoneyPipe,
MonthPipe,
ScrollActiveDirective,
ShortcutComponent,
ShortDatePipe,
ShortTimePipe,
@ -61,6 +65,7 @@ import {
UserReportComponent,
],
exports: [
AutocompleteComponent,
CloakDirective,
ColorPickerComponent,
DayOfWeekPipe,
@ -73,6 +78,7 @@ import {
ModalViewDirective,
MoneyPipe,
MonthPipe,
ScrollActiveDirective,
ShortcutComponent,
ShortDatePipe,
ShortTimePipe,

2
src/Squidex/app/shared/services/auth.service.ts

@ -67,8 +67,6 @@ export class AuthService {
return;
}
Log.logger = console;
this.userManager = new UserManager({
client_id: 'squidex-frontend',
scope: 'squidex-api openid profile squidex-profile',

Loading…
Cancel
Save