UNPKG

13.8 kBPlain TextView Raw
1/**
2 * Copyright (c) 2015 NAVER Corp.
3 * egjs projects are licensed under the MIT license
4 */
5
6import Viewport from "./Viewport";
7import { OriginalStyle, FlickingPanel, ElementLike, DestroyOption, BoundingBox } from "../types";
8import { DEFAULT_PANEL_CSS, EVENTS } from "../consts";
9import { addClass, applyCSS, parseArithmeticExpression, parseElement, getProgress, restoreStyle, hasClass, getBbox } from "../utils";
10
11class 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 // Index of cloned panel, zero-based integer(original: -1, cloned: [0, 1, 2, ...])
24 // if cloneIndex is 0, that means it's first cloned panel of original panel
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
490export default Panel;
491
\No newline at end of file