Skip to content

Commit

Permalink
Merge pull request #50 from edcarroll/develop
Browse files Browse the repository at this point in the history
v0.6.0 into master
  • Loading branch information
edcarroll authored Mar 28, 2017
2 parents 7560e32 + 25cd5d7 commit db884ad
Show file tree
Hide file tree
Showing 55 changed files with 989 additions and 209 deletions.
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
/tmp

## ngc logs

*.ngfactory.ts
*.ngsummary.json

Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ Now you're good to go!

## Dependencies

* [Angular 2](https://angular.io) (>=2.0.0)
* [Angular 2](https://angular.io) (^4.0.0)
* [Semantic UI CSS](http://semantic-ui.com/) (jQuery is **not** required)

## Components
Expand All @@ -62,6 +62,7 @@ The current list of available components with links to their docs is below:
* [Rating](https://edcarroll.github.io/ng2-semantic-ui/#/components/rating)
* [Search](https://edcarroll.github.io/ng2-semantic-ui/#/components/search)
* [Select](https://edcarroll.github.io/ng2-semantic-ui/#/components/select)
* [Sidebar](https://edcarroll.github.io/ng2-semantic-ui/#/components/sidebar)
* [Tabs](https://edcarroll.github.io/ng2-semantic-ui/#/components/tabs)
* [Transition](https://edcarroll.github.io/ng2-semantic-ui/#/components/transition)

Expand Down
5 changes: 4 additions & 1 deletion angular-cli.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@
"tsconfig": "tsconfig.json",
"prefix": "demo",
"mobile": false,
"styles": ["styles.css"],
"styles": [
"css/styles.css",
"css/code.css"
],
"scripts": [],
"environmentSource": "environments/environment.ts",
"environments": {
Expand Down
9 changes: 8 additions & 1 deletion components/dropdown/dropdown-menu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@ import {SuiTransition, Transition} from '../transition/transition';
import {DropdownService, DropdownAutoCloseType} from './dropdown.service';
import {TransitionController} from '../transition/transition-controller';
import {KeyCode} from '../util/util';
// Polyfill for IE
import "element-closest";

interface AugmentedElement extends Element {
closest(selector:string):AugmentedElement;
}

@Directive({
// We must attach to every '.item' as Angular doesn't support > selectors.
Expand Down Expand Up @@ -137,7 +143,8 @@ export class SuiDropdownMenu extends SuiTransition implements AfterContentInit {
e.stopPropagation();

if (this._service.autoCloseMode == DropdownAutoCloseType.ItemClick) {
if (e.srcElement.classList.contains("item")) {
const target = e.target as AugmentedElement;
if (this.element.nativeElement.contains(target.closest(".item")) && !/input|textarea/i.test(target.tagName)) {
// Once an item is selected, we can close the entire dropdown.
this._service.setOpenState(false, true);
}
Expand Down
2 changes: 1 addition & 1 deletion components/dropdown/dropdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export class SuiDropdown implements AfterContentInit {

@HostBinding('attr.tabindex')
public get tabIndex() {
return this.isDisabled ? -1 : 0;
return (this.isDisabled || this.service.isNested) ? null : 0;
}

@Input()
Expand Down
1 change: 1 addition & 0 deletions components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export * from "./progress/progress.module";
export * from "./rating/rating.module";
export * from "./search/search.module";
export * from "./select/select.module";
export * from "./sidebar/sidebar.module";
export * from "./tabs/tab.module";
export * from "./transition/transition.module";

Expand Down
3 changes: 3 additions & 0 deletions components/popup/popup.directive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,9 @@ export class SuiPopupDirective {
if (!this._popupComponentRef) {
const factory = this._componentFactoryResolver.resolveComponentFactory(SuiPopup);
this._popupComponentRef = this._viewContainerRef.createComponent(factory);

// Move the generated element to the body to avoid any positioning issues.
document.querySelector("body").appendChild(this._popupComponentRef.location.nativeElement);

this._popup.onClose.subscribe(() => {
this._popupComponentRef.destroy();
Expand Down
4 changes: 2 additions & 2 deletions components/search/search.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {FormsModule} from "@angular/forms";
import {SuiDropdownModule} from "../dropdown/dropdown.module";
import {SuiTransitionModule} from "../transition/transition.module";
import {SuiSearch, SuiSearchValueAccessor} from './search';
import {SearchService} from './search.service';
import {SearchService, LookupFn} from './search.service';

@NgModule({
imports: [
Expand All @@ -23,4 +23,4 @@ import {SearchService} from './search.service';
})
export class SuiSearchModule {}

export {SearchService};
export {SearchService, LookupFn};
30 changes: 23 additions & 7 deletions components/search/search.service.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import {readValue} from '../util/util';

// Define useful types to avoid any.
export type LookupFn<T> = (query:string) => Promise<T[]>
export type LookupFn<T> = (query:string) => Promise<T[]> | Promise<T>;
export type QueryLookupFn<T> = (query:string) => Promise<T[]>;
export type ItemLookupFn<T, U> = (query:string, initial:U) => Promise<T>;
export type ItemsLookupFn<T, U> = (query:string, initial:U[]) => Promise<T[]>;

type CachedArray<T> = { [query:string]:T[] };

// T extends JavascriptObject so we can do a recursive search on the object.
export class SearchService<T> {
// Stores the available options.
private _options:T[];
Expand Down Expand Up @@ -36,6 +39,22 @@ export class SearchService<T> {
this.reset();
}

public get queryLookup() {
return this._optionsLookup as QueryLookupFn<T>;
}

public get hasItemLookup() {
return this.optionsLookup && this.optionsLookup.length == 2;
}

public itemLookup<U>(initial:U) {
return (this._optionsLookup as ItemLookupFn<T, U>)(undefined, initial);
}

public itemsLookup<U>(initial:U[]) {
return (this._optionsLookup as ItemsLookupFn<T, U>)(undefined, initial);
}

public get optionsField() {
return this._optionsField
}
Expand Down Expand Up @@ -112,7 +131,7 @@ export class SearchService<T> {
if (this._optionsLookup) {
this._isSearching = true;

this._optionsLookup(this._query)
this.queryLookup(this._query)
.then(results => {
// Unset 'loading' state, and display & cache the results.
this._isSearching = false;
Expand Down Expand Up @@ -170,12 +189,9 @@ export class SearchService<T> {

// Resets the search back to a pristine state.
private reset() {
this._query = "";
this._results = [];
if (this.allowEmptyQuery) {
this._results = this._options;
}
this._resultsCache = {};
this._isSearching = false;
this.updateQuery("");
}
}
13 changes: 3 additions & 10 deletions components/search/search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ import {PositioningService, PositioningPlacement} from '../util/positioning.serv
export class SuiSearch<T> implements AfterViewInit {
public dropdownService:DropdownService;
public searchService:SearchService<T>;
public position:PositioningService;

@ViewChild(SuiDropdownMenu)
private _menu:SuiDropdownMenu;
Expand Down Expand Up @@ -78,7 +77,7 @@ export class SuiSearch<T> implements AfterViewInit {
// Sets local or remote options by determining whether a function is passed.
@Input()
public set options(options:T[] | LookupFn<T>) {
if (typeof(options) == "function") {
if (typeof options == "function") {
this.searchService.optionsLookup = options;
return;
}
Expand Down Expand Up @@ -131,10 +130,6 @@ export class SuiSearch<T> implements AfterViewInit {

public ngAfterViewInit() {
this._menu.service = this.dropdownService;

// Initialse the positioning service to correctly display the results.
// This adds support for repositioning the results above the search when there isn't enough space below.
this.position = new PositioningService(this._element, this._menu.element, PositioningPlacement.BottomLeft);
}

// Selects an item.
Expand All @@ -160,10 +155,8 @@ export class SuiSearch<T> implements AfterViewInit {

// Sets a specific item to be selected, updating the query automatically.
public writeValue(item:T) {
if (item) {
this.selectedItem = item;
this.searchService.updateQuery(this.readValue(item) as string, () => {});
}
this.selectedItem = item;
this.searchService.updateQuery(item ? this.readValue(item) as string : "");
}
}

Expand Down
5 changes: 5 additions & 0 deletions components/select/multi-select-label.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,9 @@ export class SuiMultiSelectLabel<T> extends SuiTransition implements ISelectRend
this._transitionController.animate(new Transition("scale", 100, TransitionDirection.Out, () =>
this.onDeselected.emit(this.value)));
}

@HostListener("click", ["$event"])
public onClick(event:MouseEvent) {
event.stopPropagation();
}
}
19 changes: 13 additions & 6 deletions components/select/multi-select.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,9 @@ export class SuiMultiSelect<T, U> extends SuiSelectBase<T, U> implements AfterVi
}

protected optionsUpdateHook() {
if (this._writtenOptions && this.options.length > 0) {
if (this._writtenOptions && this.searchService.options.length > 0) {
// If there were values written by ngModel before the options had been loaded, this runs to fix it.
this.selectedOptions = this._writtenOptions.map(v => this.options.find(o => v == this.valueGetter(o)));
this.selectedOptions = this._writtenOptions.map(v => this.searchService.options.find(o => v == this.valueGetter(o)));

if (this.selectedOptions.length == this._writtenOptions.length) {
this._writtenOptions = null;
Expand Down Expand Up @@ -112,13 +112,20 @@ export class SuiMultiSelect<T, U> extends SuiSelectBase<T, U> implements AfterVi

public writeValues(values:U[]) {
if (values instanceof Array) {
if (this.options.length > 0) {
if (this.searchService.options.length > 0) {
// If the options have already been loaded, we can immediately match the ngModel values to options.
this.selectedOptions = values.map(v => this.options.find(o => v == this.valueGetter(o)));
this.selectedOptions = values.map(v => this.findOption(this.searchService.options, v));
}
if (values != [] && this.selectedOptions.length == 0) {
// Otherwise, cache the written value for when options are set.
this._writtenOptions = values;
if (this.valueField && this.searchService.hasItemLookup) {
// If the search service has a selected lookup function, make use of that to load the initial values.
this.searchService.itemsLookup<U>(values)
.then(r => this.selectedOptions = r);
}
else {
// Otherwise, cache the written value for when options are set.
this._writtenOptions = values;
}
}
}
}
Expand Down
27 changes: 19 additions & 8 deletions components/select/select-base.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {Component, ViewChild, HostBinding, ElementRef, HostListener, Input, ContentChildren, QueryList, ViewChildren, AfterContentInit, EventEmitter, Output, Renderer, TemplateRef, ViewContainerRef} from '@angular/core';
import {DropdownService} from '../dropdown/dropdown.service';
import {SearchService} from '../search/search.service';
import {SearchService, LookupFn} from '../search/search.service';
import {readValue, KeyCode} from '../util/util';
import {PositioningService, PositioningPlacement} from '../util/positioning.service';
import {SuiDropdownMenu, SuiDropdownMenuItem} from '../dropdown/dropdown-menu';
Expand Down Expand Up @@ -65,12 +65,14 @@ export abstract class SuiSelectBase<T, U> implements AfterContentInit {
public placeholder:string;

@Input()
public get options() {
return this.searchService.options;
}

public set options(options:T[]) {
this.searchService.options = options;
public set options(options:T[] | LookupFn<T>) {
if (typeof options == "function") {
this.searchService.optionsLookup = options;
}
else {
this.searchService.options = options;
}

this.optionsUpdateHook();
}

Expand All @@ -87,6 +89,10 @@ export abstract class SuiSelectBase<T, U> implements AfterContentInit {

public set query(query:string) {
this.queryUpdateHook();
this.updateQuery(query);
}

protected updateQuery(query:string) {
// Update the query then open the dropdown, as after keyboard input it should always be open.
this.searchService.updateQuery(query, () =>
this.dropdownService.setOpenState(true));
Expand Down Expand Up @@ -174,6 +180,11 @@ export abstract class SuiSelectBase<T, U> implements AfterContentInit {
throw new Error("Not implemented");
}

protected findOption(options:T[], value:U) {
// Tries to find an option in options array
return options.find(o => value == this.valueGetter(o));
}

@HostListener("click", ['$event'])
public onClick(e:MouseEvent) {
e.stopPropagation();
Expand Down Expand Up @@ -203,7 +214,7 @@ export abstract class SuiSelectBase<T, U> implements AfterContentInit {
// Helper that draws the provided template beside the provided ViewContainerRef.
protected drawTemplate(siblingRef:ViewContainerRef, value:T) {
siblingRef.clear();
// Use of `$implicit` means use of <template let-option> syntax is supported.
// Use of `$implicit` means use of <ng-template let-option> syntax is supported.
siblingRef.createEmbeddedView(this.optionTemplate, { '$implicit': value });
}
}
25 changes: 19 additions & 6 deletions components/select/select.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import {SuiSelectBase} from './select-base';
import {NG_VALUE_ACCESSOR, ControlValueAccessor} from '@angular/forms';
import {ISelectRenderedOption} from './select-option';

export type SingleItemLookup<T, U> = (query:string, initial?:U) => Promise<T>;

@Component({
selector: 'sui-select',
template: `
Expand Down Expand Up @@ -41,9 +43,9 @@ export class SuiSelect<T, U> extends SuiSelectBase<T, U> {
}

protected optionsUpdateHook() {
if (this._writtenOption && this.options.length > 0) {
if (this._writtenOption && this.searchService.options.length > 0) {
// If there was an value written by ngModel before the options had been loaded, this runs to fix it.
this.selectedOption = this.options.find(o => this._writtenOption == this.valueGetter(o));
this.selectedOption = this.findOption(this.searchService.options, this._writtenOption);
if (this.selectedOption) {
this._writtenOption = null;
this.drawSelectedOption();
Expand Down Expand Up @@ -85,13 +87,24 @@ export class SuiSelect<T, U> extends SuiSelectBase<T, U> {

public writeValue(value:U) {
if (value != null) {
if (this.options.length > 0) {
if (this.searchService.options.length > 0) {
// If the options have already been loaded, we can immediately match the ngModel value to an option.
this.selectedOption = this.options.find(o => value == this.valueGetter(o));
this.selectedOption = this.findOption(this.searchService.options, value);
}
if (!this.selectedOption) {
// Otherwise, cache the written value for when options are set.
this._writtenOption = value;
if (this.valueField && this.searchService.hasItemLookup) {
// If the search service has a selected lookup function, make use of that to load the initial value.
(this.searchService.itemLookup<U>(value) as Promise<T>)
.then(r => {
this.selectedOption = r;
this.drawSelectedOption();
});
return;
}
else {
// Otherwise, cache the written value for when options are set.
this._writtenOption = value;
}
}
}

Expand Down
Loading

0 comments on commit db884ad

Please sign in to comment.