Je veux détecter où une MouseEvent
s'est produite, en coordonnées par rapport à l'élément sur lequel le clic a été effectué. Pourquoi? Parce que je veux ajouter un élément enfant positionné de manière absolue à l'emplacement cliqué.
Je sais comment le détecter en l'absence de transformations CSS3 (voir la description ci-dessous). Cependant, lorsque j'ajoute une transformation CSS3, mon algorithme se casse et je ne sais pas comment le réparer.
Je n'utilise aucune bibliothèque JavaScript et je veux comprendre comment les choses fonctionnent en JavaScript simple. Alors, s'il vous plaît, ne répondez pas avec "utilisez simplement jQuery".
À propos, je veux une solution qui fonctionne pour tous les MouseEvents, pas seulement pour "cliquer". Cela n’importe pas, car j’estime que tous les événements de souris partagent les mêmes propriétés. Par conséquent, la même solution devrait fonctionner pour tous.
Selon la spécification DOM niveau 2 , a MouseEvent
a peu de propriétés permettant d'obtenir les coordonnées de l'événement:
screenX
et screenY
renvoient les coordonnées de l'écran (l'Origine est le coin supérieur gauche du moniteur de l'utilisateur)clientX
et clientY
renvoient les coordonnées relatives à la fenêtre de visualisation du document.Ainsi, afin de trouver la position de la MouseEvent
par rapport au contenu de l'élément cliqué, je dois faire le calcul suivant:
ev.clientX - this.getBoundingClientRect().left - this.clientLeft + this.scrollLeft
ev.clientX
est la coordonnée relative à la fenêtre du documentthis.getBoundingClientRect().left
est la position de l'élément par rapport à la fenêtre de visualisation du documentthis.clientLeft
est la quantité de bordure (et barre de défilement) entre la limite d'élément et les coordonnées internesthis.scrollLeft
est la quantité de défilement à l'intérieur de l'élémentgetBoundingClientRect()
, clientLeft
et scrollLeft
sont spécifiés dans Module de vue CSSOM .
Déroutant? Essayez le morceau suivant de JavaScript et HTML . En cliquant, un point rouge devrait apparaître exactement là où le clic a eu lieu. Cette version est "assez simple" et fonctionne comme prévu.
function click_handler(ev) {
var rect = this.getBoundingClientRect();
var left = ev.clientX - rect.left - this.clientLeft + this.scrollLeft;
var top = ev.clientY - rect.top - this.clientTop + this.scrollTop;
var dot = document.createElement('div');
dot.setAttribute('style', 'position:absolute; width: 2px; height: 2px; top: '+top+'px; left: '+left+'px; background: red;');
this.appendChild(dot);
}
document.getElementById("experiment").addEventListener('click', click_handler, false);
<div id="experiment" style="border: 5px inset #AAA; background: #CCC; height: 400px; position: relative; overflow: auto;">
<div style="width: 900px; height: 2px;"></div>
<div style="height: 900px; width: 2px;"></div>
</div>
Maintenant, essayez d’ajouter une CSS transform
:
#experiment {
transform: scale(0.5);
-moz-transform: scale(0.5);
-o-transform: scale(0.5);
-webkit-transform: scale(0.5);
/* Note that this is a very simple transformation. */
/* Remember to also think about more complex ones, as described below. */
}
L'algorithme ignore les transformations et calcule donc une mauvaise position. De plus, les résultats sont différents entre Firefox 3.6 et Chrome 12. Opera 11.50 se comporte exactement comme Chrome.
Dans cet exemple, la seule transformation était la mise à l'échelle. Je pouvais donc multiplier le facteur de mise à l'échelle pour calculer la coordonnée correcte. Cependant, si nous pensons à des transformations arbitraires (échelle, rotation, inclinaison, translation, matrice) et même à des transformations imbriquées (un élément transformé dans un autre élément transformé), nous avons vraiment besoin d'un meilleur moyen de calculer les coordonnées.
Le comportement que vous rencontrez est correct et votre algorithme ne rompt pas. Tout d'abord, CSS3 Transforms est conçu pour ne pas interférer avec le modèle de boîte.
Pour essayer et expliquer ...
Lorsque vous appliquez une transformation CSS3 à un élément. L'élément assume une sorte de positionnement relatif. En ce sens que les éléments environnants ne sont pas affectés par l'élément transformé.
par exemple. imaginez trois div dans une rangée horizontale. Si vous appliquez une transformation d’échelle pour réduire la taille du div du centre. Les divisions environnantes ne se déplaceront pas pour occuper l'espace qui occupait auparavant l'élément transformé.
exemple: http://jsfiddle.net/AshMokhberi/bWwkC/
Ainsi, dans le modèle de boîte, l'élément ne change pas réellement de taille. Seulement, sa taille de rendu change.
Vous devez également garder à l’esprit que vous appliquez une transformation d’échelle, de sorte que la taille «réelle» de vos éléments est en fait la même que sa taille originale. Vous ne faites que changer sa taille perçue.
Expliquer..
Imaginez que vous créez une div avec une largeur de 1000px et que vous la réduisiez à la moitié. La taille interne de la div est toujours 1000px, pas 500px.
Donc, la position de vos points est correcte par rapport à la taille "réelle" de la div.
J'ai modifié votre exemple pour illustrer.
Instructions
http://jsfiddle.net/AshMokhberi/EwQLX/
Ainsi, afin que les coordonnées des clics de la souris correspondent à l'emplacement visible sur le div, vous devez comprendre que la souris restitue des coordonnées en fonction de la fenêtre et que vos décalages de div sont également basés sur sa taille "réelle". .
Comme la taille de votre objet est relative par rapport à la fenêtre, la seule solution consiste à mettre à l’échelle les coordonnées de décalage avec la même valeur d’échelle que votre div.
Toutefois, cela peut être délicat en fonction du lieu où vous définissez la propriété Transform-Origin de votre div. Comme cela va effectuer les compensations.
Vois ici.
http://jsfiddle.net/AshMokhberi/KmDxj/
J'espère que cela t'aides.
si element est conteneur et positionné en absolu ou relatif, vous pouvez placer l'élément à l'intérieur de l'élément. .et après chaque déplacement, utilisez document.elementFromPoint (event.clientX, event.clientY) =))))
Vous pouvez utiliser la recherche binaire pour le rendre plus rapide . Semble terrible, mais cela fonctionne
http://jsfiddle.net/3VT5N/3/ - démo
De plus, la méthode Webkit webkitConvertPointFromPageToNode peut être utilisée:
var div = document.createElement('div'), scale, point;
div.style.cssText = 'position:absolute;left:-1000px;top:-1000px';
document.body.appendChild(div);
scale = webkitConvertPointFromNodeToPage(div, new WebKitPoint(0, 0));
div.parentNode.removeChild(div);
scale.x = -scale.x / 1000;
scale.y = -scale.y / 1000;
point = webkitConvertPointFromPageToNode(element, new WebKitPoint(event.pageX * scale.x, event.pageY * scale.y));
point.x = point.x / scale.x;
point.y = point.y / scale.x;
une autre façon est de placer 3 divs dans les coins de cet élément, que trouver la matrice de transformation ... mais ne fonctionne également que pour les conteneurs positionnés éléments - 4esn0k
Pour obtenir les coordonnées d'un MouseEvent
Voir également:
clientX
à partir du bord de remplissage de l'élément au lieu du contenu Edge: Bogues du navigateur GTalbot MSIE 8: event.offsetX, event.offsetY en tant que coordonnées de la souris à l'intérieur du champ de remplissage de la cibleDe loin le plus rapide. La réponse acceptée prend environ 40 à 70 ms sur mon site de transformations 3d, elle prend généralement moins de 20 ( violon ):
function getOffset(event,elt){
var st=new Date().getTime();
var iterations=0;
//if we have webkit, then use webkitConvertPointFromPageToNode instead
if(webkitConvertPointFromPageToNode){
var webkitPoint=webkitConvertPointFromPageToNode(elt,new WebKitPoint(event.clientX,event.clientY));
//if it is off-element, return null
if(webkitPoint.x<0||webkitPoint.y<0)
return null;
return {
x: webkitPoint.x,
y: webkitPoint.y,
time: new Date().getTime()-st
}
}
//make full-size element on top of specified element
var cover=document.createElement('div');
//add styling
cover.style.cssText='height:100%;width:100%;opacity:0;position:absolute;z-index:5000;';
//and add it to the document
elt.appendChild(cover);
//make sure the event is in the element given
if(document.elementFromPoint(event.clientX,event.clientY)!==cover){
//remove the cover
cover.parentNode.removeChild(cover);
//we've got nothing to show, so return null
return null;
}
//array of all places for rects
var rectPlaces=['topleft','topcenter','topright','centerleft','centercenter','centerright','bottomleft','bottomcenter','bottomright'];
//function that adds 9 rects to element
function addChildren(elt){
iterations++;
//loop through all places for rects
rectPlaces.forEach(function(curRect){
//create the element for this rect
var curElt=document.createElement('div');
//add class and id
curElt.setAttribute('class','offsetrect');
curElt.setAttribute('id',curRect+'offset');
//add it to element
elt.appendChild(curElt);
});
//get the element form point and its styling
var eltFromPoint=document.elementFromPoint(event.clientX,event.clientY);
var eltFromPointStyle=getComputedStyle(eltFromPoint);
//Either return the element smaller than 1 pixel that the event was in, or recurse until we do find it, and return the result of the recursement
return Math.max(parseFloat(eltFromPointStyle.getPropertyValue('height')),parseFloat(eltFromPointStyle.getPropertyValue('width')))<=1?eltFromPoint:addChildren(eltFromPoint);
}
//this is the innermost element
var correctElt=addChildren(cover);
//find the element's top and left value by going through all of its parents and adding up the values, as top and left are relative to the parent but we want relative to teh wall
for(var curElt=correctElt,correctTop=0,correctLeft=0;curElt!==cover;curElt=curElt.parentNode){
//get the style for the current element
var curEltStyle=getComputedStyle(curElt);
//add the top and left for the current element to the total
correctTop+=parseFloat(curEltStyle.getPropertyValue('top'));
correctLeft+=parseFloat(curEltStyle.getPropertyValue('left'));
}
//remove all of the elements used for testing
cover.parentNode.removeChild(cover);
//the returned object
var returnObj={
x: correctLeft,
y: correctTop,
time: new Date().getTime()-st,
iterations: iterations
}
return returnObj;
}
et incluez également les CSS suivants dans la même page:
.offsetrect{
position: absolute;
opacity: 0;
height: 33.333%;
width: 33.333%;
}
#topleftoffset{
top: 0;
left: 0;
}
#topcenteroffset{
top: 0;
left: 33.333%;
}
#toprightoffset{
top: 0;
left: 66.666%;
}
#centerleftoffset{
top: 33.333%;
left: 0;
}
#centercenteroffset{
top: 33.333%;
left: 33.333%;
}
#centerrightoffset{
top: 33.333%;
left: 66.666%;
}
#bottomleftoffset{
top: 66.666%;
left: 0;
}
#bottomcenteroffset{
top: 66.666%;
left: 33.333%;
}
#bottomrightoffset{
top: 66.666%;
left: 66.666%;
}
Essentiellement, il divise l'élément en 9 carrés et détermine le clic dans lequel était le clic via document.elementFromPoint
. Il divise ensuite cela en 9 carrés plus petits, etc. jusqu'à ce qu'il soit précis au pixel près. Je sais que je l'ai trop commenté. La réponse acceptée est plusieurs fois plus lente que cela.
EDIT: Il est maintenant encore plus rapide, et si l’utilisateur utilise Chrome ou Safari, il utilisera une fonction native conçue à cet effet au lieu des 9 secteurs et pourra le faire systématiquement en MOINS DE 2 MILLISECONDS!
Cela semble très bien fonctionner pour moi
var elementNewXPosition = (event.offsetX != null) ? event.offsetX : event.originalEvent.layerX;
var elementNewYPosition = (event.offsetY != null) ? event.offsetY : event.originalEvent.layerY;
EDIT: ma réponse est non testée, WIP, je mettrai à jour lorsque je le ferai fonctionner.
J'implémente un polyfill des geomtetry-interfaces . La méthode DOMPoint.matrixTransform
que je ferai ensuite, ce qui signifie que nous devrions pouvoir écrire ce qui suit pour mapper une coordonnée de clic sur un élément DOM transformé (éventuellement imbriqué):
// target is the element nested somewhere inside the scene.
function multiply(target) {
let result = new DOMMatrix;
while (target && /* insert your criteria for knowing when you arrive at the root node of the 3D scene*/) {
const m = new DOMMatrix(target.style.transform)
result.preMultiplySelf(m) // see w3c DOMMatrix (geometry-interfaces)
target = target.parentNode
}
return result
}
// inside click handler
// traverse from nested node to root node and multiply on the way
const matrix = multiply(node)
const relativePoint = DOMPoint(clickX, clickY, 0, 800).matrixTransform(matrix)
relativePoint
sera le point relatif à la surface de l'élément sur lequel vous avez cliqué.
Un w3c DOMMatrix peut être construit avec un argument de chaîne de transformation CSS, ce qui le rend très facile à utiliser en JavaScript.
Malheureusement, cela ne fonctionne pas encore (seul Firefox a une implémentation d’interfaces géométriques et mon polyfill n’accepte pas encore de chaîne de transformation CSS). Voir: https://github.com/trusktr/geometry-interfaces/blob/7872f1f78a44e6384529e22505e6ca0ba9b30a4d/src/DOMMatrix.js#L15-L18
Je mettrai à jour cela une fois que je l'aurai mis en œuvre et que j'aurai un exemple de travail. Les demandes de tirage sont les bienvenues!
EDIT: la valeur 800
est la perspective de la scène. Je ne suis pas sûr que ce soit la quatrième valeur du constructeur DOMPoint
lorsque nous avons l’intention de faire quelque chose comme cela. De plus, je ne suis pas sûr de devoir utiliser preMultiplySelf
ou postMultiplySelf
. Je le découvrirai une fois que je l'aurai au moins fonctionné (les valeurs peuvent être incorrectes au début) et mettra à jour ma réponse.
J'ai ce problème et je commence à essayer de calculer la matrice . J'ai créé une bibliothèque autour de celle-ci: https://github.com/ombr/referentiel
$('.referentiel').each ->
ref = new Referentiel(this)
$(this).on 'click', (e)->
input = [e.pageX, e.pageY]
p = ref.global_to_local(input)
$pointer = $('.pointer', this)
$pointer.css('left', p[0])
$pointer.css('top', p[1])
Qu'est-ce que tu penses ?
var p = $( '.divName' );
var position = p.position();
var left = (position.left / 0.5);
var top = (position.top / 0.5);
Je travaille sur un polyfill pour transcoder les coordonnées DOM. L'API GeometryUtils n'est pas encore disponible (@see https://drafts.csswg.org/cssom-view/ ). J'ai créé un code "simple" en 2014 pour transformer les coordonnées, comme localToGlobal, globalToLocal et localToLocal. Ce n'est pas encore fini, mais ça fonctionne :) Je pense que je vais le finir dans les mois à venir (05.07.2017), donc si vous avez encore besoin d'une API pour effectuer la transformation des coordonnées, essayez-la: https: // github. com/jsidea/jsideabibliothèque principale de jsidea . Son pas encore stable (pre alpha) . Vous pouvez l'utiliser comme ça:
Créez votre instance de transformation:
var transformer = jsidea.geom.Transform.create(yourElement);
Le modèle de boîte que vous souhaitez transformer (par défaut: "border", sera remplacé plus tard par ENUM):
var toBoxModel = "border";
Le modèle de boîte d'où proviennent vos coordonnées d'entrée (par défaut: "bordure"):
var fromBoxModel = "border";
Transformez vos coordonnées globales (ici {x: 50, y: 100, z: 0}) en espace local. Le point résultant a 4 composantes: x, y, z et w.
var local = transformer.globalToLocal(50, 100, 0, toBoxModel, fromBoxModel);
J'ai implémenté d'autres fonctions telles que localToGlobal et localToLocal . Si vous voulez l'essayer, téléchargez simplement la version build et utilisez le fichier jsidea.min.js.
Téléchargez la première version ici: Téléchargez le code TypeScript
N'hésitez pas à changer le code, je ne le mets jamais sous licence :)