diff --git a/.gitignore b/.gitignore index b4ced5e11..4dcd9f6b3 100644 --- a/.gitignore +++ b/.gitignore @@ -5,7 +5,6 @@ /tmp ## ngc logs - *.ngfactory.ts *.ngsummary.json diff --git a/README.md b/README.md index 8deb26e58..456acbe62 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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) diff --git a/angular-cli.json b/angular-cli.json index 4c985940a..32ad12b03 100644 --- a/angular-cli.json +++ b/angular-cli.json @@ -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": { diff --git a/components/dropdown/dropdown-menu.ts b/components/dropdown/dropdown-menu.ts index d49457b49..5ef7f5d55 100644 --- a/components/dropdown/dropdown-menu.ts +++ b/components/dropdown/dropdown-menu.ts @@ -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. @@ -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); } diff --git a/components/dropdown/dropdown.ts b/components/dropdown/dropdown.ts index 776c1e87d..7802e6625 100644 --- a/components/dropdown/dropdown.ts +++ b/components/dropdown/dropdown.ts @@ -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() diff --git a/components/index.ts b/components/index.ts index b04b11698..8f4b56a61 100644 --- a/components/index.ts +++ b/components/index.ts @@ -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"; diff --git a/components/popup/popup.directive.ts b/components/popup/popup.directive.ts index 453ab8c54..603a93e57 100644 --- a/components/popup/popup.directive.ts +++ b/components/popup/popup.directive.ts @@ -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(); diff --git a/components/search/search.module.ts b/components/search/search.module.ts index e56917b85..2e24f7a13 100644 --- a/components/search/search.module.ts +++ b/components/search/search.module.ts @@ -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: [ @@ -23,4 +23,4 @@ import {SearchService} from './search.service'; }) export class SuiSearchModule {} -export {SearchService}; \ No newline at end of file +export {SearchService, LookupFn}; \ No newline at end of file diff --git a/components/search/search.service.ts b/components/search/search.service.ts index 3342efb46..f581f4b16 100644 --- a/components/search/search.service.ts +++ b/components/search/search.service.ts @@ -1,10 +1,13 @@ import {readValue} from '../util/util'; // Define useful types to avoid any. -export type LookupFn = (query:string) => Promise +export type LookupFn = (query:string) => Promise | Promise; +export type QueryLookupFn = (query:string) => Promise; +export type ItemLookupFn = (query:string, initial:U) => Promise; +export type ItemsLookupFn = (query:string, initial:U[]) => Promise; + type CachedArray = { [query:string]:T[] }; -// T extends JavascriptObject so we can do a recursive search on the object. export class SearchService { // Stores the available options. private _options:T[]; @@ -36,6 +39,22 @@ export class SearchService { this.reset(); } + public get queryLookup() { + return this._optionsLookup as QueryLookupFn; + } + + public get hasItemLookup() { + return this.optionsLookup && this.optionsLookup.length == 2; + } + + public itemLookup(initial:U) { + return (this._optionsLookup as ItemLookupFn)(undefined, initial); + } + + public itemsLookup(initial:U[]) { + return (this._optionsLookup as ItemsLookupFn)(undefined, initial); + } + public get optionsField() { return this._optionsField } @@ -112,7 +131,7 @@ export class SearchService { 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; @@ -170,12 +189,9 @@ export class SearchService { // 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(""); } } \ No newline at end of file diff --git a/components/search/search.ts b/components/search/search.ts index 4221eb105..3a187fd6b 100644 --- a/components/search/search.ts +++ b/components/search/search.ts @@ -39,7 +39,6 @@ import {PositioningService, PositioningPlacement} from '../util/positioning.serv export class SuiSearch implements AfterViewInit { public dropdownService:DropdownService; public searchService:SearchService; - public position:PositioningService; @ViewChild(SuiDropdownMenu) private _menu:SuiDropdownMenu; @@ -78,7 +77,7 @@ export class SuiSearch implements AfterViewInit { // Sets local or remote options by determining whether a function is passed. @Input() public set options(options:T[] | LookupFn) { - if (typeof(options) == "function") { + if (typeof options == "function") { this.searchService.optionsLookup = options; return; } @@ -131,10 +130,6 @@ export class SuiSearch 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. @@ -160,10 +155,8 @@ export class SuiSearch 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 : ""); } } diff --git a/components/select/multi-select-label.ts b/components/select/multi-select-label.ts index 0449a10c4..b15bb97af 100644 --- a/components/select/multi-select-label.ts +++ b/components/select/multi-select-label.ts @@ -55,4 +55,9 @@ export class SuiMultiSelectLabel 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(); + } } \ No newline at end of file diff --git a/components/select/multi-select.ts b/components/select/multi-select.ts index 828426d00..67e12baa0 100644 --- a/components/select/multi-select.ts +++ b/components/select/multi-select.ts @@ -51,9 +51,9 @@ export class SuiMultiSelect extends SuiSelectBase 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; @@ -112,13 +112,20 @@ export class SuiMultiSelect extends SuiSelectBase 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(values) + .then(r => this.selectedOptions = r); + } + else { + // Otherwise, cache the written value for when options are set. + this._writtenOptions = values; + } } } } diff --git a/components/select/select-base.ts b/components/select/select-base.ts index e5489c2fa..5634a6f9b 100644 --- a/components/select/select-base.ts +++ b/components/select/select-base.ts @@ -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'; @@ -65,12 +65,14 @@ export abstract class SuiSelectBase 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) { + if (typeof options == "function") { + this.searchService.optionsLookup = options; + } + else { + this.searchService.options = options; + } + this.optionsUpdateHook(); } @@ -87,6 +89,10 @@ export abstract class SuiSelectBase 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)); @@ -174,6 +180,11 @@ export abstract class SuiSelectBase 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(); @@ -203,7 +214,7 @@ export abstract class SuiSelectBase 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