web-dev-qa-db-fra.com

Comment obtenir les coordonnées MouseEvent d'un élément qui a transformé CSS3?

Je veux détecter 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.


Informations d'arrière-plan

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 document
  • this.getBoundingClientRect().left est la position de l'élément par rapport à la fenêtre de visualisation du document
  • this.clientLeft est la quantité de bordure (et barre de défilement) entre la limite d'élément et les coordonnées internes
  • this.scrollLeft est la quantité de défilement à l'intérieur de l'élément

getBoundingClientRect() , clientLeft et scrollLeft sont spécifiés dans Module de vue CSSOM .

Expérience sans transformation CSS (ça marche)

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>

Expérimentez en ajoutant une transformation CSS (ça échoue)

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.

45
Denilson Sá Maia

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

  1. Cliquez sur le div et maintenez votre souris dans la même position.
  2. Trouvez le point dans la mauvaise position.
  3. Appuyez sur Q, le div deviendra la taille correcte.
  4. Déplacez votre souris pour trouver le point dans la position correcte à l'endroit où vous avez cliqué.

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.

14
AshHeskes

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

9
4esn0k

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;
3
4esn0k

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

démo: http://jsfiddle.net/dAwfF/3/

3
4esn0k
2
Garrett

De 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!

2
markasoftware

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; 
1
bryan

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.

0
trusktr

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 ?

0
Luc Boissaye

Fonctionne bien relative ou absolue :) solution simple

var p = $( '.divName' );
var position = p.position();  
var left = (position.left  / 0.5);
var top =  (position.top  / 0.5);
0
raghugolconda

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 :)

0
jbenker