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

Reply via email to