UNPKG

13.7 kBPlain TextView Raw
1/**
2 * Copyright (c) 2015 NAVER Corp.
3 * egjs projects are licensed under the MIT license
4 */
5
6import Panel from "./Panel";
7import { FlickingOptions } from "../types";
8import { findIndex, counter } from "../utils";
9
10class PanelManager {
11 private cameraElement: HTMLElement;
12 private options: FlickingOptions;
13 private panels: Panel[];
14 private clones: Panel[][];
15 // index range of existing panels
16 private range: {
17 min: number;
18 max: number;
19 };
20 private length: number;
21 private lastIndex: number;
22 private cloneCount: number;
23
24 constructor(
25 cameraElement: HTMLElement,
26 options: FlickingOptions,
27 ) {
28 this.cameraElement = cameraElement;
29 this.panels = [];
30 this.clones = [];
31 this.range = {
32 min: -1,
33 max: -1,
34 };
35 this.length = 0;
36 this.cloneCount = 0;
37 this.options = options;
38 this.lastIndex = options.lastIndex;
39 }
40
41 public firstPanel(): Panel | undefined {
42 return this.panels[this.range.min];
43 }
44
45 public lastPanel(): Panel | undefined {
46 return this.panels[this.range.max];
47 }
48
49 public allPanels(): ReadonlyArray<Panel> {
50 return [
51 ...this.panels,
52 ...this.clones.reduce((allClones, clones) => [...allClones, ...clones], []),
53 ];
54 }
55
56 public originalPanels(): ReadonlyArray<Panel> {
57 return this.panels;
58 }
59
60 public clonedPanels(): ReadonlyArray<Panel[]> {
61 return this.clones;
62 }
63
64 public replacePanels(newPanels: Panel[], newClones: Panel[][]): void {
65 this.panels = newPanels;
66 this.clones = newClones;
67
68 this.range = {
69 min: findIndex(newPanels, panel => Boolean(panel)),
70 max: newPanels.length - 1,
71 };
72 this.length = newPanels.filter(panel => Boolean(panel)).length;
73 }
74
75 public has(index: number): boolean {
76 return !!this.panels[index];
77 }
78
79 public get(index: number): Panel | undefined {
80 return this.panels[index];
81 }
82
83 public getPanelCount(): number {
84 return this.length;
85 }
86
87 public getLastIndex(): number {
88 return this.lastIndex;
89 }
90
91 public getRange(): Readonly<{ min: number, max: number }> {
92 return this.range;
93 }
94
95 public getCloneCount(): number {
96 return this.cloneCount;
97 }
98
99 public setLastIndex(lastIndex: number): void {
100 this.lastIndex = lastIndex;
101
102 const firstPanel = this.firstPanel();
103 const lastPanel = this.lastPanel();
104
105 if (!firstPanel || !lastPanel) {
106 return; // no meaning of updating range & length
107 }
108
109 // Remove panels above new last index
110 const range = this.range;
111 if (lastPanel.getIndex() > lastIndex) {
112 const removingPanels = this.panels.splice(lastIndex + 1);
113 this.length -= removingPanels.length;
114
115 const firstRemovedPanel = removingPanels.filter(panel => !!panel)[0];
116 const possibleLastPanel = firstRemovedPanel.prevSibling;
117 if (possibleLastPanel) {
118 range.max = possibleLastPanel.getIndex();
119 } else {
120 range.min = -1;
121 range.max = -1;
122 }
123
124 if (this.shouldRender()) {
125 removingPanels.forEach(panel => panel.removeElement());
126 }
127 }
128 }
129
130 public setCloneCount(cloneCount: number): void {
131 this.cloneCount = cloneCount;
132 }
133
134 // Insert at index
135 // Returns pushed elements from index, inserting at 'empty' position doesn't push elements behind it
136 public insert(index: number, newPanels: Panel[]): number {
137 const panels = this.panels;
138 const range = this.range;
139 const isCircular = this.options.circular;
140 const lastIndex = this.lastIndex;
141
142 // Find first panel that index is greater than inserting index
143 const nextSibling = this.findFirstPanelFrom(index);
144
145 // if it's null, element will be inserted at last position
146 // https://developer.mozilla.org/ko/docs/Web/API/Node/insertBefore#Syntax
147 const firstPanel = this.firstPanel();
148 const siblingElement = nextSibling
149 ? nextSibling.getElement()
150 : isCircular && firstPanel
151 ? firstPanel.getClonedPanels()[0].getElement()
152 : null;
153
154 // Insert panels before sibling element
155 this.insertNewPanels(newPanels, siblingElement);
156
157 let pushedIndex = newPanels.length;
158 // Like when setting index 50 while visible panels are 0, 1, 2
159 if (index > range.max) {
160 newPanels.forEach((panel, offset) => {
161 panels[index + offset] = panel;
162 });
163 } else {
164 const panelsAfterIndex = panels.slice(index, index + newPanels.length);
165 // Find empty from beginning
166 let emptyPanelCount = findIndex(panelsAfterIndex, panel => !!panel);
167 if (emptyPanelCount < 0) {
168 // All empty
169 emptyPanelCount = panelsAfterIndex.length;
170 }
171 pushedIndex = newPanels.length - emptyPanelCount;
172
173 // Insert removing empty panels
174 panels.splice(index, emptyPanelCount, ...newPanels);
175
176 // Remove panels after last index
177 if (panels.length > lastIndex + 1) {
178 const removedPanels = panels.splice(lastIndex + 1)
179 .filter(panel => Boolean(panel));
180 this.length -= removedPanels.length;
181
182 // Find first
183 const newLastIndex = lastIndex - findIndex(this.panels.concat().reverse(), panel => !!panel);
184
185 // Can be filled with empty after newLastIndex
186 this.panels.splice(newLastIndex + 1);
187 this.range.max = newLastIndex;
188
189 if (this.shouldRender()) {
190 removedPanels.forEach(panel => panel.removeElement());
191 }
192 }
193 }
194
195 // Update index of previous panels
196 if (pushedIndex > 0) {
197 panels.slice(index + newPanels.length).forEach(panel => {
198 panel.setIndex(panel.getIndex() + pushedIndex);
199 });
200 }
201
202 // Update state
203 this.length += newPanels.length;
204 this.updateIndex(index);
205
206 if (isCircular) {
207 this.addNewClones(index, newPanels, newPanels.length - pushedIndex, nextSibling);
208 const clones = this.clones;
209 const panelCount = this.panels.length;
210 if (clones[0] && clones[0].length > lastIndex + 1) {
211 clones.forEach(cloneSet => {
212 cloneSet.splice(panelCount);
213 });
214 }
215 }
216
217 return pushedIndex;
218 }
219
220 public replace(index: number, newPanels: Panel[]): Panel[] {
221 const panels = this.panels;
222 const range = this.range;
223 const options = this.options;
224 const isCircular = options.circular;
225
226 // Find first panel that index is greater than inserting index
227 const nextSibling = this.findFirstPanelFrom(index + newPanels.length);
228
229 // if it's null, element will be inserted at last position
230 // https://developer.mozilla.org/ko/docs/Web/API/Node/insertBefore#Syntax
231 const firstPanel = this.firstPanel();
232 const siblingElement = nextSibling
233 ? nextSibling.getElement()
234 : isCircular && firstPanel
235 ? firstPanel.getClonedPanels()[0].getElement()
236 : null;
237
238 // Insert panels before sibling element
239 this.insertNewPanels(newPanels, siblingElement);
240
241 if (index > range.max) {
242 // Temporarily insert null at index to use splice()
243 (panels[index] as any) = null;
244 }
245
246 const replacedPanels = panels.splice(index, newPanels.length, ...newPanels);
247 const wasNonEmptyCount = replacedPanels.filter(panel => Boolean(panel)).length;
248
249 // Suppose inserting [1, 2, 3] at 0 position when there were [empty, 1]
250 // So length should be increased by 3(inserting panels) - 1(non-empty panels)
251 this.length += newPanels.length - wasNonEmptyCount;
252 this.updateIndex(index);
253
254 if (isCircular) {
255 this.addNewClones(index, newPanels, newPanels.length, nextSibling);
256 }
257
258 if (this.shouldRender()) {
259 replacedPanels.forEach(panel => panel && panel.removeElement());
260 }
261
262 return replacedPanels;
263 }
264
265 public remove(index: number, deleteCount: number = 1): Panel[] {
266 const isCircular = this.options.circular;
267 const panels = this.panels;
268 const clones = this.clones;
269 // Delete count should be equal or larger than 0
270 deleteCount = Math.max(deleteCount, 0);
271
272 const deletedPanels = panels
273 .splice(index, deleteCount)
274 .filter(panel => !!panel);
275
276 if (this.shouldRender()) {
277 deletedPanels.forEach(panel => panel.removeElement());
278 }
279
280 if (isCircular) {
281 clones.forEach(cloneSet => {
282 cloneSet.splice(index, deleteCount);
283 });
284 }
285
286 // Update indexes
287 panels
288 .slice(index)
289 .forEach(panel => {
290 panel.setIndex(panel.getIndex() - deleteCount);
291 });
292
293 // Check last panel is empty
294 let lastIndex = panels.length - 1;
295 if (!panels[lastIndex]) {
296 const reversedPanels = panels.concat().reverse();
297 const nonEmptyIndexFromLast = findIndex(reversedPanels, panel => !!panel);
298 lastIndex = nonEmptyIndexFromLast < 0
299 ? -1 // All empty
300 : lastIndex - nonEmptyIndexFromLast;
301
302 // Remove all empty panels from last
303 panels.splice(lastIndex + 1);
304 if (isCircular) {
305 clones.forEach(cloneSet => {
306 cloneSet.splice(lastIndex + 1);
307 });
308 }
309 }
310
311 // Update range & length
312 this.range = {
313 min: findIndex(panels, panel => !!panel),
314 max: lastIndex,
315 };
316 this.length -= deletedPanels.length;
317
318 if (this.length <= 0) {
319 // Reset clones
320 this.clones = [];
321 this.cloneCount = 0;
322 }
323
324 return deletedPanels;
325 }
326
327 public chainAllPanels() {
328 const allPanels = this.allPanels().filter(panel => !!panel);
329 const allPanelsCount = allPanels.length;
330
331 if (allPanelsCount <= 1) {
332 return;
333 }
334
335 allPanels.slice(1, allPanels.length - 1).forEach((panel, idx) => {
336 const prevPanel = allPanels[idx];
337 const nextPanel = allPanels[idx + 2];
338
339 panel.prevSibling = prevPanel;
340 panel.nextSibling = nextPanel;
341 });
342
343 const firstPanel = allPanels[0];
344 const lastPanel = allPanels[allPanelsCount - 1];
345
346 firstPanel.prevSibling = null;
347 firstPanel.nextSibling = allPanels[1];
348 lastPanel.prevSibling = allPanels[allPanelsCount - 2];
349 lastPanel.nextSibling = null;
350
351 if (this.options.circular) {
352 firstPanel.prevSibling = lastPanel;
353 lastPanel.nextSibling = firstPanel;
354 }
355 }
356
357 public insertClones(cloneIndex: number, index: number, clonedPanels: Panel[], deleteCount: number = 0): void {
358 const clones = this.clones;
359 const lastIndex = this.lastIndex;
360
361 if (!clones[cloneIndex]) {
362 const newClones: Panel[] = [];
363 clonedPanels.forEach((panel, offset) => {
364 newClones[index + offset] = panel;
365 });
366
367 clones[cloneIndex] = newClones;
368 } else {
369 const insertTarget = clones[cloneIndex];
370
371 if (index >= insertTarget.length) {
372 clonedPanels.forEach((panel, offset) => {
373 insertTarget[index + offset] = panel;
374 });
375 } else {
376 insertTarget.splice(index, deleteCount, ...clonedPanels);
377 // Remove panels after last index
378 if (clonedPanels.length > lastIndex + 1) {
379 clonedPanels.splice(lastIndex + 1);
380 }
381 }
382 }
383 }
384
385 // clones are operating in set
386 public removeClonesAfter(cloneIndex: number): void {
387 const panels = this.panels;
388
389 panels.forEach(panel => {
390 panel.removeClonedPanelsAfter(cloneIndex);
391 });
392 this.clones.splice(cloneIndex);
393 }
394
395 public findPanelOf(element: HTMLElement): Panel | undefined {
396 const allPanels = this.allPanels();
397 for (const panel of allPanels) {
398 if (!panel) {
399 continue;
400 }
401 const panelElement = panel.getElement();
402 if (panelElement.contains(element)) {
403 return panel;
404 }
405 }
406 }
407
408 public findFirstPanelFrom(index: number): Panel | undefined {
409 for (const panel of this.panels.slice(index)) {
410 if (panel && panel.getIndex() >= index && panel.getElement().parentNode) {
411 return panel;
412 }
413 }
414 }
415
416 private addNewClones(index: number, originalPanels: Panel[], deleteCount: number, nextSibling: Panel | undefined) {
417 const cameraElement = this.cameraElement;
418 const cloneCount = this.getCloneCount();
419 const lastPanel = this.lastPanel();
420 const lastPanelClones: Panel[] = lastPanel
421 ? lastPanel.getClonedPanels()
422 : [];
423 const nextSiblingClones: Panel[] = nextSibling
424 ? nextSibling.getClonedPanels()
425 : [];
426
427 for (const cloneIndex of counter(cloneCount)) {
428 const cloneNextSibling = nextSiblingClones[cloneIndex];
429 const lastPanelSibling = lastPanelClones[cloneIndex];
430
431 const cloneSiblingElement = cloneNextSibling
432 ? cloneNextSibling.getElement()
433 : lastPanelSibling
434 ? lastPanelSibling.getElement().nextElementSibling
435 : null;
436
437 const newClones = originalPanels.map(panel => {
438 const clone = panel.clone(cloneIndex);
439
440 if (this.shouldRender()) {
441 cameraElement.insertBefore(clone.getElement(), cloneSiblingElement);
442 }
443
444 return clone;
445 });
446
447 this.insertClones(cloneIndex, index, newClones, deleteCount);
448 }
449 }
450
451 private updateIndex(insertingIndex: number) {
452 const panels = this.panels;
453 const range = this.range;
454
455 const newLastIndex = panels.length - 1;
456 if (newLastIndex > range.max) {
457 range.max = newLastIndex;
458 }
459 if (insertingIndex < range.min || range.min < 0) {
460 range.min = insertingIndex;
461 }
462 }
463
464 private insertNewPanels(newPanels: Panel[], siblingElement: HTMLElement | null) {
465 if (this.shouldRender()) {
466 const fragment = document.createDocumentFragment();
467 newPanels.forEach(panel => fragment.appendChild(panel.getElement()));
468 this.cameraElement.insertBefore(fragment, siblingElement);
469 }
470 }
471
472 private shouldRender(): boolean {
473 const options = this.options;
474
475 return !options.renderExternal && !options.renderOnlyVisible;
476 }
477}
478
479export default PanelManager;