Skip to content

Commit a8c2687

Browse files
authored
Merge pull request #90 from edcarroll/develop
v0.8.1 into master
2 parents b40f28c + 9f876ef commit a8c2687

File tree

18 files changed

+217
-60
lines changed

18 files changed

+217
-60
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ Now you're good to go!
6060

6161
## Dependencies
6262

63-
* [Angular](https://angular.io) (^4.0.0)
63+
* [Angular](https://angular.io) (^4.1.0)
6464
* [Semantic UI CSS](http://semantic-ui.com/) (jQuery is **not** required)
6565

6666
## Components

components/collapse/collapse.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,6 @@ export class SuiCollapse {
4444
return this._pristine ? 0 : this.collapseDuration;
4545
}
4646

47-
// Timer for window resize counter.
48-
private _resizeTimeout:number;
49-
5047
public constructor(private _element:ElementRef, private _renderer:Renderer2) {
5148
this._pristine = true;
5249

components/modal/modal-config.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ export class ModalConfig<T, U = null, V = null> {
2727
// Whether or not the modal has basic styles applied.
2828
public isBasic:boolean;
2929

30+
// Whether or not the modal should always display a scrollbar.
31+
public mustScroll:boolean;
32+
3033
// Transition to display modal with.
3134
public transition:string;
3235
// Duration of the modal & dimmer transitions.
@@ -41,6 +44,8 @@ export class ModalConfig<T, U = null, V = null> {
4144
this.isFullScreen = false;
4245
this.isBasic = false;
4346

47+
this.mustScroll = false;
48+
4449
this.transition = "scale";
4550
this.transitionDuration = 500;
4651
}

components/modal/modal.service.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export class SuiModalService {
2525
$implicit: modal.context,
2626
// `let-modal="modal"`
2727
modal: componentRef.instance.controls
28-
});
28+
})
2929
}
3030
// If the config is for a component based modal,
3131
else if (modal instanceof ComponentModalConfig) {
@@ -59,6 +59,10 @@ export class SuiModalService {
5959
contentElement.remove();
6060
}
6161

62+
// Remove the template div from the DOM. This is to fix some styling issues.
63+
const templateElement = modalComponent.templateSibling.element.nativeElement as Element;
64+
templateElement.remove();
65+
6266
// Attach the new modal component to the application.
6367
this._applicationRef.attachView(componentRef.hostView);
6468

components/modal/modal.ts

Lines changed: 59 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {ModalConfig, ModalSize} from './modal-config';
1616
[class.active]="transitionController?.isVisible"
1717
[class.fullscreen]="isFullScreen"
1818
[class.basic]="isBasic"
19+
[class.scrolling]="mustScroll"
1920
#modal>
2021
2122
<!-- Configurable close icon -->
@@ -25,7 +26,15 @@ import {ModalConfig, ModalSize} from './modal-config';
2526
<!-- @ViewChild reference so we can insert elements beside this div. -->
2627
<div #templateSibling></div>
2728
</div>
28-
`
29+
`,
30+
styles: [`
31+
.scrolling {
32+
position: absolute !important;
33+
margin-top: 3.5rem !important;
34+
margin-bottom: 3.5rem !important;
35+
top: 0;
36+
}
37+
`]
2938
})
3039
export class SuiModal<T, U> implements OnInit, AfterViewInit {
3140
@Input()
@@ -83,6 +92,23 @@ export class SuiModal<T, U> implements OnInit, AfterViewInit {
8392
@Input()
8493
public isBasic:boolean;
8594

95+
// Whether the modal currently is displaying a scrollbar.
96+
private _mustScroll:boolean;
97+
// Whether or not the modal should always display a scrollbar.
98+
private _mustAlwaysScroll:boolean;
99+
100+
@Input()
101+
public get mustScroll() {
102+
return this._mustScroll;
103+
}
104+
105+
public set mustScroll(mustScroll:boolean) {
106+
this._mustScroll = mustScroll;
107+
// 'Cache' value in _mustAlwaysScroll so that if `true`, _mustScroll isn't ever auto-updated.
108+
this._mustAlwaysScroll = mustScroll;
109+
this.updateScroll();
110+
}
111+
86112
public transitionController:TransitionController;
87113

88114
// Transition to display modal with.
@@ -124,16 +150,21 @@ export class SuiModal<T, U> implements OnInit, AfterViewInit {
124150
}
125151

126152
public ngOnInit() {
127-
// Transition the modal to be visible.
128-
this.transitionController.animate(new Transition(this.transition, this.transitionDuration, TransitionDirection.In));
129153
// Use a slight delay as the `<sui-dimmer>` cancels the initial transition.
130-
setTimeout(() => this.dimBackground = true);
154+
setTimeout(() => {
155+
// Transition the modal to be visible.
156+
this.transitionController.animate(new Transition(this.transition, this.transitionDuration, TransitionDirection.In))
157+
this.dimBackground = true;
158+
});
131159
}
132160

133161
public ngAfterViewInit() {
134162
// Update margin offset to center modal correctly on-screen.
135163
const element = this._modalElement.nativeElement as Element;
136-
this._renderer.setStyle(element, "margin-top", `-${element.clientHeight / 2}px`);
164+
setTimeout(() => {
165+
this._renderer.setStyle(element, "margin-top", `-${element.clientHeight / 2}px`);
166+
this.updateScroll();
167+
});
137168
}
138169

139170
// Updates the modal with the specified configuration.
@@ -145,6 +176,8 @@ export class SuiModal<T, U> implements OnInit, AfterViewInit {
145176
this.isFullScreen = config.isFullScreen;
146177
this.isBasic = config.isBasic;
147178

179+
this.mustScroll = config.mustScroll;
180+
148181
this.transition = config.transition;
149182
this.transitionDuration = config.transitionDuration;
150183
}
@@ -175,11 +208,31 @@ export class SuiModal<T, U> implements OnInit, AfterViewInit {
175208
}
176209
}
177210

211+
// Decides whether the modal needs to reposition to allow scrolling.
212+
private updateScroll() {
213+
// Semantic UI modal margin is 3.5rem, which is relative to the global font size, so for compatibility:
214+
const fontSize = parseFloat(window.getComputedStyle(document.documentElement, null).getPropertyValue('font-size'));
215+
const margin = fontSize * 3.5;
216+
217+
// _mustAlwaysScroll works by stopping _mustScroll from being automatically updated, so it stays `true`.
218+
if (!this._mustAlwaysScroll && this._modalElement) {
219+
const element = this._modalElement.nativeElement as Element;
220+
221+
// The modal must scroll if the window height is smaller than the modal height + both margins.
222+
this._mustScroll = window.innerHeight < element.clientHeight + margin * 2;
223+
}
224+
}
225+
178226
@HostListener("document:keyup", ["$event"])
179-
public onKeyup(e:KeyboardEvent) {
227+
public onDocumentKeyup(e:KeyboardEvent) {
180228
if (e.keyCode == KeyCode.Escape) {
181229
// Close automatically covers case of `!isClosable`, so check not needed.
182230
this.close();
183231
}
184232
}
233+
234+
@HostListener("window:resize")
235+
public onDocumentResize() {
236+
this.updateScroll();
237+
}
185238
}

components/popup/popup.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import Popper from "popper.js";
1616
<div class="content">{{ config.text }}</div>
1717
</ng-container>
1818
<div #templateSibling></div>
19-
<sui-popup-arrow *ngIf="!config.isBasic" [placement]="_positioningService.placement" [inverted]="config.isInverted"></sui-popup-arrow>
19+
<sui-popup-arrow *ngIf="!config.isBasic" [placement]="_positioningService.actualPlacement" [inverted]="config.isInverted"></sui-popup-arrow>
2020
</div>
2121
`,
2222
styles: [`
@@ -83,7 +83,7 @@ export class SuiPopup implements IPopup {
8383
// Returns the direction (`top`, `left`, `right`, `bottom`) of the current placement.
8484
public get direction() {
8585
if (this._positioningService) {
86-
return this._positioningService.placement.split(" ").shift();
86+
return this._positioningService.actualPlacement.split(" ").shift();
8787
}
8888
}
8989

components/search/search.service.ts

Lines changed: 25 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import {readValue} from '../util/util';
22

33
// Define useful types to avoid any.
4-
export type LookupFn<T> = (query:string) => Promise<T[]> | Promise<T>;
5-
export type QueryLookupFn<T> = (query:string) => Promise<T[]>;
6-
export type ItemLookupFn<T, U> = (query:string, initial:U) => Promise<T>;
7-
export type ItemsLookupFn<T, U> = (query:string, initial:U[]) => Promise<T[]>;
4+
export type LookupFn<T> = (query:string) => (Promise<T[]> | Promise<T> | T[] | T);
5+
export type QueryLookupFn<T> = (query:string) => (Promise<T[]> | T[]);
6+
export type ItemLookupFn<T, U> = (query:string, initial:U) => (Promise<T> | T);
7+
export type ItemsLookupFn<T, U> = (query:string, initial:U[]) => (Promise<T[]> | T[]);
88

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

@@ -131,18 +131,27 @@ export class SearchService<T> {
131131
if (this._optionsLookup) {
132132
this._isSearching = true;
133133

134-
this.queryLookup(this._query)
135-
.then(results => {
136-
// Unset 'loading' state, and display & cache the results.
137-
this._isSearching = false;
138-
this.updateResults(results);
139-
return callback(null);
140-
})
141-
.catch(error => {
142-
// Unset 'loading' state, and throw the returned error without updating the results.
143-
this._isSearching = false;
144-
return callback(error);
145-
});
134+
const lookupFinished = (results:T[]) => {
135+
this._isSearching = false;
136+
137+
this.updateResults(results);
138+
return callback(null);
139+
}
140+
141+
const queryLookup = this.queryLookup(this._query);
142+
143+
if (queryLookup instanceof Promise) {
144+
queryLookup
145+
.then(results => lookupFinished(results))
146+
.catch(error => {
147+
// Unset 'loading' state, and throw the returned error without updating the results.
148+
this._isSearching = false;
149+
return callback(error);
150+
});
151+
}
152+
else {
153+
lookupFinished(queryLookup);
154+
}
146155
return;
147156
}
148157

components/select/multi-select.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,8 +113,15 @@ export class SuiMultiSelect<T, U> extends SuiSelectBase<T, U> implements AfterVi
113113
if (values.length > 0 && this.selectedOptions.length == 0) {
114114
if (this.valueField && this.searchService.hasItemLookup) {
115115
// If the search service has a selected lookup function, make use of that to load the initial values.
116-
this.searchService.itemsLookup<U>(values)
117-
.then(r => this.selectedOptions = r);
116+
const lookupFinished = (items:T[]) => this.selectedOptions = items;
117+
118+
const itemsLookup = this.searchService.itemsLookup<U>(values);
119+
if (itemsLookup instanceof Promise) {
120+
itemsLookup.then(r => lookupFinished(r));
121+
}
122+
else {
123+
lookupFinished(itemsLookup);
124+
}
118125
}
119126
else {
120127
// Otherwise, cache the written value for when options are set.

components/select/select.ts

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -86,12 +86,18 @@ export class SuiSelect<T, U> extends SuiSelectBase<T, U> implements CustomValueA
8686
if (!this.selectedOption) {
8787
if (this.valueField && this.searchService.hasItemLookup) {
8888
// If the search service has a selected lookup function, make use of that to load the initial value.
89-
(this.searchService.itemLookup<U>(value) as Promise<T>)
90-
.then(r => {
91-
this.selectedOption = r;
92-
93-
this.drawSelectedOption();
94-
});
89+
const lookupFinished = (i:T) => {
90+
this.selectedOption = i;
91+
this.drawSelectedOption();
92+
}
93+
94+
const itemLookup = this.searchService.itemLookup<U>(value);
95+
if (itemLookup instanceof Promise) {
96+
itemLookup.then(r => lookupFinished(r));
97+
}
98+
else {
99+
lookupFinished(itemLookup);
100+
}
95101
return;
96102
}
97103
else {

components/util/positioning.service.ts

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,43 @@ function placementToPopper(placement:PositioningPlacement):PopperPlacement {
5757
return chosenPlacement.join("-") as PopperPlacement;
5858
}
5959

60+
function popperToPlacement(popper:string):PositioningPlacement {
61+
if (!popper || popper == "inherit") {
62+
return "inherit";
63+
}
64+
65+
const [direction, alignment] = popper.split("-");
66+
67+
let chosenPlacement = [direction];
68+
69+
switch (direction) {
70+
case "top":
71+
case "bottom":
72+
switch (alignment) {
73+
case "start":
74+
chosenPlacement.push("left");
75+
break;
76+
case "end":
77+
chosenPlacement.push("right");
78+
break;
79+
}
80+
break;
81+
case "left":
82+
case "right":
83+
switch (alignment) {
84+
case "start":
85+
chosenPlacement.push("top");
86+
break;
87+
case "end":
88+
chosenPlacement.push("bottom");
89+
break;
90+
}
91+
break;
92+
}
93+
94+
return chosenPlacement.join(" ") as PositioningPlacement;
95+
}
96+
6097
export class PositioningService {
6198
public readonly anchor:ElementRef;
6299
public readonly subject:ElementRef;
@@ -65,7 +102,7 @@ export class PositioningService {
65102
private _popperState:Popper.Data;
66103
private _placement:PositioningPlacement;
67104

68-
public get placement():PositioningPlacement {
105+
public get placement() {
69106
return this._placement;
70107
}
71108

@@ -75,6 +112,14 @@ export class PositioningService {
75112
this.update();
76113
}
77114

115+
public get actualPlacement() {
116+
if (!this._popperState) {
117+
return PositioningPlacement.Inherit;
118+
}
119+
120+
return popperToPlacement(this._popperState.placement);
121+
}
122+
78123
public get state() {
79124
return this._popperState;
80125
}

0 commit comments

Comments
 (0)