qraymo Garden

Powered by 🌱Roam Garden

ViktorTabori Long Tap

/*
 * Viktor's Roam Mobile Long tap to Exluce Filters and Right click on bullets + pages + title
 * version: 0.3
 * author: @ViktorTabori
 *
 * How to install it:
 *  - go to page [[roam/js]]
 *  - create a node with: { {[[roam/js]]}}
 *  - create a clode block under it, and change its type from clojure to javascript
 *  - allow the running of the javascript on the {{[[roam/js]]}} node
 *  - long tap a filter and it gets excluded
 *  - long tap on bullets to simulate right click
 *  - long tap on titles, page references to open in sidebar
 */
if (window.ViktorMobileLongTap) window.ViktorMobileLongTap.stop();
window.ViktorMobileLongTap = /*window.ViktorMobileLongTap ||*/ (function(){
    // max wait for second tap in ms
    var doLog = true,
        minWaitTime = 200, // turn taps into long taps after this amount of milliseconds
        clickBlockTime = 800, // clicks get cancelled after long taps within this timeframe in milliseconds
        animTime = 400,
        highlightColor = 'rgba(255, 165, 0, 0.1)', // background color of selected block
        deduplicateSidebar = true,
        added = false,
        last = new Date(),
        popupWidth = 230+25, // popup width in pixel plus margin
        tapStatus = {status: false, target: null, latestLongTap: null}, // status of long tap tracking
        css = document.createElement('style');
    css.id = 'CSSViktorMobileLongTap';
    css.innerHTML = `
        :root {
          --animate-delay: 0ms;
          --animate-duration: ${animTime}ms;
          --animation-overshoot: 1.02;
        }

        .animate__animated {
          -webkit-animation-duration: 1s;
          animation-duration: 1s;
          -webkit-animation-duration: var(--animate-duration);
          animation-duration: var(--animate-duration);
          -webkit-animation-fill-mode: both;
          animation-fill-mode: both;
        }

        @-webkit-keyframes pulseReverse {
          from, to {
            -webkit-transform: scale3d(1, 1, 1);
            transform: scale3d(1, 1, 1);
          }

          50% {
            -webkit-transform: scale3d(0.95, 0.95, 0.95);
            transform: scale3d(0.95, 0.95, 0.95);
          }
          
          90% {
            -webkit-transform: scale3d(var(--animation-overshoot), var(--animation-overshoot), var(--animation-overshoot));
            transform: scale3d(var(--animation-overshoot), var(--animation-overshoot), var(--animation-overshoot));
          }
        }
        @keyframes pulseReverse {
          from, to {
            -webkit-transform: scale3d(1, 1, 1);
            transform: scale3d(1, 1, 1);
          }

          50% {
            -webkit-transform: scale3d(0.95, 0.95, 0.95);
            transform: scale3d(0.95, 0.95, 0.95);
          }
          
          90% {
            -webkit-transform: scale3d(var(--animation-overshoot), var(--animation-overshoot), var(--animation-overshoot));
            transform: scale3d(var(--animation-overshoot), var(--animation-overshoot), var(--animation-overshoot));
          }
        }
        .animate__pulseReverse {
          -webkit-animation-name: pulseReverse;
          animation-name: pulseReverse;
          -webkit-animation-timing-function: ease-in-out;
          animation-timing-function: ease-in-out;
        }

        /* fix popover menu and left sidebar menu order */
        .bp3-transition-container { 
          z-index:9999!important; 
        }
        `;

    // start the plugin
    start();

    // return public attributes
    return {
        added: added,
        start: start,
        stop: stop,
    };

    // install and run the plugin
    function start() {
        if (added) return;
        added = true;

        'click mousedown mouseup contextmenu touchstart touchmove touchend selectionchange'.split(' ').forEach(type=>{
            document.addEventListener(type, process, {passive: false, capture: true});
        });

        document.head.appendChild(css);

        if (doLog) console.log('** long tap installed **');
    }

    // stop the plugin
    function stop() {
        if (!added) return;
        added = false;

        'click mousedown mouseup contextmenu touchstart touchmove touchend selectionchange'.split(' ').forEach(type=>{
            document.removeEventListener(type, process, {passive: false, capture: true});
        });

        if (css.parentNode) css.parentNode.removeChild(css);

        if (doLog) console.log('** long tap STOPPED **');
    }

    // process touch and click events
    function process(e) {
        //console.log(e.type,(new Date().getTime())-last.getTime(),e.simulated,e.target);
        last = new Date();
        var target = e.target;
        var location = {
            x: e.clientX || e.targetTouches&&e.targetTouches.length&&e.targetTouches[0].clientX, 
            y: e.clientY || e.targetTouches&&e.targetTouches.length&&e.targetTouches[0].clientY,
        };

        // fn - this will be called on long taps
        var action;

        // stop long taps on touchmove and touchend
        if (e.type == 'touchmove' || e.type == 'touchend') {
            //console.log('** abort touch **');
            tapStatus.status = false;
            return;
        }
        // prevent clicks after long taps
        if (e.type.match(/click|contextmenu|mouse|selectionchange/i)) {
            //console.log('  ',tapStatus.latestLongTap&&((new Date()).getTime() - tapStatus.latestLongTap.getTime()),clickBlockTime,!e.simulated,tapStatus.latestLongTap && ((new Date()).getTime() - tapStatus.latestLongTap.getTime()) < clickBlockTime && !e.simulated, e.type=='selectionchange'&&e);
            if (tapStatus.latestLongTap && ((new Date()).getTime() - tapStatus.latestLongTap.getTime()) < clickBlockTime && !e.simulated) {
                //console.log('  ','!! CLICK prevented');
                e.preventDefault();
                e.stopPropagation();
                if (window.getSelection().rangeCount) window.getSelection().removeAllRanges();
            }
            tapStatus.status = false;
            return;
        }

        // filter, search, and page reference long tap to shift click
        try {   
            if (target.classList && (
                    target.classList.contains('rm-search-title') // search result
                    || target.closest('.bp3-popover-content button.bp3-button') && target.parentNode.firstChild.nodeName.match(/button/i) // filter
                    || target.closest('.rm-page-ref') // page reference
                    ) 
                || target.closest('.rm-pages-title-text') // all pages search
                ) {
                action = function(){
                    var sidebar;

                    // deduplicate sidebar: close already open sidebar elements and open a new one as first child, except filter tags
                    if (deduplicateSidebar && (!target.classList || !target.classList.contains('bp3-button'))) {
                        sidebar = document.getElementById('roam-right-sidebar-content') && document.getElementById('roam-right-sidebar-content').children || [];
                        var close = Array.from(sidebar).filter(function(el){ var title=el.querySelector('h1'); return title && title.textContent == target.textContent });
                        close.forEach(function(el){
                            simulateClick(el.querySelector('.bp3-icon-cross'), ['mousedown', 'click', 'mouseup'], true);
                        });
                    }

                    // simulate shift click
                    simulateClick(target, ['mousedown', 'click', 'mouseup'], true, {shiftKey:true});

                    // animation for page reference click: page links in all pages page or blocks
                    var _animate = sidebar && (target.closest('.rm-pages-title-col') || target.closest('.flex-h-box'));
                    //console.log('animate',_animate);
                    if (_animate) {
                        animateCSS(_animate, ['pulseReverse']);
                    }
                }
            }
        } catch(_) { }

        // right click on bullets, titles, page references
        if (!action)
            try {
                // bullet right click
                if (target.closest('.controls')) {
                    target = target.closest('.controls');

                    // set callback function for long tap
                    action = function() {
                        // calculate where the popup should open on the left side if there is not enough space
                        var bound = target.getBoundingClientRect();
                        var left = (bound.left + bound.width) < popupWidth ? 0 : bound.left + bound.width - popupWidth;

                        // simulate right click
                        simulateClick(target, ['contextmenu'], false, {clientX:left, clientY:(bound.top+bound.height/2)});

                        // calculate how much we have to move the block
                        var transform = left == 0 ? popupWidth - bound.left - bound.width : 0;
                        // find block
                        var el = target.closest('.roam-block-container');
                        if (!el) return;

                        // move block to the right
                        el.style.webkitTransform = 'translate3d('+transform+'px, 0, 0)';
                        el.style.transform = 'translate3d('+transform+'px, 0, 0)';
                        // change background color
                        el.style.backgroundColor = highlightColor;

                        // look for overlay close
                        (new MutationObserver(function(mutations, obs){
                            mutations.forEach(function(mutation){
                                if (mutation.attributeName == 'class' && !document.body.classList.contains('bp3-overlay-open')) {
                                    // remove transform
                                    el.style.webkitTransform = '';
                                    el.style.transform = '';

                                    // remove coloring
                                    el.style.backgroundColor = '';

                                    // remove observer
                                    obs.disconnect();
                                }
                            });
                        })).observe(document.body, { attributes: true });
                    }               
                }

                // title right click
                if (target.closest('.rm-title-display')) {
                    var bound = target.getBoundingClientRect();

                    action = function() {
                        simulateClick(target, ['contextmenu'], false, {clientX:location.x, clientY:location.y});
                    }
                }
            } catch(_) { }

        if (!action) {
            //console.log('NO action **');
            return;
        }

        // 
        if (e.type == 'touchstart') {
            //console.log('start waiting **');
            tapStatus.status=true;
            tapStatus.target=e.target;
            setTimeout(function(){
                if (tapStatus.status) {
                    //console.log('LONG TAP **',(new Date()).getTime() - last.getTime());
                    last = new Date();

                    tapStatus.status = false;
                    tapStatus.latestLongTap = new Date();

                    // run action
                    simulateTouch(target, ['touchend']);
                    action();
                } else {
                    //console.log('NO longer tapping **');
                }
            }, minWaitTime);
        }
    }

    // mouse click emulation
    function simulateClick(element, events, leftButton, opts) {
        setTimeout(function(){
            events.forEach(function(type){
                var _event = new MouseEvent(type, {
                    view: window,
                    bubbles: true,
                    cancelable: true,
                    buttons: leftButton?1:2,
                    ...opts,
                });
                _event.simulated = true;
                element.dispatchEvent(_event);
            });
        }, 0);
    }

    // mouse click emulation
    function simulateTouch(element, events, opts) {
        setTimeout(function(){
            events.forEach(function(type){
                var _event = new TouchEvent(type, {
                    view: window,
                    bubbles: true,
                    cancelable: true,
                    ...opts,
                });
                _event.simulated = true;
                element.dispatchEvent(_event);
            });
        }, 0);
    }

    function animateCSS(node, animations, prefix) {
        var prefix = prefix || 'animate__';

        // We create a Promise and return it
        return new Promise((resolve, reject) => {
            animations = animations.map(function(animation){return `${prefix}${animation}`});
            animations.push(`${prefix}animated`);

            node.classList.add(...animations);

            // When the animation ends, we clean the classes and resolve the Promise
            function handleAnimationEnd() {
                node.classList.remove(...animations);
                node.removeEventListener('animationend', handleAnimationEnd);

                resolve('Animation ended');
            }

            node.addEventListener('animationend', handleAnimationEnd);
        });
    }
})();
ViktorTabori Long Tap