Demo page:
PHP Code:
<script type="text/javascript" src="http://jqueryjs.googlecode.com/files/jquery-1.3.2.min.js"></script>
<script type="text/javascript" src="jquery.optionTree.js"></script>
<label for="gcategory" id="goocattitle">Edit Google Product Category:</label>
<p>You can search the taxonomy here: <a class="external" href="http://support.google.com/merchants/bin/answer.py?hl=en-GB&answer=1705911">Taxonomy Search</a></p>
<div>
<input type="text" name="demo7" />
</div>
<div class="results" id="demo7-result"></div>
<script type="text/javascript">
$(function() {
var options = {
empty_value: 'null',
indexed: true, // the data in tree is indexed by values (ids), not by labels
on_each_change: 'get-subtree.php', // this file will be called with 'id' parameter, JSON data must be returned
choose: function(level) {
return 'Choose level ' + level;
},
loading_image: 'ajax-load.gif',
show_multiple: 21, // if true - will set the size to show all options
choose: '', // no choose item
set_value_on: 'each'
};
var displayParents = function() {
var labels = []; // initialize array
$(this).siblings('select') // find all select
.find(':selected') // and their current options
.each(function() { labels.push($(this).text()); }); // and add option text to array
$('#demo7-result').empty();
$('<textarea id="goocategory" name="goocategory" cols="100">').text(labels.join(' > ')).appendTo('#demo7-result'); // and display the labels
}
$.getJSON('get-subtree.php', function(tree) { // initialize the tree by loading the file first
$('input[name=demo7]').optionTree(tree, options).change(displayParents);
});
});
</script>
</div>
get-sub-tree.php:
PHP Code:
<?php
require_once 'LazyTaxonomyReader.php';
$reader = new LazyTaxonomyReader('taxonomy.txt');
$line_no = (isset($_GET['id']) && is_numeric($_GET['id']) ? (int) $_GET['id'] : null);
echo json_encode($reader->getDirectDescendants($line_no));
?>
LazyTaxonomyReader.php:
PHP Code:
class LazyTaxonomyReader {
private $base = null;
private $separator = ' > ';
protected $lines;
public function __construct($file = 'taxonomy.txt') {
$this->lines = file($file, FILE_IGNORE_NEW_LINES);
}
public function setBaseNode($line_no) {
if (is_null($line_no)) {
$this->base = null;
return;
}
if (!array_key_exists($line_no, $this->lines)) {
throw new IllegalArgumentException("Invalid line number.");
}
$this->base = $this->lines[$line_no];
}
public function getDirectDescendants($line_no = null) {
$this->setBaseNode($line_no);
// select only lines that are directly below current base node
$direct = array_filter($this->lines, array($this, 'isDirectlyBelowBase'));
// return only last part of their names
return array_map(array($this, 'getLastNode'), $direct);
}
protected function getLastNode($line) {
if (strpos($line, $this->separator) === false) {
// no separator present
return $line;
}
// strip up to and including last separator
return substr($line, strrpos($line, $this->separator) + strlen($this->separator));
}
protected function isDirectlyBelowBase($line) {
// starting text that must be present
if (is_null($this->base)) {
$start = '';
} else {
$start = $this->base . $this->separator;
}
if ($start !== '') {
$starts_at_base = (strpos($line, $start) === 0);
if (!$starts_at_base) { // starts with something different
return false;
}
// remove start text AND the following separator
$line = str_replace($start, '', $line);
}
// we're direct descendants if we have no separators left on the line
if (strpos($line, $this->separator) !== false)
return false;
return true;
}
}
jquery.optionTree.js:
Code:
(function($){
$.fn.optionTree = function(tree, options) {
options = $.extend({
choose: 'Choose...', // string with text or function that will be passed current level and returns a string
show_multiple: false, // show multiple values (if true takes number of items as size, or number (eg. 12) to show fixed size)
preselect: {},
loading_image: '', // show an ajax loading graphics (animated gif) while loading ajax (eg. /ajax-loader.gif)
select_class: '',
leaf_class: 'final',
empty_value: '', // what value to set the input to if no valid option was selected
on_each_change: false, // URL to lazy load (JSON, 'id' parameter will be added) or function. See default_lazy_load
set_value_on: 'leaf', // leaf - sets input value only when choosing leaf node. 'each' - sets value on each level change.
// makes sense only then indexed=true
indexed: true,
preselect_only_once: false // if true, once preselected items will be chosen, the preselect list is cleared. This is to allow
// changing the higher level options without automatically changing lower levels when a whole subtree is in preselect list
}, options || {});
var cleanName = function (name) {
return name.replace(/_*$/, '');
};
var removeNested = function (name) {
$("select[name^='"+ name + "']").remove();
};
var setValue = function(name, value) {
$("input[name='" + cleanName(name) + "']").val(value).change();
};
// default lazy loading function
var default_lazy_load = function(value) {
var input = this;
if ( options.loading_image !== '' ) {
// show loading animation
$("<img>")
.attr('src', options.loading_image)
.attr('class', 'optionTree-loader')
.insertAfter(input);
}
$.getJSON(options.lazy_load, {id: value}, function(tree) {
$('.optionTree-loader').remove();
var prop;
for (prop in tree) {
if (tree.hasOwnProperty(prop)) { // tree not empty
$(input).optionTree(tree, options);
return;
}
}
// tree empty, call value switch
$(input).optionTree(value, options);
});
};
if (typeof options.on_each_change === 'string') { // URL given as an onchange
options.lazy_load = options.on_each_change;
options.on_each_change = default_lazy_load;
}
var isPreselectedFor = function(clean, v) {
if (!options.preselect || !options.preselect[clean]) {
return false;
}
if ($.isArray(options.preselect[clean])) {
return $.inArray(v, options.preselect[clean]) !== -1;
}
return (options.preselect[clean] === v);
};
return this.each(function() {
var name = $(this).attr('name') + "_";
// remove all dynamic options of lower levels
removeNested(name);
if (typeof tree === "object") { // many options exists for current nesting level
// create select element with all the options
// and bind onchange event to recursively call this function
var $select = $("<select>").attr('name',name)
.change(function() {
if (this.options[this.selectedIndex].value !== '') {
if ($.isFunction(options.on_each_change)) {
removeNested(name + '_');
options.on_each_change.apply(this, [this.options[this.selectedIndex].value, tree]);
} else {
// call with value as a first parameter
$(this).optionTree(tree[this.options[this.selectedIndex].value], options);
}
if (options.set_value_on === 'each') {
setValue(name, this.options[this.selectedIndex].value);
}
} else {
removeNested(name + '_');
setValue(name, options.empty_value);
}
});
var text_to_choose = '';
if (jQuery.isFunction(options.choose)) {
var level = $(this).siblings().andSelf().filter('select').length;
text_to_choose = options.choose.apply(this, [level]);
} else if ( options.choose !== '' ) {
text_to_choose = options.choose;
}
// if show multiple -> show open select
var count_tree_objects = 0;
if ( text_to_choose !== '' ) {
// we have a default value
count_tree_objects++;
}
if (options.show_multiple > 1) {
count_tree_objects = options.show_multiple;
} else if (options.show_multiple === true) {
$.each(tree, function() {
count_tree_objects++;
});
}
if ( count_tree_objects > 1 ){
$select.attr('size', count_tree_objects);
}
if ($(this).is('input')) {
$select.insertBefore(this);
} else {
$select.insertAfter(this);
}
if (options.select_class) {
$select.addClass(options.select_class);
}
if ( text_to_choose !== '' ) {
$("<option>").html(text_to_choose).val('').appendTo($select);
}
var foundPreselect = false;
$.each(tree, function(k, v) {
var label, value;
if (options.indexed) {
label = v;
value = k;
} else {
label = value = k;
}
var o = $("<option>").html(label)
.attr('value', value);
var clean = cleanName(name);
if (options.leaf_class && typeof value !== 'object') { // this option is a leaf node
o.addClass(options.leaf_class);
}
o.appendTo($select);
if (isPreselectedFor(clean, value)) {
o.get(0).selected = true;
foundPreselect = true;
}
});
if (foundPreselect) {
$select.change();
}
if (!foundPreselect && options.preselect_only_once) { // clear preselect on first not-found level
options.preselect[cleanName(name)] = null;
}
} else if (options.set_value_on === 'leaf') { // single option is selected by the user (function called via onchange event())
if (options.indexed) {
setValue(name, this.options[this.selectedIndex].value);
} else {
setValue(name, tree);
}
}
});
};
}(jQuery));
Bookmarks