Artikel top billede

(Foto: Computerworld)

Web 2.0 programmering - del 3

I sidste afsnit så du, hvor enkelt AJAX-kald kan skrives
ved hjælp af prototype. Denne gang skal du se,
hvordan nogle af prototypes venner kan bruges i
dine web-applikationer.

Af Kenneth Geisshirt, Alt om Data

Denne artikel er oprindeligt bragt på Alt om Data. Computerworld overtog i november 2022 Alt om Data. Du kan læse mere om overtagelsen her.

I en html-formular er det ikke svært at angive, at du ønsker at give brugeren mulighed for at uploade filer. Du bruger normalt typen file til html-taggen input. Endvidere skal formularen have encType (encoding type) sat til multipart/form-data. Når brugeren klikker på formularens submit-knap, så sender browseren indholdet, inklusive filer, af formularens felter til en (PHP-)funktion på serveren. Selve funktionen bliver angivet i action-attributten i form-tagget. Alt dette kender du uden tvivl og har brugt det flere gange.

Problemet er bare, at hele web-siden nu skal genereres på serveren, hvilket kan tage lang tid. Endvidere har du måske andre data på siden, som du nu skal huske at genskabe.

I min foto-applikation er der mange informationer på administrationssiden, og upload af et billede er kun en mindre funktion. Det giver et mere naturligt flow for brugeren, hvis han/hun blot kan uploade et billede uafhængigt af, hvad hun ellers er i gang med.

Heldigvis findes der en løsning. Tricket er at bruge en usynlig iframe til at håndtere upload. En iframe er så at sige en selvstændig web-side, som du kan placere indeni en anden side, uden at skulle ændre på modersiden. Der findes et lille bibliotek, som bygger ovenpå prototype til netop dette. Det hedder iframesubmit (i infoboks ”Links” finder du en url, hvor du kan downloade det). Desværre er dokumentationen sparsom, men i infoboks ”Brugen af iframesubmit” finder du html/JavaScript-koden til upload-boksen i min foto-applikation.

Som du kan se, er action sat til det PHP-script, som tager sig af upload på backend-siden, (du finder dette PHP-script i infoboks ”Brugen af iframesubmit (backend)”), men onSubmit er dog også sat.

Det betyder, at når brugeren klikker på submit-knappen, kaldes JavaScript-koden som står i onSubmit og action bruges så som et slags argument til Form.IframeSubmit. Selve Form.IframeSubmit er en constructor som opretter en iframe og sender data igennem den til PHP-funktionen.

Det er muligt at bruge callbacks i Form.IframeSubmit, for eksempel bruger jeg onComplete til at udføre kode når upload af billedet er færdig. Funktionen updateScroller er ganske udramatisk, idet den tilføjer billedet til oversigten til venstre del på administrationssiden, således at brugeren kan begynde at arbejde med det nye billede. Du finder funktionen i infoboks ”updateScroller”.

Redigering af billeder

En del af Web 2.0-bølgen er et forsøg på at gøre dig uafhængig af din computer forstået på den måde, at du kan bruge en vilkårlig computer, bare den har en web-browser og en internetforbindelse. Med andre ord, Web 2.0 lader pendulet svinge tilbage til de tider, hvor alt it-arbejde skete gennem en terminal. Men nu hedder terminalen ikke VT-100 med en lille grøn skærm, den moderne terminal hedder Internet Explorer, Safari, Firefox eller noget helt fjerde og er en web-browser.

Nu vil jeg gerne frigøre ejeren af min fotoapplikation for avancerede og/eller dyre billedbehandlingssprogrammer (The GIMP, Photoshop, og så videre). Det medfører, at jeg er nødt til at give mulighed for at kunne redigere billederne direkte i browseren.

Det virker umiddelbart som et højt mål, men der findes et fantastisk bibliotek som bygger på prototype og scriptaculous der kan give mig noget af denne funktionalitet. Biblioteket hedder Image Cropper UI og i infoboks ”Links” finder du en url til dets hjemmeside.

Image Cropper er ikke et fuldstændig billedbehandlingsprogram, idet det kun kan bruges til at beskære billeder. Men det er dog den mest elementære operation på fotografier, så jeg lader mig nøjes med biblioteket.

Ideen er, at ejeren af foto-applikationen klikker på et billede i oversigten til venstre. Billedet kommer så frem i midten, og her kan ejeren så beskære billedet, inden han/hun gemmer det igen. Uover at kunne beskære billedet, skal ejeren også kunne tilføje tags, men denne funktionalitet vil blive forklaret senere i denne artikel.

En web-browser kan reagere på en lang række hændelser, for eksempel klik med musen. Faktisk er det muligt at binde en JavaScript-funktion til en hændelse på et bestemt html-element. Det gør det meget enkelt at gøre det muligt for ejeren at klikke på et billede, så det kommer frem i en slags redigeringstilstand. Prototype tilbyder nogle særlige funktioner til at binde funktioner sammen med hændelser.

I infoboks ”updateScroller” finder du et eksempel på, hvordan du binder en funktion til en hændelse. Linjen Event.observe(li, 'click', editImage.bindAsEventListener(this, 'image-'+i)); binder et museklik til et element (refereret til gennem variablen li) med funktionen editImage. Tricket med at bruge bindAsEventListener er, at det er muligt at binde argumenter til funktionen editImage men funktionen kaldes (med disse argumenter) først, når en bruger klikker på elementet. Dette går under betegnelsen function closure i JavaScript-kredse. Selv om det ved første øjekast virker lidt besværligt, betyder det, at selve funktionen (her editImage) bliver meget lettere at skrive, da argumenterne sendes med over.

Funktionen editImage sætter så redigeringsfunktionerne op, se infoboks ”editImage”. Bemærk at argumenterne til funktionen tilgås på en lidt sjov måde ved hjælp af det indbyggede JavaScript-variabel arguments. Der er en InPlaceEditor, så ejeren kan skrive en lille beskrivelse af billedet, og linjen new Cropper.Img('image-editor', {onEndCrop: onEndCrop}); sætter Image Cropper op. Strengen image-editor er id på en DIV, hvor Image Cropper kan få lov til at udføre sit arbejde, og hvor billedet er blevet sat ind. Som så mange andre prototype-biblioteker, benytter Image Cropper sig også af callbacks, idet onEndCrop kaldes, når beskæringen er slut (når brugeren slipper venstre museknap). Denne callback-funktion kaldes med to argumenter, som begge er objekter, der henholdsvis fortæller, hvor beskæringen begynder, og hvor stor den er. I min fotoapplikation indsættes disse værdier under billedet som en del af en html-formular.

Drag and Drop

Du kender træk-og-slip (drag'n'drop) fra dine desktop-applikationer. Ideen er, at du kan trække objekter eller elementer fra et sted til et andet, for eksempel en fil fra en folder over i skraldespanden. Du er nok så vant til denne funktion, at du ikke tænker over, at du gør det flere gange om dagen. Men kan du trække html-elementer fra et sted til et andet i dine web-applikationer?

Det lyder avanceret, og det er det egentlig også. Skal du implementere drag'n'drop, skal du finde musehændelser og holde styr på, hvor museklik og -bevægelser sker i din applikation. En række JavaScript-funktioner skal bindes til disse hændelser og du skal huske at fjerne elementet et sted og indsætte det igen det nye sted på web-siden. Alt dette skal implementeres med JavaScript ved brug af DOM (Document Object Model). Endvidere skal du sikre dig, at din implementation virker med alle de browsere, som dine brugere kunne finde på at bruge, og det er sikkert ikke helt den samme kode, du skal bruge.

Nu kan du glæde dig over, at folkene bag scriptaculous også finder dette temmelig besværligt. Derfor har de udviklet en række klasser, som gør brug af prototypes hændelsesklasser til at gøre drag'n'drop meget lettere at implementere.

I min foto-applikation bruger jeg drag'n'drop til at lade ejeren tildele tags til billederne. Alle tags står på i listen til højre, og ejeren kan hele tiden definere nye tags. Når ejeren vælger et billede, skal det være muligt at trække tags fra listen over til billedet og derved markere det med denne tag.

I infoboks ”Klargøring af drag'n'drop” finder du et eksempel på JavaScript-kode, som sætter drag'n'drop op. Eksemplet er taget fra min foto-applikation, og klargøring sker som en del af AJAX-kaldet, som indlæser alle tidligere definerede tags. Scriptaculous deler drag'n'drop i to områder. Det første er området, hvor der kan trækkes fra (Draggable), og det andet område er, hvor der kan slippes (Droppable). Det er lettest, hvis de elementer der kan trækkes i, er opremset som list-items (html-elementet li). Du kan jo altid give listen en stil, så prikkerne foran hver punkt ikke ses.

Klargøring af træk-område sker ved, at liste alle elementerne ved hjælp af getElementsByTagName og gøre det til et array med $A-funktionen. Med et array i hånden, kan du bruge en iterator (each), og elegant oprette et objekt til at styre træk-delen (new Draggable). Et træk-objekt kan have en række valgfrie options, hvor jeg har valgt et par stykker til min applikation. Kombinationen af revert og ghosting betyder, at et element fortsætter med at være synligt på den oprindelig plads, selv når du trækker i det.

Slip-område sættes op med Droppables.add, hvor første argument er det html-element, hvor objekter skal kunne slippes. Callback-funktionen onDrop er den funktion, som skal kaldes, når brugeren slipper objektet. Denne onDrop-funktion vil typisk fjerne objektet fra det oprindelige sted og indsætte det i slip-området. I infoboks ”onDrop-funktion” finder du et eksempel fra min fotoapplikation.

Næste gang

I dette afsnit har du set tre forskellige biblioteker, der alle bruger prototype som fundament. Det er tre meget forskellige bibliotekter, men fælles for dem alle er, at de gør dine web-applikationer mere Web 2.0-agtige, uden at jeg her vil kvantificere denne størrelse.

Der findes et stort antal lignende biblioteker, som jeg vil opfordre dig til at gå på jagt i Scripteka-kataloget efter. I langt de fleste tilfælde kan du prøve bibliotekerne i små demoer på deres respektive hjemmesider.

Scriptaculous kan meget mere end drag'n'drop. Især har biblioteket en lang række effekter, som du kan bruge i dine web-applikationer.

I næste og sidste afsnit af denne serie vil du se, hvordan besøgende kan søge efter billeder i min fotoapplikation, og hvordan billederne kan præsenteres på en lidt fancy måde.

<form encType='multipart/form-data' action='saveimage.php' onSubmit='new Form.IframeSubmit(this, {onComplete: function(msg) { updateScroller();}}); return false;' method='POST'>
<input id='image_file' name='image_file' type=file>
<input type=submit value='Upload'>
</form>

include_once('../db.php');

$filename = $_FILES['image_file']['name'];
$uploaddir = '/home/kneth/Projects/ZPhoto/img/';
$uploadfile = $uploaddir.basename($filename);
move_uploaded_file($_FILES['image_file']['tmp_name'], $uploadfile);

$query = "INSERT INTO image (filename, crdate) VALUES ('".$filename."', NOW())";
mysql_query($query);

function updateScroller() {
var filename = $F('image_file');
var i = $('scroller-inner').childNodes.length+1;
var img = new Element('img', {id: 'image-'+i});
img.writeAttribute({src: '/zphoto/img/'+filename, width: '100px'});
var li = new Element('li');
Event.observe(li, 'click', editImage.bindAsEventListener(this, 'image-'+i));
li.appendChild(img);
$('scroller-inner').appendChild(li);
}

function editImage(e) {
var args = $A(arguments);
var id = args[1];
$('editor-inner').update();
$('image-filename').value = $(id).src;
var img = new Element('img', {id: 'image-editor', width: '100%'});
img.writeAttribute({src: $(id).src});
$('editor-inner').appendChild(img);
$('editor-dimensions').show();
new Cropper.Img('image-editor', {onEndCrop: onEndCrop});
$('image-caption').show();
$('image-caption-inner').update('Your caption here!');
new Ajax.InPlaceEditor('image-caption-inner', '/zphoto/admin/echo.php', {cols: 50});
$('image-tags').show();
}

function loadTags() {
$('taglist').update('');
new Ajax.Request('/zphoto/tags.php', {
method: 'get',
onSuccess: function(msg) {
var tags = msg.responseText.evalJSON();
// add tags
tags.each(function(t) {
var li = new Element('li').update(t.text);
$('taglist').appendChild(li);
});
// add drag-n-drop
$A($('taglist').getElementsByTagName('li')).each(function(item) {
new Draggable(item, {revert: true, ghosting: true});
});
Droppables.add('image-tags', {onDrop: moveTag});
}
});
}

function moveTag(draggable, dropZone) {
draggable.parentNode.removeChild(draggable);
$('image-tags-inner').appendChild(draggable);
}

[themepacific_accordion]
[themepacific_accordion_section title="Fakta"]

Links

[/themepacific_accordion_section]
[themepacific_accordion_section title="Fakta"]

Yslow

[/themepacific_accordion_section]
[/themepacific_accordion]