Last active
July 11, 2023 17:10
-
-
Save thesved/48cab2307cf0598fcc5cd37643d36cb4 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* 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); | |
}); | |
} | |
})(); |
@mkosma that is right, this is the never better solution for the right click problem on touch devices, it is also easier to hold a tap than trying to double-tap to the same spot.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Hi Viktor - does this replace your double-click script? I couldn’t get that to work on my iPad (roam kept wanting to open a bullet/tree in its own window).