1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 | import Viewport from "./Viewport";
|
7 | import { OriginalStyle, FlickingPanel, ElementLike, DestroyOption, BoundingBox } from "../types";
|
8 | import { DEFAULT_PANEL_CSS, EVENTS } from "../consts";
|
9 | import { addClass, applyCSS, parseArithmeticExpression, parseElement, getProgress, restoreStyle, hasClass, getBbox } from "../utils";
|
10 |
|
11 | class Panel implements FlickingPanel {
|
12 | public viewport: Viewport;
|
13 | public prevSibling: Panel | null;
|
14 | public nextSibling: Panel | null;
|
15 |
|
16 | protected state: {
|
17 | index: number;
|
18 | position: number;
|
19 | relativeAnchorPosition: number;
|
20 | size: number;
|
21 | isClone: boolean;
|
22 | isVirtual: boolean;
|
23 |
|
24 |
|
25 | cloneIndex: number;
|
26 | originalStyle: OriginalStyle;
|
27 | cachedBbox: BoundingBox | null;
|
28 | };
|
29 | private element: HTMLElement;
|
30 | private original?: Panel;
|
31 | private clonedPanels: Panel[];
|
32 |
|
33 | public constructor(
|
34 | element?: HTMLElement | null,
|
35 | index?: number,
|
36 | viewport?: Viewport,
|
37 | ) {
|
38 | this.viewport = viewport!;
|
39 | this.prevSibling = null;
|
40 | this.nextSibling = null;
|
41 | this.clonedPanels = [];
|
42 |
|
43 | this.state = {
|
44 | index: index!,
|
45 | position: 0,
|
46 | relativeAnchorPosition: 0,
|
47 | size: 0,
|
48 | isClone: false,
|
49 | isVirtual: false,
|
50 | cloneIndex: -1,
|
51 | originalStyle: {
|
52 | className: "",
|
53 | style: "",
|
54 | },
|
55 | cachedBbox: null,
|
56 | };
|
57 | this.setElement(element);
|
58 | }
|
59 |
|
60 | public resize(givenBbox?: BoundingBox): void {
|
61 | const state = this.state;
|
62 | const options = this.viewport.options;
|
63 | const bbox = givenBbox
|
64 | ? givenBbox
|
65 | : this.getBbox();
|
66 | this.state.cachedBbox = bbox;
|
67 | const prevSize = state.size;
|
68 |
|
69 | state.size = options.horizontal
|
70 | ? bbox.width
|
71 | : bbox.height;
|
72 |
|
73 | if (prevSize !== state.size) {
|
74 | state.relativeAnchorPosition = parseArithmeticExpression(options.anchor, state.size);
|
75 | }
|
76 |
|
77 | if (!state.isClone) {
|
78 | this.clonedPanels.forEach(panel => {
|
79 | const cloneState = panel.state;
|
80 |
|
81 | cloneState.size = state.size;
|
82 | cloneState.cachedBbox = state.cachedBbox;
|
83 | cloneState.relativeAnchorPosition = state.relativeAnchorPosition;
|
84 | });
|
85 | }
|
86 | }
|
87 |
|
88 | public unCacheBbox(): void {
|
89 | this.state.cachedBbox = null;
|
90 | }
|
91 |
|
92 | public getProgress() {
|
93 | const viewport = this.viewport;
|
94 | const options = viewport.options;
|
95 | const panelCount = viewport.panelManager.getPanelCount();
|
96 | const scrollAreaSize = viewport.getScrollAreaSize();
|
97 |
|
98 | const relativeIndex = (options.circular ? Math.floor(this.getPosition() / scrollAreaSize) * panelCount : 0) + this.getIndex();
|
99 | const progress = relativeIndex - viewport.getCurrentProgress();
|
100 |
|
101 | return progress;
|
102 | }
|
103 |
|
104 | public getOutsetProgress() {
|
105 | const viewport = this.viewport;
|
106 | const outsetRange = [
|
107 | -this.getSize(),
|
108 | viewport.getRelativeHangerPosition() - this.getRelativeAnchorPosition(),
|
109 | viewport.getSize(),
|
110 | ];
|
111 | const relativePanelPosition = this.getPosition() - viewport.getCameraPosition();
|
112 | const outsetProgress = getProgress(relativePanelPosition, outsetRange);
|
113 |
|
114 | return outsetProgress;
|
115 | }
|
116 |
|
117 | public getVisibleRatio() {
|
118 | const viewport = this.viewport;
|
119 | const panelSize = this.getSize();
|
120 | const relativePanelPosition = this.getPosition() - viewport.getCameraPosition();
|
121 | const rightRelativePanelPosition = relativePanelPosition + panelSize;
|
122 |
|
123 | const visibleSize = Math.min(viewport.getSize(), rightRelativePanelPosition) - Math.max(relativePanelPosition, 0);
|
124 | const visibleRatio = visibleSize >= 0
|
125 | ? visibleSize / panelSize
|
126 | : 0;
|
127 |
|
128 | return visibleRatio;
|
129 | }
|
130 |
|
131 | public focus(duration?: number): void {
|
132 | const viewport = this.viewport;
|
133 | const currentPanel = viewport.getCurrentPanel();
|
134 | const hangerPosition = viewport.getHangerPosition();
|
135 | const anchorPosition = this.getAnchorPosition();
|
136 | if (hangerPosition === anchorPosition || !currentPanel) {
|
137 | return;
|
138 | }
|
139 |
|
140 | const currentPosition = currentPanel.getPosition();
|
141 | const eventType = currentPosition === this.getPosition()
|
142 | ? ""
|
143 | : EVENTS.CHANGE;
|
144 |
|
145 | viewport.moveTo(this, viewport.findEstimatedPosition(this), eventType, null, duration);
|
146 | }
|
147 |
|
148 | public update(updateFunction: ((element: HTMLElement) => any) | null = null, shouldResize: boolean = true): void {
|
149 | const identicalPanels = this.getIdenticalPanels();
|
150 |
|
151 | if (updateFunction) {
|
152 | identicalPanels.forEach(eachPanel => {
|
153 | updateFunction(eachPanel.getElement());
|
154 | });
|
155 | }
|
156 |
|
157 | if (shouldResize) {
|
158 | identicalPanels.forEach(eachPanel => {
|
159 | eachPanel.unCacheBbox();
|
160 | });
|
161 | this.viewport.addVisiblePanel(this);
|
162 | this.viewport.resize();
|
163 | }
|
164 | }
|
165 |
|
166 | public prev(): FlickingPanel | null {
|
167 | const viewport = this.viewport;
|
168 | const options = viewport.options;
|
169 | const prevSibling = this.prevSibling;
|
170 |
|
171 | if (!prevSibling) {
|
172 | return null;
|
173 | }
|
174 |
|
175 | const currentIndex = this.getIndex();
|
176 | const currentPosition = this.getPosition();
|
177 | const prevPanelIndex = prevSibling.getIndex();
|
178 | const prevPanelPosition = prevSibling.getPosition();
|
179 | const prevPanelSize = prevSibling.getSize();
|
180 |
|
181 | const hasEmptyPanelBetween = currentIndex - prevPanelIndex > 1;
|
182 | const notYetMinPanel = options.infinite
|
183 | && currentIndex > 0
|
184 | && prevPanelIndex > currentIndex;
|
185 |
|
186 | if (hasEmptyPanelBetween || notYetMinPanel) {
|
187 | // Empty panel exists between
|
188 | return null;
|
189 | }
|
190 |
|
191 | const newPosition = currentPosition - prevPanelSize - options.gap;
|
192 |
|
193 | let prevPanel = prevSibling;
|
194 | if (prevPanelPosition !== newPosition) {
|
195 | prevPanel = prevSibling.clone(prevSibling.getCloneIndex(), true);
|
196 | prevPanel.setPosition(newPosition);
|
197 | }
|
198 |
|
199 | return prevPanel;
|
200 | }
|
201 |
|
202 | public next(): FlickingPanel | null {
|
203 | const viewport = this.viewport;
|
204 | const options = viewport.options;
|
205 | const nextSibling = this.nextSibling;
|
206 | const lastIndex = viewport.panelManager.getLastIndex();
|
207 |
|
208 | if (!nextSibling) {
|
209 | return null;
|
210 | }
|
211 |
|
212 | const currentIndex = this.getIndex();
|
213 | const currentPosition = this.getPosition();
|
214 | const nextPanelIndex = nextSibling.getIndex();
|
215 | const nextPanelPosition = nextSibling.getPosition();
|
216 |
|
217 | const hasEmptyPanelBetween = nextPanelIndex - currentIndex > 1;
|
218 | const notYetMaxPanel = options.infinite
|
219 | && currentIndex < lastIndex
|
220 | && nextPanelIndex < currentIndex;
|
221 |
|
222 | if (hasEmptyPanelBetween || notYetMaxPanel) {
|
223 | return null;
|
224 | }
|
225 |
|
226 | const newPosition = currentPosition + this.getSize() + options.gap;
|
227 |
|
228 | let nextPanel = nextSibling;
|
229 | if (nextPanelPosition !== newPosition) {
|
230 | nextPanel = nextSibling.clone(nextSibling.getCloneIndex(), true);
|
231 | nextPanel.setPosition(newPosition);
|
232 | }
|
233 |
|
234 | return nextPanel;
|
235 | }
|
236 |
|
237 | public insertBefore(element: ElementLike | ElementLike[]): FlickingPanel[] {
|
238 | const viewport = this.viewport;
|
239 | const parsedElements = parseElement(element);
|
240 | const firstPanel = viewport.panelManager.firstPanel()!;
|
241 | const prevSibling = this.prevSibling;
|
242 | // Finding correct inserting index
|
243 | // While it should insert removing empty spaces,
|
244 | // It also should have to be bigger than prevSibling' s index
|
245 | const targetIndex = prevSibling && firstPanel.getIndex() !== this.getIndex()
|
246 | ? Math.max(prevSibling.getIndex() + 1, this.getIndex() - parsedElements.length)
|
247 | : Math.max(this.getIndex() - parsedElements.length, 0);
|
248 |
|
249 | return viewport.insert(targetIndex, parsedElements);
|
250 | }
|
251 |
|
252 | public insertAfter(element: ElementLike | ElementLike[]): FlickingPanel[] {
|
253 | return this.viewport.insert(this.getIndex() + 1, element);
|
254 | }
|
255 |
|
256 | public remove(): FlickingPanel {
|
257 | this.viewport.remove(this.getIndex());
|
258 |
|
259 | return this;
|
260 | }
|
261 |
|
262 | public destroy(option: Partial<DestroyOption>): void {
|
263 | if (!option.preserveUI) {
|
264 | const originalStyle = this.state.originalStyle;
|
265 |
|
266 | restoreStyle(this.element, originalStyle);
|
267 | }
|
268 |
|
269 | // release resources
|
270 | for (const x in this) {
|
271 | (this as any)[x] = null;
|
272 | }
|
273 | }
|
274 |
|
275 | public getElement(): HTMLElement {
|
276 | return this.element;
|
277 | }
|
278 |
|
279 | public getAnchorPosition(): number {
|
280 | return this.state.position + this.state.relativeAnchorPosition;
|
281 | }
|
282 |
|
283 | public getRelativeAnchorPosition(): number {
|
284 | return this.state.relativeAnchorPosition;
|
285 | }
|
286 |
|
287 | public getIndex(): number {
|
288 | return this.state.index;
|
289 | }
|
290 |
|
291 | public getPosition(): number {
|
292 | return this.state.position;
|
293 | }
|
294 |
|
295 | public getSize(): number {
|
296 | return this.state.size;
|
297 | }
|
298 |
|
299 | public getBbox(): BoundingBox {
|
300 | const state = this.state;
|
301 | const viewport = this.viewport;
|
302 | const element = this.element;
|
303 | const options = viewport.options;
|
304 |
|
305 | if (!element) {
|
306 | state.cachedBbox = {
|
307 | x: 0,
|
308 | y: 0,
|
309 | width: 0,
|
310 | height: 0,
|
311 | };
|
312 | } else if (!state.cachedBbox) {
|
313 | const wasVisible = Boolean(element.parentNode);
|
314 | const cameraElement = viewport.getCameraElement();
|
315 | if (!wasVisible) {
|
316 | cameraElement.appendChild(element);
|
317 | viewport.addVisiblePanel(this);
|
318 | }
|
319 | state.cachedBbox = getBbox(element, options.useOffset);
|
320 |
|
321 | if (!wasVisible && viewport.options.renderExternal) {
|
322 | cameraElement.removeChild(element);
|
323 | }
|
324 | }
|
325 | return state.cachedBbox!;
|
326 | }
|
327 |
|
328 | public isClone(): boolean {
|
329 | return this.state.isClone;
|
330 | }
|
331 |
|
332 | public getOverlappedClass(classes: string[]): string | undefined {
|
333 | const element = this.element;
|
334 |
|
335 | for (const className of classes) {
|
336 | if (hasClass(element, className)) {
|
337 | return className;
|
338 | }
|
339 | }
|
340 | }
|
341 |
|
342 | public getCloneIndex(): number {
|
343 | return this.state.cloneIndex;
|
344 | }
|
345 |
|
346 | public getClonedPanels(): Panel[] {
|
347 | const state = this.state;
|
348 |
|
349 | return state.isClone
|
350 | ? this.original!.getClonedPanels()
|
351 | : this.clonedPanels;
|
352 | }
|
353 |
|
354 | public getIdenticalPanels(): Panel[] {
|
355 | const state = this.state;
|
356 |
|
357 | return state.isClone
|
358 | ? this.original!.getIdenticalPanels()
|
359 | : [this, ...this.clonedPanels];
|
360 | }
|
361 |
|
362 | public getOriginalPanel(): Panel {
|
363 | return this.state.isClone
|
364 | ? this.original!
|
365 | : this;
|
366 | }
|
367 |
|
368 | public setIndex(index: number): void {
|
369 | const state = this.state;
|
370 |
|
371 | state.index = index;
|
372 | this.clonedPanels.forEach(panel => panel.state.index = index);
|
373 | }
|
374 |
|
375 | public setPosition(pos: number): this {
|
376 | this.state.position = pos;
|
377 |
|
378 | return this;
|
379 | }
|
380 |
|
381 | public setPositionCSS(offset: number = 0): void {
|
382 | if (!this.element) {
|
383 | return;
|
384 | }
|
385 | const state = this.state;
|
386 | const pos = state.position;
|
387 | const options = this.viewport.options;
|
388 | const elementStyle = this.element.style;
|
389 | const currentElementStyle = options.horizontal
|
390 | ? elementStyle.left
|
391 | : elementStyle.top;
|
392 | const styleToApply = `${pos - offset}px`;
|
393 |
|
394 | if (!state.isVirtual && currentElementStyle !== styleToApply) {
|
395 | options.horizontal
|
396 | ? elementStyle.left = styleToApply
|
397 | : elementStyle.top = styleToApply;
|
398 | }
|
399 | }
|
400 |
|
401 | public clone(cloneIndex: number, isVirtual: boolean = false, element?: HTMLElement | null): Panel {
|
402 | const state = this.state;
|
403 | const viewport = this.viewport;
|
404 | let cloneElement = element;
|
405 |
|
406 | if (!cloneElement && this.element) {
|
407 | cloneElement = isVirtual ? this.element : this.element.cloneNode(true) as HTMLElement;
|
408 | }
|
409 | const clonedPanel = new Panel(cloneElement, state.index, viewport);
|
410 | const clonedState = clonedPanel.state;
|
411 |
|
412 | clonedPanel.original = state.isClone
|
413 | ? this.original
|
414 | : this;
|
415 | clonedState.isClone = true;
|
416 | clonedState.isVirtual = isVirtual;
|
417 | clonedState.cloneIndex = cloneIndex;
|
418 | // Inherit some state values
|
419 | clonedState.size = state.size;
|
420 | clonedState.relativeAnchorPosition = state.relativeAnchorPosition;
|
421 | clonedState.originalStyle = state.originalStyle;
|
422 | clonedState.cachedBbox = state.cachedBbox;
|
423 |
|
424 | if (!isVirtual) {
|
425 | this.clonedPanels.push(clonedPanel);
|
426 | } else {
|
427 | clonedPanel.prevSibling = this.prevSibling;
|
428 | clonedPanel.nextSibling = this.nextSibling;
|
429 | }
|
430 |
|
431 | return clonedPanel;
|
432 | }
|
433 |
|
434 | public removeElement(): void {
|
435 | if (!this.viewport.options.renderExternal) {
|
436 | const element = this.element;
|
437 | element.parentNode && element.parentNode.removeChild(element);
|
438 | }
|
439 |
|
440 | // Do the same thing for clones
|
441 | if (!this.state.isClone) {
|
442 | this.removeClonedPanelsAfter(0);
|
443 | }
|
444 | }
|
445 |
|
446 | public removeClonedPanelsAfter(start: number): void {
|
447 | const options = this.viewport.options;
|
448 | const removingPanels = this.clonedPanels.splice(start);
|
449 |
|
450 | if (!options.renderExternal) {
|
451 | removingPanels.forEach(panel => {
|
452 | panel.removeElement();
|
453 | });
|
454 | }
|
455 | }
|
456 |
|
457 | public setElement(element?: HTMLElement | null): void {
|
458 | if (!element) {
|
459 | return;
|
460 | }
|
461 | const currentElement = this.element;
|
462 | if (element !== currentElement) {
|
463 | const options = this.viewport.options;
|
464 |
|
465 | if (currentElement) {
|
466 | if (options.horizontal) {
|
467 | element.style.left = currentElement.style.left;
|
468 | } else {
|
469 | element.style.top = currentElement.style.top;
|
470 | }
|
471 | } else {
|
472 | const originalStyle = this.state.originalStyle;
|
473 |
|
474 | originalStyle.className = element.getAttribute("class");
|
475 | originalStyle.style = element.getAttribute("style");
|
476 | }
|
477 |
|
478 | this.element = element;
|
479 |
|
480 | if (options.classPrefix) {
|
481 | addClass(element, `${options.classPrefix}-panel`);
|
482 | }
|
483 |
|
484 | // Update size info after applying panel css
|
485 | applyCSS(this.element, DEFAULT_PANEL_CSS);
|
486 | }
|
487 | }
|
488 | }
|
489 |
|
490 | export default Panel;
|
491 |
|
\ | No newline at end of file |