1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 | import Panel from "./Panel";
|
7 | import { FlickingOptions } from "../types";
|
8 | import { findIndex, counter } from "../utils";
|
9 |
|
10 | class PanelManager {
|
11 | private cameraElement: HTMLElement;
|
12 | private options: FlickingOptions;
|
13 | private panels: Panel[];
|
14 | private clones: Panel[][];
|
15 |
|
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;
|
107 | }
|
108 |
|
109 |
|
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 |
|
135 |
|
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 |
|
143 | const nextSibling = this.findFirstPanelFrom(index);
|
144 |
|
145 |
|
146 |
|
147 | const firstPanel = this.firstPanel();
|
148 | const siblingElement = nextSibling
|
149 | ? nextSibling.getElement()
|
150 | : isCircular && firstPanel
|
151 | ? firstPanel.getClonedPanels()[0].getElement()
|
152 | : null;
|
153 |
|
154 |
|
155 | this.insertNewPanels(newPanels, siblingElement);
|
156 |
|
157 | let pushedIndex = newPanels.length;
|
158 |
|
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 |
|
166 | let emptyPanelCount = findIndex(panelsAfterIndex, panel => !!panel);
|
167 | if (emptyPanelCount < 0) {
|
168 |
|
169 | emptyPanelCount = panelsAfterIndex.length;
|
170 | }
|
171 | pushedIndex = newPanels.length - emptyPanelCount;
|
172 |
|
173 |
|
174 | panels.splice(index, emptyPanelCount, ...newPanels);
|
175 |
|
176 |
|
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 |
|
183 | const newLastIndex = lastIndex - findIndex(this.panels.concat().reverse(), panel => !!panel);
|
184 |
|
185 |
|
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 |
|
196 | if (pushedIndex > 0) {
|
197 | panels.slice(index + newPanels.length).forEach(panel => {
|
198 | panel.setIndex(panel.getIndex() + pushedIndex);
|
199 | });
|
200 | }
|
201 |
|
202 |
|
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 |
|
227 | const nextSibling = this.findFirstPanelFrom(index + newPanels.length);
|
228 |
|
229 |
|
230 |
|
231 | const firstPanel = this.firstPanel();
|
232 | const siblingElement = nextSibling
|
233 | ? nextSibling.getElement()
|
234 | : isCircular && firstPanel
|
235 | ? firstPanel.getClonedPanels()[0].getElement()
|
236 | : null;
|
237 |
|
238 |
|
239 | this.insertNewPanels(newPanels, siblingElement);
|
240 |
|
241 | if (index > range.max) {
|
242 |
|
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 |
|
250 |
|
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 |
|
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 |
|
287 | panels
|
288 | .slice(index)
|
289 | .forEach(panel => {
|
290 | panel.setIndex(panel.getIndex() - deleteCount);
|
291 | });
|
292 |
|
293 |
|
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
|
300 | : lastIndex - nonEmptyIndexFromLast;
|
301 |
|
302 |
|
303 | panels.splice(lastIndex + 1);
|
304 | if (isCircular) {
|
305 | clones.forEach(cloneSet => {
|
306 | cloneSet.splice(lastIndex + 1);
|
307 | });
|
308 | }
|
309 | }
|
310 |
|
311 |
|
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 |
|
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 |
|
378 | if (clonedPanels.length > lastIndex + 1) {
|
379 | clonedPanels.splice(lastIndex + 1);
|
380 | }
|
381 | }
|
382 | }
|
383 | }
|
384 |
|
385 |
|
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 |
|
479 | export default PanelManager;
|