Προς το περιεχόμενο

Φωτισμός κειμένου με JavaScript


Skeftomilos

Προτεινόμενες αναρτήσεις

Δημοσ.

Όσοι χρησιμοποιείτε Firefox και έχετε την έκδοση 1.0 ή νεότερη θα ξέρετε ήδη ότι η λειτουργία Find βελτιώθηκε πολύ σε σχέση με ότι υπήρχε παλαιότερα. Εκτός από τη γνωστή λέξη προς λέξη αναζήτηση, μπορούμε τώρα να φωτίσουμε ταυτόχρονα όλες τις λέξεις που βρέθηκαν. Μάλιστα δε χρειάζεται καν να πατάμε κάποιο κουμπί Find! Απλά γράφουμε τη λέξη που ψάχνουμε, και ενώ πληκτρολογούμε ο Firefox ανιχνεύει τη σελίδα και εμφανίζει τα αποτελέσματα:

 

Page-Highlight-Firefox.png

 

Μου δημιουργήθηκε η απορία αν αυτή η χρήσιμη λειτουργία μπορούσε να ενσωματωθεί σε μία οποιαδήποτε web σελίδα, ανεξάρτητα δηλαδή από το browser που χρησιμοποιεί ο χρήστης. Αν γινόταν κάτι τέτοιο θα μπορούσε να βρει ενδεχομένως και άλλες εφαρμογές, πιο εστιασμένες ίσως στις ιδιαιτερότητες κάθε σελίδας. Για παράδειγμα ο χρήστης θα μπορούσε να επιλέξει από μία έτοιμη λίστα λέξεων αυτή που θέλει να φωτίσει. Σε κάθε περίπτωση θα ήταν ευκαιρία και για ένα ενδιαφέρον tutorial με θέμα το HTML-DOM scripting! Μου πήρε ένα πρωινό η υλοποίηση αλλά νομίζω ότι άξιζε τον κόπο. Το αποτέλεσμα είναι το παρακάτω:

 

Page-Highlight-Result.png

 

Αν θέλετε να δοκιμάσετε τη σελίδα στην πράξη, ένα ζωντανό παράδειγμα είναι εδώ. Όσοι θέλετε μπορείτε να αντιγράψετε τον κώδικα και να προσθέσετε τη δυνατότητα highlighting στις σελίδες σας.

 

Αν και θα ήθελα να περιγράψω τον κώδικα αναλυτικά, τελικά προέκυψε λίγο περισσότερο περίπλοκος απ'όσο θα ήθελα. Έτσι θα αρκεστώ σε μία κάπως higher level περιγραφή.

 

Όλα ξεκινάνε από το πεδίο κειμένου <input>. Δε χρειάστηκε να το τοποθετήσω σε φόρμα, του έδωσα μόνο ένα id και φυσικά ένα συμβάν (onKeyUp) που καλεί τη ρουτίνα highlight(). Δοκίμασα και τα συμβάντα onChange, onKeyDown και onKeyPress αλλά δεν ήταν κατάλληλα.

 

><input type="text" id="highlight-text" onKeyUp="highlight()">

Προτού δούμε τη ρουτίνα highlight() ας σκεφτούμε λίγο τι θέλουμε να κάνει ο κώδικας. Προφανώς θέλουμε να κόβουμε και να ράβουμε τον HTML κώδικα της σελίδας μέσω του Document Object Model (DOM), ανάλογα με τη λέξη που έχει πληκτρολογήσει ο χρήστης. Ας υποθέσουμε για παράδειγμα πως όλο το κείμενο της σελίδας είναι η παρακάτω παράγραφος:

 

><p id="content">
 Ο λαός των Σουμερίων
</p>

Εάν ο χρήστης γράψει τη λέξη "των", ο HTML κώδικας θα πρέπει να γίνει κάπως έτσι:

 

><p id="content">
 Ο λαός <span class="yellow">των</span> Σουμερίων
</p>

Όπου yellow είναι ένα style που έχουμε ορίσει στην αρχή της σελίδας: span.yellow { background-color: yellow }. Αυτά στο επίπεδο της HTML. Στο επίπεδο του DOM συμβαίνουν άλλα πράγματα. Η ιεραρχία αρχικά ήταν:

 

Element node <p>

.. Text node ("Ο λαός των Σουμερίων")

 

Και μετά την αλλαγή έγινε:

 

Element node <p>

.. Text node ("Ο λαός ")

.. Element node <span>

.. .. Text node ("των")

.. Text node (" Σουμερίων")

 

Ένα πιο οπτικό παράδειγμα ίσως βοηθήσει:

 

Page-Highlight-Tut.png

 

Νομίζω ότι καταλάβατε τι θέλουμε να κάνουμε. Βέβαια πρέπει να φροντίσουμε και για την αντίστροφη διαδικασία, γιατί πριν από κάθε νέα αναζήτηση πρέπει να αποκαθιστούμε την αρχική μορφή του κειμένου. Τελικά αυτό μου φάνηκε και το πιο δύσκολο μέρος του κώδικα (έκρυβε πολλά bugs!) Συνολικά το script αποτελείται από τέσσερις ρουτίνες:

 

- highlight() : κάνει μερικούς διαδικαστικούς ελέγχους και ξεκινά την επόμενη ρουτίνα.

- highlight_element_recursive() : ψάχνει για childNodes εντός του τρέχοντος node, και μετά καλεί τον εαυτό της.

- highlight_text_recursive() : σπάει σε <span> ένα Text node με αναδρομικές κλήσεις στον εαυτό της.

- flatten_element() : συνενώνει τα διάσπαρτα <span> και Text nodes σε ένα μεγάλο Text node.

 

Ας δούμε πρώτα τη highlight():

 

>var highlight_max_words = 100
var highlight_min_letters = 2

var highlight_cnt = 0
var highlight_prev_text = ""

function highlight() {
 var text = document.getElementById("highlight-text").value
 if (text != highlight_prev_text) {
   highlight_cnt = 0
   var re = new RegExp().compile(text, "i")
   highlight_element_recursive(document.getElementById("content"), re, text.length)
   highlight_prev_text = text
 }
}

Βλέπετε ότι έχουμε βάλει δύο όρια στο script για να αντιμετωπίσουμε πιθανά προβλήματα απόδοσης. Φωτισμός λέξεων με τουλάχιστον 2 γράμματα, και συνολικά μέχρι 100 λέξεις το πολύ. Στο δικό μου υπολογιστή (Celeron 2,4 Ghz) το script λειτουργεί αρκετά καλά μέσα σ'αυτά τα όρια. Μία πιο σοφιστικέ επιλογή θα ήταν ίσως η διακοπή του script μετά από παρέλευση ορισμένου χρονικού διαστήματος (π.χ. 100 msec).

 

Επιπλέον εξετάζουμε τυχόν ομοιότητα με την προηγούμενη αναζήτηση. Δε θέλουμε να ξανακάνουμε highlight αν δεν έχει αλλάξει το περιεχόμενο του πεδίου κειμένου. Δυστυχώς ενώ το συμβάν onChange δεν τρέχει όσο συχνά θα θέλαμε, το συμβάν onKeyUp παρατρέχει!

 

Το τελευταίο ενδιαφέρον σημείο είναι η χρήση μίας Regular Expression στον κώδικά μας. Η JavaScript δε διαθέτει κάποιο αμεσότερο τρόπο για case insensitive συγκρίσεις string, και έτσι πρέπει να καταφύγουμε σ'αυτή την κάπως υπερβολική μέθοδο. Η εντολή var re = new RegExp().compile(text, "i") δημιουργεί μία compiled (=γρήγορη) Regular Expression που περιέχει απλά το προς αναζήτηση κείμενο (text) και είναι και case insensitive (παράμετρος "i"). Θα τη χρησιμοποιήσουμε αργότερα με τον εξής τρόπο: "Ο λαός των Σουμερίων".search(re) που επιστέφει τη θέση που βρέθηκε η λέξη στο συγκεκριμένο string, ή -1 αν δε βρεθεί.

 

Περνάμε στη ρουτίνα highlight_element_recursive():

 

>function highlight_element_recursive(element, re, len) {
 flatten_element(element)
 var child = element.firstChild

 while(child && highlight_cnt < highlight_max_words) {
   if (child.nodeType == 1) {
     highlight_element_recursive(child, re, len)
   } else if (child.nodeType == 3) {
     if (len >= highlight_min_letters) highlight_text_recursive(child, re, len)
   }
   child = child.nextSibling
 }
}

Η αρχική μου επιλογή ήταν ένα βρόγχος for() που θα εξέταζε ένα-ένα τα στοιχεία της συλλογής element.childNodes. Όμως αντιμετώπισα το πρόβλημα ότι η συλλογή αυτή αυξανόταν αυτόματα με κάθε νέα εισαγωγή <span>, με αποτέλεσμα ... ατέρμονας βρόγχος! Αντικατέστησα λοιπόν το βρόγχο for() με ένα βρόγχο while() που αποδείχτηκε πιο ελέγξιμος. Sibling σημαίνει στα αγγλικά αδελφός, και η εντολή child = child.nextSibling αλλάζει το τρέχον child στον επόμενο αδελφό του. Δεδομένου ότι η εισαγωγή των <span> γίνεται αργότερα με την insertBefore() δηλαδή προσθήκη πριν, δεν έχουμε πρόβλημα ζητώντας τον αδελφό μετά!

 

Πρέπει να εξηγήσουμε και τους μαγικούς αριθμούς 1 και 3 ως τιμές της ιδιότητας nodeType. Η τιμή 1 σημαίνει element. Τα elements δεν περιέχουν ποτέ κείμενο παρά μόνα άλλα elements ή text nodes. Η τιμή 3 σημαίνει text node. Οι text nodes δεν περιέχουν τίποτα άλλο εκτός από κείμενο.

 

Ας περάσουμε τώρα στη ρουτίνα highlight_text_recursive():

 

>function highlight_text_recursive(node, re, len) {
 var pos = node.nodeValue.search(re)
 if (pos > -1) {
   var left = node.nodeValue.substring(0, pos)
   var middle = node.nodeValue.substring(pos, pos + len)
   var right = node.nodeValue.substring(pos + len)

   var left_node = document.createTextNode(left)

   var middle_element = document.createElement("span")
   middle_element.className = "yellow"
   middle_element.innerHTML = middle

   node.nodeValue = right
   node.parentNode.insertBefore(left_node, node)
   node.parentNode.insertBefore(middle_element, node)

   highlight_cnt++
   if (highlight_cnt < highlight_max_words) highlight_text_recursive(node, re, len)
 }
}

Η ρουτίνα αυτή τρέχει πάντα με όρισμα ένα text node. Αναζητά την πρώτη εμφάνιση της ζητούμενης λέξης και, αν υπάρχει, προσθέτει ένα <span> αριστερά, και ένα text node αριστερότερα. Έπειτα αποκόπτει το κείμενο της node που μεταφέρθηκε στα νέα elements και καλεί αναδρομικά τον εαυτό της αναζητώντας την επόμενη πιθανή εμφάνιση της λέξης. Νομίζω ότι ο κώδικας είναι αρκετά εύγλωττος, γι αυτό ας περάσουμε κατευθείαν στην τελευταία ρουτίνα, τη flatten_element():

 

>function flatten_element(element) {
 var child = element.firstChild
 while(child) {
   if ((child.nodeType == 1) && (child.className == "yellow")) {
     var prev = child.previousSibling
     var next = child.nextSibling
     var is_prev_text = prev ? (prev.nodeType == 3) : false
     var is_next_text = next ? (next.nodeType == 3) : false
     if (is_prev_text && is_next_text) {
       prev.nodeValue += child.innerHTML + next.nodeValue
       element.removeChild(child)
       child = next.nextSibling
       element.removeChild(next)
     } else if (is_prev_text) {
       prev.nodeValue += child.innerHTML
       element.removeChild(child)
       child = next
     } else if (is_next_text) {
       next.nodeValue = child.innerHTML + next.nodeValue
       element.removeChild(child)
       child = next.nextSibling
     } else {
       element.replaceChild(document.createTextNode(child.innerHTML), child)
       child = next
     }
   } else {
     child = child.nextSibling
   }
 }
}

Η δυσκολία αυτής της ρουτίνας προκύπτει από το πλήθος των περιπτώσεων που εξετάζει. Αφού βεβαιωθούμε ότι βρήκαμε ένα από τα ειδικά <span> φωτισμού (class="yellow"), πρέπει να προβλέψουμε τέσσερις διαφορετικές περιπτώσεις:

 

(α) Το <span> είναι ανάμεσα σε text nodes.

(β) Πριν από το <span> υπάρχει text node.

(γ) Μετά το <span> υπάρχει text node.

(δ) Το <span> δε γειτνιάζει με text nodes.

 

Για κάθε περίπτωση πρέπει να ακολουθήσουμε διαφορετική στρατηγική συνένωσης των στοιχείων, και να επιλέξουμε το κατάλληλο node για τη συνέχεια. Κατά τ'άλλα δεν υπάρχει ιδιαίτερη δυσκολία και ο κώδικας είναι σχεδόν self-documented. Ίσως να μπορούσαμε να είχαμε κάνει και αυτή τη ρουτίνα να λειτουργεί με αναδρομή, αλλά δε νομίζω ότι ο κώδικάς μας θα κέρδιζε τίποτα σε απλότητα.

 

Ουφ! Τελειώσαμε! Αν είχατε την υπομονή να παρακολουθήσετε την περιγραφή μέχρις εδώ, θα έχετε μάθει πιστεύω αρκετά χρήσιμα πράγματα. Κατ'αρχήν μάθατε ότι το HTML-DOM είναι μία πλήρης και πανίσχυρη συλλογή αντικειμένων, που μπορούν να προκαλέσουν από τις πιο σεμνές μέχρι τις πιο ριζικές αλλαγές σε μία web σελίδα. Ήρθατε σε επαφή με σχεδόν τις μισές από τις ιδιότητες και τις μεθόδους αυτών των αντικειμένων. Επιπλέον είδατε μία συντακτική ποικιλία από κώδικα JavaScript, καθώς και μερικές σχετικά προχωρημένες προγραμματιστικές τεχνικές.

 

Τώρα μάλλον θα θέλετε να μάθετε το κατά πόσο είναι όλα αυτά ασφαλή και αξιόπιστα. Μήπως αφορούν κάποια σπάνια και εξωτική ράτσα συσκευών που διαθέτει το 1% των χρηστών? Κάθε άλλο. Όλοι οι σύγχρονοι browsers (Firefox, IE, Opera) υποστηρίζουν πλήρως το DOM. Η σελίδα μας λειτουργεί το ίδιο καλά σε όλους αυτούς τους browsers. Η μόνη έγνοια που θα πρέπει να έχουμε είναι εκείνοι οι στατιστικά λίγοι χρήστες που προτιμούν την περιήγηση στο Internet χωρίς JavaScript. Σε αυτούς φυσικά το script δε θα τρέξει και το highlight δε θα γίνει. Τι πρέπει να κάνουμε άραγε με αυτούς? Να κρύψουμε το highligh panel ώστε να μην τους ενοχλεί ή να το αφήσουμε επίτηδες ορατό για να βλέπουν τι χάνουν? Η επιλογή δική σας. Ο κώδικας που κρύβει το panel είναι ο εξής:

 

><noscript>
 <style>
   div#highlight-panel { display:none }
 </style>
</noscript>

Αν ενδιαφέρεστε για περισσότερα κόλπα με JavaScript και DOM, έχω πρόσφατα γράψει και τα εξής:

 

- Αυτόματη συμπλήρωση links με JavaScript

- Μενού με links και ένδειξη τρέχουσας σελίδας

- Πώς φορτώνονται οι εικόνες με τη χρονική σειρά που θέλω?

Δημοσ.

Poly kalo Skeftomilos. Polla paixnidia me th Javascript mporeite na vreite an psa3ete gia "bookmarklets" sto google. Ta bookmarklets mporoun na tre3oun kai san mikra ergaleia apo8hkeymena sta bookmarks tou browser sas alla kai na enswmato8oun se selides, an to 8elhsete.

 

D.

Δημοσ.

Πολύ καλή η παρατήρησή σου Dionisos για τα bookmarklets. Δε είχα ιδέα για την ύπαρξή τους. Πραγματικά είναι πρακτικότατα και δίνουν μεγάλες δυνατότητες για τη χρήση JavaScript στο web. Για όσους δεν τα ξέρουν θα κάνω μία σύντομη παρουσίαση.

 

Ας υποθέσουμε ότι σε μία σελίδα υπάρχει ο παρακάτω κώδικας:

 

><a href="javascript:resizeTo(640, 480)">Resize 640 x 480</a>

Νομίζω καταλαβαίνετε τη θα γίνει αν ο χρήστης κάνει κλικ. Το παράθυρο του browser θα μικρύνει στις συγκεκριμένες διαστάσεις. Αυτό μπορεί να είναι χρήσιμο για το χρήστη. Αν λοιπόν θέλει κάποια στιγμή να κάνει αυτή τη μεταβολή, θα πρέπει πρώτα να βρει και να φορτώσει τη σελίδα αυτή και μετά να κάνει κλικ σε αυτό το ψευδο-link. Αρκετό μπέρδεμα, εκτός ... εκτός εάν ... εκτός αν κάνει bookmark το link! Σατανική ιδέα! Κάνοντας bookmark το ψευδο-link αποκόπτει το script από τη σελίδα στην οποία υπήρχε, και μπορεί να το καλέσει οποιαδήποτε στιγμή και μάλιστα στο context μίας άλλης σελίδας!

 

Για να γίνουν περισσότερο αντιληπτές οι δυνατότητες αυτής της τεχνικής θα δώσω ένα άλλο παράδειγμα:

 

><a href="javascript:alert(document.documentElement.innerHTML.length + " bytes")">Page-Size</a>

Αυτό το script αν γίνει bookmarklet θα εμφανίζει το πλήθος των χαρακτήρων της σελίδας που είναι φορτωμένη εκείνη τη στιγμή. Με τα bookmarklets μπορούμε επομένως να τρέχουμε το δικό μας κώδικα JavaScript σε οποιαδήποτε σελίδα του Web! Θέλετε άλλο παράδειγμα?

 

><a href='javascript:h_if();
        function h_if() { var iframes = document.getElementsByTagName("iframe");
          for (var i = 0; i < iframes.length; i++) { 
            var iframe = iframes[i];
            iframe.style.display = "none";
            iframe.src = "about:blank";
          }
        }'>Hide IFrames</a>

Χωρίς σχόλια! :wink:

 

Περιορισμοί:

- Firefox: Δε φαίνεται να υπάρχει όριο στο μέγεθος του script που μπορεί να αποθηκευτεί ως bookmarklet.

- Opera: Ισχύει το ίδιο.

- Internet Explorer: Η ορολογία είναι διαφορετική: favorites αντί bookmarks. Δε λειτουργούν τα scripts με μέγεθος μεγαλύτερο από 500 bytes περίπου.

 

Αν νομίζετε ότι θέλετε τη λειτουργία highlight ως χρήστες, έχω ετοιμάσει το σχετικό bookmarklet και μπορείτε να το βρείτε εδώ. Το script είναι κοντά στα 2,5 KB, επομένως όσοι είστε χρήστες του Internet Explorer μην κάνετε τον κόπο. Όσοι πάλι είστε χρήστες του Firefox, προφανώς δεν το χρειάζεστε. Επομένως απομένουν οι χρήστες του Opera να οφεληθούν κάτι από το συγκεκριμένο bookmarklet. Ένα παράδειγμα χρήσης:

 

Bookmarklet-Highlight-1.png

 

Bookmarklet-Highlight-2.png

 

Bookmarklet-Highlight-3.png

 

Μία άλλη παρεμφερής ιδέα με τα bookmarklets είναι ένα καινούργιο extension του Firefox με το όνομα Greasemonkey. Δίνει τη δυνατότητα να τρέχουμε τα δικά μας scripts με αυτόματο τρόπο, μόλις ολοκληρωθεί το φόρτωμα μίας web σελίδας. Περίμενα καιρό να εμφανιστεί κάτι τέτοιο, και πραγματικά ενθουσιάστηκα μόλις έμαθα για την ύπαρξή του. Ένα άρθρο μου που περιγράφει την εγκατάσταση και λειτουργία του είναι το παρακάτω:

 

- Το web για ασυμβίβαστους: Bookmarklets, Greasemonkey B)

Αρχειοθετημένο

Αυτό το θέμα έχει αρχειοθετηθεί και είναι κλειστό για περαιτέρω απαντήσεις.

  • Δημιουργία νέου...