Canh Ngo pushed to branch feature/cmng-psp1 at cms-community / hippo-addon-channel-manager
Commits: 897c869f by Arthur Bogaart at 2016-03-23T01:57:07+01:00 CHANNELMGR-470 Add device detection module To be able to safely decide if a browser has native scroll support when dragging, we need to use a pre-compiled list and check it the hard way, no feature detection possible. - - - - - a1ac9872 by Arthur Bogaart at 2016-03-23T02:05:32+01:00 CHANNELMGR-470 Add ScrollService for scrolling the page while dragging Wire up the ScrollService in the DragDropService and use the DragDropService.dragging state to decide if scrolling can start. Currently the ScrollService scrolls only up and down (using velocity animations). It attaches a mouse-enter and mouse-leave event handler to the container element and uses these trigger to initiate or stop a scroll on the page. it only attaches event listeners to browsers that lack native scroll support while dragging. The duration of a scroll animation is between 500 and 1500 ms based on the distance to scroll, although this is a very simple impl. We might want to slow it down a bit to 500-2000 ms. - - - - - 634ef611 by Arthur Bogaart at 2016-03-23T02:06:41+01:00 CHANNELMGR-470 Add unit test for ScrollService - - - - - 54ad8c87 by Arthur Bogaart at 2016-03-23T02:16:29+01:00 CHANNELMGR-470 Fix an issue in Firefox where the height of the iframe is incorrect upon load The throttle function executes the callback and then starts blocking incoming calls. This means that the state of the callback owner can be out of sync, as happens in Firefox when using the throttleService to throttle MutationObserver callbacks. Moving the callback call into the timeout handler ensures the state is correct, but might trigger a second callback. This might need a different approach, like maybe re-using the throttle from lodash. - - - - - 9bef9aab by Canh Ngo at 2016-03-24T13:01:04+01:00 CHANNELMGR-470: refactored after review. - - - - - e0c57e7a by Canh Ngo at 2016-03-24T15:07:19+01:00 CHANNELMGR-470: Reintegrated 'feature/cmng-psp1-CHANNELMGR-470' into feature/cmng-psp1 - - - - - 9 changed files: - frontend-ng/bower.json - frontend-ng/karma.conf.js - frontend-ng/src/angularjs/channel/hippoIframe/dragDrop.service.js - frontend-ng/src/angularjs/channel/hippoIframe/hippoIframe.js - + frontend-ng/src/angularjs/channel/hippoIframe/scroll.service.fixture.html - + frontend-ng/src/angularjs/channel/hippoIframe/scroll.service.js - + frontend-ng/src/angularjs/channel/hippoIframe/scroll.service.spec.js - frontend-ng/src/angularjs/utils/throttle.service.js - frontend-ng/src/index.html Changes: ===================================== frontend-ng/bower.json ===================================== --- a/frontend-ng/bower.json +++ b/frontend-ng/bower.json @@ -12,7 +12,8 @@ "dragula.js": "3.6.8", "es6-shim": "0.34.0", "jquery": "2.2.0", - "velocity": "1.2.3" + "velocity": "1.2.3", + "ng-device-detector": "3.0.1" }, "devDependencies": { "angular-mocks": "1.4.8" ===================================== frontend-ng/karma.conf.js ===================================== --- a/frontend-ng/karma.conf.js +++ b/frontend-ng/karma.conf.js @@ -33,6 +33,8 @@ module.exports = function karmaConfig(config) { `${cfg.bowerDir}/jquery/dist/jquery.js`, `${cfg.bowerDir}/velocity/velocity.js`, `${cfg.bowerDir}/dragula.js/dist/dragula.js`, + `${cfg.bowerDir}/re-tree/re-tree.js`, + `${cfg.bowerDir}/ng-device-detector/ng-device-detector.js`, ]; options.files = [ ===================================== frontend-ng/src/angularjs/channel/hippoIframe/dragDrop.service.js ===================================== --- a/frontend-ng/src/angularjs/channel/hippoIframe/dragDrop.service.js +++ b/frontend-ng/src/angularjs/channel/hippoIframe/dragDrop.service.js @@ -18,7 +18,7 @@ const COMPONENT_QA_CLASS = 'qa-dragula-component'; export class DragDropService { - constructor($rootScope, $q, DomService, HstService, PageStructureService, ScalingService, ChannelService) { + constructor($rootScope, $q, DomService, HstService, PageStructureService, ScalingService, ChannelService, ScrollService) { 'ngInject'; this.$rootScope = $rootScope; @@ -28,6 +28,7 @@ export class DragDropService { this.PageStructureService = PageStructureService; this.ScalingService = ScalingService; this.ChannelService = ChannelService; + this.ScrollService = ScrollService; this.draggingOrClicking = false; } @@ -36,6 +37,7 @@ export class DragDropService { this.iframeJQueryElement = iframeJQueryElement; this.baseJQueryElement = baseJQueryElement; + this.ScrollService.init(iframeJQueryElement, baseJQueryElement); this.iframeJQueryElement.on('load', () => this._onLoad()); } @@ -75,9 +77,15 @@ export class DragDropService { this.drake.on('drop', (el, target, source, sibling) => this._onDrop(el, target, source, sibling)); this._handleComponentClick(containers); + this.ScrollService.enable(() => this.draggingOrClicking); }); } + disable() { + this.ScrollService.disable(); + this._destroyDrake(); + } + _injectDragula(iframe) { const appRootUrl = this.DomService.getAppRootUrl(); @@ -111,10 +119,6 @@ export class DragDropService { }); } - disable() { - this._destroyDrake(); - } - startDragOrClick($event, structureElement) { this.draggingOrClicking = true; this._dispatchEventInIframe($event, structureElement); ===================================== frontend-ng/src/angularjs/channel/hippoIframe/hippoIframe.js ===================================== --- a/frontend-ng/src/angularjs/channel/hippoIframe/hippoIframe.js +++ b/frontend-ng/src/angularjs/channel/hippoIframe/hippoIframe.js @@ -21,10 +21,12 @@ import { HippoIframeCtrl } from './hippoIframe.controller'; import { HstCommentsProcessorService } from './hstCommentsProcessor.service'; import { LinkProcessorService } from './linkProcessor.service'; import { ScalingService } from './scaling.service'; +import { ScrollService } from './scroll.service'; import { DragDropService } from './dragDrop.service'; export const channelHippoIframeModule = angular .module('hippo-cm.channel.hippoIframe', [ + 'ng.deviceDetector', overlayModule.name, componentAdderModule.name, ]) @@ -33,4 +35,5 @@ export const channelHippoIframeModule = angular .service('hstCommentsProcessorService', HstCommentsProcessorService) .service('linkProcessorService', LinkProcessorService) .service('ScalingService', ScalingService) + .service('ScrollService', ScrollService) .service('DragDropService', DragDropService); ===================================== frontend-ng/src/angularjs/channel/hippoIframe/scroll.service.fixture.html ===================================== --- /dev/null +++ b/frontend-ng/src/angularjs/channel/hippoIframe/scroll.service.fixture.html @@ -0,0 +1,23 @@ +<!-- + ~ Copyright 2016 Hippo B.V. (http://www.onehippo.com) + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<div id="test-hippo-iframe" style="padding: 40px;"> + <div class="channel-iframe-base" style="overflow: auto; height: 200px;"> + <div class="channel-iframe-canvas"> + <div class="channel-iframe-element" style="height: 400px;"></div> + </div> + </div> +</div> ===================================== frontend-ng/src/angularjs/channel/hippoIframe/scroll.service.js ===================================== --- /dev/null +++ b/frontend-ng/src/angularjs/channel/hippoIframe/scroll.service.js @@ -0,0 +1,106 @@ +/* + * Copyright 2016 Hippo B.V. (http://www.onehippo.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const EVENT_NAMESPACE = '.scroll-events'; +const DURATION_MIN = 500; +const DURATION_MAX = 1500; + +export class ScrollService { + + constructor(deviceDetector, BROWSERS) { + 'ngInject'; + + const browsersWithNativeSupport = [BROWSERS.CHROME, BROWSERS.MS_EDGE, BROWSERS.OPERA]; + this._hasNativeSupport = browsersWithNativeSupport.indexOf(deviceDetector.browser) > -1; + } + + init(el, container, easing = 'ease-in-out') { + this.el = el; + this.container = container; + this.easing = easing; + } + + enable(scrollAllowed = () => true) { + if (this._hasNativeSupport) { + return; + } + + this.container + .on(`mouseenter${EVENT_NAMESPACE}`, () => this.stopScrolling()) + .on(`mouseleave${EVENT_NAMESPACE}`, (data) => { + if (scrollAllowed()) { + this.startScrolling(data.pageX, data.pageY); + } + }); + } + + disable() { + if (this._hasNativeSupport) { + return; + } + + this.stopScrolling(); + if (this.container) { + this.container.off(EVENT_NAMESPACE); + } + } + + startScrolling(mouseX, mouseY) { + const containerOffset = this.container.offset(); + const containerHeight = this.container.outerHeight(); + const containerScrollTop = this.container.scrollTop(); + const containerTop = containerOffset.top; + const containerBottom = containerTop + containerHeight; + + let offset = 0; + if (mouseY < containerTop) { + // scroll up to top position + offset = -containerScrollTop; + } else if (mouseY >= containerBottom) { + // scroll down to the bottom position + const contentHeight = this.el.outerHeight(); + offset = (contentHeight - containerHeight) - containerScrollTop; + } + + if (offset) { + this._scroll(offset); + } + } + + stopScrolling() { + if (this.el) { + this.el.velocity('stop'); + } + } + + _scroll(offset) { + this.el + .velocity('stop') + .velocity('scroll', { + container: this.container, + duration: this._calculateDuration(offset), + easing: this.easing, + offset, + }); + } + + + _calculateDuration(distance) { + distance = Math.abs(distance); + const duration = distance * 2; + return Math.min(Math.max(DURATION_MIN, duration), DURATION_MAX); + } +} ===================================== frontend-ng/src/angularjs/channel/hippoIframe/scroll.service.spec.js ===================================== --- /dev/null +++ b/frontend-ng/src/angularjs/channel/hippoIframe/scroll.service.spec.js @@ -0,0 +1,144 @@ +/* + * Copyright 2016 Hippo B.V. (http://www.onehippo.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +describe('ScrollService', () => { + 'use strict'; + + let ScrollService; + let baseJQueryElement; + let iframeJQueryElement; + let velocitySpy; + + beforeEach(() => { + module('hippo-cm.channel.hippoIframe'); + + jasmine.getFixtures().load('channel/hippoIframe/scroll.service.fixture.html'); + + baseJQueryElement = $j('.channel-iframe-base'); + iframeJQueryElement = $j('.channel-iframe-element'); + iframeJQueryElement.velocity = () => {}; + velocitySpy = spyOn(iframeJQueryElement, 'velocity').and.returnValue(iframeJQueryElement); + + inject((_ScrollService_) => { + ScrollService = _ScrollService_; + ScrollService.init(iframeJQueryElement, baseJQueryElement); + }); + }); + + it('should be possible to configure the type of easing', () => { + ScrollService.init(null, null, 'custom-easing'); + expect(ScrollService.easing).toEqual('custom-easing'); + }); + + describe('in a browser with native drag&drop scroll support', () => { + beforeEach(() => { + ScrollService._hasNativeSupport = true; + }); + + it('should not start/stop scrolling on mouse enter/leave events', () => { + const startScroll = spyOn(ScrollService, 'startScrolling'); + const stopScroll = spyOn(ScrollService, 'stopScrolling'); + ScrollService.enable(); + + baseJQueryElement.trigger('mouseleave'); + expect(startScroll).not.toHaveBeenCalled(); + + baseJQueryElement.trigger('mouseenter'); + expect(stopScroll).not.toHaveBeenCalled(); + }); + + it('should not stop scrolling or remove event listeners when disabled', () => { + const baseEventSpy = spyOn(baseJQueryElement, 'off').and.callThrough(); + const stopScroll = spyOn(ScrollService, 'stopScrolling'); + ScrollService.disable(); + + expect(stopScroll).not.toHaveBeenCalled(); + expect(baseEventSpy).not.toHaveBeenCalled(); + }); + }); + + describe('in a browser without native drag&drop scroll support', () => { + beforeEach(() => { + ScrollService._hasNativeSupport = false; + }); + + it('should start/stop scrolling on mouse enter/leave events', () => { + const startScroll = spyOn(ScrollService, 'startScrolling'); + const stopScroll = spyOn(ScrollService, 'stopScrolling'); + ScrollService.enable(); + + baseJQueryElement.trigger('mouseleave'); + expect(startScroll).toHaveBeenCalled(); + + baseJQueryElement.trigger('mouseenter'); + expect(stopScroll).toHaveBeenCalled(); + }); + + it('should not start scrolling when the shouldScroll argument returns false', () => { + const startScroll = spyOn(ScrollService, 'startScrolling'); + ScrollService.enable(() => false); + + baseJQueryElement.trigger('mouseleave'); + expect(startScroll).not.toHaveBeenCalled(); + }); + + it('should stop velocity and remove event listeners when disabled', () => { + const baseEventSpy = spyOn(baseJQueryElement, 'off').and.callThrough(); + ScrollService.disable(); + + expect(velocitySpy).toHaveBeenCalledWith('stop'); + expect(baseEventSpy).toHaveBeenCalled(); + }); + + it('should not scroll up when at the top and mouse leaves on top', () => { + spyOn(ScrollService, '_scroll'); + + ScrollService.enable(); + ScrollService.startScrolling(null, 10); + + expect(ScrollService._scroll).not.toHaveBeenCalled(); + }); + + it('should scroll down when at the top and mouse leaves at the bottom', () => { + spyOn(ScrollService, '_scroll'); + + ScrollService.enable(); + ScrollService.startScrolling(null, 260); + + expect(ScrollService._scroll).toHaveBeenCalledWith(200); + }); + + it('should not scroll down when at the bottom and mouse leaves at the bottom', () => { + spyOn(ScrollService, '_scroll'); + + ScrollService.enable(); + baseJQueryElement.scrollTop(200); + ScrollService.startScrolling(null, 260); + + expect(ScrollService._scroll).not.toHaveBeenCalled(); + }); + + it('should not scroll down when at the bottom and mouse leaves at the bottom', () => { + spyOn(ScrollService, '_scroll'); + + ScrollService.enable(); + baseJQueryElement.scrollTop(200); + ScrollService.startScrolling(null, 10); + + expect(ScrollService._scroll).toHaveBeenCalledWith(-200); + }); + }); +}); ===================================== frontend-ng/src/angularjs/utils/throttle.service.js ===================================== --- a/frontend-ng/src/angularjs/utils/throttle.service.js +++ b/frontend-ng/src/angularjs/utils/throttle.service.js @@ -23,10 +23,10 @@ export class ThrottleService { let wait = false; // Initially, we're not waiting return () => { // We return a throttled function if (!wait) { // If we're not waiting - callback.call(); // Execute users function wait = true; // Prevent future invocations setTimeout(() => { // After a period of time wait = false; // And allow future invocations + callback.call(); // Execute users function }, limit); } }; ===================================== frontend-ng/src/index.html ===================================== --- a/frontend-ng/src/index.html +++ b/frontend-ng/src/index.html @@ -46,6 +46,8 @@ <script src="bower_components/angular-translate/angular-translate.js"></script> <script src="bower_components/angular-translate-loader-static-files/angular-translate-loader-static-files.js"></script> <script src="bower_components/angular-ui-router/release/angular-ui-router.js"></script> + <script src="bower_components/re-tree/re-tree.js"></script> + <script src="bower_components/ng-device-detector/ng-device-detector.js"></script> <script src="node_modules/systemjs/dist/system-polyfills.js"></script> <script src="scripts/index.js"></script> View it on GitLab: https://code.onehippo.org/cms-community/hippo-addon-channel-manager/compare/1c6bcb149f38e639a17a5712837b711362b324e7...e0c57e7aa2d570f29610fb6ef2289acb4c948101
_______________________________________________ Hippocms-svn mailing list Hippocms-svn@lists.onehippo.org https://lists.onehippo.org/mailman/listinfo/hippocms-svn