User-Interface-Pattern: Tap-and-Hold or Long Press

Share your advanced knowledge/code with the community.
Dirk Geppert
Posts: 336
Joined: Fri Sep 22, 2017 7:02 am

User-Interface-Pattern: Tap-and-Hold or Long Press

Post by Dirk Geppert »

An additional interaction option, e.g., to mark or favorite something.

Code: Select all

; ==============================
; Long-Press on Gadget Desktop/Mobile
; 09/2025
; ==============================

Enumeration #PB_Event_FirstCustomValue
  #PB_Event_MouseLongPress
EndEnumeration

Global G_Win_LongPress.i
Global G_Timer_LongPress.i = 1
Global G_LongPressActive.i = #False
Global G_MoveThreshold.i   = 10  ; px: Bewegung > Threshold => cancel

; ---- Timer feuert Long-Press
Procedure onMouseLongpress()
  If EventTimer() = G_Timer_LongPress And G_LongPressActive
    G_LongPressActive = #False
    RemoveWindowTimer(G_Win_LongPress, G_Timer_LongPress)
    PostEvent(#PB_Event_MouseLongPress, G_Win_LongPress, G_Timer_LongPress)
    Debug "onMouseLongpress() -> LONG PRESS"
  EndIf
EndProcedure

; ---- Abbruch bei Loslassen
Procedure onMouseUp()
  If G_LongPressActive
    G_LongPressActive = #False
    RemoveWindowTimer(G_Win_LongPress, G_Timer_LongPress)
    Debug "onMouseUp() -> Timer Removed"
  EndIf
EndProcedure

; ---- Start (Down)
Procedure onMouseDown()
  If G_LongPressActive = #False
    G_LongPressActive = #True
    AddWindowTimer(G_Win_LongPress, G_Timer_LongPress, 600) ; 0.6 s
    Debug "onMouseDown() -> Timer Added"
  EndIf
EndProcedure

; ---- Sonstiger Abbruch
Procedure onMouseCancel()
  If G_LongPressActive
    G_LongPressActive = #False
    RemoveWindowTimer(G_Win_LongPress, G_Timer_LongPress)
    Debug "onMouseCancel() -> Timer Cancelled"
  EndIf
EndProcedure

; ---- Initialisierung für EIN Gadget
; Aufruf: onMouseInit(GadgetID(#Gadget))  ODER onMouseInit(#Gadget)
Procedure onMouseInit(Gadget)
  EnableJS
    (function(){
      // ---------- 1) Gadget-Handle -> echtes DOM-Element ----------
      function resolveGadgetElement(g){
        // a) schon ein DOM-Element?
        if (g && g.nodeType === 1) return g;
        // b) SpiderBasic-Wrapper mit .div?
        if (g && g.div && g.div.nodeType === 1) return g.div;
        // c) Nummer/Handle -> spider_GadgetID(...)
        try {
          if (typeof g === 'number' && typeof spider_GadgetID === 'function') {
            var obj = spider_GadgetID(g);
            if (obj) {
              if (obj.div && obj.div.nodeType === 1) return obj.div;
              // Fallbacks für ältere/andere Builds:
              if (obj.nodeType === 1) return obj;
              if (obj.element && obj.element.nodeType === 1) return obj.element;
              if (obj.root && obj.root.nodeType === 1) return obj.root;
            }
          }
        } catch(e) {}
        return null;
      }

      var gadgetInput = v_gadget;                 // PB-Param (wird zu v_gadget)
      var target = resolveGadgetElement(gadgetInput);
      if (!target) {
        console.warn('[LongPress] Konnte DOM-Element nicht resolven aus', gadgetInput);
        return;
      }

      // Pro Gadget nur einmal binden
      if (target.__lp_bound) return;
      target.__lp_bound = true;
      target.classList.add('lp-target');

      // ---------- 2) iOS/Safari: CSS + touch-action ----------
      if (!document.__lp_css_injected) {
        document.__lp_css_injected = true;
        var css = "html,body{-webkit-touch-callout:none;-webkit-user-select:none;user-select:none}.lp-target{touch-action:none;-ms-touch-action:none}";
        var style = document.createElement('style');
        style.type = 'text/css';
        style.appendChild(document.createTextNode(css));
        document.head.appendChild(style);
      }

      // ---------- 3) State + Hilfsfunktionen ----------
      var startX=0, startY=0, moved=false, scrollAbortAttached=false;

      function getXY(e){
        if (e.touches && e.touches[0]) return {x:e.touches[0].clientX, y:e.touches[0].clientY};
        return {x:(e.clientX||0), y:(e.clientY||0)};
      }
      function insideTarget(evt){
        var t = evt.target;
        return (t === target) || target.contains(t);
      }
      function startPressFromEvent(e){
        var p = getXY(e); startX=p.x; startY=p.y; moved=false;
        f_onmousedown();
        if (!scrollAbortAttached) {
          scrollAbortAttached = true;
          window.addEventListener('scroll', onScrollCancel, {passive:true, once:true});
        }
      }
      function onMoveCheck(e){
        if (!g_g_longpressactive) return;
        var p = getXY(e);
        if (Math.abs(p.x - startX) > g_g_movethreshold || Math.abs(p.y - startY) > g_g_movethreshold) {
          moved = true;
          f_onmousecancel();
        }
      }
      function onScrollCancel(){ f_onmousecancel(); scrollAbortAttached=false; }

      // ---------- 4) Listener im CAPTURE-Mode an DOCUMENT ----------
      var doc = target.ownerDocument || document;
      var hasPointer = !!window.PointerEvent;

      if (hasPointer) {
        doc.addEventListener('pointerdown', function(e){
          if (!insideTarget(e)) return;
          var isMouseLeft = (e.pointerType === 'mouse' && e.button === 0);
          var isTouch     = (e.pointerType === 'touch');
          if (!(isMouseLeft || isTouch)) return;
          try { e.preventDefault(); } catch(_){}
          startPressFromEvent(e);
        }, {capture:true, passive:false});  // <-- capture:true ist der Schlüssel!

        doc.addEventListener('pointermove', function(e){
          if (!insideTarget(e)) return;
          onMoveCheck(e);
        }, {capture:true, passive:true});

        doc.addEventListener('pointerup', function(e){
          if (!insideTarget(e)) return;
          var isMouseLeft = (e.pointerType === 'mouse' && e.button === 0);
          var isTouch     = (e.pointerType === 'touch');
          if (isMouseLeft || isTouch) f_onmouseup();
        }, {capture:true, passive:true});

        doc.addEventListener('pointercancel', function(e){
          if (!insideTarget(e)) return;
          f_onmousecancel();
        }, {capture:true, passive:true});

        doc.addEventListener('pointerleave', function(e){
          if (!insideTarget(e)) return;
          f_onmousecancel();
        }, {capture:true, passive:true});

      } else {
        // ---- Touch-Fallback (ältere Browser)
        doc.addEventListener('touchstart', function(e){
          if (!insideTarget(e)) return;
          try { e.preventDefault(); } catch(_){}
          startPressFromEvent(e);
        }, {capture:true, passive:false});

        doc.addEventListener('touchmove', function(e){
          if (!insideTarget(e)) return;
          onMoveCheck(e);
        }, {capture:true, passive:true});

        doc.addEventListener('touchend', function(e){
          if (!insideTarget(e)) return;
          f_onmouseup();
        }, {capture:true, passive:true});

        doc.addEventListener('touchcancel', function(e){
          if (!insideTarget(e)) return;
          f_onmousecancel();
        }, {capture:true, passive:true});

        // Maus-Fallback (Desktop)
        doc.addEventListener('mousedown', function(e){
          if (!insideTarget(e)) return;
          if (e.button !== 0) return;
          try { e.preventDefault(); } catch(_){}
          startPressFromEvent(e);
        }, {capture:true, passive:false});

        doc.addEventListener('mousemove', function(e){
          if (!insideTarget(e)) return;
          onMoveCheck(e);
        }, {capture:true, passive:true});

        doc.addEventListener('mouseup', function(e){
          if (!insideTarget(e)) return;
          if (e.button === 0) f_onmouseup();
        }, {capture:true, passive:true});
      }

      // Kontextmenü am Ziel abschalten (optional global)
      target.addEventListener('contextmenu', function(e){ e.preventDefault(); }, {passive:false});
    })();
  DisableJS

  ; Timer-Callback nur EINMAL binden
  Static timerBound.i
  If timerBound = 0
    BindEvent(#PB_Event_Timer, @onMouseLongpress())
    timerBound = 1
  EndIf
EndProcedure

; ==============================
; Demo
; ==============================
CompilerIf #PB_Compiler_IsMainFile
  Procedure onLongPressDebug()
    
    Debug "LongPress detected (Custom Event)"
    Protected n, txt.s = ""
    
    n = GetGadgetState(0)
    If n >= 0
      If GetGadgetItemText(0, n, 0) = ""
        txt = "⭐"
      EndIf
      SetGadgetItemText(0, n, txt, 0)
    Endif
    
  EndProcedure

  G_Win_LongPress = OpenWindow(#PB_Any, 0, 0, 420, 320, "Long Press Demo", #PB_Window_SystemMenu | #PB_Window_ScreenCentered)

  ; ButtonGadget(0, 20, 20, 180, 44, "Press me  ~ 1 Sec.")
  
  ListIconGadget(0, 20, 20, 360, 200, "", 100)
  AddGadgetColumn(0, 1, "Name", 240)
  AddGadgetItem(0, -1, #LF$ + "Jane Doe")
  AddGadgetItem(0, -1, #LF$ + "John Doe")
  
  TextGadget(1, 20, 250, 360, 50, "Press and hold the item to mark it as a favorite.")
  onMouseInit(GadgetID(0)) 
  BindEvent(#PB_Event_MouseLongPress, @onLongPressDebug())
CompilerEndIf