Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Native HTML5 Drag and Drop in Mobile Safari (iPad, iPod, iPhone)?

I've successfully implemented native HTML5 Drag and Drop for moving HTML elements inside a page (just, say, a div from one place to another, nothing related to interacting with the host OS' files whatsoever). This works fine in Chrome and Safari on a PC but I can't start a drag operation in my iPad's Safari.

I've found this so far:

Using Drag and Drop From JavaScript

Safari, Dashboard, and WebKit-based applications include support for customizing the behavior of drag and drop operations within your HTML pages.

Note: This technology is supported only on desktop versions of Safari. For iPhone OS, use DOM Touch, described in Handling Events (part of Safari Web Content Guide) and Safari DOM Additions Reference.

Here. But it's outdated (2009-06-08).

Doe's anyone know if it is possible to use native HTML5 in Mobile Safari? (I don't want javascript-framework like solutions like jQuery UI).

Thanks!

like image 336
Gabriel Avatar asked Jul 06 '11 18:07

Gabriel


People also ask

What is HTML5 drag and drop?

HTML Drag and Drop interfaces enable applications to use drag-and-drop features in browsers. The user may select draggable elements with a mouse, drag those elements to a droppable element, and drop them by releasing the mouse button.

Does HTML5 support drag and drop?

Complete HTML/CSS Course 2022 Now HTML 5 came up with a Drag and Drop (DnD) API that brings native DnD support to the browser making it much easier to code up. HTML 5 DnD is supported by all the major browsers like Chrome, Firefox 3.5 and Safari 4 etc.


3 Answers

I found this incredibly useful javascript code that adds the iOS event shims for you:

https://github.com/timruffles/ios-html5-drag-drop-shim

Complete code just in case the code is deleted sometime in the future (it happens too often):

(function(doc) {

  log = noop; // noOp, remove this line to enable debugging

  var coordinateSystemForElementFromPoint;

  function main(config) {
    config = config || {};

    coordinateSystemForElementFromPoint = navigator.userAgent.match(/OS 5(?:_\d+)+ like Mac/) ? "client" : "page";

    var div = doc.createElement('div');
    var dragDiv = 'draggable' in div;
    var evts = 'ondragstart' in div && 'ondrop' in div;

    var needsPatch = !(dragDiv || evts) || /iPad|iPhone|iPod/.test(navigator.userAgent);
    log((needsPatch ? "" : "not ") + "patching html5 drag drop");

    if(!needsPatch) return;

    if(!config.enableEnterLeave) {
      DragDrop.prototype.synthesizeEnterLeave = noop;
    }

    doc.addEventListener("touchstart", touchstart);
  }

  function DragDrop(event, el) {

    this.touchPositions = {};
    this.dragData = {};
    this.el = el || event.target

    event.preventDefault();

    log("dragstart");

    this.dispatchDragStart()
    this.elTranslation = readTransform(this.el);

    this.listen()

  }

  DragDrop.prototype = {
    listen: function() {
      var move = onEvt(doc, "touchmove", this.move, this);
      var end = onEvt(doc, "touchend", ontouchend, this);
      var cancel = onEvt(doc, "touchcancel", cleanup, this);

      function ontouchend(event) {
        this.dragend(event, event.target);
        cleanup();
      }
      function cleanup() {
        log("cleanup");
        this.touchPositions = {};
        this.el = this.dragData = null;
        return [move, end, cancel].forEach(function(handler) {
          return handler.off();
        });
      }
    },
    move: function(event) {
      var deltas = { x: [], y: [] };

      [].forEach.call(event.changedTouches,function(touch, index) {
        var lastPosition = this.touchPositions[index];
        if (lastPosition) {
          deltas.x.push(touch.pageX - lastPosition.x);
          deltas.y.push(touch.pageY - lastPosition.y);
        } else {
          this.touchPositions[index] = lastPosition = {};
        }
        lastPosition.x = touch.pageX;
        lastPosition.y = touch.pageY;
      }.bind(this))

      this.elTranslation.x += average(deltas.x);
      this.elTranslation.y += average(deltas.y);
      this.el.style["z-index"] = "999999";
      this.el.style["pointer-events"] = "none";
      writeTransform(this.el, this.elTranslation.x, this.elTranslation.y);

      this.synthesizeEnterLeave(event);
    },
    synthesizeEnterLeave: function(event) {
      var target = elementFromTouchEvent(this.el,event)
      if (target != this.lastEnter) {
        if (this.lastEnter) {
          this.dispatchLeave();
        }
        this.lastEnter = target;
        this.dispatchEnter();
      }
    },
    dragend: function(event) {

      // we'll dispatch drop if there's a target, then dragEnd. If drop isn't fired
      // or isn't cancelled, we'll snap back
      // drop comes first http://www.whatwg.org/specs/web-apps/current-work/multipage/dnd.html#drag-and-drop-processing-model
      log("dragend");

      if (this.lastEnter) {
        this.dispatchLeave();
      }

      var target = elementFromTouchEvent(this.el,event)

      if (target) {
        log("found drop target " + target.tagName);
        this.dispatchDrop(target)
      } else {
        log("no drop target, scheduling snapBack")
        once(doc, "dragend", this.snapBack, this);
      }

      var dragendEvt = doc.createEvent("Event");
      dragendEvt.initEvent("dragend", true, true);
      this.el.dispatchEvent(dragendEvt);
    },
    dispatchDrop: function(target) {
      var snapBack = true;

      var dropEvt = doc.createEvent("Event");
      dropEvt.initEvent("drop", true, true);
      dropEvt.dataTransfer = {
        getData: function(type) {
          return this.dragData[type];
        }.bind(this)
      };
      dropEvt.preventDefault = function() {
         // https://www.w3.org/Bugs/Public/show_bug.cgi?id=14638 - if we don't cancel it, we'll snap back
        this.el.style["z-index"] = "";
        this.el.style["pointer-events"] = "auto";
        snapBack = false;
        writeTransform(this.el, 0, 0);
      }.bind(this);

      once(doc, "drop", function() {
        log("drop event not canceled");
        if (snapBack) this.snapBack()
      },this);

      target.dispatchEvent(dropEvt);
    },
    dispatchEnter: function() {

      var enterEvt = doc.createEvent("Event");
      enterEvt.initEvent("dragenter", true, true);
      enterEvt.dataTransfer = {
        getData: function(type) {
          return this.dragData[type];
        }.bind(this)
      };

      this.lastEnter.dispatchEvent(enterEvt);
    },
    dispatchLeave: function() {

      var leaveEvt = doc.createEvent("Event");
      leaveEvt.initEvent("dragleave", true, true);
      leaveEvt.dataTransfer = {
        getData: function(type) {
          return this.dragData[type];
        }.bind(this)
      };

      this.lastEnter.dispatchEvent(leaveEvt);
      this.lastEnter = null;
    },
    snapBack: function() {
      once(this.el, "webkitTransitionEnd", function() {
        this.el.style["pointer-events"] = "auto";
        this.el.style["z-index"] = "";
        this.el.style["-webkit-transition"] = "none";
      },this);
      setTimeout(function() {
        this.el.style["-webkit-transition"] = "all 0.2s";
        writeTransform(this.el, 0, 0)
      }.bind(this));
    },
    dispatchDragStart: function() {
      var evt = doc.createEvent("Event");
      evt.initEvent("dragstart", true, true);
      evt.dataTransfer = {
        setData: function(type, val) {
          this.dragData[type] = val;
          return val;
        }.bind(this),
        dropEffect: "move"
      };
      this.el.dispatchEvent(evt);
    }
  }

  // event listeners
  function touchstart(evt) {
    var el = evt.target;
    do {
      if (el.hasAttribute("draggable")) {
        evt.preventDefault();
        new DragDrop(evt,el);
      }
    } while((el = el.parentNode) && el !== doc.body)
  }

  // DOM helpers
  function elementFromTouchEvent(el,event) {
    var touch = event.changedTouches[0];
    var target = doc.elementFromPoint(
      touch[coordinateSystemForElementFromPoint + "X"],
      touch[coordinateSystemForElementFromPoint + "Y"]
    );
    return target
  }

  function readTransform(el) {
    var transform = el.style["-webkit-transform"];
    var x = 0
    var y = 0
    var match = /translate\(\s*(\d+)[^,]*,\D*(\d+)/.exec(transform)
    if(match) {
      x = parseInt(match[1],10)
      y = parseInt(match[2],10)
    }
    return { x: x, y: y };
  }

  function writeTransform(el, x, y) {
    var transform = el.style["-webkit-transform"].replace(/translate\(\D*\d+[^,]*,\D*\d+[^,]*\)\s*/g, '');
    el.style["-webkit-transform"] = transform + " translate(" + x + "px," + y + "px)";
  }

  function onEvt(el, event, handler, context) {
    if(context) handler = handler.bind(context)
    el.addEventListener(event, handler);
    return {
      off: function() {
        return el.removeEventListener(event, handler);
      }
    };
  }

  function once(el, event, handler, context) {
    if(context) handler = handler.bind(context)
    function listener(evt) {
      handler(evt);
      return el.removeEventListener(event,listener);
    }
    return el.addEventListener(event,listener);
  }


  // general helpers
  function log(msg) {
    console.log(msg);
  }

  function average(arr) {
    if (arr.length === 0) return 0;
    return arr.reduce((function(s, v) {
      return v + s;
    }), 0) / arr.length;
  }

  function noop() {}

  main(window.iosDragDropShim);


})(document);
like image 118
Mike Bethany Avatar answered Oct 20 '22 01:10

Mike Bethany


Touch events do work now on Safari too (including Mobile). This article has nice code snippets that you can start from:

http://www.html5rocks.com/en/mobile/touch/

like image 40
alex Avatar answered Oct 20 '22 02:10

alex


I've just been trying to get native drag&drop working in ios safari (ipad pro with 14.0.1), and I'm updating this answer as it kept coming up as my first google result (and no other q&a seems to be up to date).

Following https://developer.apple.com/library/archive/documentation/AppleApplications/Conceptual/SafariJSProgTopics/DragAndDrop.html I have found native html5 drag&drop now works in safari, with a few specific notes;

  • You MUST set some data in OnDragStart's event; (the effect settings seem to be optional) Event.dataTransfer.setData('text/plain', 'hello');

  • you MUST Event.preventDefault(); in OnDrop otherwise safari will load the data you added (unless that's your intention)

  • OnDragOver event handler needs Event.preventDefault(); otherwise drop fails (unless intended, as docuemnted)

  • On ios safari, if you set OnDragStart's Event.dataTransfer.dropEffect='copy' and in OnDragOver's Event.dataTransfer.dropEffect='link' the drop fails (no OnDrop()); Seems to be the only combination that fails

I thought you required

  • CSS -webkit-user-drag: Element; and `-webkit-user-drop: element; or if you prefer
Element.style.setProperty('webkitUserDrag','element');
Element.style.setProperty('webkitUserDrop','element');

but it seems just the usual draggable attribute is enough Element.setAttribute('draggable',true);

This seems to be the bare minimum to drag & drop an element

        Element.setAttribute('draggable',true);
        
        function OnDragOver(Event)
        {
            Element.setAttribute('DragOver',true);
            Event.stopPropagation();    //  let child accept and don't pass up to parent element
            Event.preventDefault();     //  ios to accept drop
            Event.dataTransfer.dropEffect = 'copy';//   move has no icon? adding copy shows +
        }
        function OnDragLeave(Event)
        {
            Element.removeAttribute('DragOver');
        }
        function OnDrop(Event)
        {
            Element.removeAttribute('DragOver');
            Event.preventDefault();     //  dont let page attempt to load our data
            Event.stopPropagation();
        }
        function OnDragStart(Event)
        {
            Event.stopPropagation(); // let child take the drag
            Event.dataTransfer.dropEffect = 'move';
            Event.dataTransfer.setData('text/plain', 'hello');
        }
        Element.addEventListener('dragstart',OnDragStart);
        Element.addEventListener('drop',OnDrop);
        Element.addEventListener('dragover',OnDragOver);
        Element.addEventListener('dragleave',OnDragLeave);
like image 21
Soylent Graham Avatar answered Oct 20 '22 02:10

Soylent Graham