-
Notifications
You must be signed in to change notification settings - Fork 106
/
scroll.plugin.js
181 lines (161 loc) · 6.55 KB
/
scroll.plugin.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
import $ from 'external/jquery';
import AtkPlugin from './atk.plugin';
/**
* Add dynamic scrolling to a View that can accept page argument in URL.
*
* default options are:
* padding: 20 The amount of padding needed prior to request a page load.
* initialPage: 1 The initial page load when calling this plugin.
* appendTo: null The html element where new content should be append to.
* stateContext: null A jQuery selector, where you would like Fomantic-UI, to apply the stateContext to during the api call. if null, then a default loader will be apply to the bottom of the $inner element.
*/
export default class AtkScrollPlugin extends AtkPlugin {
main() {
// check if we are initialized already because loading content
// can recall this plugin and screw up page number.
if (this.$el.data('__atkScroll')) {
return false;
}
const defaultSettings = {
padding: 20,
initialPage: 1,
appendTo: null,
hasFixTableHeader: false,
tableContainerHeight: 400,
tableHeaderColor: '#ffffff',
stateContext: null,
};
// set default option if not set.
this.settings.options = { ...defaultSettings, ...this.settings.options };
this.isWaiting = false;
this.nextPage = this.settings.options.initialPage + 1;
if (this.settings.options.hasFixTableHeader) {
this.isWindow = false;
this.$scroll = this.$el.parent();
this.$inner = this.$el;
this.setTableHeader();
} else {
// check if scroll apply vs Window or inside our element.
this.isWindow = this.$el.css('overflow-y') === 'visible';
this.$scroll = this.isWindow ? $(window) : this.$el;
// is Inner the element itself or it's children.
this.$inner = this.isWindow ? this.$el : this.$el.children();
}
// the target element within container where new content is appendTo.
this.$target = this.settings.options.appendTo ? this.$inner.find(this.settings.options.appendTo) : this.$inner;
this.$scroll.on('scroll', this.onScroll.bind(this));
// if there is no scrollbar, then try to load next page too
if (!this.hasScrollbar()) {
this.loadContent();
}
}
/**
* Add fix table header.
*/
setTableHeader() {
if (this.$el.parent().length > 0) {
let $tableCopy = null;
this.$el.parent().height(this.settings.options.tableContainerHeight);
this.$el.addClass('fixed');
$tableCopy = this.$el.clone(true, true);
$tableCopy.attr('id', $tableCopy.attr('id') + '_');
$tableCopy.find('tbody, tfoot').remove();
$tableCopy.css({
position: 'absolute',
'background-color': this.settings.options.tableHeaderColor,
border: this.$el.find('th').eq(1).css('border-left'),
'z-index': 1,
});
this.$scroll.prepend($tableCopy);
this.$el.find('thead').hide();
this.$el.css('margin-top', $tableCopy.find('thead').height());
}
}
/**
* Check if scrolling require adding content.
*/
onScroll(event) {
const borderTopWidth = Number.parseInt(this.$el.css('borderTopWidth'), 10);
const borderTopWidthInt = Number.isNaN(borderTopWidth) ? 0 : borderTopWidth;
// this.$el padding top value.
const paddingTop = Number.parseInt(this.$el.css('paddingTop'), 10) + borderTopWidthInt;
// Either the scroll bar position using window or the container element top position otherwise.
const topHeight = this.isWindow ? $(window).scrollTop() : this.$scroll.offset().top;
// Inner top value. If using Window, this value does not change, otherwise represent the inner element top value when scroll.
const innerTop = this.$inner.length > 0 ? this.$inner.offset().top : 0;
// The total height.
const totalHeight = Math.ceil(topHeight - innerTop + this.$scroll.height() + paddingTop);
if (!this.isWaiting && totalHeight + this.settings.options.padding >= this.$inner.outerHeight()) {
this.loadContent();
}
}
/**
* Check if container element has vertical scrollbar.
*
* @returns {boolean}
*/
hasScrollbar() {
const innerHeight = this.isWindow ? Math.ceil(this.$el.height()) : Math.ceil(this.$inner.height());
const scrollHeight = Math.ceil(this.$scroll.height());
return innerHeight > scrollHeight;
}
/**
* Put scroll in idle mode.
*/
idle() {
this.isWaiting = true;
}
/**
* Ask server for more content.
*/
loadContent() {
if (!this.settings.options.stateContext) {
this.addLoader();
}
this.isWaiting = true;
this.$inner.api({
on: 'now',
url: this.settings.url,
data: { ...this.settings.urlOptions, page: this.nextPage },
method: 'GET',
stateContext: this.settings.options.stateContext,
onComplete: this.onComplete.bind(this),
});
}
/**
* Use response to append content to element and setup next content to be loaded.
* Set response.id to null in order for apiService.onSuccess to bypass
* replacing html content. Js return from server response will still be execute.
*/
onComplete(response, element) {
this.removeLoader();
if (response.success) {
if (response.html) {
this.$target.append(response.html);
if (response.noMoreScrollPages) {
this.idle();
} else {
this.isWaiting = false;
this.nextPage++;
// if there is no scrollbar, then try to load next page too
if (!this.hasScrollbar()) {
this.loadContent();
}
}
}
response.id = null;
}
}
addLoader() {
const $parent = this.$inner.parent().hasClass('atk-overflow-auto') ? this.$inner.parent().parent() : this.$inner.parent();
$parent.append($('<div id="atkScrollLoader"><div class="ui section hidden divider"></div><div class="ui active centered inline loader basic segment"></div></div>'));
}
removeLoader() {
$('#atkScrollLoader').remove();
}
}
AtkScrollPlugin.DEFAULTS = {
url: null,
urlOptions: {},
options: {},
};