Merge pull request #38 from glyptodon/fix-ipad-scroll

GUAC-955: Fix handling of scroll on iDevices
This commit is contained in:
Mike Jumper
2014-12-28 14:52:26 -08:00
17 changed files with 417 additions and 46 deletions

View File

@@ -23,4 +23,4 @@
/** /**
* The module for code used to connect to a connection or balancing group. * The module for code used to connect to a connection or balancing group.
*/ */
angular.module('client', ['auth', 'history', 'osk', 'rest', 'textInput', 'touch']); angular.module('client', ['auth', 'element', 'history', 'osk', 'rest', 'textInput', 'touch']);

View File

@@ -26,6 +26,14 @@
angular.module('home').controller('clientController', ['$scope', '$routeParams', '$injector', angular.module('home').controller('clientController', ['$scope', '$routeParams', '$injector',
function clientController($scope, $routeParams, $injector) { function clientController($scope, $routeParams, $injector) {
// Required types
var ClientProperties = $injector.get('ClientProperties');
var ScrollState = $injector.get('ScrollState');
// Required services
var connectionGroupService = $injector.get('connectionGroupService');
var connectionService = $injector.get('connectionService');
/** /**
* The minimum number of pixels a drag gesture must move to result in the * The minimum number of pixels a drag gesture must move to result in the
* menu being shown or hidden. * menu being shown or hidden.
@@ -156,11 +164,6 @@ angular.module('home').controller('clientController', ['$scope', '$routeParams',
remaining: 15 remaining: 15
}; };
// Get services for reading connections and groups
var connectionGroupService = $injector.get('connectionGroupService');
var connectionService = $injector.get('connectionService');
var ClientProperties = $injector.get('ClientProperties');
// Client settings and state // Client settings and state
$scope.clientProperties = new ClientProperties(); $scope.clientProperties = new ClientProperties();
@@ -178,6 +181,13 @@ angular.module('home').controller('clientController', ['$scope', '$routeParams',
$scope.menuShown = false; $scope.menuShown = false;
}; };
/**
* The current scroll state of the menu.
*
* @type ScrollState
*/
$scope.menuScrollState = new ScrollState();
// Update the model when clipboard data received from client // Update the model when clipboard data received from client
$scope.$on('guacClientClipboard', function clientClipboardListener(event, client, mimetype, clipboardData) { $scope.$on('guacClientClipboard', function clientClipboardListener(event, client, mimetype, clipboardData) {
$scope.clipboardData = clipboardData; $scope.clipboardData = clipboardData;
@@ -228,10 +238,19 @@ angular.module('home').controller('clientController', ['$scope', '$routeParams',
// Hide menu when the user swipes from the right // Hide menu when the user swipes from the right
$scope.menuDrag = function menuDrag(inProgress, startX, startY, currentX, currentY, deltaX, deltaY) { $scope.menuDrag = function menuDrag(inProgress, startX, startY, currentX, currentY, deltaX, deltaY) {
// Hide menu if swipe gesture is detected
if (Math.abs(currentY - startY) < MENU_DRAG_VERTICAL_TOLERANCE if (Math.abs(currentY - startY) < MENU_DRAG_VERTICAL_TOLERANCE
&& startX - currentX >= MENU_DRAG_DELTA) && startX - currentX >= MENU_DRAG_DELTA)
$scope.menuShown = false; $scope.menuShown = false;
// Scroll menu by default
else {
$scope.menuScrollState.left -= deltaX;
$scope.menuScrollState.top -= deltaY;
}
return false;
}; };
// Update menu or client based on dragging gestures // Update menu or client based on dragging gestures

View File

@@ -0,0 +1,87 @@
/*
* Copyright (C) 2014 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
/**
* A directive which provides a fullscreen environment for its content.
*/
angular.module('client').directive('guacViewport', [function guacViewport() {
return {
// Element only
restrict: 'E',
scope: false,
transclude: true,
templateUrl: 'app/client/templates/guacViewport.html',
controller: ['$window', '$document', '$element',
function guacViewportController($window, $document, $element) {
/**
* The fullscreen container element.
*
* @type Element
*/
var element = $element.find('.viewport')[0];
/**
* The main document object.
*
* @type Document
*/
var document = $document[0];
/**
* The current adjusted height of the viewport element, if any.
*
* @type Number
*/
var currentAdjustedHeight = null;
// Fit container within visible region when window scrolls
$window.onscroll = function fitScrollArea() {
// Pull scroll properties
var scrollLeft = document.body.scrollLeft;
var scrollTop = document.body.scrollTop;
var scrollWidth = document.body.scrollWidth;
var scrollHeight = document.body.scrollHeight;
// Calculate new height
var adjustedHeight = $window.innerHeight - scrollTop;
// Only update if not in response to our own call to scrollTo()
if (scrollLeft !== scrollWidth && scrollTop !== scrollHeight
&& currentAdjustedHeight !== adjustedHeight) {
// Adjust element to fit exactly within visible area
element.style.height = adjustedHeight + 'px';
currentAdjustedHeight = adjustedHeight;
// Scroll to bottom
$window.scrollTo(scrollWidth, scrollHeight);
}
};
}]
};
}]);

View File

@@ -39,7 +39,7 @@ body.client {
.client-view { .client-view {
display: table; display: table;
position: fixed; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
width: 100%; width: 100%;

View File

@@ -23,9 +23,6 @@
.keyboard-container { .keyboard-container {
text-align: center; text-align: center;
position: fixed;
left: 0;
bottom: 0;
width: 100%; width: 100%;
margin: 0; margin: 0;
padding: 0; padding: 0;
@@ -34,10 +31,5 @@
background: #222; background: #222;
opacity: 0.85; opacity: 0.85;
visibility: hidden;
z-index: 1; z-index: 1;
} }
.keyboard-container.shown {
visibility: visible;
}

View File

@@ -0,0 +1,30 @@
/*
* Copyright (C) 2013 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
.viewport {
position: fixed;
bottom: 0;
left: 0;
width: 100%;
height: 100%;
overflow: hidden;
}

View File

@@ -21,38 +21,42 @@
--> -->
<!-- Client view --> <!-- Client view -->
<div class="client-view"> <guac-viewport>
<div class="client-view">
<!-- Central portion of view --> <!-- Central portion of view -->
<div class="client-body" guac-touch-drag="clientDrag" guac-touch-pinch="clientPinch"> <div class="client-body" guac-touch-drag="clientDrag" guac-touch-pinch="clientPinch">
<!-- Client --> <!-- Client -->
<guac-client <guac-client
client-properties="clientProperties" client-properties="clientProperties"
id="id" id="id"
connection-parameters="connectionParameters"/></guac-client> connection-parameters="connectionParameters"/></guac-client>
</div> </div>
<!-- Bottom portion of view --> <!-- Bottom portion of view -->
<div class="client-bottom"> <div class="client-bottom">
<!-- Text input -->
<div class="text-input-container" ng-show="showTextInput">
<guac-text-input needs-focus="showTextInput"></guac-text-input>
</div>
<!-- On-screen keyboard -->
<div class="keyboard-container" ng-show="showOSK">
<guac-osk layout="'CLIENT.URL_OSK_LAYOUT' | translate"/>
</div>
<!-- Text input -->
<div class="text-input-container" ng-show="showTextInput">
<guac-text-input needs-focus="showTextInput"></guac-text-input>
</div> </div>
</div> </div>
</div>
<!-- On-screen keyboard --> </guac-viewport>
<div class="keyboard-container" ng-class="{shown: showOSK}">
<guac-osk layout="'CLIENT.URL_OSK_LAYOUT' | translate"/>
</div>
<!-- Menu --> <!-- Menu -->
<div ng-class="{open: menuShown}" id="menu" guac-touch-drag="menuDrag"> <div ng-class="{open: menuShown}" id="menu" guac-touch-drag="menuDrag" guac-scroll="menuScrollState">
<h2>{{'CLIENT.SECTION_HEADER_CLIPBOARD' | translate}}</h2> <h2>{{'CLIENT.SECTION_HEADER_CLIPBOARD' | translate}}</h2>
<div class="content" id="clipboard-settings"> <div class="content" id="clipboard-settings">
<p class="description">{{'CLIENT.HELP_CLIPBOARD' | translate}}</p> <p class="description">{{'CLIENT.HELP_CLIPBOARD' | translate}}</p>

View File

@@ -0,0 +1,23 @@
<div class="viewport" ng-transclude>
<!--
Copyright (C) 2014 Glyptodon LLC
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
-->
</div>

View File

@@ -23,7 +23,7 @@
/** /**
* A directive which allows elements to be manually focused / blurred. * A directive which allows elements to be manually focused / blurred.
*/ */
angular.module('login').directive('guacFocus', ['$timeout', '$parse', function guacFocus($timeout, $parse) { angular.module('element').directive('guacFocus', ['$timeout', '$parse', function guacFocus($timeout, $parse) {
return { return {
restrict: 'A', restrict: 'A',

View File

@@ -0,0 +1,85 @@
/*
* Copyright (C) 2014 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
/**
* A directive which allows elements to be manually scrolled, and for their
* scroll state to be observed.
*/
angular.module('element').directive('guacScroll', [function guacScroll() {
return {
restrict: 'A',
link: function linkGuacScroll($scope, $element, $attrs) {
/**
* The current scroll state of the element.
*
* @type ScrollState
*/
var guacScroll = $scope.$eval($attrs.guacScroll);
/**
* The element which is being scrolled, or monitored for changes
* in scroll.
*
* @type Element
*/
var element = $element[0];
/**
* Returns the current left edge of the scrolling rectangle.
*
* @returns {Number}
* The current left edge of the scrolling rectangle.
*/
var getScrollLeft = function getScrollLeft() {
return guacScroll.left;
};
/**
* Returns the current top edge of the scrolling rectangle.
*
* @returns {Number}
* The current top edge of the scrolling rectangle.
*/
var getScrollTop = function getScrollTop() {
return guacScroll.top;
};
// Update underlying scrollLeft property when left changes
$scope.$watch(getScrollLeft, function scrollLeftChanged(left) {
element.scrollLeft = left;
guacScroll.left = element.scrollLeft;
});
// Update underlying scrollTop property when top changes
$scope.$watch(getScrollTop, function scrollTopChanged(top) {
element.scrollTop = top;
guacScroll.top = element.scrollTop;
});
} // end guacScroll link function
};
}]);

View File

@@ -0,0 +1,27 @@
/*
* Copyright (C) 2014 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
/**
* Module for manipulating element state, such as focus or scroll position, as
* well as handling browser events.
*/
angular.module('element', []);

View File

@@ -0,0 +1,63 @@
/*
* Copyright (C) 2014 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
/**
* Provides the ScrollState class definition.
*/
angular.module('element').factory('ScrollState', [function defineScrollState() {
/**
* Creates a new ScrollState, representing the current scroll position of
* an arbitrary element. This constructor initializes the properties of the
* new ScrollState with the corresponding properties of the given template.
*
* @constructor
* @param {ScrollState|Object} [template={}]
* The object whose properties should be copied within the new
* ScrollState.
*/
var ScrollState = function ScrollState(template) {
// Use empty object by default
template = template || {};
/**
* The left edge of the view rectangle within the scrollable area. This
* value naturally increases as the user scrolls right.
*
* @type Number
*/
this.left = template.left || 0;
/**
* The top edge of the view rectangle within the scrollable area. This
* value naturally increases as the user scrolls down.
*
* @type Number
*/
this.top = template.top || 0;
};
return ScrollState;
}]);

View File

@@ -23,4 +23,4 @@
/** /**
* The module for the login functionality. * The module for the login functionality.
*/ */
angular.module('login', []); angular.module('login', ['element']);

View File

@@ -57,6 +57,13 @@ angular.module('osk').directive('guacOsk', [function guacOsk() {
*/ */
var main = $element[0]; var main = $element[0];
/**
* The element which functions as a detector for size changes.
*
* @type Element
*/
var resizeSensor = $element.find('.resize-sensor')[0];
/** /**
* Event listener which resizes the current keyboard, if any, such * Event listener which resizes the current keyboard, if any, such
* that it fits within available space. * that it fits within available space.
@@ -73,8 +80,10 @@ angular.module('osk').directive('guacOsk', [function guacOsk() {
$scope.$watch("layout", function setLayout(layout) { $scope.$watch("layout", function setLayout(layout) {
// Remove current keyboard // Remove current keyboard
keyboard = null; if (keyboard) {
main.innerHTML = ""; main.removeChild(keyboard.getElement());
keyboard = null;
}
// Load new keyboard // Load new keyboard
if (layout) { if (layout) {
@@ -96,18 +105,13 @@ angular.module('osk').directive('guacOsk', [function guacOsk() {
$rootScope.$broadcast('guacSyntheticKeyup', keysym); $rootScope.$broadcast('guacSyntheticKeyup', keysym);
}; };
// Resize keyboard whenever window changes size // Resize keyboard whenever element changes size
$window.addEventListener('resize', resizeListener); resizeSensor.contentWindow.addEventListener('resize', resizeListener);
} }
}); // end layout scope watch }); // end layout scope watch
// Clean up event listeners upon destroy
$scope.$on('$destroy', function destroyKeyboard() {
$window.removeEventListener('resize', resizeListener);
});
}] }]
}; };

View File

@@ -20,6 +20,18 @@
* THE SOFTWARE. * THE SOFTWARE.
*/ */
.osk .resize-sensor {
height: 100%;
width: 100%;
position: absolute;
left: 0;
top: 0;
overflow: hidden;
border: none;
opacity: 0;
z-index: -1;
}
.guac-keyboard { .guac-keyboard {
display: inline-block; display: inline-block;
width: 100%; width: 100%;

View File

@@ -0,0 +1,22 @@
<!--
Copyright (C) 2014 Glyptodon LLC
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
-->
<html><body></body></html>

View File

@@ -21,4 +21,7 @@
THE SOFTWARE. THE SOFTWARE.
--> -->
<!-- Resize sensor -->
<iframe class="resize-sensor" src="app/osk/templates/blank.html"></iframe>
</div> </div>