From ba7707a1847308615ed167273971997d30202979 Mon Sep 17 00:00:00 2001 From: Alain Dumesny Date: Sun, 24 Jan 2021 22:10:43 -0800 Subject: [PATCH 1/2] initial jquery.ui.touch-punch file copied form original https://github.com/furf/jquery-ui-touch-punch got repo (not maintained since 2014) for #1413 --- src/jq/jquery.ui.touch-punch.js | 180 ++++++++++++++++++++++++++++++++ 1 file changed, 180 insertions(+) create mode 100644 src/jq/jquery.ui.touch-punch.js diff --git a/src/jq/jquery.ui.touch-punch.js b/src/jq/jquery.ui.touch-punch.js new file mode 100644 index 000000000..4fdcabc61 --- /dev/null +++ b/src/jq/jquery.ui.touch-punch.js @@ -0,0 +1,180 @@ +/*! + * jQuery UI Touch Punch 0.2.3 + * + * Copyright 2011–2014, Dave Furfero + * Dual licensed under the MIT or GPL Version 2 licenses. + * + * Depends: + * jquery.ui.widget.js + * jquery.ui.mouse.js + */ +(function ($) { + + // Detect touch support + $.support.touch = 'ontouchend' in document; + + // Ignore browsers without touch support + if (!$.support.touch) { + return; + } + + var mouseProto = $.ui.mouse.prototype, + _mouseInit = mouseProto._mouseInit, + _mouseDestroy = mouseProto._mouseDestroy, + touchHandled; + + /** + * Simulate a mouse event based on a corresponding touch event + * @param {Object} event A touch event + * @param {String} simulatedType The corresponding mouse event + */ + function simulateMouseEvent (event, simulatedType) { + + // Ignore multi-touch events + if (event.originalEvent.touches.length > 1) { + return; + } + + event.preventDefault(); + + var touch = event.originalEvent.changedTouches[0], + simulatedEvent = document.createEvent('MouseEvents'); + + // Initialize the simulated mouse event using the touch event's coordinates + simulatedEvent.initMouseEvent( + simulatedType, // type + true, // bubbles + true, // cancelable + window, // view + 1, // detail + touch.screenX, // screenX + touch.screenY, // screenY + touch.clientX, // clientX + touch.clientY, // clientY + false, // ctrlKey + false, // altKey + false, // shiftKey + false, // metaKey + 0, // button + null // relatedTarget + ); + + // Dispatch the simulated event to the target element + event.target.dispatchEvent(simulatedEvent); + } + + /** + * Handle the jQuery UI widget's touchstart events + * @param {Object} event The widget element's touchstart event + */ + mouseProto._touchStart = function (event) { + + var self = this; + + // Ignore the event if another widget is already being handled + if (touchHandled || !self._mouseCapture(event.originalEvent.changedTouches[0])) { + return; + } + + // Set the flag to prevent other widgets from inheriting the touch event + touchHandled = true; + + // Track movement to determine if interaction was a click + self._touchMoved = false; + + // Simulate the mouseover event + simulateMouseEvent(event, 'mouseover'); + + // Simulate the mousemove event + simulateMouseEvent(event, 'mousemove'); + + // Simulate the mousedown event + simulateMouseEvent(event, 'mousedown'); + }; + + /** + * Handle the jQuery UI widget's touchmove events + * @param {Object} event The document's touchmove event + */ + mouseProto._touchMove = function (event) { + + // Ignore event if not handled + if (!touchHandled) { + return; + } + + // Interaction was not a click + this._touchMoved = true; + + // Simulate the mousemove event + simulateMouseEvent(event, 'mousemove'); + }; + + /** + * Handle the jQuery UI widget's touchend events + * @param {Object} event The document's touchend event + */ + mouseProto._touchEnd = function (event) { + + // Ignore event if not handled + if (!touchHandled) { + return; + } + + // Simulate the mouseup event + simulateMouseEvent(event, 'mouseup'); + + // Simulate the mouseout event + simulateMouseEvent(event, 'mouseout'); + + // If the touch interaction did not move, it should trigger a click + if (!this._touchMoved) { + + // Simulate the click event + simulateMouseEvent(event, 'click'); + } + + // Unset the flag to allow other widgets to inherit the touch event + touchHandled = false; + }; + + /** + * A duck punch of the $.ui.mouse _mouseInit method to support touch events. + * This method extends the widget with bound touch event handlers that + * translate touch events to mouse events and pass them to the widget's + * original mouse event handling methods. + */ + mouseProto._mouseInit = function () { + + var self = this; + + // Delegate the touch handlers to the widget's element + self.element.bind({ + touchstart: $.proxy(self, '_touchStart'), + touchmove: $.proxy(self, '_touchMove'), + touchend: $.proxy(self, '_touchEnd') + }); + + // Call the original $.ui.mouse init method + _mouseInit.call(self); + }; + + /** + * Remove the touch event handlers + */ + mouseProto._mouseDestroy = function () { + + var self = this; + + // Delegate the touch handlers to the widget's element + self.element.unbind({ + touchstart: $.proxy(self, '_touchStart'), + touchmove: $.proxy(self, '_touchMove'), + touchend: $.proxy(self, '_touchEnd') + }); + + // Call the original $.ui.mouse destroy method + _mouseDestroy.call(self); + }; + +})(jQuery); \ No newline at end of file From 0d13d9410c2ad4228aa22a32d69e4cbd50e1bd3d Mon Sep 17 00:00:00 2001 From: Alain Dumesny Date: Mon, 25 Jan 2021 09:39:32 -0800 Subject: [PATCH 2/2] support mobile in jq version out of the box * fix #1413 #444 * we are now using the latest v1.0.8 touch-punch branch from https://github.com/RWAP/jquery-ui-touch-punch and compiling in JQ version (only 188k -> 190k) so mobiles work out of the box * HTML5 v3+ does not currently support `touchmove` events. This will be added in a future release. * added a mobile.html demo showing usage, updated doc/readme --- Gruntfile.js | 1 + README.md | 10 ++- demo/advance.html | 6 -- demo/index.html | 1 + demo/mobile.html | 26 +++++++ doc/CHANGES.md | 4 +- doc/README.md | 2 +- src/jq/gridstack-dd-jqueryui.ts | 1 + src/jq/jquery.ui.touch-punch.js | 134 +++++++++++++++++++++++--------- src/types.ts | 6 +- webpack.config.js | 1 + 11 files changed, 145 insertions(+), 47 deletions(-) create mode 100644 demo/mobile.html diff --git a/Gruntfile.js b/Gruntfile.js index 052de2053..44eb73cfb 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -56,6 +56,7 @@ module.exports = function(grunt) { files: { 'dist/jq/jquery.js': 'src/jq/jquery.js', 'dist/jq/jquery-ui.js': 'src/jq/jquery-ui.js', + 'dist/jq/jquery.ui.touch-punch.js': 'src/jq/jquery.ui.touch-punch.js', } } }, diff --git a/README.md b/README.md index e6835d2e9..779685512 100644 --- a/README.md +++ b/README.md @@ -260,8 +260,12 @@ Note: It's not recommended to enable `nw`, `n`, `ne` resizing handles. Their beh ## Touch devices support -Please use [jQuery UI Touch Punch](https://github.com/furf/jquery-ui-touch-punch) to make jQuery UI Draggable/Resizable -working on touch-based devices. +NOTE: gridstack v3.2+ jq version now compile this in, so it works out of the box, so need for anything. + +NOTE2: HTML5 v3+ does not currently support `touchmove` events. This will be added in a future release. + +Use latest RWAP branch of [jQuery UI Touch Punch](https://github.com/RWAP/jquery-ui-touch-punch) to make jQuery UI Draggable/Resizable +working on touch-based devices (which we now also include in v3.2 as `dist/jq/jquery.ui.touch-punch.js`). ```html @@ -277,7 +281,7 @@ let options = { GridStack.init(options); ``` -If you're still experiencing issues on touch devices please check [#444](https://github.com/gridstack/gridstack.js/issues/444) +See [example](http://gridstack.github.io/gridstack.js/demo/mobile.html). If you're still experiencing issues on touch devices please check [#444](https://github.com/gridstack/gridstack.js/issues/444) # gridstack.js for specific frameworks diff --git a/demo/advance.html b/demo/advance.html index 889ae2183..b499d4074 100644 --- a/demo/advance.html +++ b/demo/advance.html @@ -57,12 +57,6 @@

Advanced Demo

+ + + +

Simple mobile demo

+

uses v3.2+ jquery version which now includes jquery.ui.touch-punch by default (small 2k)

+
+ + + diff --git a/doc/CHANGES.md b/doc/CHANGES.md index ed51631ef..ed1c9c8b9 100644 --- a/doc/CHANGES.md +++ b/doc/CHANGES.md @@ -48,7 +48,9 @@ Change log ## 3.1.5-dev -- TBD +- fix [1413](https://github.com/gridstack/gridstack.js/issues/1413) website & lib works on mobile. We now compile the latest v1.0.8 `jquery.ui.touch-punch` +into the JQ version (only 2k) so mobile devices (android, iphone, ipad, touchpad) are supported out of the box. +HTML5 version will require re-write to plain `mousemove` & mobile `touchmove` instead of drag events in a future release. ## 3.1.5 (2021-1-23) diff --git a/doc/README.md b/doc/README.md index 330ce8c37..d25643ea4 100644 --- a/doc/README.md +++ b/doc/README.md @@ -67,7 +67,7 @@ gridstack.js API * `true` the resizing handles are always shown even if the user is not hovering over the widget * advance condition such as this mobile browser agent check: `alwaysShowResizeHandle: /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test( navigator.userAgent )` - See [example](http://gridstack.github.io/gridstack.js/demo/advance.html) + See [example](http://gridstack.github.io/gridstack.js/demo/mobile.html) - `animate` - turns animation on to smooth transitions (default: `true`) - `auto` - if `false` gridstack will not initialize existing items (default: `true`) - `cellHeight` - one cell height (default: `auto`). Can be: diff --git a/src/jq/gridstack-dd-jqueryui.ts b/src/jq/gridstack-dd-jqueryui.ts index 37061f165..7b9dd3831 100644 --- a/src/jq/gridstack-dd-jqueryui.ts +++ b/src/jq/gridstack-dd-jqueryui.ts @@ -17,6 +17,7 @@ import { GridItemHTMLElement, DDDragInOpt } from '../types'; import * as $ from './jquery'; // compile this in... having issues TS/ES6 app would include instead export { $ }; import './jquery-ui'; +import './jquery.ui.touch-punch'; // include for touch mobile devices // export our base class (what user should use) and all associated types export * from '../gridstack-dd'; diff --git a/src/jq/jquery.ui.touch-punch.js b/src/jq/jquery.ui.touch-punch.js index 4fdcabc61..f98eef0f2 100644 --- a/src/jq/jquery.ui.touch-punch.js +++ b/src/jq/jquery.ui.touch-punch.js @@ -1,21 +1,45 @@ /*! - * jQuery UI Touch Punch 0.2.3 + * jQuery UI Touch Punch 1.0.8 as modified by RWAP Software + * based on original touchpunch v0.2.3 which has not been updated since 2014 * + * Updates by RWAP Software to take account of various suggested changes on the original code issues + * + * Original: https://github.com/furf/jquery-ui-touch-punch * Copyright 2011–2014, Dave Furfero * Dual licensed under the MIT or GPL Version 2 licenses. * + * Fork: https://github.com/RWAP/jquery-ui-touch-punch + * * Depends: - * jquery.ui.widget.js - * jquery.ui.mouse.js + * jquery.ui.widget.js + * jquery.ui.mouse.js */ -(function ($) { - // Detect touch support - $.support.touch = 'ontouchend' in document; +(function( factory ) { + if ( typeof define === "function" && define.amd ) { - // Ignore browsers without touch support - if (!$.support.touch) { - return; + // AMD. Register as an anonymous module. + define([ "jquery", "jquery.ui" ], factory ); + } else { + + // Browser globals + factory( jQuery ); + } +}(function ($) { + + // Detect touch support - Windows Surface devices and other touch devices + $.support.mspointer = window.navigator.msPointerEnabled; + $.support.touch = ( 'ontouchstart' in document + || 'ontouchstart' in window + || window.TouchEvent + || (window.DocumentTouch && document instanceof DocumentTouch) + || navigator.maxTouchPoints > 0 + || navigator.msMaxTouchPoints > 0 + ); + + // Ignore browsers without touch or mouse support + if ((!$.support.touch && !$.support.mspointer) || !$.ui.mouse) { + return; } var mouseProto = $.ui.mouse.prototype, @@ -23,6 +47,17 @@ _mouseDestroy = mouseProto._mouseDestroy, touchHandled; + /** + * Get the x,y position of a touch event + * @param {Object} event A touch event + */ + function getTouchCoords (event) { + return { + x: event.originalEvent.changedTouches[0].pageX, + y: event.originalEvent.changedTouches[0].pageY + }; + } + /** * Simulate a mouse event based on a corresponding touch event * @param {Object} event A touch event @@ -35,28 +70,31 @@ return; } - event.preventDefault(); + // Prevent "Ignored attempt to cancel a touchmove event with cancelable=false" errors + if (event.cancelable) { + event.preventDefault(); + } var touch = event.originalEvent.changedTouches[0], simulatedEvent = document.createEvent('MouseEvents'); - + // Initialize the simulated mouse event using the touch event's coordinates simulatedEvent.initMouseEvent( simulatedType, // type - true, // bubbles - true, // cancelable - window, // view - 1, // detail - touch.screenX, // screenX - touch.screenY, // screenY - touch.clientX, // clientX - touch.clientY, // clientY - false, // ctrlKey - false, // altKey - false, // shiftKey - false, // metaKey - 0, // button - null // relatedTarget + true, // bubbles + true, // cancelable + window, // view + 1, // detail + touch.screenX, // screenX + touch.screenY, // screenY + touch.clientX, // clientX + touch.clientY, // clientY + false, // ctrlKey + false, // altKey + false, // shiftKey + false, // metaKey + 0, // button + null // relatedTarget ); // Dispatch the simulated event to the target element @@ -71,6 +109,12 @@ var self = this; + // Interaction time + this._startedMove = event.timeStamp; + + // Track movement to determine if interaction was a click + self._startPos = getTouchCoords(event); + // Ignore the event if another widget is already being handled if (touchHandled || !self._mouseCapture(event.originalEvent.changedTouches[0])) { return; @@ -103,7 +147,7 @@ return; } - // Interaction was not a click + // Interaction was moved this._touchMoved = true; // Simulate the mousemove event @@ -128,12 +172,27 @@ simulateMouseEvent(event, 'mouseout'); // If the touch interaction did not move, it should trigger a click - if (!this._touchMoved) { - - // Simulate the click event - simulateMouseEvent(event, 'click'); + // Check for this in two ways - length of time of simulation and distance moved + // Allow for Apple Stylus to be used also + var timeMoving = event.timeStamp - this._startedMove; + if (!this._touchMoved || timeMoving < 500) { + // Simulate the click event + simulateMouseEvent(event, 'click'); + } else { + var endPos = getTouchCoords(event); + if ((Math.abs(endPos.x - this._startPos.x) < 10) && (Math.abs(endPos.y - this._startPos.y) < 10)) { + + // If the touch interaction did not move, it should trigger a click + if (!this._touchMoved || event.originalEvent.changedTouches[0].touchType === 'stylus') { + // Simulate the click event + simulateMouseEvent(event, 'click'); + } + } } + // Unset the flag to determine the touch movement stopped + this._touchMoved = false; + // Unset the flag to allow other widgets to inherit the touch event touchHandled = false; }; @@ -145,11 +204,16 @@ * original mouse event handling methods. */ mouseProto._mouseInit = function () { - + var self = this; + + // Microsoft Surface Support = remove original touch Action + if ($.support.mspointer) { + self.element[0].style.msTouchAction = 'none'; + } // Delegate the touch handlers to the widget's element - self.element.bind({ + self.element.on({ touchstart: $.proxy(self, '_touchStart'), touchmove: $.proxy(self, '_touchMove'), touchend: $.proxy(self, '_touchEnd') @@ -163,11 +227,11 @@ * Remove the touch event handlers */ mouseProto._mouseDestroy = function () { - + var self = this; // Delegate the touch handlers to the widget's element - self.element.unbind({ + self.element.off({ touchstart: $.proxy(self, '_touchStart'), touchmove: $.proxy(self, '_touchMove'), touchend: $.proxy(self, '_touchEnd') @@ -177,4 +241,4 @@ _mouseDestroy.call(self); }; -})(jQuery); \ No newline at end of file +})); \ No newline at end of file diff --git a/src/types.ts b/src/types.ts index 61c7d46ff..7eae5abb9 100644 --- a/src/types.ts +++ b/src/types.ts @@ -38,7 +38,11 @@ export interface GridStackOptions { */ acceptWidgets?: boolean | string | ((element: Element) => boolean); - /** if true the resizing handles are shown even if the user is not hovering over the widget (default?: false) */ + /** possible values (default: `false` only show on hover) + * `true` the resizing handles are always shown even if the user is not hovering over the widget + * advance condition such as this mobile browser agent check: + `alwaysShowResizeHandle: /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test( navigator.userAgent )` + See [example](http://gridstack.github.io/gridstack.js/demo/mobile.html) */ alwaysShowResizeHandle?: boolean; /** turns animation on (default?: true) */ diff --git a/webpack.config.js b/webpack.config.js index 3ee891df0..a2150715c 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -23,6 +23,7 @@ module.exports = { alias: { 'jquery': '/src/jq/jquery.js', 'jquery-ui': '/src/jq/jquery-ui.js', + 'jquery.ui': '/src/jq/jquery-ui.js', } }, output: {