UNPKG

15.7 kBJavaScriptView Raw
1import { CoreTypes } from '../../../core-types';
2import { Trace } from '../../../trace';
3import { CSSType, View } from '../../core/view';
4import { GridLayout } from '../grid-layout';
5import { Animation } from '../../animation';
6import { isNumber } from '../../../utils/types';
7let RootLayoutBase = class RootLayoutBase extends GridLayout {
8 constructor() {
9 super();
10 this.popupViews = [];
11 global.rootLayout = this;
12 }
13 onLoaded() {
14 // get actual content count of rootLayout (elements between the <RootLayout> tags in the template).
15 // All popups will be inserted dynamically at a higher index
16 this.staticChildCount = this.getChildrenCount();
17 super.onLoaded();
18 }
19 _onLivesync(context) {
20 let handled = false;
21 if (this.popupViews.length > 0) {
22 this.closeAll();
23 handled = true;
24 }
25 if (super._onLivesync(context)) {
26 handled = true;
27 }
28 return handled;
29 }
30 /**
31 * Ability to add any view instance to composite views like layers.
32 *
33 * @param view
34 * @param options
35 * @returns
36 */
37 open(view, options = {}) {
38 return new Promise((resolve, reject) => {
39 if (!(view instanceof View)) {
40 return reject(new Error(`Invalid open view: ${view}`));
41 }
42 if (this.hasChild(view)) {
43 return reject(new Error(`${view} has already been added`));
44 }
45 const toOpen = [];
46 const enterAnimationDefinition = options.animation ? options.animation.enterFrom : null;
47 // keep track of the views locally to be able to use their options later
48 this.popupViews.push({ view: view, options: options });
49 if (options.shadeCover) {
50 // perf optimization note: we only need 1 layer of shade cover
51 // we just update properties if needed by additional overlaid views
52 if (this.shadeCover) {
53 // overwrite current shadeCover options if topmost popupview has additional shadeCover configurations
54 toOpen.push(this.updateShadeCover(this.shadeCover, options.shadeCover));
55 }
56 else {
57 toOpen.push(this.openShadeCover(options.shadeCover));
58 }
59 }
60 view.opacity = 0; // always begin with view invisible when adding dynamically
61 this.insertChild(view, this.getChildrenCount() + 1);
62 toOpen.push(new Promise((res, rej) => {
63 setTimeout(() => {
64 // only apply initial state and animate after the first tick - ensures safe areas and other measurements apply correctly
65 this.applyInitialState(view, enterAnimationDefinition);
66 this.getEnterAnimation(view, enterAnimationDefinition)
67 .play()
68 .then(() => {
69 this.applyDefaultState(view);
70 view.notify({ eventName: 'opened', object: view });
71 res();
72 }, (err) => {
73 rej(new Error(`Error playing enter animation: ${err}`));
74 });
75 });
76 }));
77 Promise.all(toOpen).then(() => {
78 resolve();
79 }, (err) => {
80 reject(err);
81 });
82 });
83 }
84 /**
85 * Ability to remove any view instance from composite views.
86 * Optional animation parameter to overwrite close animation declared when opening popup.
87 *
88 * @param view
89 * @param exitTo
90 * @returns
91 */
92 close(view, exitTo) {
93 return new Promise((resolve, reject) => {
94 if (!(view instanceof View)) {
95 return reject(new Error(`Invalid close view: ${view}`));
96 }
97 if (!this.hasChild(view)) {
98 return reject(new Error(`Unable to close popup. ${view} not found`));
99 }
100 const toClose = [];
101 const popupIndex = this.getPopupIndex(view);
102 const poppedView = this.popupViews[popupIndex];
103 const cleanupAndFinish = () => {
104 view.notify({ eventName: 'closed', object: view });
105 this.removeChild(view);
106 resolve();
107 };
108 // use exitAnimation that is passed in and fallback to the exitAnimation passed in when opening
109 const exitAnimationDefinition = exitTo || poppedView?.options?.animation?.exitTo;
110 // Remove view from tracked popupviews
111 this.popupViews.splice(popupIndex, 1);
112 toClose.push(new Promise((res, rej) => {
113 if (exitAnimationDefinition) {
114 this.getExitAnimation(view, exitAnimationDefinition)
115 .play()
116 .then(res, (err) => {
117 rej(new Error(`Error playing exit animation: ${err}`));
118 });
119 }
120 else {
121 res();
122 }
123 }));
124 if (this.shadeCover) {
125 // Update shade cover with the topmost popupView options (if not specifically told to ignore)
126 if (this.popupViews.length) {
127 if (!poppedView?.options?.shadeCover?.ignoreShadeRestore) {
128 const shadeCoverOptions = this.popupViews[this.popupViews.length - 1].options?.shadeCover;
129 if (shadeCoverOptions) {
130 toClose.push(this.updateShadeCover(this.shadeCover, shadeCoverOptions));
131 }
132 }
133 }
134 else {
135 // Remove shade cover animation if this is the last opened popup view
136 toClose.push(this.closeShadeCover(poppedView?.options?.shadeCover));
137 }
138 }
139 Promise.all(toClose).then(() => {
140 cleanupAndFinish();
141 }, (err) => {
142 reject(err);
143 });
144 });
145 }
146 closeAll() {
147 const toClose = [];
148 const views = this.popupViews.map((popupView) => popupView.view);
149 // Close all views at the same time and wait for all of them
150 for (const view of views) {
151 toClose.push(this.close(view));
152 }
153 return Promise.all(toClose);
154 }
155 getShadeCover() {
156 return this.shadeCover;
157 }
158 openShadeCover(options = {}) {
159 return new Promise((resolve) => {
160 if (this.shadeCover) {
161 if (Trace.isEnabled()) {
162 Trace.write(`RootLayout shadeCover already open.`, Trace.categories.Layout, Trace.messageType.warn);
163 }
164 resolve();
165 }
166 else {
167 // Create the one and only shade cover
168 const shadeCover = this.createShadeCover();
169 shadeCover.on('loaded', () => {
170 this._initShadeCover(shadeCover, options);
171 this.updateShadeCover(shadeCover, options).then(() => {
172 resolve();
173 });
174 });
175 this.shadeCover = shadeCover;
176 // Insert shade cover at index right above the first layout
177 this.insertChild(this.shadeCover, this.staticChildCount + 1);
178 }
179 });
180 }
181 closeShadeCover(shadeCoverOptions = {}) {
182 return new Promise((resolve) => {
183 // if shade cover is displayed and the last popup is closed, also close the shade cover
184 if (this.shadeCover) {
185 return this._closeShadeCover(this.shadeCover, shadeCoverOptions).then(() => {
186 if (this.shadeCover) {
187 this.shadeCover.off('loaded');
188 if (this.shadeCover.parent) {
189 this.removeChild(this.shadeCover);
190 }
191 }
192 this.shadeCover = null;
193 // cleanup any platform specific details related to shade cover
194 this._cleanupPlatformShadeCover();
195 resolve();
196 });
197 }
198 resolve();
199 });
200 }
201 topmost() {
202 return this.popupViews.length ? this.popupViews[this.popupViews.length - 1].view : null;
203 }
204 // bring any view instance open on the rootlayout to front of all the children visually
205 bringToFront(view, animated = false) {
206 return new Promise((resolve, reject) => {
207 if (!(view instanceof View)) {
208 return reject(new Error(`Invalid bringToFront view: ${view}`));
209 }
210 if (!this.hasChild(view)) {
211 return reject(new Error(`${view} not found or already at topmost`));
212 }
213 const popupIndex = this.getPopupIndex(view);
214 // popupview should be present and not already the topmost view
215 if (popupIndex < 0 || popupIndex == this.popupViews.length - 1) {
216 return reject(new Error(`${view} not found or already at topmost`));
217 }
218 // keep the popupViews array in sync with the stacking of the views
219 const currentView = this.popupViews[this.getPopupIndex(view)];
220 this.popupViews.splice(this.getPopupIndex(view), 1);
221 this.popupViews.push(currentView);
222 const exitAnimation = this.getViewExitState(view);
223 if (animated && exitAnimation) {
224 this.getExitAnimation(view, exitAnimation)
225 .play()
226 .then(() => {
227 this._bringToFront(view);
228 const initialState = this.getViewInitialState(currentView.view);
229 if (initialState) {
230 this.applyInitialState(view, initialState);
231 this.getEnterAnimation(view, initialState)
232 .play()
233 .then(() => {
234 this.applyDefaultState(view);
235 })
236 .catch((ex) => {
237 reject(new Error(`Error playing enter animation: ${ex}`));
238 });
239 }
240 else {
241 this.applyDefaultState(view);
242 }
243 })
244 .catch((ex) => {
245 this._bringToFront(view);
246 reject(new Error(`Error playing exit animation: ${ex}`));
247 });
248 }
249 else {
250 this._bringToFront(view);
251 }
252 // update shadeCover to reflect topmost's shadeCover options
253 const shadeCoverOptions = currentView?.options?.shadeCover;
254 if (shadeCoverOptions) {
255 this.updateShadeCover(this.shadeCover, shadeCoverOptions);
256 }
257 resolve();
258 });
259 }
260 getPopupIndex(view) {
261 return this.popupViews.findIndex((popupView) => popupView.view === view);
262 }
263 getViewInitialState(view) {
264 const popupIndex = this.getPopupIndex(view);
265 if (popupIndex === -1) {
266 return;
267 }
268 const initialState = this.popupViews[popupIndex]?.options?.animation?.enterFrom;
269 if (!initialState) {
270 return;
271 }
272 return initialState;
273 }
274 getViewExitState(view) {
275 const popupIndex = this.getPopupIndex(view);
276 if (popupIndex === -1) {
277 return;
278 }
279 const exitAnimation = this.popupViews[popupIndex]?.options?.animation?.exitTo;
280 if (!exitAnimation) {
281 return;
282 }
283 return exitAnimation;
284 }
285 applyInitialState(targetView, enterFrom) {
286 const animationOptions = {
287 ...defaultTransitionAnimation,
288 ...(enterFrom || {}),
289 };
290 targetView.translateX = animationOptions.translateX;
291 targetView.translateY = animationOptions.translateY;
292 targetView.scaleX = animationOptions.scaleX;
293 targetView.scaleY = animationOptions.scaleY;
294 targetView.rotate = animationOptions.rotate;
295 targetView.opacity = animationOptions.opacity;
296 }
297 applyDefaultState(targetView) {
298 targetView.translateX = 0;
299 targetView.translateY = 0;
300 targetView.scaleX = 1;
301 targetView.scaleY = 1;
302 targetView.rotate = 0;
303 targetView.opacity = 1;
304 }
305 getEnterAnimation(targetView, enterFrom) {
306 const animationOptions = {
307 ...defaultTransitionAnimation,
308 ...(enterFrom || {}),
309 };
310 return new Animation([
311 {
312 target: targetView,
313 translate: { x: 0, y: 0 },
314 scale: { x: 1, y: 1 },
315 rotate: 0,
316 opacity: 1,
317 duration: animationOptions.duration,
318 curve: animationOptions.curve,
319 },
320 ]);
321 }
322 getExitAnimation(targetView, exitTo) {
323 return new Animation([this.getExitAnimationDefinition(targetView, exitTo)]);
324 }
325 getExitAnimationDefinition(targetView, exitTo) {
326 return {
327 target: targetView,
328 ...defaultTransitionAnimation,
329 ...(exitTo || {}),
330 translate: { x: isNumber(exitTo.translateX) ? exitTo.translateX : defaultTransitionAnimation.translateX, y: isNumber(exitTo.translateY) ? exitTo.translateY : defaultTransitionAnimation.translateY },
331 scale: { x: isNumber(exitTo.scaleX) ? exitTo.scaleX : defaultTransitionAnimation.scaleX, y: isNumber(exitTo.scaleY) ? exitTo.scaleY : defaultTransitionAnimation.scaleY },
332 };
333 }
334 createShadeCover() {
335 const shadeCover = new GridLayout();
336 shadeCover.verticalAlignment = 'bottom';
337 return shadeCover;
338 }
339 updateShadeCover(shade, shadeOptions = {}) {
340 if (shadeOptions.tapToClose !== undefined && shadeOptions.tapToClose !== null) {
341 shade.off('tap');
342 if (shadeOptions.tapToClose) {
343 shade.on('tap', () => {
344 this.closeAll();
345 });
346 }
347 }
348 return this._updateShadeCover(shade, shadeOptions);
349 }
350 hasChild(view) {
351 return this.getChildIndex(view) >= 0;
352 }
353 _bringToFront(view) { }
354 _initShadeCover(view, shadeOption) { }
355 _updateShadeCover(view, shadeOption) {
356 return new Promise(() => { });
357 }
358 _closeShadeCover(view, shadeOptions) {
359 return new Promise(() => { });
360 }
361 _cleanupPlatformShadeCover() { }
362};
363RootLayoutBase = __decorate([
364 CSSType('RootLayout'),
365 __metadata("design:paramtypes", [])
366], RootLayoutBase);
367export { RootLayoutBase };
368export function getRootLayout() {
369 return global.rootLayout;
370}
371export const defaultTransitionAnimation = {
372 translateX: 0,
373 translateY: 0,
374 scaleX: 1,
375 scaleY: 1,
376 rotate: 0,
377 opacity: 1,
378 duration: 300,
379 curve: CoreTypes.AnimationCurve.easeIn,
380};
381export const defaultShadeCoverTransitionAnimation = {
382 ...defaultTransitionAnimation,
383 opacity: 0, // default to fade in/out
384};
385export const defaultShadeCoverOptions = {
386 opacity: 0.5,
387 color: '#000000',
388 tapToClose: true,
389 animation: {
390 enterFrom: defaultShadeCoverTransitionAnimation,
391 exitTo: defaultShadeCoverTransitionAnimation,
392 },
393 ignoreShadeRestore: false,
394};
395//# sourceMappingURL=root-layout-common.js.map
\No newline at end of file