Jump to content

Calling All Web Developers


fruitbat

Recommended Posts

Right, i'm still a Noobie in comparison with most when it comes to stuff like this so here goes.

What I am trying to create is a HTML page which our software will display in the form of an Iframe, with 1 input box, and when the user types some text into the input box it looks at a pre defined list of results (which are in the form of 5/6 word questions), and I want a list to populate below the input box with the questions where any words in the questions that have a match to what has been input by the user.

Upon clicking a question I want the input box to appear with the complete question that has been clicked from the list.

Basically a Predicitve Text Input Box only using my pre-defined results to search on.

I have been looking around on t'internet but can't find much help.

Anyone got any help with the best way to do this, or even point me in the right direction as to some help pages (latter preferably so I can play with it and work it out). I did see something recently on W3Schools about this recently but can't find it now, plus that example didn't have the click on answer to populate input box

Thanks in Advance

Frooty

Edited by fruitbat
Link to comment
Share on other sites

Found THIS ONE

It works but the only thing it doesn't do that I would like for this idea to work is search the complete text of the result and return anything with a match, it tries to match from letter 1 onwards and not match from any point throughout the potential results text.

Looking at the JS file that can be opened from the bottom of the page, at around line 55 (if I am reading the code right, which I may not be as thats quite a bit above my level at the moment), is where it tells it what to look for to start creating the matching results, anyone good enough to be able to add to/edit this to allow it search from any character in the string rather than letter 1.

Cheers

Frooty

Edited by fruitbat
Link to comment
Share on other sites

I'd say you're probably best off with the jquery one you found as it seems to do most of what you want, then modify the search code to look at any matches in all of the text. I'm not familiar with how it works, but I reckon it's worth a look to see if it's do-able rather than starting from scratch.

Edit: I'll have a quick look at the code and see if I can figure it out.

Edited by Village Idiot
Link to comment
Share on other sites

I'd say you're probably best off with the jquery one you found as it seems to do most of what you want, then modify the search code to look at any matches in all of the text. I'm not familiar with how it works, but I reckon it's worth a look to see if it's do-able rather than starting from scratch.

I've modified the JQuery one to lookup my potential results rather than theirs and return what I require it to, but I can't work out in the JS file how to do the "any matches in all of the text."

Frooty

Edited by fruitbat
Link to comment
Share on other sites

Tried to edit the js file, but is harder than i thought. Managed to get this far...

Replace

if( row[0].length > 0 ){
    // get the first character
    sFirstChar = row[0].substring(0, 1).toLowerCase();
    // if no lookup array for this character exists, look it up now
    if( !stMatchSets[sFirstChar] ) stMatchSets[sFirstChar] = [];
    // if the match is a string
    stMatchSets[sFirstChar].push(row);
}
with
// if the length is zero, don't add to list
if( row[0].length > 0 ){
    var tmpArray = [];
    for (var j = 1; j <= row[0].length; j++) {
        // get the next character
        sNextChar = row[0].substring(j-1, j).toLowerCase();
        // Needed to stop duplicate matches appearing in results
        var alreadyIndexed = false;
        for (var k = 0; k < tmpArray.length; k++){
            if(tmpArray[k] == sNextChar) {
                alreadyIndexed =  true;
            }
        }
        // Character has not been skipped
        if (!alreadyIndexed) {
            tmpArray.push(sNextChar);
            // if no lookup array for this character exists, look it up now
            if( !stMatchSets[sNextChar] ) stMatchSets[sNextChar] = [];
                // if the match is a string
                stMatchSets[sNextChar].push(row);
            }
        }
    }
}
^^ That looks at the whole word rather than just the first character and
if( lastKeyPressCode != 8 ){
    // fill in the value (keep the case the user has typed)
    $input.val($input.val() + sValue.substring(prev.length));
with
if( lastKeyPressCode != 8 ){
    // fill in the value
    $input.val(sValue);

^^ That ignores the character what was typed and shows the 'proper' match. Hard to explain, but take it out and you'll see what I mean.

That will match things halfway through the word/phrase but only on the first character. I'm not too sure how you can make the rest of what the user types match as well.

Like if you type "d" it would match "aberdeen" and "denmark", but then "de" would only match "denmark" whereas it should match aberdeen. Not sue how to get past this bit either, sorry.

Link to comment
Share on other sites

Ok, think I've got it now...

jQuery.autocomplete = function(input, options) {
    // Create a link to self
    var me = this;

    // Create jQuery object for input element
    var $input = $(input).attr("autocomplete", "off");

    // Apply inputClass if necessary
    if (options.inputClass) $input.addClass(options.inputClass);

    // Create results
    var results = document.createElement("div");
    // Create jQuery object for results
    var $results = $(results);
    $results.hide().addClass(options.resultsClass).css("position", "absolute");
    if( options.width > 0 ) $results.css("width", options.width);

    // Add to body element
    $("body").append(results);

    input.autocompleter = me;

    var timeout = null;
    var prev = "";
    var active = -1;
    var cache = {};
    var keyb = false;
    var hasFocus = false;
    var lastKeyPressCode = null;

    // flush cache
    function flushCache(){
        cache = {};
        cache.data = {};
        cache.length = 0;
    };

    // flush cache
    flushCache();

    // if there is a data array supplied
    if( options.data != null ){
        var sFirstChar = "", stMatchSets = {}, row = [];

        // no url was specified, we need to adjust the cache length to make sure it fits the local data store
        if( typeof options.url != "string" ) options.cacheLength = 1;

        // loop through the array and create a lookup structure
        for( var i=0; i < options.data.length; i++ ){
            // if row is a string, make an array otherwise just reference the array
            row = ((typeof options.data[i] == "string") ? [options.data[i]] : options.data[i]);

            /*// if the length is zero, don't add to list
            if( row[0].length > 0 ){
                // get the first character
                sFirstChar = row[0].substring(0, 1).toLowerCase();
                // if no lookup array for this character exists, look it up now
                if( !stMatchSets[sFirstChar] ) stMatchSets[sFirstChar] = [];
                // if the match is a string
                stMatchSets[sFirstChar].push(row);
            }*/
            
            // if the length is zero, don't add to list
            if( row[0].length > 0 ){
                // Go through each character in the data string and index a match
                var tmpArray = [];
                for (var j = 1; j <= row[0].length; j++) {
                    // get the next character
                    sNextChar = row[0].substring(j-1, j).toLowerCase();
                    // Check whether this character already exists in the temp array.
                    // If it does, we skip it to ensure the phrase is not added twice 
                    // to the cache
                    var alreadyIndexed = false;
                    for (var k = 0; k < tmpArray.length; k++){
                      if(tmpArray[k] == sNextChar) {
                        alreadyIndexed =  true;
                      }
                    }
                    // Character has not been skipped so index the match 
                    if (!alreadyIndexed) {
                       tmpArray.push(sNextChar);
                       // if no lookup array for this character exists, look it up now
                        if( !stMatchSets[sNextChar] ) stMatchSets[sNextChar] = [];
                        // if the match is a string
                        stMatchSets[sNextChar].push(row);
                    }
                }
            }
            
            
        }

        // add the data items to the cache
        for( var k in stMatchSets ){
            // increase the cache size
            options.cacheLength++;
            // add to the cache
            addToCache(k, stMatchSets[k]);
        }
    }

    $input
    .keydown(function(e) {
        // track last key pressed
        lastKeyPressCode = e.keyCode;
        switch(e.keyCode) {
            case 38: // up
                e.preventDefault();
                moveSelect(-1);
                break;
            case 40: // down
                e.preventDefault();
                moveSelect(1);
                break;
            case 9:  // tab
            case 13: // return
                if( selectCurrent() ){
                    // make sure to blur off the current field
                    $input.get(0).blur();
                    e.preventDefault();
                }
                break;
            default:
                active = -1;
                if (timeout) clearTimeout(timeout);
                timeout = setTimeout(function(){onChange();}, options.delay);
                break;
        }
    })
    .focus(function(){
        // track whether the field has focus, we shouldn't process any results if the field no longer has focus
        hasFocus = true;
    })
    .blur(function() {
        // track whether the field has focus
        hasFocus = false;
        hideResults();
    });

    hideResultsNow();

    function onChange() {
        // ignore if the following keys are pressed: [del] [shift] [capslock]
        if( lastKeyPressCode == 46 || (lastKeyPressCode > 8 && lastKeyPressCode < 32) ) return $results.hide();
        var v = $input.val();
        if (v == prev) return;
        prev = v;
        if (v.length >= options.minChars) {
            $input.addClass(options.loadingClass);
            requestData(v);
        } else {
            $input.removeClass(options.loadingClass);
            $results.hide();
        }
    };

     function moveSelect(step) {

        var lis = $("li", results);
        if (!lis) return;

        active += step;

        if (active < 0) {
            active = 0;
        } else if (active >= lis.size()) {
            active = lis.size() - 1;
        }

        lis.removeClass("ac_over");

        $(lis[active]).addClass("ac_over");

        // Weird behaviour in IE
        // if (lis[active] && lis[active].scrollIntoView) {
        //     lis[active].scrollIntoView(false);
        // }

    };

    function selectCurrent() {
        var li = $("li.ac_over", results)[0];
        if (!li) {
            var $li = $("li", results);
            if (options.selectOnly) {
                if ($li.length == 1) li = $li[0];
            } else if (options.selectFirst) {
                li = $li[0];
            }
        }
        if (li) {
            selectItem(li);
            return true;
        } else {
            return false;
        }
    };

    function selectItem(li) {
        if (!li) {
            li = document.createElement("li");
            li.extra = [];
            li.selectValue = "";
        }
        var v = $.trim(li.selectValue ? li.selectValue : li.innerHTML);
        input.lastSelected = v;
        prev = v;
        $results.html("");
        $input.val(v);
        hideResultsNow();
        if (options.onItemSelect) setTimeout(function() { options.onItemSelect(li) }, 1);
    };

    // selects a portion of the input string
    function createSelection(start, end){
        // get a reference to the input element
        var field = $input.get(0);
        if( field.createTextRange ){
            var selRange = field.createTextRange();
            selRange.collapse(true);
            selRange.moveStart("character", start);
            selRange.moveEnd("character", end);
            selRange.select();
        } else if( field.setSelectionRange ){
            field.setSelectionRange(start, end);
        } else {
            if( field.selectionStart ){
                field.selectionStart = start;
                field.selectionEnd = end;
            }
        }
        field.focus();
    };

    // fills in the input box w/the first match (assumed to be the best match)
    function autoFill(sValue){
        // if the last user key pressed was backspace, don't autofill
        if( lastKeyPressCode != 8 ){
            //$input.val(sValue);
            // select the portion of the value not typed by the user (so the next character will erase)
            //createSelection(prev.length, sValue.length);
        }
    };
    /*
    // fills in the input box w/the first match (assumed to be the best match)
    function autoFill(sValue){
        // if the last user key pressed was backspace, don't autofill
        if( lastKeyPressCode != 8 ){
            // fill in the value (keep the case the user has typed)
            $input.val($input.val() + sValue.substring(prev.length));
            // select the portion of the value not typed by the user (so the next character will erase)
            createSelection(prev.length, sValue.length);
        }
    };*/

    function showResults() {
        // get the position of the input field right now (in case the DOM is shifted)
        var pos = findPos(input);
        // either use the specified width, or autocalculate based on form element
        var iWidth = (options.width > 0) ? options.width : $input.width();
        // reposition
        $results.css({
            width: parseInt(iWidth) + "px",
            top: (pos.y + input.offsetHeight) + "px",
            left: pos.x + "px"
        }).show();
    };

    function hideResults() {
        if (timeout) clearTimeout(timeout);
        timeout = setTimeout(hideResultsNow, 200);
    };

    function hideResultsNow() {
        if (timeout) clearTimeout(timeout);
        $input.removeClass(options.loadingClass);
        if ($results.is(":visible")) {
            $results.hide();
        }
        if (options.mustMatch) {
            var v = $input.val();
            if (v != input.lastSelected) {
                selectItem(null);
            }
        }
    };

    function receiveData(q, data) {
        if (data) {
            $input.removeClass(options.loadingClass);
            results.innerHTML = "";

            // if the field no longer has focus or if there are no matches, do not display the drop down
            if( !hasFocus || data.length == 0 ) return hideResultsNow();

            if ($.browser.msie) {
                // we put a styled iframe behind the calendar so HTML SELECT elements don't show through
                $results.append(document.createElement('iframe'));
            }
            results.appendChild(dataToDom(data));
            // autofill in the complete box w/the first match as long as the user hasn't entered in more data
            if( options.autoFill && ($input.val().toLowerCase() == q.toLowerCase()) ) autoFill(data[0][0]);
            showResults();
        } else {
            hideResultsNow();
        }
    };

    function parseData(data) {
        if (!data) return null;
        var parsed = [];
        var rows = data.split(options.lineSeparator);
        for (var i=0; i < rows.length; i++) {
            var row = $.trim(rows[i]);
            if (row) {
                parsed[parsed.length] = row.split(options.cellSeparator);
            }
        }
        return parsed;
    };

    function dataToDom(data) {
        var ul = document.createElement("ul");
        var num = data.length;

        // limited results to a max number
        if( (options.maxItemsToShow > 0) && (options.maxItemsToShow < num) ) num = options.maxItemsToShow;

        for (var i=0; i < num; i++) {
            var row = data[i];
            if (!row) continue;
            var li = document.createElement("li");
            if (options.formatItem) {
                li.innerHTML = options.formatItem(row, i, num);
                li.selectValue = row[0];
            } else {
                li.innerHTML = row[0];
                li.selectValue = row[0];
            }
            var extra = null;
            if (row.length > 1) {
                extra = [];
                for (var j=1; j < row.length; j++) {
                    extra[extra.length] = row[j];
                }
            }
            li.extra = extra;
            ul.appendChild(li);
            $(li).hover(
                function() { $("li", ul).removeClass("ac_over"); $(this).addClass("ac_over"); active = $("li", ul).indexOf($(this).get(0)); },
                function() { $(this).removeClass("ac_over"); }
            ).click(function(e) { e.preventDefault(); e.stopPropagation(); selectItem(this) });
        }
        return ul;
    };

    function requestData(q) {
        if (!options.matchCase) q = q.toLowerCase();
        var data = options.cacheLength ? loadFromCache(q) : null;
        // recieve the cached data
        if (data) {
            receiveData(q, data);
        // if an AJAX url has been supplied, try loading the data now
        } else if( (typeof options.url == "string") && (options.url.length > 0) ){
            $.get(makeUrl(q), function(data) {
                data = parseData(data);
                addToCache(q, data);
                receiveData(q, data);
            });
        // if there's been no data found, remove the loading class
        } else {
            $input.removeClass(options.loadingClass);
        }
    };

    function makeUrl(q) {
        var url = options.url + "?q=" + encodeURI(q);
        for (var i in options.extraParams) {
            url += "&" + i + "=" + encodeURI(options.extraParams[i]);
        }
        return url;
    };

    function loadFromCache(q) {
        if (!q) return null;
        if (cache.data[q]) return cache.data[q];
        if (options.matchSubset) {
            for (var i = q.length - 1; i >= options.minChars; i--) {
                var qs = q.substr(0, i);
                var c = cache.data[qs];
                if (c) {
                    var csub = [];
                    for (var j = 0; j < c.length; j++) {
                        var x = c[j];
                        var x0 = x[0];
                        if (matchSubset(x0, q)) {
                            csub[csub.length] = x;
                        }
                    }
                    return csub;
                }
            }
        }
        return null;
    };

    function matchSubset(s, sub) {
        if (!options.matchCase) s = s.toLowerCase();
        var i = s.indexOf(sub);
        if (i == -1) return false;
        //return i == 0 || options.matchContains;
        return true;
    };

    this.flushCache = function() {
        flushCache();
    };

    this.setExtraParams = function(p) {
        options.extraParams = p;
    };

    this.findValue = function(){
        var q = $input.val();

        if (!options.matchCase) q = q.toLowerCase();
        var data = options.cacheLength ? loadFromCache(q) : null;
        if (data) {
            findValueCallback(q, data);
        } else if( (typeof options.url == "string") && (options.url.length > 0) ){
            $.get(makeUrl(q), function(data) {
                data = parseData(data)
                addToCache(q, data);
                findValueCallback(q, data);
            });
        } else {
            // no matches
            findValueCallback(q, null);
        }
    }

    function findValueCallback(q, data){
        if (data) $input.removeClass(options.loadingClass);

        var num = (data) ? data.length : 0;
        var li = null;

        for (var i=0; i < num; i++) {
            var row = data[i];

            if( row[0].toLowerCase() == q.toLowerCase() ){
                li = document.createElement("li");
                if (options.formatItem) {
                    li.innerHTML = options.formatItem(row, i, num);
                    li.selectValue = row[0];
                } else {
                    li.innerHTML = row[0];
                    li.selectValue = row[0];
                }
                var extra = null;
                if( row.length > 1 ){
                    extra = [];
                    for (var j=1; j < row.length; j++) {
                        extra[extra.length] = row[j];
                    }
                }
                li.extra = extra;
            }
        }

        if( options.onFindValue ) setTimeout(function() { options.onFindValue(li) }, 1);
    }

    function addToCache(q, data) {
        if (!data || !q || !options.cacheLength) return;
        if (!cache.length || cache.length > options.cacheLength) {
            flushCache();
            cache.length++;
        } else if (!cache[q]) {
            cache.length++;
        }
        cache.data[q] = data;
    };

    function findPos(obj) {
        var curleft = obj.offsetLeft || 0;
        var curtop = obj.offsetTop || 0;
        while (obj = obj.offsetParent) {
            curleft += obj.offsetLeft
            curtop += obj.offsetTop
        }
        return {x:curleft,y:curtop};
    }
}

jQuery.fn.autocomplete = function(url, options, data) {
    // Make sure options exists
    options = options || {};
    // Set url as option
    options.url = url;
    // set some bulk local data
    options.data = ((typeof data == "object") && (data.constructor == Array)) ? data : null;

    // Set default values for required options
    options.inputClass = options.inputClass || "ac_input";
    options.resultsClass = options.resultsClass || "ac_results";
    options.lineSeparator = options.lineSeparator || "\n";
    options.cellSeparator = options.cellSeparator || "|";
    options.minChars = options.minChars || 1;
    options.delay = options.delay || 400;
    options.matchCase = options.matchCase || 0;
    options.matchSubset = options.matchSubset || 1;
    options.matchContains = options.matchContains || 0;
    options.cacheLength = options.cacheLength || 1;
    options.mustMatch = options.mustMatch || 0;
    options.extraParams = options.extraParams || {};
    options.loadingClass = options.loadingClass || "ac_loading";
    options.selectFirst = options.selectFirst || false;
    options.selectOnly = options.selectOnly || false;
    options.maxItemsToShow = options.maxItemsToShow || -1;
    options.autoFill = options.autoFill || false;
    options.width = parseInt(options.width, 10) || 0;

    this.each(function() {
        var input = this;
        new jQuery.autocomplete(input, options);
    });

    // Don't break the chain
    return this;
}

jQuery.fn.autocompleteArray = function(data, options) {
    return this.autocomplete(null, options, data);
}

jQuery.fn.indexOf = function(e){
    for( var i=0; i<this.length; i++ ){
        if( this[i] == e ) return i;
    }
    return -1;
};

Only thing is, if your using the matchContains options, it'll probably no longer work.

Link to comment
Share on other sites

Top Man!

I was just about to reply as there was a snippet of code which was auto populating the first match as the 'assumed best match', so typing 'Ab' to hopefully find the result 'Town of Aberdeen' and this was overtyping the text I entered after just 1 key press.

BUT

The code you have just posted does the job perfectly, thank you very much.

Nice way to spend a couple of hours on a Sunday

Just out of curiosity, what did you change and why. Like to know so I can play and understand, works well for me when learning.

Thanks again

Frooty

Link to comment
Share on other sites

You've pretty much sussed it yourself from what you said - the autopopulating was screwing things up. I changed this

// fills in the input box w/the first match (assumed to be the best match)
    function autoFill(sValue){
        // if the last user key pressed was backspace, don't autofill
        if( lastKeyPressCode != 8 ){
            // fill in the value (keep the case the user has typed)
            $input.val($input.val() + sValue.substring(prev.length));
            // select the portion of the value not typed by the user (so the next character will erase)
            createSelection(prev.length, sValue.length);
        }
    }
to
// fills in the input box w/the first match (assumed to be the best match)
    function autoFill(sValue){
        // if the last user key pressed was backspace, don't autofill
        if( lastKeyPressCode != 8 ){
            //$input.val(sValue);
            // select the portion of the value not typed by the user (so the next character will erase)
            //createSelection(prev.length, sValue.length);
        }
    };
I should've removed these lines, but was lazy and just commented it out instead ;). What it does is stop the text field from getting automatically populated with the closest match - this was causing problems narrowing down the results when you typed the next character. and this
function matchSubset(s, sub) {
        if (!options.matchCase) s = s.toLowerCase();
        var i = s.indexOf(sub);
        if (i == -1) return false;
        return i == 0 || options.matchContains;
    };
to
function matchSubset(s, sub) {
        if (!options.matchCase) s = s.toLowerCase();
        var i = s.indexOf(sub);
        if (i == -1) return false;
        //return i == 0 || options.matchContains;
        return true;
    };

This is called everytime you type a 2nd/3rd/4th letter and it narrows down the original list of matches that were shown. Before it was only returning true where i = 0, (i.e. if "ab" was a substring of "aberdeen" but not if "de" was a substring of "aberdeen", as i would be 4 in that case. Now it returns true if there's a match anywhere in the string.

Link to comment
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

Loading...
  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...