Implement auto-complete

feature/core
Sven Slootweg 11 years ago
parent ac0017ef44
commit 7403edbfd9

@ -0,0 +1,47 @@
<?php
/*
* openNG is more free software. It is licensed under the WTFPL, which
* allows you to do pretty much anything with it, without having to
* ask permission. Commercial use is allowed, and no attribution is
* required. We do politely request that you share your modifications
* to benefit other developers, but you are under no enforced
* obligation to do so :)
*
* Please read the accompanying LICENSE document for the full WTFPL
* licensing text.
*/
if(!isset($_APP)) { die("Unauthorized."); }
$sOriginalData = array(
array(
"name" => "ChicagoVPS",
"description" => "A VPS company.",
"value" => "id-for-chicagovps",
"created" => "2013-08-02"
),
array(
"name" => "BuffaloVPS",
"description" => "A VPS company.",
"value" => "id-for-buffalovps",
"created" => "2013-08-03"
),
array(
"name" => "ColoCrossing",
"description" => "A colocation provider.",
"value" => "id-for-colocrossing",
"created" => "2013-08-06"
)
);
$sData = array();
foreach($sOriginalData as $sEntry)
{
if(strpos(strtolower($sEntry['name']), strtolower($_GET['q'])) !== false)
{
$sData[] = $sEntry;
}
}
sleep(1);

@ -33,6 +33,10 @@ $router->routes = array(
'target' => "modules/nodes/create.php",
'_json' => true
),
"^/autocomplete/search$" => array(
"target" => "modules/autocomplete/search.php",
"_json" => true
)
)
);

@ -40,6 +40,71 @@ form.inline
border-bottom-left-radius: 0px;
}
#autocomplete_search
{
display: none;
position: absolute;
width: 400px;
background-color: white;
border: 1px solid #5A6DBB;
border-radius: 0px 0px 3px 3px;
border-top: none;
-webkit-font-smoothing: antialiased;
font-family: 'Istok Web';
z-index: 9999999;
}
#autocomplete_search .entry
{
border-bottom: 1px solid #C2C2C2;
padding: 7px 9px;
color: #393939;
}
#autocomplete_search .entry:last-child
{
border-bottom: none;
}
#autocomplete_search .entry.selected
{
background-color: #4253B6;
color: white;
}
#autocomplete_search .entry .name
{
font-size: 18px;
font-weight: bold;
}
#autocomplete_search .entry .description, #autocomplete_search .entry .date
{
font-size: 14px;
}
#autocomplete_search .entry .date
{
float: right;
color: gray;
}
#autocomplete_search .entry.selected .date
{
color: white;
}
#autocomplete_search .loading, #autocomplete_search .noresults
{
padding: 8px 10px;
font-size: 17px;
}
#autocomplete_search .loading
{
font-style: italic;
}
.group-first, .group-middle
{
border-top-right-radius: 0px !important;

@ -0,0 +1,252 @@
function AutoCompleter(type) {
this.type = type;
this.template = $(".autocompleter-template[data-template=" + type + "]");
}
AutoCompleter.prototype.spawn = function(source) {
var instance = new AutoCompleterInstance(this.template.clone().removeClass("autocompleter-template").addClass("autocompleter-" + this.type).appendTo("body"), source);
return instance;
}
function AutoCompleterInstance(element, source) {
this.element = element;
this.current_selection = 0;
this.source = source;
}
AutoCompleterInstance.prototype.attachBelow = function(element) {
var left = element.offset().left;
var top = element.offset().top + element.outerHeight();
this.target = element;
this.element.css({left: left, top: top, display: "block"}).show();
this.show();
$(element).data("attached-autocomplete", this);
this.element.data("autocomplete-object", this);
this.element.disableSelection();
}
AutoCompleterInstance.prototype.remove = function() {
this.unhookKeyEvents(this.target);
this.unhookMouseEvents(this.target);
this.hide();
this.target.data("attached-autocomplete", "");
this.element.remove();
}
AutoCompleterInstance.prototype.hookKeyEvents = function(element) {
element.on("keyup.autocomplete", this._handleKeyUp.bind(this));
element.on("keydown.autocomplete", this._handleKeyDown.bind(this));
element.on("input.autocomplete", this._handleInput.bind(this));
}
AutoCompleterInstance.prototype.unhookKeyEvents = function(element) {
element.off("keyup.autocomplete");
element.off("keydown.autocomplete");
element.off("input.autocomplete");
}
AutoCompleterInstance.prototype.hookMouseEvents = function(element) {
this.element.on("mouseover.autocomplete", ".entry", this._handleMouseOver);
this.element.on("mouseup.autocomplete", ".entry", this._handleMouseClick);
}
AutoCompleterInstance.prototype.unhookMouseEvents = function(element) {
this.element.off("mouseover.autocomplete", ".entry");
this.element.on("mouseup.autocomplete", ".entry");
}
AutoCompleterInstance.prototype._handleKeyUp = function(event) {
switch(event.keyCode)
{
case 9: // Tab
case 13: // Enter/Return
this._selectCurrent();
event.stopPropagation();
event.preventDefault();
break;
}
}
AutoCompleterInstance.prototype._handleKeyDown = function(event) {
switch(event.keyCode)
{
case 9: // Tab
case 13: // Enter/Return
/* We don't want this to do anything. */
event.stopPropagation();
event.preventDefault();
break;
case 38: // Arrow Up
this._movePrevious();
event.stopPropagation();
event.preventDefault();
break;
case 40: // Arrow Down
this._moveNext();
event.stopPropagation();
event.preventDefault();
break;
case 27: // Escape
this.remove();
break;
}
}
AutoCompleterInstance.prototype._handleInput = function(event) {
clearTimeout(this.update_timer);
this.update_timer = setTimeout(this._updateItems.bind(this), 350);
}
AutoCompleterInstance.prototype._handleMouseOver = function(event) {
var selected = $(this).data("position");
var autocompleter = $(this).closest(".autocompleter").data("autocomplete-object");
autocompleter.current_selection = selected;
autocompleter._updateSelection();
}
AutoCompleterInstance.prototype._handleMouseClick = function(event) {
var autocompleter = $(this).closest(".autocompleter").data("autocomplete-object");
/* We ignore the actual represented entry; we just want to treat the currently highlighted entry
* as the one the user wants to select. In certain cases this is helpful for smooth UX, as it allows
* changing the selection while the mouse button is held - this may occur when the user changes his
* mind at the last moment. */
autocompleter._selectCurrent();
}
AutoCompleterInstance.prototype._updateSelection = function() {
this.element.find(".entry").removeClass("selected").eq(this.current_selection).addClass("selected");
}
AutoCompleterInstance.prototype._movePrevious = function() {
/* We check validity afterwards to prevent race conditions (mouse vs. keyboard). */
this.current_selection -= 1;
if(this.current_selection < 0)
{
this.current_selection = 0;
}
this._updateSelection();
}
AutoCompleterInstance.prototype._moveNext = function() {
this.current_selection += 1;
if(this.current_selection > this.total_items - 1)
{
this.current_selection = this.total_items - 1;
}
this._updateSelection();
}
AutoCompleterInstance.prototype._selectCurrent = function() {
var item = this.source.getItem(this.current_selection);
if(typeof this.callback !== "undefined")
{
this.callback(item).apply(this);
}
else
{
this.target.val(item.value);
}
this.remove();
}
AutoCompleterInstance.prototype._updateItems = function() {
var query = this.target.val();
if(query == "")
{
this.hide();
}
else
{
this.show();
}
this.element.find(".noresults, .results").hide();
this.element.find(".loading").show();
this.source.updateItems(query, this.continueUpdate.bind(this));
}
AutoCompleterInstance.prototype.continueUpdate = function() {
this.total_items = this.source.getItemCount();
if(this.total_items > 0)
{
this.element.find(".entry").slice(1).remove();
var base_element = this.element.find(".entry").eq(0);
var items = this.source.getAll();
for(i in items)
{
var item = items[i];
if(i == 0)
{
var current_element = base_element.addClass("selected").data("position", i);
}
else
{
var current_element = base_element.clone().appendTo(base_element.parent()).removeClass("selected").data("position", i);
}
current_element.find(".autocompleter-field").each(function(){
$(this).html(item[$(this).data("field")]);
});
}
this.element.find(".results").show();
this.element.find(".noresults").hide();
}
else
{
this.element.find(".results").hide();
this.element.find(".noresults").show();
}
this.element.find(".loading").hide();
}
AutoCompleterInstance.prototype.hide = function() {
this.element.hide();
$(this.target).css({"border-bottom-left-radius": "", "border-bottom-right-radius": ""});
}
AutoCompleterInstance.prototype.show = function() {
this.element.show();
$(this.target).css({"border-bottom-left-radius": 0, "border-bottom-right-radius": 0});
}
;(function($) {
$.fn.disableSelection = function() {
return this
.attr('unselectable', 'on')
.css('user-select', 'none')
.on('selectstart', false);
};
$.fn.enableSelection = function() {
return this
.attr('unselectable', 'off')
.css('user-select', 'text')
.off('selectstart');
};
$.fn.autoComplete = function(autocompleter, source, callback) {
var instance = autocompleter.spawn(source);
instance.callback = callback;
instance.attachBelow(this);
instance.hookKeyEvents(this);
instance.hookMouseEvents(this);
instance._updateItems();
this.attr("autocomplete", "off");
return this;
};
}(jQuery));

@ -77,7 +77,7 @@ function hookSubmitEvent(form, callback, error)
data: formdata,
dataType: "json",
success: function(data, xhr){ console.log(data); callback(element, data, xhr); },
error: function(data, xhr, err){ error(element, data, xhr, err); /* This handles HTTP errors, NOT application errors! */ }
error: function(data, xhr, err){ if(typeof error !== "undefined") { error(element, data, xhr, err); } /* This handles HTTP errors, NOT application errors! */ }
});
return false;
@ -177,6 +177,32 @@ function spawnTemplate(name)
return template_cache[name].clone();
}
function SearchCompletionSource(element)
{
this.element = element;
this.results = [];
}
SearchCompletionSource.prototype.getItemCount = function() {
return this.results.length;
}
SearchCompletionSource.prototype.getAll = function() {
return this.results;
}
SearchCompletionSource.prototype.getItem = function(index) {
return this.results[index];
}
SearchCompletionSource.prototype.updateItems = function(query, callback) {
$.ajax({
url: "/autocomplete/search/?q=" + escape(query),
dataType: "json",
success: function(result) { this.results = result; console.log(result); callback(); }.bind(this)
});
}
$(function(){
hookSubmitEvent($("#form_search"));
@ -215,5 +241,16 @@ $(function(){
spawnTemplate(parent.data("template-name")).insertAfter(parent).find("input").val("");
parent.data("duplicated", true);
}
})
});
autocompleter_search = new AutoCompleter("search");
//setTimeout(function(){$("#input_search_query").autoComplete(autocompleter_search, new SearchCompletionSource($("#input_search_query")))}, 1000);
$("#input_search_query").on("input", function(){
if(!$(this).data("attached-autocomplete"))
{
$("#input_search_query").autoComplete(autocompleter_search, new SearchCompletionSource($("#input_search_query")));
}
});
});

@ -6,6 +6,7 @@
<link rel="stylesheet" href="/static/css/jsde.style.css">
<script src="/static/js/jquery-1.10.2.min.js"></script>
<script src="/static/js/jquery-timing.min.js"></script>
<script src="/static/js/jquery-autocomplete.js"></script>
<script src="/static/js/jsde.js"></script>
<script src="/static/js/openng.js"></script>
<style>
@ -58,6 +59,25 @@
</span>
<a class="workspace-tab workspace-tab-add" id="workspace_tab_add" href="#">+</a>
</div> -->
<div id="autocomplete_search" class="autocompleter autocompleter-template" data-template="search">
<div class="results">
<div class="entry selected">
<span class="autocompleter-field name" data-field="name">Name</span>
<div class="clear"></div>
<span class="autocompleter-field description" data-field="description">Description</span>
<span class="autocompleter-field date" data-field="created">Creation date</span>
</div>
</div>
<div class="noresults">
No results.
</div>
<div class="loading">
Searching...
</div>
</div>
<div id="notification_area">
<!-- Notifications go here. -->
</div>

Loading…
Cancel
Save