I use jQuery.click to handle the mouse click event on Raphael graph, meanwhile, I need to handle mouse drag event, mouse drag consists of mousedown, mouseupand mousemove in Raphael.
It is difficult to distinguish click and drag because click also contain mousedown & mouseup, How can I distinguish mouse "click" & mouse "drag" then in Javascript?
I think the difference is that there is a mousemove between mousedown and mouseup in a drag, but not in a click.
You can do something like this:
const element = document.createElement('div')
element.innerHTML = 'test'
document.body.appendChild(element)
let moved
let downListener = () => {
moved = false
}
element.addEventListener('mousedown', downListener)
let moveListener = () => {
moved = true
}
element.addEventListener('mousemove', moveListener)
let upListener = () => {
if (moved) {
console.log('moved')
} else {
console.log('not moved')
}
}
element.addEventListener('mouseup', upListener)
// release memory
element.removeEventListener('mousedown', downListener)
element.removeEventListener('mousemove', moveListener)
element.removeEventListener('mouseup', upListener)
All these solutions either break on tiny mouse movements, or are overcomplicated.
Here is a simple adaptable solution using two event listeners. Delta is the distance in pixels that you must move horizontally or vertically between the up and down events for the code to classify it as a drag rather than a click. This is because sometimes you will move the mouse or your finger a few pixels before lifting it.
const delta = 6;
let startX;
let startY;
element.addEventListener('mousedown', function (event) {
startX = event.pageX;
startY = event.pageY;
});
element.addEventListener('mouseup', function (event) {
const diffX = Math.abs(event.pageX - startX);
const diffY = Math.abs(event.pageY - startY);
if (diffX < delta && diffY < delta) {
// Click!
}
});
Cleaner ES2015
let drag = false;
document.addEventListener('mousedown', () => drag = false);
document.addEventListener('mousemove', () => drag = true);
document.addEventListener('mouseup', () => console.log(drag ? 'drag' : 'click'));
Didn't experience any bugs, as others comment.
In case you are already using jQuery:
var $body = $('body');
$body.on('mousedown', function (evt) {
$body.on('mouseup mousemove', function handler(evt) {
if (evt.type === 'mouseup') {
// click
} else {
// drag
}
$body.off('mouseup mousemove', handler);
});
});
This should work well. Similar to the accepted answer (though using jQuery), but the isDragging flag is only reset if the new mouse position differs from that on mousedown event. Unlike the accepted answer, that works on recent versions of Chrome, where mousemove is fired regardless of whether mouse was moved or not.
var isDragging = false;
var startingPos = [];
$(".selector")
.mousedown(function (evt) {
isDragging = false;
startingPos = [evt.pageX, evt.pageY];
})
.mousemove(function (evt) {
if (!(evt.pageX === startingPos[0] && evt.pageY === startingPos[1])) {
isDragging = true;
}
})
.mouseup(function () {
if (isDragging) {
console.log("Drag");
} else {
console.log("Click");
}
isDragging = false;
startingPos = [];
});
You may also adjust the coordinate check in mousemove if you want to add a little bit of tolerance (i.e. treat tiny movements as clicks, not drags).
If you feel like using Rxjs:
var element = document;
Rx.Observable
.merge(
Rx.Observable.fromEvent(element, 'mousedown').mapTo(0),
Rx.Observable.fromEvent(element, 'mousemove').mapTo(1)
)
.sample(Rx.Observable.fromEvent(element, 'mouseup'))
.subscribe(flag => {
console.clear();
console.log(flag ? "drag" : "click");
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://unpkg.com/#reactivex/rxjs#5.4.1/dist/global/Rx.js"></script>
This is a direct clone of what #wong2 did in his answer, but converted to RxJs.
Also interesting use of sample. The sample operator will take the latest value from the source (the merge of mousedown and mousemove) and emit it when the inner observable (mouseup) emits.
As mrjrdnthms points out in his comment on the accepted answer, this no longer works on Chrome (it always fires the mousemove), I've adapted Gustavo's answer (since I'm using jQuery) to address the Chrome behavior.
var currentPos = [];
$(document).on('mousedown', function (evt) {
currentPos = [evt.pageX, evt.pageY]
$(document).on('mousemove', function handler(evt) {
currentPos=[evt.pageX, evt.pageY];
$(document).off('mousemove', handler);
});
$(document).on('mouseup', function handler(evt) {
if([evt.pageX, evt.pageY].equals(currentPos))
console.log("Click")
else
console.log("Drag")
$(document).off('mouseup', handler);
});
});
The Array.prototype.equals function comes from this answer
Using jQuery with a 5 pixel x/y theshold to detect the drag:
var dragging = false;
$("body").on("mousedown", function(e) {
var x = e.screenX;
var y = e.screenY;
dragging = false;
$("body").on("mousemove", function(e) {
if (Math.abs(x - e.screenX) > 5 || Math.abs(y - e.screenY) > 5) {
dragging = true;
}
});
});
$("body").on("mouseup", function(e) {
$("body").off("mousemove");
console.log(dragging ? "drag" : "click");
});
You could do this:
var div = document.getElementById("div");
div.addEventListener("mousedown", function() {
window.addEventListener("mousemove", drag);
window.addEventListener("mouseup", lift);
var didDrag = false;
function drag() {
//when the person drags their mouse while holding the mouse button down
didDrag = true;
div.innerHTML = "drag"
}
function lift() {
//when the person lifts mouse
if (!didDrag) {
//if the person didn't drag
div.innerHTML = "click";
} else div.innerHTML = "drag";
//delete event listeners so that it doesn't keep saying drag
window.removeEventListener("mousemove", drag)
window.removeEventListener("mouseup", this)
}
})
body {
outline: none;
box-sizing: border-box;
margin: 0;
padding: 0;
font-family: Arial, Helvetica, sans-serif;
overflow: hidden;
}
#div {
/* calculating -5px for each side of border in case border-box doesn't work */
width: calc(100vw - 10px);
height: calc(100vh - 10px);
border: 5px solid orange;
background-color: yellow;
font-weight: 700;
display: grid;
place-items: center;
user-select: none;
cursor: pointer;
padding: 0;
margin: 0;
}
<html>
<body>
<div id="div">Click me or drag me.</div>
</body>
</html>
Had the same problem recently with a tree list where the user can either click on the item or drag it, made this small Pointer class and put it in my utils.js
function Pointer(threshold = 10) {
let x = 0;
let y = 0;
return {
start(e) {
x = e.clientX;
y = e.clientY;
},
isClick(e) {
const deltaX = Math.abs(e.clientX - x);
const deltaY = Math.abs(e.clientY - y);
return deltaX < threshold && deltaY < threshold;
}
}
}
Here you can see it at work:
function Pointer(threshold = 10) {
let x = 0;
let y = 0;
return {
start(e) {
x = e.clientX;
y = e.clientY;
},
isClick(e) {
const deltaX = Math.abs(e.clientX - x);
const deltaY = Math.abs(e.clientY - y);
return deltaX < threshold && deltaY < threshold;
}
}
}
const pointer = new Pointer();
window.addEventListener('mousedown', (e) => pointer.start(e))
//window.addEventListener('mousemove', (e) => pointer.last(e))
window.addEventListener('mouseup', (e) => {
const operation = pointer.isClick(e)
? "Click"
: "Drag"
console.log(operation)
})
It's really this simple
var dragged = false
window.addEventListener('mousedown', function () { dragged = false })
window.addEventListener('mousemove', function () { dragged = true })
window.addEventListener('mouseup', function() {
if (dragged == true) { return }
console.log("CLICK!! ")
})
You honestly do not want to add a threshold allowing a small movement. The above is the correct, normal, feel of clicking on all desktop interfaces.
Just try it.
You can easily add an event if you like.
If just to filter out the drag case, do it like this:
var moved = false;
$(selector)
.mousedown(function() {moved = false;})
.mousemove(function() {moved = true;})
.mouseup(function(event) {
if (!moved) {
// clicked without moving mouse
}
});
Another solution for class based vanilla JS using a distance threshold
private initDetectDrag(element) {
let clickOrigin = { x: 0, y: 0 };
const dragDistanceThreshhold = 20;
element.addEventListener('mousedown', (event) => {
this.isDragged = false
clickOrigin = { x: event.clientX, y: event.clientY };
});
element.addEventListener('mousemove', (event) => {
if (Math.sqrt(Math.pow(clickOrigin.y - event.clientY, 2) + Math.pow(clickOrigin.x - event.clientX, 2)) > dragDistanceThreshhold) {
this.isDragged = true
}
});
}
Add inside the class (SOMESLIDER_ELEMENT can also be document to be global):
private isDragged: boolean;
constructor() {
this.initDetectDrag(SOMESLIDER_ELEMENT);
this.doSomeSlideStuff(SOMESLIDER_ELEMENT);
element.addEventListener('click', (event) => {
if (!this.sliderIsDragged) {
console.log('was clicked');
} else {
console.log('was dragged, ignore click or handle this');
}
}, false);
}
Pure JS with DeltaX and DeltaY
This DeltaX and DeltaY as suggested by a comment in the accepted answer to avoid the frustrating experience when trying to click and get a drag operation instead due to a one tick mousemove.
deltaX = deltaY = 2;//px
var element = document.getElementById('divID');
element.addEventListener("mousedown", function(e){
if (typeof InitPageX == 'undefined' && typeof InitPageY == 'undefined') {
InitPageX = e.pageX;
InitPageY = e.pageY;
}
}, false);
element.addEventListener("mousemove", function(e){
if (typeof InitPageX !== 'undefined' && typeof InitPageY !== 'undefined') {
diffX = e.pageX - InitPageX;
diffY = e.pageY - InitPageY;
if ( (diffX > deltaX) || (diffX < -deltaX)
||
(diffY > deltaY) || (diffY < -deltaY)
) {
console.log("dragging");//dragging event or function goes here.
}
else {
console.log("click");//click event or moving back in delta goes here.
}
}
}, false);
element.addEventListener("mouseup", function(){
delete InitPageX;
delete InitPageY;
}, false);
element.addEventListener("click", function(){
console.log("click");
}, false);
For a public action on an OSM map (position a marker on click) the question was: 1) how to determine the duration of mouse down->up (you can't imagine creating a new marker for each click) and 2) did the mouse move during down->up (i.e user is dragging the map).
const map = document.getElementById('map');
map.addEventListener("mousedown", position);
map.addEventListener("mouseup", calculate);
let posX, posY, endX, endY, t1, t2, action;
function position(e) {
posX = e.clientX;
posY = e.clientY;
t1 = Date.now();
}
function calculate(e) {
endX = e.clientX;
endY = e.clientY;
t2 = (Date.now()-t1)/1000;
action = 'inactive';
if( t2 > 0.5 && t2 < 1.5) { // Fixing duration of mouse down->up
if( Math.abs( posX-endX ) < 5 && Math.abs( posY-endY ) < 5 ) { // 5px error on mouse pos while clicking
action = 'active';
// --------> Do something
}
}
console.log('Down = '+posX + ', ' + posY+'\nUp = '+endX + ', ' + endY+ '\nAction = '+ action);
}
Based on this answer, I did this in my React component:
export default React.memo(() => {
const containerRef = React.useRef(null);
React.useEffect(() => {
document.addEventListener('mousedown', handleMouseMove);
return () => document.removeEventListener('mousedown', handleMouseMove);
}, []);
const handleMouseMove = React.useCallback(() => {
const drag = (e) => {
console.log('mouse is moving');
};
const lift = (e) => {
console.log('mouse move ended');
window.removeEventListener('mousemove', drag);
window.removeEventListener('mouseup', this);
};
window.addEventListener('mousemove', drag);
window.addEventListener('mouseup', lift);
}, []);
return (
<div style={{ width: '100vw', height: '100vh' }} ref={containerRef} />
);
})
If you want check the click or drag behavior of a specific element you can do this without having to listen to the body.
$(document).ready(function(){
let click;
$('.owl-carousel').owlCarousel({
items: 1
});
// prevent clicks when sliding
$('.btn')
.on('mousemove', function(){
click = false;
})
.on('mousedown', function(){
click = true;
});
// change mouseup listener to '.content' to listen to a wider area. (mouse drag release could happen out of the '.btn' which we have not listent to). Note that the click will trigger if '.btn' mousedown event is triggered above
$('.btn').on('mouseup', function(){
if(click){
$('.result').text('clicked');
} else {
$('.result').text('dragged');
}
});
});
.content{
position: relative;
width: 500px;
height: 400px;
background: #f2f2f2;
}
.slider, .result{
position: relative;
width: 400px;
}
.slider{
height: 200px;
margin: 0 auto;
top: 30px;
}
.btn{
display: flex;
align-items: center;
justify-content: center;
text-align: center;
height: 100px;
background: #c66;
}
.result{
height: 30px;
top: 10px;
text-align: center;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/OwlCarousel2/2.3.4/owl.carousel.min.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/OwlCarousel2/2.3.4/assets/owl.carousel.min.css" />
<div class="content">
<div class="slider">
<div class="owl-carousel owl-theme">
<div class="item">
click me without moving the mouse
</div>
<div class="item">
click me without moving the mouse
</div>
</div>
<div class="result"></div>
</div>
</div>
from #Przemek 's answer,
function listenClickOnly(element, callback, threshold=10) {
let drag = 0;
element.addEventListener('mousedown', () => drag = 0);
element.addEventListener('mousemove', () => drag++);
element.addEventListener('mouseup', e => {
if (drag<threshold) callback(e);
});
}
listenClickOnly(
document,
() => console.log('click'),
10
);
The following coding is to detect the movement of mouseup and mousedown.
It shall work for most of the cases. It also depends
on how you treat mouseevent as Click.
In JavaScript, the detection is very simple. It does not concern how
long you press or movement between the mousedown and mouseup.
Event.detail would not reset to 1 when your mouse is moved between
the mousedown and mouseup.
If you need to differentiate the click and long press, you need to
check the difference in event.timeStamp too.
// ==== add the code at the begining of your coding ====
let clickStatus = 0;
(() => {
let screenX, screenY;
document.addEventListener('mousedown', (event) => ({screenX, screenY} = event), true);
document.addEventListener('mouseup', (event) => (clickStatus = Math.abs(event.screenX - screenX) + Math.abs(event.screenY - screenY) < 3), true);
})();
// ==== add the code at the begining of your coding ====
$("#draggable").click(function(event) {
if (clickStatus) {
console.log(`click event is valid, click count: ${event.detail}`)
} else {
console.log(`click event is invalid`)
}
})
<!doctype html>
<html lang="en">
<!-- coding example from https://jqueryui.com/draggable/ -->
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>jQuery UI Draggable - Default functionality</title>
<link rel="stylesheet" href="//code.jquery.com/ui/1.12.1/themes/base/jquery-ui.css">
<link rel="stylesheet" href="/resources/demos/style.css">
<style>
#draggable { width: 150px; height: 150px; padding: 0.5em; }
</style>
<script src="https://code.jquery.com/jquery-1.12.4.js"></script>
<script src="https://code.jquery.com/ui/1.12.1/jquery-ui.js"></script>
<script>
$( function() {
$( "#draggable" ).draggable();
} );
</script>
</head>
<body>
<div id="draggable" class="ui-widget-content">
<p>Drag me around</p>
</div>
</body>
</html>
Is very easy,
el = document.getElementById("your_id");
var isDown = false;
el.addEventListener('mousedown', function () {
isDown = true;
});
el.addEventListener('mouseup', function () {
isDown = false;
});
el.addEventListener('mousemove', function () {
if (isDown) {
// your code goes here
}
});
Related
I have a 'theme toggle' which changes the look/feel of my UI. There is also a custom JS cursor that I don't need on one of the themes. I thought it would be a good idea to kill the script when the relevant class is on html as there's a lot of calculation going on with the pointer position. Rather than just use display: none and leave the script running.
There are 3 modes/themes:
Default (always first / onload)
Dark
Retro - which is the one where the script should be stopped.
I tried to achieve this using the following method. To test, I add the .retro class to html so it's the first theme, it does stop the script from running and clicking the toggle turns the script on again. But it only does it once.
Obviously removing the class onload doesn't work either but I just wanted to do the above to see if anything was happening. Can someone tell me where I'm going wrong. What's the best approach?
Essentially what I'm trying to achieve is: Run this script, unless the class .retro is present, if so stop it. If the class .retro is removed, run the script again.
/* Toggle */
const html = document.querySelector("html");
const button = document.querySelector(".contrast__link");
button.addEventListener("click", (e) => {
e.preventDefault();
if (html.classList.contains("dark-mode")) {
html.classList.remove("dark-mode");
html.classList.add("retro");
let slideIndex = swiper.activeIndex;
swiper.destroy(true, true);
swiper = initSwiper("slide", false, 999999999); // Retro: This should slide, no autoplay or loop
swiper.slideTo(slideIndex, 0);
} else if (html.classList.contains("retro")) {
html.classList.remove("retro");
let slideIndex = swiper.activeIndex;
swiper.destroy(true, true);
swiper = initSwiper("fade", true, 1200); // Default: This should fade, autoplay & loop
swiper.slideTo(slideIndex, 0);
} else {
html.classList.add("dark-mode");
}
});
/* Cursor */
function createHandler(callback) {
return function(event) {
if (!document.documentElement.classList.contains('retro')) {
callback.call(this, event)
}
}
}
window.addEventListener('mousemove', createHandler(function() {
var cursor = document.querySelector(".cursor");
var cursorTrail = document.querySelector(".cursor-trail");
var a = document.querySelectorAll("a");
var timeout;
window.addEventListener(
"mousemove",
function(e) {
var x = e.clientX;
var y = e.clientY;
cursor.style.transform = `translate(${x - 2}px, ${y - 2}px)`;
if (!timeout) {
timeout = setTimeout(function() {
timeout = null;
cursorTrail.style.transform = `translate(${x - 16}px, ${y - 16}px)`;
}, 24);
}
},
false
);
/**
* Add/remove classes on click (anywhere).
*/
document.addEventListener("mousedown", function() {
cursor.classList.add("cursor--click");
});
document.addEventListener("mouseup", function() {
cursor.classList.remove("cursor--click");
});
/**
* Add/remove set classes on hover.
*
* 1. This used to start with `a.forEach((item) => {` but changed to `let` so
* that an additional (non-anchor) item could be targeted. `#hello` is for
* the image on the 404 page.
*/
// a.forEach((item) => {
let links = document.querySelectorAll('a, #hello'); /* [1] */
links.forEach((item) => { /* [1] */
item.addEventListener("mouseover", () => {
cursorTrail.classList.add("cursor-trail--hover");
});
item.addEventListener("mouseleave", () => {
cursorTrail.classList.remove("cursor-trail--hover");
});
});
/**
* Add custom classes on hover if the cursor needs to be manipulated in a
* unique way. If an element has a `data-interaction=""` value set. This will
* be added as a class to the cursor on hover. For example, this is used to
* style the prev/next arrows on the carousel.
*
* This could be set using a specific class but I've just left it targeting all
* `a` elements for now. Which will add a class of `undefined` if no dataset is
* specified.
*/
a.forEach((item) => {
const interaction = item.dataset.interaction;
item.addEventListener("mouseover", () => {
cursor.classList.add(interaction);
});
item.addEventListener("mouseleave", () => {
cursor.classList.remove(interaction);
});
});
}))
.contrast__link {
background: white;
}
.cursor {
background: red;
height: 20px;
width: 20px;
position: fixed;
}
.dark-mode {
background: black;
}
.retro {
background: blue;
}
Toggle Classes
<div class="cursor"><span></span></div>
<div class="cursor-trail"></div>
I would suggest to add and remove "mousemove" listener inside "click" listener, when you toggling the theme. So if user selects "retro", the event listener should be removed, and when it is another theme, the listener should be added.
As #Mykhailo Svyrydovych suggested, to avoid any overhead of the unneeded events call, you would need to have explictly named functions for each event handler, then you could use removeEventListener when you're toggling to the retro theme, removing the handler of each mousemove, mouseenter, mouseleave, mouseup and mousedown events that you don't want to run on that theme. Of course, you would need to re-bind all the events again using the proper addEventListener calls when toggling to the other two themes.
But I think that, sometimes, the easiest approach is good enough. If you measure the performance and then discover that the events keeping to fire isn't a big concern for your case, go for it. You can easily avoid the unwanted effects of the custom cursor, inside each event handler, just by checking a simple boolean:
const html = document.documentElement;
const button = document.querySelector(".contrast__link");
const cursor = document.querySelector(".cursor");
const cursorTrail = document.querySelector(".cursor-trail");
const a = document.querySelectorAll("a");
let timeout;
let customCursor = true;
button.addEventListener("click", (e) => {
e.preventDefault();
if (html.classList.contains("dark-mode")) {
html.classList.remove("dark-mode");
html.classList.add("retro");
customCursor = false;
cursor.style.display = "none";
} else if (html.classList.contains("retro")) {
html.classList.remove("retro");
customCursor = true;
cursor.style.display = "block";
} else {
html.classList.add("dark-mode");
customCursor = true;
cursor.style.display = "block";
}
});
window.addEventListener(
"mousemove",
function(e) {
if (!customCursor) return;
var x = e.clientX;
var y = e.clientY;
cursor.style.display = "block";
cursor.style.transform = `translate(${x - 2}px, ${y - 2}px)`;
if (!timeout) {
timeout = setTimeout(function() {
cursorTrail.style.transform = `translate(${x - 16}px, ${y - 16}px)`;
}, 24);
}
},
false
);
document.addEventListener("mousedown", () => {
if (!customCursor) return;
cursor.classList.add("cursor--click");
});
document.addEventListener("mouseup", () => {
if (!customCursor) return;
cursor.classList.remove("cursor--click");
});
let links = document.querySelectorAll('a, #hello');
links.forEach((item) => {
item.addEventListener("mouseover", () => {
if (!customCursor) return;
cursorTrail.classList.add("cursor-trail--hover");
});
item.addEventListener("mouseleave", () => {
if (!customCursor) return;
cursorTrail.classList.remove("cursor-trail--hover");
});
});
a.forEach((item) => {
const interaction = item.dataset.interaction;
item.addEventListener("mouseover", () => {
if (!customCursor) return;
cursor.classList.add(interaction);
});
item.addEventListener("mouseleave", () => {
if (!customCursor) return;
cursor.classList.remove(interaction);
});
});
.contrast__link {
background: white;
}
.cursor {
background: red;
height: 20px;
width: 20px;
position: fixed;
display: none;
}
.dark-mode {
background: black;
}
.retro {
background: blue;
}
Toggle Classes
<div class="cursor"><span></span></div>
<div class="cursor-trail"></div>
P.S.: I removed the Swiper calls from my code sample because they appeared to be totally unrelated to the question being asked, and were causing console errors.
I have tried with simple CSS code, it's working
.retro .cursor{
display:none
}
but you want to stop the function also, so tried the following that also worked. When the theme is retro on mouse move the cursor changing function is not calling
/* Toggle */
const html = document.querySelector("html");
const cursor = document.querySelector(".cursor");
function addClass() {
if (html.classList.contains("dark-mode")) {
html.classList.remove("dark-mode");
html.classList.add("retro");
cursor.style.display = 'none'
} else if (html.classList.contains("retro")) {
html.classList.remove("retro");
} else {
html.classList.add("dark-mode");
}
}
document.addEventListener('mousemove', function(e) {
if (html.classList.contains("retro")) {
cursor.style.display = 'none'
} else {
cursor.style.display = 'block'
var x = e.clientX;
var y = e.clientY;
cursor.style.transform = `translate(${x - 2}px, ${y - 2}px)`;
}
});
.cursor {
background: red;
height: 20px;
width: 20px;
position: fixed;
}
.dark-mode {
background: black;
}
.retro {
background: blue;
}
<div class="cursor"></div>
<button onclick='addClass()'>Add Class</button>
You can use element insertion and window properties to load/unload code.
Look at code in snippet, there's simple system for theme changing.
When on retro, mouse clicks will be logged in console.
In my snippet, script will be working only on retro theme, but that doesn't changes concept.
const themeList = {
'default': {
enable() {
app.classList.add('default');
},
disable() {
app.classList.remove('default');
},
},
'dark-mode': {
enable() {
app.classList.add('dark-mode');
},
disable() {
app.classList.remove('dark-mode');
},
},
'retro': {
enable() {
app.classList.add('retro');
js = document.createElement("script");
js.id = 'retroThemeScript';
js.innerHTML = `
window.fnToBeRemoved = () => console.log('mouse down');
app.addEventListener("mousedown", window.fnToBeRemoved);
` // you can replace code with next line, so no need to store code in RAM
//js.src = '/scripts/retroScript.js';
js.type = 'text/javascript';
app.appendChild(js);
this.scriptElement = js;
},
disable() {
app.classList.remove('retro');
app.removeEventListener('mousedown', window.fnToBeRemoved);
delete window.fnToBeRemoved;
app.removeChild(this.scriptElement);
},
}
}
const app = document.querySelector('#app');
let currentTheme;
function setTheme(themeName) {
if (currentTheme)
themeList[currentTheme].disable();
themeList[themeName].enable();
currentTheme = themeName;
}
setTheme('default');
#app {
font-size: 24px;
width: 500px;
height: 500px;
background: silver;
}
#app.retro{
background: magenta;
}
#app.dark-mode {
background: gray;
}
<button onclick="setTheme('default')">Default</button>
<button onclick="setTheme('dark-mode')">Dark</button>
<button onclick="setTheme('retro')">Retro</button>
<div id="app">
Wanna cake?
</div>
In react I have an element that I want to drag and drop. Right now I have a dragStart and dragEnd that uses ev.dataTransfer.setData(data) to drag and drop my element to a different box.
However, I also want to be able to do this on a touch screen. When I use a touchscreen, the dragStart doesn't do anything. I figured out instead that I should use TouchStart and TouchEnd for the touchscreen. However, there is no dataTransfer event with touchStart. I'm wondering what the equivalent would be?
If there is no equivalent, what would be a good way for me to drag and drop my elements on touchscreen?
Thanks
Reactjs is abit alien to me, but generally in vanilla, dataTransfer for touch can be done by storing your data into object/variable. Basically that's all dataTransfer do while you drag an element.
To do this, ontouchstart event, store the data that you want to carry like you did with dataTransfer.setData(), then simulate the dragging of an element by duplicating it.
touchstart(e, item, index, arr) {
this.state.touch['item'] = item
this.state.touch['index'] = index
this.state.touch['arr'] = arr
let image = document.createElement("img"); // Create a new element
image.setAttribute("id", "image-float");
// get the image from the stored reference
image.src = 'https://cdn.quasar.dev/img/avatar' + item + '.jpg';
image.width = 100
image.height = 100
// position the image to the touch, can be improve to detect the position of touch inside the image
let left = e.touches[0].pageX;
let top = e.touches[0].pageY;
image.style.position = 'absolute'
image.style.left = left + 'px';
image.style.top = top + 'px';
image.style.opacity = 0.5;
document.getElementById('app').appendChild(image);
}
then in ontouchmove event, you can have the duplicated element to follow cursor, to simulate the dragging effect. while on touchmove event, we can detect either our cursor is inside the dropzone element, like you would normally have as onDrop event for DnD. this is the manual part that DnD has helped to smoothen the experience, but for touch, we need to do the detection by ourself.
touchmove(e) {
let image = document.getElementById('image-float')
// this will give us the dragging feeling of the element while actually it's a different element
let left = e.touches[0].pageX;
let top = e.touches[0].pageY;
image.style.position = 'absolute'
image.style.left = left + 'px';
image.style.top = top + 'px';
let topDrop = this.getDropzone(e, left, top, 'top')
let botDrop = this.getDropzone(e, left, top, 'bottom')
if (!topDrop && !botDrop) this.state.touch.drop = null
if (topDrop) this.state.touch.drop = 'top'
if (botDrop) this.state.touch.drop = 'bottom'
}
getDropzone(e, left, top, id) {
let rect1 = document.getElementById(id).getBoundingClientRect();
// to detect the overlap of mouse into the dropzone, as alternative of mouseover
var overlap = !(rect1.right < left ||
rect1.left > left ||
rect1.bottom < top ||
rect1.top > top)
return overlap
}
finally, in our ontouchend event, we do the logic as we have for our dataTransfer.getData(). Because we already detect if our cursor is within the dropzone or not, so we can perform the logic accordingly
touchend() {
let image = document.getElementById('image-float')
image.remove()
let item = this.state.touch.item
let index = this.state.touch.index
let arr = this.state[this.state.touch.arr]
let drop = this.state[this.state.touch.drop]
/* console.log(this.state.touch.arr) */
if (this.state.touch.drop != null) {
if (drop != arr) {
drop.push(item)
arr.splice(index, 1)
}
return this.setState({
state: {
drop: this.state[drop],
arr: this.state[arr]
}
})
}
}
Below, is my attempt on having this delivered in Reactjs
class TodoApp extends React.Component {
constructor(props) {
super(props)
this.state = {
top: [
'2',
'3',
'4',
],
bottom: [],
touch: {}
}
}
dragstart(e, item, index, arr) {
e.dataTransfer.dropEffect = 'move'
e.dataTransfer.effectAllowed = 'move'
e.dataTransfer.setData('item', item)
e.dataTransfer.setData('index', index)
e.dataTransfer.setData('arr', arr)
}
drop(e, drop) {
let item = e.dataTransfer.getData('item')
let index = e.dataTransfer.getData('index')
let arr = e.dataTransfer.getData('arr')
if (drop != item.arr) {
this.state[drop].push(item)
this.state[arr].splice(index, 1)
}
return this.setState({
state: {
drop: this.state[drop],
arr: this.state[arr]
}
})
}
touchstart(e, item, index, arr) {
this.state.touch['item'] = item
this.state.touch['index'] = index
this.state.touch['arr'] = arr
let image = document.createElement("img"); // Create a new element
image.setAttribute("id", "image-float");
// get the image from the stored reference
image.src = 'https://cdn.quasar.dev/img/avatar' + item + '.jpg';
image.width = 100
image.height = 100
// position the image to the touch, can be improve to detect the position of touch inside the image
let left = e.touches[0].pageX;
let top = e.touches[0].pageY;
image.style.position = 'absolute'
image.style.left = left + 'px';
image.style.top = top + 'px';
image.style.opacity = 0.5;
document.getElementById('app').appendChild(image);
}
touchmove(e) {
let image = document.getElementById('image-float')
// this will give us the dragging feeling of the element while actually it's a different element
let left = e.touches[0].pageX;
let top = e.touches[0].pageY;
image.style.position = 'absolute'
image.style.left = left + 'px';
image.style.top = top + 'px';
let topDrop = this.getDropzone(e, left, top, 'top')
let botDrop = this.getDropzone(e, left, top, 'bottom')
if (!topDrop && !botDrop) this.state.touch.drop = null
if (topDrop) this.state.touch.drop = 'top'
if (botDrop) this.state.touch.drop = 'bottom'
}
getDropzone(e, left, top, id) {
let rect1 = document.getElementById(id).getBoundingClientRect();
// to detect the overlap of mouse into the dropzone, as alternative of mouseover
var overlap = !(rect1.right < left ||
rect1.left > left ||
rect1.bottom < top ||
rect1.top > top)
return overlap
}
touchend() {
let image = document.getElementById('image-float')
image.remove()
let item = this.state.touch.item
let index = this.state.touch.index
let arr = this.state[this.state.touch.arr]
let drop = this.state[this.state.touch.drop]
/* console.log(this.state.touch.arr) */
if (this.state.touch.drop != null) {
if (drop != arr) {
drop.push(item)
arr.splice(index, 1)
}
return this.setState({
state: {
drop: this.state[drop],
arr: this.state[arr]
}
})
}
}
render() {
return ( <
div className = "container" >
<
div id = "top"
className = "dropzone"
onDrop = {
(event) => this.drop(event, 'top')
}
onDragOver = {
(e) => {
e.preventDefault()
}
}
onDragEnter = {
(e) => {
e.preventDefault()
}
} > {
this.state.top.map((item, i) => {
return ( < img src = {
'https://cdn.quasar.dev/img/avatar' + item + '.jpg'
}
width = "100"
height = "100"
key = {
i
}
onDragStart = {
(event) => this.dragstart(event, item, i, 'top')
}
onTouchStart = {
(event) => this.touchstart(event, item, i, 'top')
}
onTouchMove = {
(event) => this.touchmove(event)
}
onTouchEnd = {
(event) => this.touchend()
}
draggable = 'true' /
>
)
})
} < /div>
<
div id = "bottom"
className = "dropzone"
onDrop = {
(event) => this.drop(event, 'bottom')
}
onDragOver = {
(e) => {
e.preventDefault()
}
}
onDragEnter = {
(e) => {
e.preventDefault()
}
} > {
this.state.bottom.map((item, i) => {
return ( < img src = {
'https://cdn.quasar.dev/img/avatar' + item + '.jpg'
}
width = "100"
height = "100"
key = {
i
}
onDragStart = {
(event) => this.dragstart(event, item, i, 'bottom')
}
onDragStart = {
(event) => this.dragstart(event, item, i, 'bottom')
}
onTouchStart = {
(event) => this.touchstart(event, item, i, 'bottom')
}
onTouchMove = {
(event) => this.touchmove(event)
}
onTouchEnd = {
(event) => this.touchend()
}
draggable = 'true' /
>
)
})
} < /div> </div >
)
}
}
ReactDOM.render( < TodoApp / > , document.getElementById("app"))
body {
background: #20262E;
padding: 20px;
font-family: Helvetica;
}
#app {
background: #fff;
border-radius: 4px;
padding: 20px;
transition: all 0.2s;
}
button {
color: #4fc08d;
}
button {
background: none;
border: solid 1px;
border-radius: 2em;
font: inherit;
padding: 0.75em 2em;
}
.dropzone {
display: flex;
height: fit-content;
min-width: 50px;
min-height: 50px;
background: #2D2D2D;
margin: 10px;
padding: 10px;
}
.dropzone>* {
margin: 0 5px;
}
.container {
overflow: hidden;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="app"></div>
So basically i wanted to make a set of divs that can be selected.
If a div clicked and mouse is moved, it will do something to all selected divs.
I wanted to test with 1 div selection first but I'm facing a problem.
If i clicked and move the mouse to quickly it will change the cursor to no-drop and the function wont execute unless I clicked again.
Here is the code:
boxes = document.querySelectorAll('.box');
let itemList = [];
let selectedList = [];
//I made selectedList because i want to put multiple selection later.
for (let box of boxes) {
itemList.push(box);
box.addEventListener("mousedown", mousedown);
};
//Each box listen mousedown and put into itemList.
function mousedown(e) {
item = e.target;
deselectAll(); //First, i will deselect all div if one of the divs clicked
item.selected = true; //Then, i select the div that clicked
selectedListUpdate(); //Then i update the selectedList to determine which one selected and not
window.addEventListener('mousemove', mousemove);
function mousemove(e) {
for (let item of selectedList) {
item.style.background = getRandomColor();
//This function will do something to all item in selectedList if the mouse move.
//Later, i want to change this function to function that can drag the div.
};
};
window.addEventListener('mouseup', mouseup);
function mouseup(e) {
window.removeEventListener('mouseup', mouseup);
window.removeEventListener('mousemove', mousemove);
}; //If i let go the mouse, moving mouse no longer do something but selected items are still selected
};
function selectedListUpdate() {
selectedList = itemList.filter(x => x.selected == true);
for (let x of selectedList) {
x.style.outline = "white solid 2px";
};
unselectedList = itemList.filter(x => x.selected == false)
for (let x of unselectedList) {
x.style.outline = "0";
};
}; //outline every item in selectedList
function deselectAll(e) {
for (x of itemList) {
x.selected = false;
};
selectedList = [];
}; //Reset selectedList
function getRandomColor() {
let r = Math.floor((Math.random() * 155) + 100);
let g = Math.floor((Math.random() * 155) + 100);
let b = Math.floor((Math.random() * 155) + 100);
return `rgb(${r} ${g} ${b})`;
}; //Example of function to execute when mouse is moving
body {
margin: 0px;
background: #333;
}
.box {
margin: 10px;
background: white;
width: 100px;
height: 100px;
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>TEST</title>
</head>
<body>
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
</body>
</html>
I think cancelling the drag event on the boxes seems to work?
boxes = document.querySelectorAll('.box');
let itemList = [];
let selectedList = [];
//I made selectedList because i want to put multiple selection later.
for (let box of boxes) {
itemList.push(box);
box.addEventListener("mousedown", mousedown);
box.addEventListener("dragstart", dragStart);
};
//Each box listen mousedown and put into itemList.
function mousedown(e) {
item = e.target;
deselectAll(); //First, i will deselect all div if one of the divs clicked
item.selected = true; //Then, i select the div that clicked
selectedListUpdate(); //Then i update the selectedList to determine which one selected and not
window.addEventListener('mousemove', mousemove);
function mousemove(e) {
for (let item of selectedList) {
item.style.background = getRandomColor();
//This function will do something to all item in selectedList if the mouse move.
//Later, i want to change this function to function that can drag the div.
};
};
window.addEventListener('mouseup', mouseup);
function mouseup(e) {
window.removeEventListener('mouseup', mouseup);
window.removeEventListener('mousemove', mousemove);
}; //If i let go the mouse, moving mouse no longer do something but selected items are still selected
};
function dragStart(e) {
e.preventDefault();
return false;
}
function selectedListUpdate() {
selectedList = itemList.filter(x => x.selected == true);
for (let x of selectedList) {
x.style.outline = "white solid 2px";
};
unselectedList = itemList.filter(x => x.selected == false)
for (let x of unselectedList) {
x.style.outline = "0";
};
}; //outline every item in selectedList
function deselectAll(e) {
for (x of itemList) {
x.selected = false;
};
selectedList = [];
}; //Reset selectedList
function getRandomColor() {
let r = Math.floor((Math.random() * 155) + 100);
let g = Math.floor((Math.random() * 155) + 100);
let b = Math.floor((Math.random() * 155) + 100);
return `rgb(${r} ${g} ${b})`;
}; //Example of function to execute when mouse is moving
body {
margin: 0px;
background: #333;
}
.box {
margin: 10px;
background: white;
width: 100px;
height: 100px;
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>TEST</title>
</head>
<body>
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
</body>
</html>
I want to create a web page in which a specific image is capable to drag.
so this was my code.
<! doctype html>
<html>
<head>
<title>
</title>
<script src="13.8.js"></script>
</head>
<body>
<img src="file:\\C:\Users\X555\Pictures\poster\yellowflowers.png" id="image" alt="white lion">
<script>
var mustDrag;
var image
function set() {
image = document.getElementById("image");
image.addEventListener("mousedown", function () { mustDrag = true }, false);
window.addEventListener("mouseup", function () { mustDrag = false; }, false);
window.addEventListener("mousemove", drag, false);
}
function drag(e) {
if (mustDrag) {
e.target.style.position = 'absolute';
e.target.style.left = Number(e.x);
e.target.style.top = Number(e.y);
}
}
window.addEventListener("load", set, false);
</script>
</body>
</html>
but it doesn't function correctly .sometimes it drag whit it shouldn't. and usually throw the image in lower-left side of the window while it mustn't. I don't realize at all that whats wrong with this code?
Refer this Example, hope you will get your answer
function startDrag(e) {
// determine event object
if (!e) {
var e = window.event;
}
if(e.preventDefault) e.preventDefault();
// IE uses srcElement, others use target
targ = e.target ? e.target : e.srcElement;
if (targ.className != 'dragme') {return};
// calculate event X, Y coordinates
offsetX = e.clientX;
offsetY = e.clientY;
// assign default values for top and left properties
if(!targ.style.left) { targ.style.left='0px'};
if (!targ.style.top) { targ.style.top='0px'};
// calculate integer values for top and left
// properties
coordX = parseInt(targ.style.left);
coordY = parseInt(targ.style.top);
drag = true;
// move div element
document.onmousemove=dragDiv;
return false;
}
function dragDiv(e) {
if (!drag) {return};
if (!e) { var e= window.event};
// var targ=e.target?e.target:e.srcElement;
// move div element
targ.style.left=coordX+e.clientX-offsetX+'px';
targ.style.top=coordY+e.clientY-offsetY+'px';
return false;
}
function stopDrag() {
drag=false;
}
window.onload = function() {
document.onmousedown = startDrag;
document.onmouseup = stopDrag;
}
.dragme{
position:relative;
width: 270px;
height: 203px;
cursor: move;
}
#draggable {
background-color: #ccc;
border: 1px solid #000;
}
<img src="https://lh4.googleusercontent.com/-8tqTFxi2ebU/Ufo4j_thf7I/AAAAAAAADFM/_ZBQctm9e8E/w270-h203-no/flower.jpg" alt="drag-and-drop image script"
title="drag-and-drop image script" class="dragme">
<div id="draggable" class="dragme">Test </div>
I am trying this code but i get: document.getElementsByName(...).style is undefined
I have also a problem with the delegation, i think. Any help?
<html>
<head>
<style type="text/css">
#toolTip {
position:relative;
width:200px;
margin-top:-90px;
}
#toolTip p {
padding:10px;
background-color:#f9f9f9;
border:solid 1px #a0c7ff;
-moz-border-radius:5px;-ie-border-radius:5px;-webkit-border-radius:5px;-o-border-radius:5px;border-radius:5px;
}
#tailShadow {
position:absolute;
bottom:-8px;
left:28px;
width:0;height:0;
border:solid 2px #fff;
box-shadow:0 0 10px 1px #555;
}
#tail1 {
position:absolute;
bottom:-20px;
left:20px;
width:0;height:0;
border-color:#a0c7ff transparent transparent transparent;
border-width:10px;
border-style:solid;
}
#tail2 {
position:absolute;
bottom:-18px;
left:20px;
width:0;height:0;
border-color:#f9f9f9 transparent transparent transparent;
border-width:10px;
border-style:solid;
}
</style>
<script type='text/javascript'>
function load () {
var elements = document.getElementsByName('toolTip');
for(var i=0; i<elements.length; i++) {
document.getElementsByName(elements[i]).style.visibility = 'hidden';
}
}
</script>
</head>
<body onload="load()">
<br><br><br><br><br><br><br><br><br><br><br><br>
<a class="hd"
onMouseOver="document.getElementsByName('toolTip')[0].style.visibility = 'visible'"
onmouseout ="document.getElementsByName('toolTip')[0].style.visibility = 'hidden'">aqui</a>
<div id="toolTip" name="toolTip">
<p>i can haz css tooltip</p>
<div id="tailShadow"></div>
<div id="tail1"></div>
<div id="tail2"></div>
</div>
<br><br><br>
<a class="hd"
onMouseOver="document.getElementsByName('toolTip')[0].style.visibility = 'visible'"
onmouseout ="document.getElementsByName('toolTip')[0].style.visibility = 'hidden'">aqui</a>
<div id="toolTip" name="toolTip">
<p>i can haz css tooltip</p>
<div id="tailShadow"></div>
<div id="tail1"></div>
<div id="tail2"></div>
</div>
</body>
</html>
demo
Try changing the id toolTip to a class:
<div class="toolTip">...</div>
And change your JS to use the display style-thing, rather than visibility, nd the onmouseover's are best dealt with using JS event delegation:
function load()
{
var i, tooltips = document.getElementsByClassName('toolTip'),
mouseOver = function(e)
{//handler for mouseover
e = e || window.event;
var i, target = e.target || e.srcElement,
targetToolTip = target.nextElementSibling || nextSibling;//gets the next element in DOM (ie the tooltip)
//check if mouse is over a relevant element:
if (target.tagName.toLowerCase() !== 'a' || !target.className.match(/\bhd\b/))
{//nope? stop here, then
return e;
}
targetToolTip.style.display = 'block';//make visible
for (i=0;i<tooltips.length;i++)
{//closures are neat --> you have a reference to all tooltip elements from load scope
if (tooltips[i] !== targetToolTip)
{//not the one you need to see
tooltips[i].style.display = 'none';
}
}
};
for (i=0;i<tooltips.length;i++)
{
tooltips[i].style.display = 'none';
}
//add listener:
if (document.body.addEventListener)
{//IE > 9, chrome, safari, FF...
document.body.addEventListener('mouseover',mouseOver,false);
}
else
{//IE8
document.body.attachEvent('onmouseover',mouseOver);
}
}
Google JavaScript event delegation and closures if this code isn't clear, but that's just how I would tackle this kind of thing. IMO, it's fairly efficient (you could use the closure scope to keep track of the tooltip that's currently visible and not loop through all of them, too, that would be even better:
function load()
{
var i, tooltips = document.getElementsByClassName('toolTip'),
currentToolTip,//<-- reference currently visible
mouseOver = function(e)
{
e = e || window.event;
var i, target = e.target || e.srcElement,
targetToolTip = target.nextElementSibling || nextSibling;
if (target.tagName.toLowerCase() !== 'a' || !target.className.match(/\bhd\b/) || targetToolTip === currentToolTip)
{//add check for currently visible TT, if so, no further action required
return e;
}
if (currentToolTip !== undefined)
{
currentToolTip.style.display = 'none';//hide currently visible
}
targetToolTip.style.display = 'block';//make new visible
currentToolTip = targetToolTip;//keep reference for next event
};
for (i=0;i<tooltips.length;i++)
{
tooltips[i].style.display = 'none';
}
if (document.body.addEventListener)
{
document.body.addEventListener('mouseover',mouseOver,false);
}
else
{
document.body.attachEvent('onmouseover',mouseOver);
}
}
And you're there.
Edit:
To hide the tooltip on mouseout, you can either add a second listener directly:
function load()
{
var i, tooltips = document.getElementsByClassName('toolTip'),
currentToolTip,//<-- reference currently visible
mouseOver = function(e)
{
e = e || window.event;
var i, target = e.target || e.srcElement,
targetToolTip = target.nextElementSibling || nextSibling;
if (target.tagName.toLowerCase() !== 'a' || !target.className.match(/\bhd\b/) || targetToolTip === currentToolTip)
{//add check for currently visible TT, if so, no further action required
return e;
}
if (currentToolTip !== undefined)
{
currentToolTip.style.display = 'none';//hide currently visible
}
targetToolTip.style.display = 'block';//make new visible
currentToolTip = targetToolTip;//keep reference for next event
},
mouseOut = function(e)
{
e = e || window.event;
var movedTo = document.elementFromPoint(e.clientX,e.clientY);//check where the cursor is NOW
if (movedTo === curentToolTip || currentToolTip === undefined)
{//if cursor moved to tooltip, don't hide it, if nothing is visible, stop
return e;
}
currentTooltip.style.display = 'none';
currentTooltip = undefined;//no currentToolTip anymore
};
for (i=0;i<tooltips.length;i++)
{
tooltips[i].style.display = 'none';
}
if (document.body.addEventListener)
{
document.body.addEventListener('mouseover',mouseOver,false);
document.body.addEventListener('mouseout',mouseOut,false);
}
else
{
document.body.attachEvent('onmouseover',mouseOver);
document.body.attachEvent('onmouseout',mouseOut);
}
}
Note, this is completely untested. I'm not entirely sure if IE < 9 supports elementFromPoint (gets the DOM element that is rendered at certain coordinates), or even if the IE event object has the clientX and clientY properties, but I figure a quick google will tell you more, including how to get the coordinates and the element that is to be found under the cursor in old, crummy, ghastly IE8, but this should help you on your way. Of course, if you don't want the contents of the tooltip to be selectable, just change the mouseOut function to:
mouseOut = function(e)
{
e = e || window.event;
var target = e.target || e.srcElement;
if (currentToolTip)
{
currentToolTip.style.diplay = 'none';
currentToolTip = undefined;
}
};
No need to check if the mouseout was on the correct element, just check if there is a current tooltip, and hide it.
Try using classes to mark the tooltips:
<div id="toolTip1" class="toolTip">
<p>i can haz css tooltip</p>
<div id="tailShadow"></div>
<div id="tail1"></div>
<div id="tail2"></div>
</div>
And JQuery to toggle the visibility using the class as the selector:
$('.toolTip').attr('visibility', 'hidden')
Definitely clean up the non-unique Id's - this will cause you no end of troubles otherwise
Your problem is likely because you're using the same id for both the tooltips. This is invalid; an id should be unique -- only one element in a given page should have a specific ID.
If you need a shared identifier for multiple objects, use a class instead.
I built a tooltip with a border in pure js that doesn't use hover.
html
<div id="infoId" class='info' style="font-variant:small-caps;text-align:center;padding-top:10px;">
<span id="innerspanid">
</span>
</div>
</div>
<input id="startbtn" class="getstartedbtn" type="button" value="Start >" />
</div>
js
function getTextWidth(text, font) {
// re-use canvas object for better performance
const canvas = getTextWidth.canvas || (getTextWidth.canvas = document.createElement("canvas"));
const context = canvas.getContext("2d");
context.font = font;
const metrics = context.measureText(text);
return metrics.width;
}
function getCssStyle(element, prop) {
return window.getComputedStyle(element, null).getPropertyValue(prop);
}
function getCanvasFontSize(el = document.body) {
const fontWeight = getCssStyle(el, 'font-weight') || 'normal';
const fontSize = getCssStyle(el, 'font-size') || '16px';
const fontFamily = getCssStyle(el, 'font-family') || 'Times New Roman';
return `${fontWeight} ${fontSize} ${fontFamily}`;
}
let arrowDimensionWidth = 20;
let arrowDimensionHeight = 20;
let tooltipTextHorizontalMargin = 50;
function openTooltip(text) {
let innerSpan = document.getElementById("innerspanid");
innerSpan.innerHTML = text;
let computedW = getTextWidth(text, getCanvasFontSize(innerSpan)) + tooltipTextHorizontalMargin;
let pointer = document.getElementById('pointer')
pointer.style.right = (((computedW / 2) - (arrowDimensionWidth / 2)) - (0)) + 'px';
let elem = document.getElementById('tooltipHost').parentNode.querySelector('div.info_container');
elem.style.left = ((tooltipHost.getBoundingClientRect().width - computedW) / 2) + "px";
elem.style.width = computedW + "px";
elem.style.display = 'block';
}
function buildTooltip() {
let elements = document.querySelectorAll('div.tooltip');
// Create a canvas element where the triangle will be drawn
let canvas = document.createElement('canvas');
canvas.width = arrowDimensionWidth; // arrow width
canvas.height = arrowDimensionHeight; // arrow height
let ctx = canvas.getContext('2d');
ctx.strokeStyle = 'darkred'; // Border color
ctx.fillStyle = 'white'; // background color
ctx.lineWidth = 1;
ctx.translate(-0.5, -0.5); // Move half pixel to make sharp lines
ctx.beginPath();
ctx.moveTo(1, canvas.height); // lower left corner
ctx.lineTo((canvas.width / 2), 1); // upper right corner
ctx.lineTo(canvas.width, canvas.height); // lower right corner
ctx.fill(); // fill the background
ctx.stroke(); // stroke it with border
ctx.fillRect(0, canvas.height - 0.5, canvas.width - 1, canvas.height + 2); //fix bottom row
// Create a div element where the triangle will be set as background
pointer = document.createElement('div');
pointer.id = "pointer"
pointer.style.width = canvas.width + 'px';
pointer.style.height = canvas.height + 'px';
pointer.innerHTML = ' ' // non breaking space
pointer.style.backgroundImage = 'url(' + canvas.toDataURL() + ')';
pointer.style.position = 'absolute';
pointer.style.top = '2px';
pointer.style.zIndex = '1'; // place it over the other elements
let idx;
let len;
for (idx = 0, len = elements.length; idx < len; ++idx) {
let elem = elements[idx];
let text = elem.querySelector('div.info');
let info = document.createElement('div');
text.parentNode.replaceChild(info, text);
info.className = 'info_container';
info.appendChild(pointer.cloneNode());
info.appendChild(text);
}
}
window.addEventListener('load', buildTooltip);
window.addEventListener('load', wireup);
function wireup() {
document.getElementById('startbtn').addEventListener('click', function (evt1) {
openTooltip("bad email no # sign");
return false;
});
}
css
div.tooltip {
position: relative;
display: inline-block;
}
div.tooltip > div.info {
display: none;
}
div.tooltip div.info_container {
position: absolute;
left: 0px;
width: 100px;
height: 70px;
display: none;
}
div.tooltip div.info {
position: absolute;
left: 0px;
text-align: left;
background-color: white;
font-size: 18px;
left: 1px;
right: 1px;
top: 20px;
bottom: 1px;
color: #000;
padding: 5px;
overflow: auto;
border: 1px solid darkred;
border-radius: 5px;
}