From 6709fdca40adcabf6025163d1cd8b899d1c4d958 Mon Sep 17 00:00:00 2001 From: Oleksii Kurinnyi Date: Mon, 27 May 2019 09:07:10 +0300 Subject: [PATCH] Fix loader animation on factory loading page (#13415) * add loader directive to demo-components Signed-off-by: Oleksii Kurinnyi * refactor loader Signed-off-by: Oleksii Kurinnyi * code clean-up Signed-off-by: Oleksii Kurinnyi * fix loader animation Signed-off-by: Oleksii Kurinnyi --- .../demo-components.controller.ts | 28 +- .../app/demo-components/demo-components.html | 24 ++ .../app/demo-components/demo-components.styl | 4 + .../load-factory/load-factory.controller.ts | 6 +- .../load-factory/load-factory.service.ts | 16 +- .../loader/che-loader-crane.directive.ts | 337 +++++++++++------- .../widget/loader/che-loader-crane.styl | 38 +- 7 files changed, 291 insertions(+), 162 deletions(-) diff --git a/dashboard/src/app/demo-components/demo-components.controller.ts b/dashboard/src/app/demo-components/demo-components.controller.ts index 318d4d367e1..0a7d8c4af5c 100644 --- a/dashboard/src/app/demo-components/demo-components.controller.ts +++ b/dashboard/src/app/demo-components/demo-components.controller.ts @@ -19,7 +19,7 @@ import { import {ICheEditModeOverlayConfig} from '../../components/widget/edit-mode-overlay/che-edit-mode-overlay.directive'; import {CheNotification} from '../../components/notification/che-notification.factory'; -enum Tab {Font, Panel, Selecter, Icons, Dropdown_button, Buttons, Input, List, Label_container, Stack_selector, Popover, Edit_mode_overlay} +enum Tab {Font, Panel, Selecter, Icons, Dropdown_button, Buttons, Input, List, Label_container, Stack_selector, Popover, Edit_mode_overlay, Loader} /** * This class is handling the controller for the demo of components @@ -57,6 +57,8 @@ export class DemoComponentsController { overlayConfig: ICheEditModeOverlayConfig; + loader: any; + /** * Default constructor that is using resource */ @@ -152,6 +154,8 @@ export class DemoComponentsController { disabled: false } }; + + this.createLoader(); } /** @@ -182,4 +186,26 @@ export class DemoComponentsController { this.numberIsChanged++; } + createLoader(): void { + this.loader = {}; + const allSteps = [ + {text: 'Loading factory', inProgressText: '', logs: '', hasError: false}, + {text: 'Looking for devfile', inProgressText: '', logs: '', hasError: false}, + {text: 'Initializing workspace', inProgressText: 'Provision workspace and associating it with the existing user', logs: '', hasError: false}, + {text: 'Starting workspace runtime', inProgressText: 'Retrieving the stack\'s image and launching it', logs: '', hasError: false}, + {text: 'Starting workspace agent', inProgressText: 'Agents provide RESTful services like intellisense and SSH', logs: '', hasError: false}, + {text: 'Open IDE', inProgressText: '', logs: '', hasError: false} + ] + this.loader.getLoadingSteps = () => allSteps; + let currentProgressStep = 0; + this.loader.getCurrentProgressStep = () => currentProgressStep; + this.loader.nextStep = () => { + currentProgressStep++; + currentProgressStep = currentProgressStep % (allSteps.length); + } + this.loader.pause = () => { + this.loader.paused = !this.loader.paused; + }; + } + } diff --git a/dashboard/src/app/demo-components/demo-components.html b/dashboard/src/app/demo-components/demo-components.html index c1804294f9e..b75ca5c693d 100644 --- a/dashboard/src/app/demo-components/demo-components.html +++ b/dashboard/src/app/demo-components/demo-components.html @@ -475,6 +475,30 @@
This is h6 + + +
+ Current step: {{demoComponentsController.loader.getCurrentProgressStep()}} +
+
+ +
+
+ +
+ + +
+ + +
+
+ +
+ diff --git a/dashboard/src/app/demo-components/demo-components.styl b/dashboard/src/app/demo-components/demo-components.styl index 363440809f8..631f883ee5a 100644 --- a/dashboard/src/app/demo-components/demo-components.styl +++ b/dashboard/src/app/demo-components/demo-components.styl @@ -19,3 +19,7 @@ label min-width 100px + + .animation-pause + .che-loader-animation + animation-play-state: paused diff --git a/dashboard/src/app/factories/load-factory/load-factory.controller.ts b/dashboard/src/app/factories/load-factory/load-factory.controller.ts index 2dd3b1fad9d..bb1feddf82e 100644 --- a/dashboard/src/app/factories/load-factory/load-factory.controller.ts +++ b/dashboard/src/app/factories/load-factory/load-factory.controller.ts @@ -11,7 +11,7 @@ */ 'use strict'; import {CheAPI} from '../../../components/api/che-api.factory'; -import {LoadFactoryService} from './load-factory.service'; +import {LoadFactoryService, FactoryLoadingStep} from './load-factory.service'; import {CheNotification} from '../../../components/notification/che-notification.factory'; import {RouteHistory} from '../../../components/routing/route-history.service'; import {CheJsonRpcApi} from '../../../components/api/json-rpc/che-json-rpc-api.factory'; @@ -127,7 +127,7 @@ export class LoadFactoryController { } else { this.loadFactoryService.goToNextStep(); this.$timeout(() => { - this.processFactorySource(); + this.processFactorySource(); }, 1500); } }, (error: any) => { @@ -623,7 +623,7 @@ export class LoadFactoryController { * * @returns {any} */ - getLoadingSteps(): any { + getLoadingSteps(): FactoryLoadingStep[] { return this.loadFactoryService.getFactoryLoadingSteps(); } diff --git a/dashboard/src/app/factories/load-factory/load-factory.service.ts b/dashboard/src/app/factories/load-factory/load-factory.service.ts index 5648647641f..98f2ae2d9d9 100644 --- a/dashboard/src/app/factories/load-factory/load-factory.service.ts +++ b/dashboard/src/app/factories/load-factory/load-factory.service.ts @@ -11,6 +11,13 @@ */ 'use strict'; +export interface FactoryLoadingStep { + text: string; + logs: string; + hasError: boolean; + inProgressText?: string; +} + /** * This class is handling the service for the factory loading. * @author Ann Shumilova @@ -18,7 +25,7 @@ export class LoadFactoryService { private loadFactoryInProgress: boolean; private currentProgressStep: number; - private loadingSteps: Array; + private loadingSteps: Array; /** * Default constructor that is using resource @@ -27,7 +34,6 @@ export class LoadFactoryService { this.loadFactoryInProgress = false; this.currentProgressStep = 0; - this.loadingSteps = [ {text: 'Loading factory', inProgressText: '', logs: '', hasError: false}, {text: 'Looking for devfile', inProgressText: '', logs: '', hasError: false}, @@ -56,9 +62,9 @@ export class LoadFactoryService { /** * Returns the information of the factory's loading steps. * - * @returns {Array} loading steps of the factory + * @returns {Array} loading steps of the factory */ - getFactoryLoadingSteps(): Array { + getFactoryLoadingSteps(): Array { return this.loadingSteps; } @@ -91,7 +97,7 @@ export class LoadFactoryService { * Reset the loading progress. */ resetLoadProgress(): void { - this.loadingSteps.forEach((step: any) => { + this.loadingSteps.forEach((step: FactoryLoadingStep) => { step.logs = ''; step.hasError = false; }); diff --git a/dashboard/src/components/widget/loader/che-loader-crane.directive.ts b/dashboard/src/components/widget/loader/che-loader-crane.directive.ts index 2f4839f6670..7250a8b2833 100644 --- a/dashboard/src/components/widget/loader/che-loader-crane.directive.ts +++ b/dashboard/src/components/widget/loader/che-loader-crane.directive.ts @@ -24,8 +24,9 @@ interface ICheLoaderCraneScope extends ng.IScope { */ export class CheLoaderCrane implements ng.IDirective { - static $inject = ['$timeout', '$window']; + static $inject = ['$q', '$timeout', '$window']; + $q: ng.IQService; $timeout: ng.ITimeoutService; $window: ng.IWindowService; @@ -44,144 +45,78 @@ export class CheLoaderCrane implements ng.IDirective { /** * Default constructor that is using resource */ - constructor($timeout: ng.ITimeoutService, $window: ng.IWindowService) { + constructor($q: ng.IQService, $timeout: ng.ITimeoutService, $window: ng.IWindowService) { + this.$q = $q; this.$timeout = $timeout; this.$window = $window; } link($scope: ICheLoaderCraneScope, $element: ng.IAugmentedJQuery): void { - let jqCrane = $element.find('.che-loader-crane'), - craneHeight = jqCrane.height(), - craneWidth = jqCrane.width(), - jqCraneLoad = $element.find('#che-loader-crane-load'), - jqCraneScaleWrap = $element.find('.che-loader-crane-scale-wrapper'), + const jqCraneScaleWrap = $element.find('.che-loader-crane-scale-wrapper'), jqCreateProjectContentPage = angular.element('#create-project-content-page'), - jqBody = angular.element(document).find('body'), - scaleStep = 0.05, - scaleMin = 0.6, + jqBody = angular.element(document).find('body'); - newStep, - animationStopping = false, - animationRunning = false; + const stepsNumber = $scope.allSteps.length - $scope.excludeSteps.length; + const loader = new Loader(this.$q, this.$timeout, $element, stepsNumber, $scope.switchOnIteration); - let applyScale = (element: any, scale: number) => { - if (!element.nodeType) { - return; - } - let jqElement = angular.element(element); - jqElement.css('transform', 'scale(' + scale + ')'); - jqElement.css('height', craneHeight * scale); - jqElement.css('width', craneWidth * scale); - }, - hasScrollMoreThan = (domElement: any, diff: number) => { - if (!domElement.nodeType) { - domElement = domElement[0]; - } - if (!domElement) { - return; - } - return domElement.scrollHeight - domElement.offsetHeight > diff; - }, - isVisibilityPartial = (domElement: any) => { - if (!domElement.nodeType) { - domElement = domElement[0]; - } - if (!domElement) { - return; - } - let rect = domElement.getBoundingClientRect(); - return rect.top < 0; - }, - setCraneSize = () => { - let scale = scaleMin; + const scaleStep = 0.05; + const scaleMin = 0.6; + let setCraneSize = () => { + let scale = scaleMin; - applyScale(jqCraneScaleWrap, scale); - jqCraneScaleWrap.css('display', 'block'); + this.applyScale(scale, jqCraneScaleWrap, loader.height, loader.width); + jqCraneScaleWrap.css('display', 'block'); - // do nothing if loader is hidden by hide-sm directive - if ($element.find('.che-loader-crane-scale-wrapper:visible').length === 0) { - return; - } + // do nothing if loader is hidden by hide-sm directive + if ($element.find('.che-loader-crane-scale-wrapper:visible').length === 0) { + return; + } - // hide loader if there is scroll on minimal scale - if ( - // check loader visibility on ide loading or factory loading - (isVisibilityPartial(jqCrane) + const loaderPartiallyHidden = this.elementPartiallyHidden(loader.element); + const bodyHasScroll = this.elementHasScroll(jqBody); + const createProjectContentPageHasScroll = this.elementHasScroll(jqCreateProjectContentPage); + + // hide loader if there is scroll on minimal scale + if ( + // check loader visibility on ide loading or factory loading + (loaderPartiallyHidden // check whether scroll is present on project creating page - || hasScrollMoreThan(jqBody, 0) || hasScrollMoreThan(jqCreateProjectContentPage, 0)) + || bodyHasScroll || createProjectContentPageHasScroll) && scale === scaleMin) { - jqCraneScaleWrap.css('display', 'none'); - return; - } + jqCraneScaleWrap.css('display', 'none'); + return; + } - while (scale < 1) { - applyScale(jqCraneScaleWrap, scale + scaleStep); + while (scale < 1) { + this.applyScale(scale + scaleStep, jqCraneScaleWrap, loader.height, loader.width); - // check for scroll appearance - if ( - // check loader visibility on ide loading or factory loading - isVisibilityPartial(jqCrane) + // check for scroll appearance + if ( + // check loader visibility on ide loading or factory loading + loaderPartiallyHidden // check whether scroll is present on project creating page - || hasScrollMoreThan(jqBody, 0) || hasScrollMoreThan(jqCreateProjectContentPage, 0)) { - applyScale(jqCraneScaleWrap, scale); - break; - } - - scale = scale + scaleStep; - } - }, - setAnimation = () => { - jqCrane.removeClass('che-loader-no-animation'); - }, - setNoAnimation = () => { - animationRunning = false; - jqCrane.addClass('che-loader-no-animation'); - }, - setCurrentStep = () => { - // clear all previously added 'step-#' and 'layer-#' classes - for (let i = 0; i < $scope.allSteps.length; i++) { - jqCrane.removeClass('step-' + i); - jqCraneLoad.removeClass('layer-' + i); + || bodyHasScroll || createProjectContentPageHasScroll) { + this.applyScale(scale, jqCraneScaleWrap, loader.height, loader.width); + break; } - // avoid next layer blinking - let currentLayer = $element.find('.layers-in-box').find('.layer-' + newStep); - currentLayer.css('visibility', 'hidden'); - this.$timeout(() => { - currentLayer.removeAttr('style'); - }, 500); - - jqCrane.addClass('step-' + newStep); - jqCraneLoad.addClass('layer-' + newStep); - }; + scale = scale + scaleStep; + } + }; $scope.$watch(() => { return $scope.step; - }, (newStepStr: string) => { - const newVal = parseInt(newStepStr, 10); - - // try to stop animation on last step - if (newVal === $scope.allSteps.length - 1) { - animationStopping = true; + }, (nextStepStr: string) => { + const nextStep = parseInt(nextStepStr, 10); - if (!$scope.switchOnIteration) { - // stop animation immediately if it shouldn't wait until next iteration - setNoAnimation(); - } - } - - // skip steps excluded - if ($scope.excludeSteps.indexOf(newStepStr) !== -1) { + // skip excluded step + if ($scope.excludeSteps.indexOf(nextStepStr) !== -1) { return; } - newStep = newVal; - - // go to next step - // if animation hasn't run yet or it shouldn't wait until next iteration - if (!animationRunning || !$scope.switchOnIteration) { - setAnimation(); - setCurrentStep(); + loader.setStep(nextStep); + if (nextStep === $scope.allSteps.length) { + loader.stopAnimation(); } }); @@ -189,17 +124,10 @@ export class CheLoaderCrane implements ng.IDirective { $scope.$watch(() => { return $element.find('.che-loader-crane:visible').length; }, () => { - if (angular.isFunction(destroyResizeEvent)) { return; } - jqCrane = $element.find('.che-loader-crane'); - jqCraneLoad = $element.find('#che-loader-crane-load'); - jqCraneScaleWrap = $element.find('.che-loader-crane-scale-wrapper'); - jqCreateProjectContentPage = angular.element('#create-project-content-page'); - jqBody = angular.element(document).find('body'); - // initial resize this.$timeout(() => { setCraneSize(); @@ -216,17 +144,168 @@ export class CheLoaderCrane implements ng.IDirective { }); }); - if ($scope.switchOnIteration) { - $element.find('.che-loader-animation.trolley-block').bind('animationstart', () => { - animationRunning = true; - }); - $element.find('.che-loader-animation.trolley-block').bind('animationiteration', () => { - setCurrentStep(); + } + + applyScale(scale: number, element: ng.IAugmentedJQuery, elementHeight: number, elementWidth: number) { + let jqElement = angular.element(element); + jqElement.css('transform', 'scale(' + scale + ')'); + jqElement.css('height', elementHeight * scale); + jqElement.css('width', elementWidth * scale); + } + + // hasScroll + elementHasScroll(element: ng.IAugmentedJQuery) { + let domElement = element[0]; + if (!domElement) { + return; + } + return domElement.scrollHeight - domElement.offsetHeight > 0; + } + + elementPartiallyHidden(element: ng.IAugmentedJQuery): boolean { + let domElement = element[0]; + if (!domElement) { + return false; + } + let rect = domElement.getBoundingClientRect(); + return rect.top < 0; + } + +} + +class Loader { + private $q: ng.IQService; + private $timeout: ng.ITimeoutService; + private $element: ng.IAugmentedJQuery; + + private loader: ng.IAugmentedJQuery; + // todo: try to remove this + private load: ng.IAugmentedJQuery; + + // if `true` then wait when current iteration ends and then render next step + private changeAnimationOnIteration: boolean = true; + private animationStopping: boolean = false; + private animationRunning: boolean = false; + private animationIterationDeferred: ng.IDeferred; + + currentStep: number = 0; + stepsNumber: number = 0; + maxStepsNumber: number = 4; + height: number; + width: number; + + constructor( + $q: ng.IQService, + $timeout: ng.ITimeoutService, + $element: ng.IAugmentedJQuery, + stepsNumber: number, + changeAnimationOnIteration: boolean + ) { + this.$q = $q; + this.$timeout = $timeout; + this.$element = $element; + + this.stepsNumber = stepsNumber; - if (animationStopping) { - setNoAnimation(); + this.changeAnimationOnIteration = changeAnimationOnIteration; + + this.initialize(); + } + + private initialize(): void { + this.loader = this.$element.find('.che-loader-crane'); + this.height = this.loader.height(); + this.width = this.loader.width(); + + this.load = this.$element.find('#che-loader-crane-load'); + + this.loader.find('.che-loader-animation.trolley-block .layer') + .bind('animationstart', () => { + this.animationRunning = true; + }) + .bind('animationiteration', () => { + this.animationIterationDeferred.resolve(); + this.animationIterationDeferred = this.$q.defer(); + + if (this.animationStopping) { + this.animationRunning = false; + this.loader.addClass('che-loader-no-animation'); + return; } }); + } + + get element(): ng.IAugmentedJQuery { + return this.loader; + } + + setStep(step: number): void { + this.currentStep = step > this.maxStepsNumber ? this.maxStepsNumber : step; + + // stop animation on last step + if (step === this.stepsNumber) { + this.lastStep(); + return; + } + + if (!this.animationIterationDeferred) { + this.animationIterationDeferred = this.$q.defer(); + } + + this.animationIterationDeferred.promise.then(() => { + this.drawStep(); + this.loader.removeClass('che-loader-no-animation'); + }); + + if ( + // if animation is not running at the moment + !this.animationRunning + // or it can be changed before an iteration ends + || !this.changeAnimationOnIteration + ) { + this.animationIterationDeferred.resolve(); + this.animationIterationDeferred = this.$q.defer(); + } + } + + private lastStep(): void { + if (!this.changeAnimationOnIteration) { + // stop animation immediately + this.stopAnimation(); + } else { + // or wait until an iteration ends + this.animationStopping = true; } } + + stopAnimation(): void { + if (this.changeAnimationOnIteration === false) { + this.animationRunning = false; + this.loader.addClass('che-loader-no-animation'); + } + } + + private drawStep(): void { + // clear all previously added 'step-#' and 'layer-#' classes + let steps = ''; + let layers = ''; + for (let i = 0; i < this.maxStepsNumber; i++) { + steps += `step-${i}`; + layers += `layers-${i}`; + } + this.loader.removeClass(steps); + this.load.removeClass(layers); + + // avoid next layer blinking + let currentLayer = this.loader.find('.layers-in-box').find('.layer-' + this.currentStep); + currentLayer.css('visibility', 'hidden'); + + this.$timeout(() => { + currentLayer.removeAttr('style'); + }, 500); + + this.loader.addClass('step-' + this.currentStep); + this.load.addClass('layer-' + this.currentStep); + } + } diff --git a/dashboard/src/components/widget/loader/che-loader-crane.styl b/dashboard/src/components/widget/loader/che-loader-crane.styl index 3fcad4ca68d..918da7d5297 100644 --- a/dashboard/src/components/widget/loader/che-loader-crane.styl +++ b/dashboard/src/components/widget/loader/che-loader-crane.styl @@ -106,7 +106,7 @@ $tower-offset-top = $load-jib-height $rope-thickness = 2px $layer-side-length-max = 125px -$layers-hash = { +$layers-map = { '1': { 'top-side-color': $layer-stage-1-top-side-color 'left-side-color': $layer-stage-1-left-side-color @@ -135,23 +135,9 @@ $layers-hash = { 'layer-side-length': $layer-side-length-max - 45px 'offset': 3 } - '5': { - 'top-side-color': $layer-stage-1-top-side-color - 'left-side-color': $layer-stage-1-left-side-color - 'right-side-color': $layer-stage-1-right-side-color - 'layer-side-length': $layer-side-length-max - 60px - 'offset': 4 - } - '6': { - 'top-side-color': $layer-stage-1-top-side-color - 'left-side-color': $layer-stage-1-left-side-color - 'right-side-color': $layer-stage-1-right-side-color - 'layer-side-length': $layer-side-length-max - 75px - 'offset': 5 - } } $layer-height = 30px -$layer-offset-top = $layers-hash['1']['layer-side-length'] +$layer-offset-top = $layers-map['1']['layer-side-length'] $layer-slide-in-offset-left = $layer-offset-top $layer-rope-height = $layer-offset-top @@ -191,7 +177,7 @@ $rope-3-end-point-height = $rope-3-height + $rope-3-moving-distance $layers-in-box-offset-top = $box-offset-top /* ANIMATIONS */ -$animations-hash = { +$animations-map = { 'slide-trolley': { '0': { transform: translate3d(0,0,0) @@ -281,9 +267,9 @@ $animations-hash = { } } } -for $name in $animations-hash +for $name in $animations-map @keyframes {$name} - for $percent, $transform in $animations-hash[$name] + for $percent, $transform in $animations-map[$name] {1% * $percent} {$transform} @@ -821,7 +807,7 @@ $che-crane-width = ($load-jib-1-width + $load-jib-2-width + $load-jib-3-width + // .box -for $index, $layer-props in $layers-hash +for $index, $layer-props in $layers-map $layer-side-length = $layer-props['layer-side-length'] $top-side-color = $layer-props['top-side-color'] $left-side-color = $layer-props['left-side-color'] @@ -937,14 +923,14 @@ for $index, $layer-props in $layers-hash // define visibility of layers in box .che-loader-crane .layers-in-box - .layer-1, .layer-2, .layer-3, .layer-4 + .layer-1, .layer-2, .layer-3, .layer-4, .layer-5 opacity 0 .che-loader-crane.step-1 .layers-in-box .layer-1 opacity 1 - .layer-2, .layer-3, .layer-4 + .layer-2, .layer-3, .layer-4, .layer-5 opacity 0 .che-loader-crane.step-2 @@ -954,7 +940,7 @@ for $index, $layer-props in $layers-hash .layer-1, .layer-2 opacity 1 - .layer-3, .layer-4 + .layer-3, .layer-4, .layer-5 opacity 0 .che-loader-crane.step-3 @@ -965,7 +951,7 @@ for $index, $layer-props in $layers-hash .layer-1, .layer-2, .layer-3 opacity 1 - .layer-4 + .layer-4, .layer-5 opacity 0 .che-loader-crane.step-4 @@ -976,6 +962,8 @@ for $index, $layer-props in $layers-hash .layer-1, .layer-2, .layer-3, .layer-4 opacity 1 + .layer-5 + opacity 0 .che-loader-crane.step-5 .layers-in-box @@ -985,6 +973,8 @@ for $index, $layer-props in $layers-hash .layer-1, .layer-2, .layer-3, .layer-4 opacity 1 + .layer-5 + opacity 0 .layers-in-box position relative