Skip to content

Instantly share code, notes, and snippets.

@phillipadsmith
Created April 24, 2009 23:42
Show Gist options
  • Save phillipadsmith/101405 to your computer and use it in GitHub Desktop.
Save phillipadsmith/101405 to your computer and use it in GitHub Desktop.
Search-as-you-type interface for Bricolage categories.
// ==UserScript==
// @name Bricolage category search
// @namespace http://wiki.bricolage.cc/greasemonkey
// @description Search-as-you-type interface for Bricolage categories. Works for "New Story", "New Media", and "New Category" pages.
// @include */workflow/profile/*/new/*
// @include */admin/profile/category
// ==/UserScript==
// Create an object, used below
var CatSearch = {
lastIndex : 0,
lastText : "",
get catSelect() {
if (! this._catSelect) {
var isStoryProf = location.pathname.indexOf('story') !== -1;
var isCatProf = location.pathname.indexOf('category') !== -1;
var ename = isStoryProf ? 'story_prof|new_category_id' : isCatProf ? 'parent_id' : 'media_prof|category__id';
var selectList = document.getElementsByName(ename);
if (selectList.length) {
this._catSelect = selectList.item(0);
}
}
return this._catSelect;
},
get searchBox() {
return this._searchBox;
},
addSearchBox : function() {
if (this._searchBox) {
return;
}
var textBox = document.createElement('input');
textBox.setAttribute('type', 'text');
textBox.setAttribute('id', 'catsearch');
textBox.setAttribute('class', 'textInput');
textBox.setAttribute('name', 'catsearch');
textBox.setAttribute('size', '50');
textBox.setAttribute('maxlength', '256');
this._searchBox = textBox;
var sel = this.catSelect;
sel.parentNode.insertBefore(this._searchBox, sel);
this.cacheIndex("", 0);
},
cacheIndex : function(str, i) {
this._lookup[str] = i;
},
lookupIndex : function(str) {
if (this._lookup[str] !== undefined) {
return this._lookup[str];
}
return -1;
},
searchAsYouType : function(event) {
var searchBox = event.target;
var key = event.keyCode;
var isEnterKey = key === event.DOM_VK_ENTER || key === event.DOM_VK_RETURN;
var newText = searchBox.value;
var newIndex = this.lookupIndex(newText);
if (newIndex !== -1 && !isEnterKey) {
this.catSelect.options[newIndex].selected = true;
} else {
var oldText = this.lastText;
var d = newText.length - oldText.length;
var newIndex = -1;
if (d > 0 && newText.indexOf(oldText) == 0) {
// The new text is longer, with the beginning being
// the old text, so the user added onto the end;
// therefore we can search forward from where we stopped before.
d = 1;
newIndex = this.lastIndex;
} else if (d < 0 && oldText.indexOf(newText) == 0) {
// The old text is longer, with the beginning being
// the new text, so the user deleted from the end;
// therefore we can search backward from where we stopped before.
d = -1;
newIndex = this.lastIndex;
} else if (d === 0 && isEnterKey) {
// The user hit Enter, so we continue searching forward.
d = 1;
newIndex = newText.length ? this.lastIndex + 1 : this.lastIndex;
} else {
// The user: added/deleted text somewhere besides the end,
// or selected a character and replaced with something else;
// in all these cases, we start the search over.
d = 1;
newIndex = 0;
}
// Search unless the text is unchanged or empty
var searchMatched = 0;
if (isEnterKey || newText.length > 0 || oldText !== newText) {
var opts = this.catSelect.options;
var optsLen = opts.length;
for (var i = newIndex; i >= 0 && i < optsLen; i += d) {
var opt = opts[i];
var cat = opt.text;
if (cat.indexOf(newText) !== -1) {
newIndex = i;
opt.selected = true;
searchMatched = 1;
this.cacheIndex(cat, newIndex);
if (! isEnterKey) {
// don't save for Enter, otherwise for example
// category /new will get cached for /new2, /new3, etc.
this.cacheIndex(newText, newIndex);
}
break;
}
}
}
if (newText.length === 0 || !searchMatched) {
newIndex = 0;
this.catSelect.options[0].selected = true;
}
}
// Save stuff for next time
this.lastText = newText;
this.lastIndex = newIndex;
// Prevent form submit with Enter (Bricolage form validation)
if (isEnterKey) {
event.preventBubble();
event.preventDefault();
}
// Make sure focus is back in the search box
searchBox.focus();
},
init : function(event) {
this.lastIndex = 0;
this.lastText = "";
this._lookup = {};
},
_catSelect: null,
_searchBox: null,
_lookup: {}
};
// Entry point of the greasemonkey script, wrapped in an anonymous
// function to avoid polluting the original page's namespace
(function() {
// Check that we're on the right page, that there's a select list
var catSelect = CatSearch.catSelect;
if (catSelect) {
var opts = catSelect.options;
if (! opts.length) {
return;
}
} else {
return;
}
// Add the search box above the category select list
CatSearch.addSearchBox();
var searchBox = CatSearch.searchBox;
// Add the search-as-you-type callback
searchBox.addEventListener('keyup', function(event) {
CatSearch.searchAsYouType(event);
}, true);
// Prevent Bricolage from doing onsubmit validation for Enter during search
searchBox.addEventListener('keypress', function(event) {
if (event.keyCode === event.DOM_VK_ENTER || event.keyCode === event.DOM_VK_RETURN) {
event.preventBubble();
event.preventDefault();
}
}, true);
searchBox.addEventListener('keydown', function(event) {
if (event.keyCode === event.DOM_VK_ENTER || event.keyCode === event.DOM_VK_RETURN) {
event.preventBubble();
event.preventDefault();
}
}, true);
// Initialize on load, in case categories change and to clear the cache
window.addEventListener('load', function(event) {
CatSearch.init();
}, true);
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment