From 3385ec012f999a3b2cc3b10ab837aecd0e461e8b Mon Sep 17 00:00:00 2001 From: Ingo Schommer Date: Mon, 12 Apr 2010 03:33:56 +0000 Subject: [PATCH] FEATURE: upgrading the search functionality of the TreeDropdownTree with pluggable search function BUGFIX: the search was only operating on the part of the tree (as returned by markPartialTree), now it searches globally MINOR: renamed 'filter' to 'search' (from r97031) git-svn-id: svn://svn.silverstripe.com/silverstripe/open/modules/sapphire/trunk@102424 467b73ca-7a2a-4603-9d3b-597d59a354a9 --- forms/TreeDropdownField.php | 73 ++++++++++++++++++++++++++++----- javascript/TreeSelectorField.js | 32 ++++++++------- 2 files changed, 80 insertions(+), 25 deletions(-) diff --git a/forms/TreeDropdownField.php b/forms/TreeDropdownField.php index 38d8008f5..ac9b8e4a1 100755 --- a/forms/TreeDropdownField.php +++ b/forms/TreeDropdownField.php @@ -18,7 +18,12 @@ class TreeDropdownField extends FormField { /** * @ignore */ - protected $sourceObject, $keyField, $labelField, $filterCallback, $baseID = 0; + protected $sourceObject, $keyField, $labelField, $filterCallback, $searchCallback, $baseID = 0; + + /** + * Used by field search to leave only the relevant entries + */ + protected $searchIds = null, $searchExpanded = array(); /** * @param string $name the field name @@ -27,11 +32,11 @@ class TreeDropdownField extends FormField { * @param string $keyField to field on the source class to save as the field value (default ID). * @param string $labelField the field name to show as the human-readable value on the tree (default Title). */ - public function __construct($name, $title = null, $sourceObject = 'Group', $keyField = 'ID', $labelField = 'Title', $showFilter = false) { + public function __construct($name, $title = null, $sourceObject = 'Group', $keyField = 'ID', $labelField = 'Title', $showSearch = false) { $this->sourceObject = $sourceObject; $this->keyField = $keyField; $this->labelField = $labelField; - $this->showFilter = $showFilter; + $this->showSearch = $showSearch; if(!Object::has_extension($this->sourceObject, 'Hierarchy')) { throw new Exception ( @@ -64,6 +69,19 @@ class TreeDropdownField extends FormField { $this->filterCallback = $callback; } + /** + * Set a callback used to search the hierarchy globally, before even applying the filter. + * + * @param callback $callback + */ + public function setSearchFunction($callback) { + if(!is_callable($callback, true)) { + throw new InvalidArgumentException('TreeDropdownField->setSearchFunction(): not passed a valid callback'); + } + + $this->searchCallback = $callback; + } + /** * @return string */ @@ -98,12 +116,12 @@ class TreeDropdownField extends FormField { 'name' => $this->name, 'value' => $this->value ) - ) . ($this->showFilter ? + ) . ($this->showSearch ? $this->createTag( 'input', array( 'class' => 'items', - 'value' => '(Choose or type filter)' + 'value' => '(Choose or type search)' ) ) : $this->createTag ( @@ -134,7 +152,7 @@ class TreeDropdownField extends FormField { public function tree(SS_HTTPRequest $request) { $isSubTree = false; - $this->filter = Convert::Raw2SQL($request->getVar('filter')); + $this->search = Convert::Raw2SQL($request->getVar('search')); if($ID = (int) $request->latestparam('ID')) { $obj = DataObject::get_by_id($this->sourceObject, $ID); @@ -153,7 +171,10 @@ class TreeDropdownField extends FormField { if(!$this->baseID || !$obj) $obj = singleton($this->sourceObject); } - if ($this->filterCallback || $this->sourceObject == 'Folder' || $this->filter != "") + if ( $this->search != "" ) + $this->populateIDs(); + + if ($this->filterCallback || $this->sourceObject == 'Folder' || $this->search != "" ) $obj->setMarkingFilterFunction(array($this, "filterMarking")); $obj->markPartialTree(); @@ -178,7 +199,7 @@ class TreeDropdownField extends FormField { /** * Marking function for the tree, which combines different filters sensibly. If a filter function has been set, - * that will be called. If the source is a folder, automatically filter folder. And if filter text is set, filter on that + * that will be called. If the source is a folder, automatically filter folder. And if search text is set, filter on that * too. Return true if all applicable conditions are true, false otherwise. * @param $node * @return unknown_type @@ -186,12 +207,42 @@ class TreeDropdownField extends FormField { function filterMarking($node) { if ($this->filterCallback && !call_user_func($this->filterCallback, $node)) return false; if ($this->sourceObject == "Folder" && $node->ClassName != 'Folder') return false; - if ($this->filter != "") { - $f = $this->labelField; - return (strpos(strtoupper($node->$f), strtoupper($this->filter)) === FALSE) ? false : true; + if ($this->search != "") { + return isset($this->searchIds[$node->ID]) && $this->searchIds[$node->ID] ? true : false; } + return true; } + + /** + * Populate $this->searchIds with the IDs of the pages matching the searched parameter and their parents. + * Reverse-constructs the tree starting from the leaves. Initially taken from CMSSiteTreeFilter. + */ + protected function populateIDs() { + if ( $this->searchCallback ) + $res = call_user_func($this->searchCallback, $this->sourceObject, $this->labelField, $this->search); + else + $res = DataObject::get($this->sourceObject, "$this->labelField LIKE '%$this->search%'"); + + if( $res ) { + /* And keep a record of parents we don't need to get parents of themselves, as well as IDs to mark */ + foreach($res as $row) { + if ($row->ParentID) $parents[$row->ParentID] = true; + $this->searchIds[$row->ID] = true; + } + + while (!empty($parents)) { + $res = DB::query('SELECT "ParentID", "ID" FROM '.$this->sourceObject.' WHERE "ID" in ('.implode(',',array_keys($parents)).')'); + $parents = array(); + + foreach($res as $row) { + if ($row['ParentID']) $parents[$row['ParentID']] = true; + $this->searchIds[$row['ID']] = true; + $this->searchExpanded[$row['ID']] = true; + } + } + } + } /** * Get the object where the $keyField is equal to a certain value diff --git a/javascript/TreeSelectorField.js b/javascript/TreeSelectorField.js index 3e7b5825c..cb9a50045 100755 --- a/javascript/TreeSelectorField.js +++ b/javascript/TreeSelectorField.js @@ -7,15 +7,15 @@ TreeDropdownField.prototype = { // Hook up all the fieldy bits this.editLink = this.getElementsByTagName('a')[0]; if (this.getElementsByTagName('span').length > 0) { - // no filter, humanItems is a span + // no search, humanItems is a span this.humanItems = this.getElementsByTagName('span')[0]; this.inputTag = this.getElementsByTagName('input')[0]; } else { - // filter is present, humanItems is an input + // search is present, humanItems is an input this.inputTag = this.getElementsByTagName('input')[0]; this.humanItems = this.getElementsByTagName('input')[1]; - this.humanItems.onkeyup = this.filter_onkeyup; + this.humanItems.onkeyup = this.search_onkeyup; } this.editLink.treeDropdownField = this; this.humanItems.treeDropdownField = this; @@ -115,7 +115,7 @@ TreeDropdownField.prototype = { saveCurrentState: function() { this.origHumanText = this.getHumanText(); this.defaultCleared = false; - this.filtered = false; + this.searched = false; }, restoreOriginalState: function() { @@ -176,7 +176,7 @@ TreeDropdownField.prototype = { var ajaxURL = this.helperURLBase() + 'tree/'; ajaxURL += $('SecurityID') ? '&SecurityID=' + $('SecurityID').value : ''; if($('Form_EditForm_Locale')) ajaxURL += "&locale=" + $('Form_EditForm_Locale').value; - if (this.filter() != null) ajaxURL += "&filter=" + this.filter(); + if (this.search() != null) ajaxURL += "&search=" + this.search(); new Ajax.Request(ajaxURL, { method : 'get', onSuccess : after, @@ -184,8 +184,8 @@ TreeDropdownField.prototype = { }) }, - filter: function() { - if (this.humanItems.nodeName != 'INPUT' || !this.filtered) return null; + search: function() { + if (this.humanItems.nodeName != 'INPUT' || !this.searched) return null; return this.humanItems.value; }, @@ -229,8 +229,8 @@ TreeDropdownField.prototype = { var ajaxURL = this.options.dropdownField.helperURLBase() + 'tree/' + this.getIdx(); ajaxURL += $('SecurityID') ? '&SecurityID=' + $('SecurityID').value : ''; if($('Form_EditForm_Locale')) ajaxURL += "&locale=" + $('Form_EditForm_Locale').value; - // ajaxExpansion is called in context of TreeNode, not Tree, so filter() doesn't exist. - if (this.filter && this.filter() != null) ajaxURL += "&filter=" + this.filter(); + // ajaxExpansion is called in context of TreeNode, not Tree, so search() doesn't exist. + if (this.search && this.search() != null) ajaxURL += "&search=" + this.search(); new Ajax.Request(ajaxURL, { onSuccess : this.installSubtree.bind(this), @@ -262,7 +262,7 @@ TreeDropdownField.prototype = { }, updateTreeLabel: function() { - if (this.humanItems.nodeName == 'INPUT') return; // don't update the filter + if ( this.searched || (this.humanItems.nodeName == 'INPUT' && !this.inputTag.value) ) return; // don't update the search var treeNode; if(treeNode = $('selector-' + this.getName() + '-' + this.inputTag.value)) { this.setHumanText(treeNode.getTitle()); @@ -302,16 +302,20 @@ TreeDropdownField.prototype = { return false; }, - filter_onkeyup: function(e) { + search_onkeyup: function(e) { if(typeof window.event!="undefined") e=window.event; //code for IE if (e.keyCode == 27) { // esc, cancel the selection and hide the tree. this.treeDropdownField.restoreOriginalState(); this.treeDropdownField.hideTree(); } else { - this.treeDropdownField.filtered = true; - this.treeDropdownField.deleteTreeNode(); - this.treeDropdownField.showTree(); + var that = this; + clearTimeout(this.timeout); + this.timeout = setTimeout(function() { + that.treeDropdownField.searched = true; + that.treeDropdownField.deleteTreeNode(); + that.treeDropdownField.showTree(); + }, 750); } },