Kirjautuminen

Haku

Tehtävät

Keskustelu: Koodit: HTML: Drag'n'resize -moduuli PC ja kosketuslaitteille

uta [22.02.2013 19:26:10]

#

Tein itselleni pienen drag/resize moduulin, ja ajattelin tehdä siitä tutoriaalin. Vertailun vuoksi esimerkiksi jQueryllä tämän esimerkin toiminnot vaatisivat kolmen(3)! kirjaston incluudaamisen (jQuery.js + jQueryUI.js + jQueryTouchPunch.js). Toki niissä on paljon enemmän ominaisuuksia, mutta usein niistä suurin osa jää käyttämättä ja se ylimääräinen kasa koodia vain roikkuu perässä hidastamassa suorituskykyä. Varsinkin jos elementtejä on paljon, nopeuseron todella huomaa.

Tämä moduuli toimii kaikilla moderneilla ja ei niin moderneilla selaimilla (testattu IE7-9, Chrome, FF, Opera ja Safari). Kosketuslaitteista testattu ikivanhalla iPod Touchilla, jossa toimi mainiosti.

Mutta, asiaan. Aloitaan tekemällä Html-sivu, johon tehdään neljä laatikkoa jotka olisi tarkoitus tehdä muokattavaksi moduulin avulla;

<!DOCTYPE html>
<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
    <title></title>
    <style>
        body{
            background-color: #FDFDFD
        }
        #box-holder{
            position: relative;
            margin: 20px auto;
            height: 360px;
            width: 422px;
        }
        .box{
            /* pakkokäynnistää GPU-kiihdytyksen, jolloin elementin liike
            olisi mahollisimman sulava (kohdennettu lähinnä Safarille, jossa on
            tällä huomattava ero) */
            -webkit-transform: translatez(0);
            box-shadow: 0px 0px 4px #EEEEEE;
            border: 1px solid #CCCCCC;
            background: #FFFFFF;
            position: absolute;
            height: 170px;
            width: 200px
        }
        #box-b{ left: 220px }
        #box-c{ top: 190px }
        #box-d{ top: 190px; left: 220px }

        .handle{
            background-color: #DDDDDD;
            vertical-align: middle;
            position: absolute;
            text-align: center;
            line-height: 0.3;
            cursor: nwse-resize;
            height: 25px;
            width: 25px;
            bottom: 0;
            right: 0
        }
    </style>
  </head>
<body>
    <div id="box-holder">
        <div class="box" id="box-a"><div class="handle" id="handle-a"></div></div>
        <div class="box" id="box-b"><div class="handle" id="handle-b"></div></div>
        <div class="box" id="box-c"><div class="handle" id="handle-c"></div></div>
        <div class="box" id="box-d"><div class="handle" id="handle-d"></div></div>
    </div>
</body>
</html>

Sitten lähdetään kirjoittelemaan itse moduulia. Moduulilla tässä tapauksessa tarkoitan yhtä 'singleton'-instanssia jossa voi pyöritellä raahailuun tarvittavia settejä sisäisesti. Tämä onnistuu javascriptin closure-ominaisuuden avulla jossa moduuli kapseloidaan välittömästi suoritettavan funktion sisään, jonka arvot ja funktiot ovat vain sen itsensä käytössä eivätkä näy sen ulkopuolelle. Rakenne on seuraava.

var DragnResize = (function () {

    'use strict';

    //--------------------------------------------------------------------------
    // muuttujien ja funktioiden alustukset
    //--------------------------------------------------------------------------


    //--------------------------------------------------------------------------
    //muuttujien ja funktioiden määrittelyt
    //--------------------------------------------------------------------------


    //--------------------------------------------------------------------------
    //moduulin ulkopuoliseen käyttöön palautettava data.
    return {
        //...
    };
    //--------------------------------------------------------------------------

}());

Tässä moduulissa alustusosio määritellään seuraavaasti;

    //ydinfunktiot
var dragStart, drag, dragEnd, resizeStart, resize, resizeEnd,
    //käsiteltävä elementti
    element, startElemX, startElemY, startHeight, startWidth,
    //DOM-eventin tyyppi (mousedown, touchstart jne..
    onStart, onMove, onEnd,
    //hiiren/sormen sijanti
    startUserX, startUserY,

    //helperit. toinen lisää, ja toinen poistaa DOM-kuuntelijan
    addListener = function (elem, event, func) {
        if (document.addEventListener) {
            elem.addEventListener(event, func, false);//selaimet lukuunottamatta IE:tä
        } else {
            elem.attachEvent('on' + event, func);//IE
        }
    },
    removeListener = function (element, event, handler) {
        if (document.removeEventListener) {
            element.removeEventListener(event, handler, false);
        } else {
            element.detachEvent('on' + event, handler);
        }
    };

    //tallennetaan evetien tyypit laitteen mukaan
    if (!Object.prototype.hasOwnProperty.call(window, 'ontouchstart')) {
        onStart = 'mousedown';
        onMove  = 'mousemove';
        onEnd   = 'mouseup';
    } else {
        onStart = 'touchstart';
        onMove  = 'touchmove';
        onEnd   = 'touchend';
    }

Määrittelyosioon laitetaan aluksi;

dragStart = function (elem, e) {

    element = elem;
    console.log('Painoit elementtiä ' + element.id);

    //lisätään kuuntelijat, milloin:
    addListener(document, onMove, drag);  //liikutetaan hiirtä/sormea
    addListener(document, onEnd, dragEnd);//nostetaan hiiren nappi/sormi

    /* raahaus-, ja lopetuskuuntelija kannattaa kohdistaa dokumentin
     * juureen ettei funktioiden suoritukset katkeaisi hiiren poistuessa
     * tarkasti määritellyn elementin päältä. */
};
drag = function (e) {

    console.log('Raahaat elementtiä ' + element.id);

};
dragEnd = function () {

    console.log('Lopetit elementin ' + element.id + ' raahaamisen');

    /* On tärkeää muistaa myös poistaa lisätyt kuuntelija.
     * Muutoin elemnetti jää seuraamaan hiirtä vaikka on nostettu
     * hiiren nappi ylös. Not nice. */
    removeListener(document, onMove, drag);
    removeListener(document, onEnd, dragEnd);
};


resizeStart = function (elem, e) {

    element = elem;
    console.log('Painoit elemnttiä' + element.id);

};
resize = function (e) {

    console.log('Muutat elementin ' + element.id + ' korkeutta');

};
resizeEnd = function () {

    console.log('Lopetit elementin ' + element.id + ' korkeuden muuttamisen');

    removeListener(document, onMove, resize);
    removeListener(document, onEnd, resizeEnd);

};

Ja palautetaan ulkopuolelle kaksi funktiota: toinen lisää elementille kuuntelijan joka
asettaa elementin raahattavaksi ja toinen niin että sen kokoa voi muuttaa;

return {
    draggable: function (elem) {
        addListener (elem, onStart, function(e) {
            dragStart(elem, e);
        });
    },
    resizable: function (handle) {
        addListener (handle, onStart, function(e) {
            resizeStart(handle.parentNode, e);
        });
    }
};

Tässä vaiheessa voit testata koodin toimintaa lisäämällä elementeille kuuntelijat. Lisää Html-sivuun script-elementti johon kirjoitetaan;

//window kutsuu onload funktiota heti, kun dokumentin sisältö on latautunut
window.onload = function () {

    var boxes = document.getElementById('box-holder').children, i;

    //lisätään jokaiselle laatikolle kuuntelija
    //jota kutsutaan kun elementtiä klikataan/sormeillaan
    for (i = 0; i < boxes.length; i += 1) {

        DragnResize.draggable(boxes[i]);
        DragnResize.resizable(boxes[i].firstChild);

    }

}

// Copy&Pasteta moduuli tähän

Nyt consoleen pitäisi tulla funktioihin kirjoitettu kuvaus tapahtumasta kun laatikkoa painetaan, liikutetaan hiirtä ym. Jos joku ei ennen käyttänyt consolea, suosittelen vahvasti tutustumaan aiheeseen. Pikaohje kuinka consolen viestit saa näkyviin eri selaimilla;

Firefox: lataa firebug ja käynnistä se oikeasta ylälaidasta ja näkymäksi console

Chrome: Ctrl+Shift+J / hiiren oikea klikkaus -> tarkastele elementtiä -> console

IE: F12 -> vasemmalta script -> oikealta console

Sitten. Muokataan määrittelyosio lopulliseen muotoonsa, ensin vuorossa raahaustoiminto.

dragStart-funktion console.log() paikalle;

//tallennetaan elementin aloitussijainti
startElemX = parseInt(elem.offsetLeft);
startElemY = parseInt(elem.offsetTop);

//ja hiiren/sormen aloitussijainti
if (!e.touches) {
    startUserX = e.clientX;
    startUserY = e.clientY;
} else {
    startUserX = e.touches[0].clientX;
    startUserY = e.touches[0].clientY;
}

if (!element.style.position) {
    element.style.position = 'absolute';
}

drag-funktion console.log() paikalle;

/* Jokaisella tämän funktion kutsukerralla lisätään tai vähennetään
 * elementin x-, ja y -sijaintia verrattuna sen alkuperäiseen sijaintiin */
var currentUserX, currentUserY;

//Tämä poistaa kosketuslaitteiden natiivin skrollauksen
e.returnValue = false; // IE
if (e.preventDefault) {// muut selaimet
    e.preventDefault();
}

if (!e.touches) {
    currentUserX = e.clientX;
    currentUserY = e.clientY;
} else {
    currentUserX = e.touches[0].clientX;
    currentUserY = e.touches[0].clientY;
}

element.style.left =
    currentUserX - //otetaan tämänhetkinen hiiren sijainti dokumentissa
    startUserX +   //vähennetään siitä lähtötilanteen sijainti
    startElemX +   //ja lisätään siihen alkuperäisen elementin sijainti
    'px';

//sama juttu suunnassa y
element.style.top = currentUserY - startUserY + startElemY + 'px';

Ja se on siinä raahauksen osalta! Elementin pitäisi seurata hiirtä/sormea sinne minne se dokumentissä ikinä vaeltaakin.

Sitten resize-osio. resizeStart- funktio kokonaisuudessaan;

var style;
element = elem;

/* Jos elementin parentilla tai child-elementillä on kuuntelijoita,
 * tämä estää sen ettei niitä kutsuta samalla. Muutoin tämä funktio
 * kutsuisi drag-funktiota joka tietysti ei ole tarkoitus */
e.cancelBubble = true; // IE
e.returnValue  = false;
if (e.stopPropagation) {
    e.stopPropagation(); // muut selaimet
    e.preventDefault();
}

if (!e.touches) {
    startUserX = e.clientX;
    startUserY = e.clientY;
} else {
    startUserX = e.touches[0].clientX;
    startUserY = e.touches[0].clientY;
}

//Näin pääsee käsiksi elementin senhetkisiin tyylimääreisiin
                                    //kunnolliset selaimet              //IE
style = (window.getComputedStyle) ? window.getComputedStyle(element) : element.currentStyle;
startHeight = parseFloat(style.height);
startWidth  = parseFloat(style.width);

addListener(document, onMove, resize);
addListener(document, onEnd, resizeEnd);

resize-funktioon console.log() paikalle;

var currentUserX, currentUserY;

if (!e.touches) {
    currentUserX = e.clientX;
    currentUserY = e.clientY;
} else {
    currentUserX = e.touches[0].clientX;
    currentUserY = e.touches[0].clientY;
}

//sama juttu kuin drag-funktiossa, erona että muutetaan kokoa
element.style.width  = currentUserX - startUserX + startWidth + 'px';
element.style.height = currentUserY - startUserY + startHeight + 'px';

Ja näin, meillä on raahattava elemetti jonka kokoakin pystyy muuttamaan!

Vuorossa protip;

Jos dokumentissa on paljon raahattavia elementtejä, ei on kovinkaan järkevää lisätä kuuntelijaa jokaiselle elementille erikseen vaan pelkästään niiden parent-elementille. Klikattu child-elementti saadaan selville viittaamalla event-objektin targetiin. IE:ssä vastaava on srcElement. Muokataan moduuli tukemaan tätä toimintoa muokkaamalla sen palauttamaa oliota niin, että sille voi passata funktion jossa määritellään parent-elementin target-elementti, ja lisätään draggable toiminto sille:

return {
    draggable: function (elem, func) {
        addListener (elem, onStart, function(e) {
            /* jos funktio on määritelty, kutsutaan sitä ja annetaan
             * parametreiksi tarvittavat arvot */
            if (func) {
                func(dragStart, e);
            } else {
            //muutoin toimitaan samalla tavalla kuin aikaisemminkin
                dragStart(elem, e);
            }
        });
    },
    resizable: function (handle, func) {
        addListener (handle, onStart, function(e) {
            if (func) {
                func(resizeStart, e);
            } else {
                resizeStart(handle.parentNode, e);
            }
        });
    }
};

Käyttö tapahtuu näin;

window.onload = function () {

    DragnResize.draggable(document.getElementById('box-holder'), function (dragStart, e) {

        var clickedElement = e.target || e.srcElement;

        if (clickedElement.className === 'box') {

            dragStart(clickedElement, e);

        }
    });
    DragnResize.resizable(document.getElementById('box-holder'), function (resizeStart, e) {

        var clickedElement = e.target || e.srcElement;

        if (clickedElement.className === 'handle') {

            resizeStart(clickedElement.parentNode, e);

        }
    });

}

Vielä mukana? Jos niin, kiitän lukijaa. Voi testata osoitteessa; http://utasgrounds.com/dragnresize/ (jos osasin htaccessata oikein :)).

Metabolix [23.02.2013 20:33:13]

#

Vinkin idea on sinänsä hyvä ja koodikin on melko siistiä. Sen sijaan esitystapa ei ole aivan paras mahdollinen. Alla on eräitä kommentteja.

Vinkin teknisestä sisällöstä

Voisi mainita, että ilman absoluuttista sijoittelua tulokset ovat kummalliset.

Älä mielellään kehota kopioimaan moduulia sivulle vaan käske laittaa se erilliseen tiedostoon, ellet pysty perustelemaan, miksi ei.

hasOwnProperty-purkka kaipaa kommenttia, jossa kerrotaan, että vika on taas IE:ssä.

Sekä drag että resize saisivat käyttää oletuksena kahvaa jo ihan symmetrian vuoksi. Silloin myös tekstin valitsemisen elementistä on mahdollista, ja user-select-CSS:n voi lisätä (parhaiten luokkana) vasta raahauksen alkaessa.

Olisi hyödyllistä, että kahvan ei olisi pakko olla suoraan ensimmäisen polven lapsielementti. Muuttaisin siksi vinkkiä näin:

draggable: function (handle, getElement) {
    getElement = getElement || function() { return handle.parentNode; };
    addListener(handle, onStart, function(e) {
        var elem = getElement(e.target || e.srcElement);
        if (elem) {
            dragStart(elem, e);
        }
    });
}

Nyt getElement-funktioksi voi antaa vaikka funktion, joka etsii puusta seuraavan draggable-elementin. Lisätyllä funktiolla ratkeaa myös tuo protip-asiasi mielestäni paremmin, kun koodin käyttäjän tarvitse vaivata päätään srcElement-asioilla.

DragnResize.draggable(document.getElementById('box-holder'), function (clickedElement) {
    if (clickedElement.className === 'box') {
        return clickedElement;
    }
});
DragnResize.resizable(document.getElementById('box-holder'), function (clickedElement) {
    if (clickedElement.className === 'handle') {
        return clickedElement.parentNode;
    }
});

Huomioni kiinnitti lisäksi, että esimerkissä elementtiä voi raahata myös sen oikeasta alakulmasta pikselin levyiseltä alueelta. Ehkä resize-kahvaa voisi siirtää vielä pikselin.

Vinkin ilmaisutavasta

Mielestäni vinkin tyyli on sekava. Voisit selittää alussa kunnolla, miten koodi toimii, ja kirjoittaa sitten koko moduulin mahdollisimman siististi ja sopivasti kommentoituna kerralla. Monessa kohdassa myös selityksesi on epäselvää tai vaikeatajuista; muista, että vinkki on suunnattu niille, jotka eivät vielä ymmärrä tätä asiaa. Hyvä kuvaus on niin selvä ja kattava, että koodi ei enää tuo merkittävästi uusia asioita vaan vain toteuttaa kuvatut toiminnot JS:llä.

Esimerkki (sekä HTML että JS) sopii paremmin loppuun, ja sitä ennen voisi laittaa väliotsikon.

Älä katko leipätekstiäsi enterillä (kuten kohdassa "kuuntelijan joka / asettaa"), kyllä se rivittyy. Joissain kohdissa (myös koodin puolella) oikoluku olisi tarpeen.

Koodissa häiritsevät lähinnä mielivaltaisesti ripotellut tyhjät rivit (esim. epäsäännöllisesti joidenkin lohkojen alussa ja lopussa). Joitain muuttujia (mm. for-silmukan i-muuttujan) voisi määritellä vasta tarvittaessa eikä etukäteen, mutta tämä on mitätön tyyliseikka. Kohdasta "var dragStart" puuttuu sisennys.

Toivottavasti jaksat vielä selventää koodia. Kiitos vaivannäöstäsi!

Vastaus

Aihe on jo aika vanha, joten et voi enää vastata siihen.

Tietoa sivustosta