ENHANCEMENT Refactored tree search javascript to support concrete and jquery, and build less UI scaffold on the serverside

API CHANGE Removed CMSMain::site_tree_filter_options
API CHANGE Removed CMSMain::SiteTreeFilterOptions() and T_SiteTreeFilterOptions(), now handled inline in CMSMain::TreeSearchForm()
ENHANCEMENT Refactored tree search PHP in CMSMain: Generate form in PHP instead of template, require less UI state, pass filtered/sanitized paramers straight into CMSMainMarkingFilter instead of relying on

git-svn-id: svn://svn.silverstripe.com/silverstripe/open/modules/cms/trunk@92618 467b73ca-7a2a-4603-9d3b-597d59a354a9
This commit is contained in:
Ingo Schommer 2009-11-21 02:37:06 +00:00
parent 8bc867c554
commit 671961cf45
6 changed files with 224 additions and 223 deletions

View File

@ -55,31 +55,10 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
'SiteTreeAsUL',
'getshowdeletedsubtree',
'getfilteredsubtree',
'batchactions'
'batchactions',
'SearchTreeForm'
);
/**
* SiteTree Columns that can be filtered using the the Site Tree Search button
*/
static $site_tree_filter_options = array(
'Title' => array('CMSMain.TITLE', 'Title'),
'MenuTitle' => array('CMSMain.MENUTITLE', 'Navigation Label'),
'ClassName' => array('CMSMain.PAGETYPE', 'Page Type'),
'Status' => array('CMSMain.STATUS', 'Status'),
'MetaDescription' => array('CMSMain.METADESC', 'Description'),
'MetaKeywords' => array('CMSMain.METAKEYWORDS', 'Keywords')
);
static function T_SiteTreeFilterOptions(){
return array(
'Title' => _t('CMSMain.TITLEOPT', 'Title', 0, 'The dropdown title in CMSMain left SiteTreeFilterOptions'),
'MenuTitle' => _t('CMSMain.MENUTITLEOPT', 'Navigation Label', 0, 'The dropdown title in CMSMain left SiteTreeFilterOptions'),
'Status' => _t('CMSMain.STATUSOPT', 'Status', 0, "The dropdown title in CMSMain left SiteTreeFilterOptions"),
'MetaDescription' => _t('CMSMain.METADESCOPT', 'Description', 0, "The dropdown title in CMSMain left SiteTreeFilterOptions"),
'MetaKeywords' => _t('CMSMain.METAKEYWORDSOPT', 'Keywords', 0, "The dropdown title in CMSMain left SiteTreeFilterOptions")
);
}
public function init() {
parent::init();
@ -155,15 +134,17 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
*
* @return string
*/
public function getfilteredsubtree() {
// Sanity and security checks
if (!isset($_REQUEST['filter'])) die('No filter passed');
if (!ClassInfo::exists($_REQUEST['filter'])) die ('That filter class does not exist');
if (!is_subclass_of($_REQUEST['filter'], 'CMSSiteTreeFilter')) die ('That is not a valid filter');
public function getfilteredsubtree($data, $form) {
$params = $form->getData();
// Do eeet!
$filter = new $_REQUEST['filter']();
return $filter->getTree();
// Get the tree
$tree = $this->getSiteTreeFor($this->stat('tree_class'), $_REQUEST['ID'], null, array(new CMSMainMarkingFilter($params), 'mark'));
// Trim off the outer tag
$tree = ereg_replace('^[ \t\r\n]*<ul[^>]*>','', $tree);
$tree = ereg_replace('</ul[^>]*>[ \t\r\n]*$','', $tree);
return $tree;
}
/**
@ -188,32 +169,6 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
return $doSet;
}
/**
* Returns the SiteTree columns that can be filtered using the the Site Tree Search button as a DataObjectSet
*/
public function SiteTreeFilterOptions() {
$filter_options = new DataObjectSet();
foreach(self::T_SiteTreeFilterOptions() as $key => $value) {
$record = array(
'Column' => $key,
'Title' => $value,
);
$filter_options->push(new ArrayData($record));
}
return $filter_options;
}
public function SiteTreeFilterDateField() {
$dateField = new CalendarDateField('SiteTreeFilterDate');
return $dateField->Field();
}
public function SiteTreeFilterPageTypeField() {
$types = SiteTree::page_type_classes(); array_unshift($types, 'All');
$source = array_combine($types, $types);
asort($source);
$optionsetField = new DropdownField('ClassName', 'ClassName', $source, 'Any');
return $optionsetField->Field();
}
public function generateDataTreeHints() {
$classes = ClassInfo::subclassesFor( $this->stat('tree_class') );
@ -1095,6 +1050,61 @@ JS;
return new Form($this, "AddPageOptionsForm", $fields, $actions);
}
/**
* Form used to filter the sitetree. It can only be used via javascript for now.
*
* @return Form
*/
function SearchTreeForm() {
// get all page types in a dropdown-compatible format
$pageTypes = SiteTree::page_type_classes();
array_unshift($pageTypes, 'All');
$pageTypes = array_combine($pageTypes, $pageTypes);
asort($pageTypes);
$form = new Form(
$this,
'SearchTreeForm',
new FieldSet(
new TextField(
'Title',
_t('CMSMain.TITLEOPT', 'Title')
),
new TextField('Content', 'Text'),
new CalendarDateField('EditedSince', _t('CMSMain_left.ss.EDITEDSINCE','Edited Since')),
new DropdownField('ClassName', 'Page Type', $pageTypes, null, 'Any'),
new TextField(
'MenuTitle',
_t('CMSMain.MENUTITLEOPT', 'Navigation Label')
),
new TextField(
'Status',
_t('CMSMain.STATUSOPT', 'Status')
),
new TextField(
'MetaDescription',
_t('CMSMain.METADESCOPT', 'Description')
),
new TextField(
'MetaKeywords',
_t('CMSMain.METAKEYWORDSOPT', 'Keywords')
)
),
new FieldSet(
new ResetFormAction(
'clear',
_t('CMSMain_left.ss.CLEAR', 'Clear')
),
new FormAction(
'getfilteredsubtree',
_t('CMSMain_left.ss.SEARCH', 'Search')
)
)
);
return $form;
}
/**
* Helper function to get page count
@ -1330,36 +1340,46 @@ JS;
class CMSMainMarkingFilter {
function __construct() {
/**
* @var array Request params (unsanitized)
*/
protected $params = array();
/**
* @param array $params Request params (unsanitized)
*/
function __construct($params = null) {
$this->ids = array();
$this->expanded = array();
$this->params = $params;
$where = array();
// Match against URLSegment, Title, MenuTitle & Content
if (isset($_REQUEST['SiteTreeSearchTerm'])) {
$term = Convert::raw2sql($_REQUEST['SiteTreeSearchTerm']);
$where[] = "\"URLSegment\" LIKE '%$term%' OR \"Title\" LIKE '%$term%' OR \"MenuTitle\" LIKE '%$term%' OR \"Content\" LIKE '%$term%'";
}
// Match against date
if (isset($_REQUEST['SiteTreeFilterDate'])) {
$date = $_REQUEST['SiteTreeFilterDate'];
$date = ((int)substr($date,6,4)) . '-' . ((int)substr($date,3,2)) . '-' . ((int)substr($date,0,2));
$where[] = "\"LastEdited\" > '$date'";
}
// Match against exact ClassName
if (isset($_REQUEST['ClassName']) && $_REQUEST['ClassName'] != 'All') {
$klass = Convert::raw2sql($_REQUEST['ClassName']);
$where[] = "\"ClassName\" = '$klass'";
}
// Partial string match against a variety of fields
foreach (CMSMain::T_SiteTreeFilterOptions() as $key => $value) {
if (!empty($_REQUEST[$key])) {
$match = Convert::raw2sql($_REQUEST[$key]);
$where[] = "\"$key\" LIKE '%$match%'";
$SQL_params = Convert::raw2sql($this->params);
foreach($SQL_params as $name => $val) {
switch($name) {
// Match against URLSegment, Title, MenuTitle & Content
case 'SiteTreeSearchTerm':
$where[] = "\"URLSegment\" LIKE '%$val%' OR \"Title\" LIKE '%$val%' OR \"MenuTitle\" LIKE '%$val%' OR \"Content\" LIKE '%$val%'";
break;
// Match against date
case 'SiteTreeFilterDate':
$val = ((int)substr($val,6,4))
. '-' . ((int)substr($val,3,2))
. '-' . ((int)substr($val,0,2));
$where[] = "\"LastEdited\" > '$val'";
break;
// Match against exact ClassName
case 'ClassName':
if($val != 'All') {
$where[] = "\"ClassName\" = '$val'";
}
break;
default:
// Partial string match against a variety of fields
if(!empty($val) && singleton("SiteTree")->hasDatabaseField($name)) {
$where[] = "\"$name\" LIKE '%$val%'";
}
}
}

View File

@ -294,9 +294,6 @@ class LeftAndMain extends Controller {
'cms/javascript/SideReports.js',
'cms/javascript/LangSelector.js',
'cms/javascript/TranslationTab.js',
'jsparty/calendar/calendar.js',
'jsparty/calendar/lang/calendar-en.js',
'jsparty/calendar/calendar-setup.js',
)
);

View File

@ -384,25 +384,19 @@ ul.tree span.untranslated a:visited {
color: #ccc
}
#left form.actionparams div.SearchCriteria {
width: 28%;
overflow: hidden;
float: left;
}
#left form.actionparams input.SearchCriteria, #left form.actionparams #InputSiteTreeFilterClassName select {
#Form_SearchTreeForm .field .middleColumn {
width: 60%;
float: left;
margin: 0;
}
#left form.actionparams #InputSiteTreeFilterDate .calendar {
margin-left: -96px;
width: 190px;
height: 141px;
#Form_SearchTreeForm .field label {
margin-left: 0;
width: 28%;
}
/* IE7 fix: */
#left form.actionparams #InputSiteTreeFilterDate table {
width: 70%;
#Form_SearchTreeForm select.options {
clear: left;
}
/* Change detection CSS */

View File

@ -205,5 +205,118 @@
});
}
}});
/**
* Control the site tree filter.
*/
$('#Form_SearchTreeForm').concrete('ss.searchTreeForm', function($) {return{
SelectEl: null,
onmatch: function() {
var self = this;
// TODO Cant bind to onsubmit/onreset directly because of IE6
this.bind('submit', function(e) {return self.submitForm(e);});
this.bind('reset', function(e) {return self.resetForm(e);});
// only the first field should be visible by default
this.find('.field').not(':first').hide();
// generate the field dropdown
this.setSelectEl($('<select name="options" class="options"></select>')
.appendTo(this.find('fieldset:first'))
.bind('change', function(e) {self.addField(e);})
);
this._setOptions();
},
_setOptions: function() {
var self = this;
// reset existing elements
self.SelectEl().find('option').remove();
// add default option
// TODO i18n
$('<option value="0">Add Criteria</option>').appendTo(self.SelectEl())
// populate dropdown values from existing fields
this.find('.field').each(function() {
$('<option />').appendTo(self.SelectEl())
.val(this.id)
.text($(this).find('label').text());
});
},
submitForm: function(e) {
var self = this;
var data = [];
// convert from jQuery object literals to hash map
$(this.serializeArray()).each(function(i, el) {
data[el.name] = el.value;
});
// Set new URL
$('#sitetree')[0].setCustomURL(this.attr('action') + '&action_getfilteredsubtree=1', data);
// Disable checkbox tree controls that currently don't work with search.
// @todo: Make them work together
if ($('#sitetree')[0].isDraggable) $('#sitetree')[0].stopBeingDraggable();
$('.checkboxAboveTree :checkbox').val(false).attr('disabled', true);
// disable buttons to avoid multiple submission
//this.find(':submit').attr('disabled', true);
this.find(':submit[name=action_getfilteredsubtree]').addClass('loading');
$('#sitetree')[0].reload({
onSuccess : function(response) {
console.debug(self);
console.debug(self.find(':submit'));
self.find(':submit').attr('disabled', false).removeClass('loading');
statusMessage('Filtered tree','good');
},
onFailure : function(response) {
self.find(':submit').attr('disabled', false).removeClass('loading');
errorMessage('Could not filter site tree<br />' + response.responseText);
}
});
return false;
},
resetForm: function(e) {
this.find('.field').clearFields().not(':first').val('').hide();
// Reset URL to default
$('#sitetree')[0].clearCustomURL();
// Enable checkbox tree controls
$('.checkboxAboveTree :checkbox').attr('disabled', 'false');
// reset all options, some of the might be removed
this._setOptions();
return false;
},
addField: function(e) {
var $select = $(e.target);
// show formfield matching the option
this.find('#' + $select.val()).show();
// remove option from dropdown, each field should just exist once
this.find('option[value=' + $select.val() + ']').remove();
// jump back to default entry
$select.val(0);
return false;
}
}});
})(jQuery);

View File

@ -153,93 +153,7 @@ SiteTreeFilter.prototype = {
});
}
}
/**
* Control the site tree filter
*/
SiteTreeFilterForm = Class.create();
SiteTreeFilterForm.applyTo('form#search_options');
SiteTreeFilterForm.prototype = {
initialize: function() {
var self = this;
Form.getElements(this).each(function(el){
if (el.type == 'submit') el.onclick = function(){self.clicked = $F(this);};
});
},
onsubmit: function() {
var filters = $H();
if (this.clicked == 'Clear') {
Form.getElements(this).each(function(el){
if (el.type == 'text') el.value = '';
else if (el.type == 'select-one') el.value = 'All';
});
document.getElementsBySelector('.SearchCriteriaContainer', this).each(function(el){
Element.hide(el);
})
}
else {
Form.getElements(this).each(function(el){
if (el.type == 'text') {
if ($F(el)) filters[el.name] = $F(el);
}
else if (el.type == 'select-one') {
if ($F(el) && $F(el) != 'All') filters[el.name] = $F(el);
}
});
}
if (filters.keys().length) {
// Set new URL
$('sitetree').setCustomURL(SiteTreeHandlers.controller_url + '/getfilteredsubtree?filter=CMSSiteTreeFilter_Search', filters);
// Disable checkbox tree controls that currently don't work with search.
// @todo: Make them work together
if ($('sitetree').isDraggable) $('sitetree').stopBeingDraggable();
document.getElementsBySelector('.checkboxAboveTree input[type=checkbox]').each(function(el){
el.value = false; el.disabled = true;
})
}
else {
// Reset URL to default
$('sitetree').clearCustomURL();
// Enable checkbox tree controls
document.getElementsBySelector('.checkboxAboveTree input[type=checkbox]').each(function(el){
el.disabled = false;
})
}
$('SiteTreeSearchButton').className = $('SiteTreeSearchClearButton').className = 'hidden';
$('searchIndicator').className = 'loading';
$('sitetree').reload({
onSuccess : function(response) {
$('SiteTreeSearchButton').className = $('SiteTreeSearchClearButton').className = 'action';
$('searchIndicator').className = '';
statusMessage('Filtered tree','good');
},
onFailure : function(response) {
errorMessage('Could not filter site tree<br />' + response.responseText);
}
});
return false;
}
}
/**
* Add Criteria Drop-down onchange action which allows more criteria to be shown
*/
SiteTreeFilterAddCriteria = Class.create();
SiteTreeFilterAddCriteria.applyTo('#SiteTreeFilterAddCriteria');
SiteTreeFilterAddCriteria.prototype = {
onchange : function() {
Element.show('Container' + this.value);
// Element.show('Text' + this.value);
// Element.show('Input' + this.value);
this.selectedIndex = 0; //reset selected criteria to prompt
}
}
/**
* Batch Actions button click action

View File

@ -32,44 +32,7 @@
</div>
<div id="TreeActions-search">
<form class="actionparams" id="search_options" action="admin/filterSiteTree">
<div>
<input type="hidden" id="SiteTreeIsFiltered" value="0" />
<div id="SearchBox">
<div class="SearchCriteria">Text:</div>
<input type="text" id="SiteTreeSearchTerm" class='SearchCriteria' name="SiteTreeSearchTerm" />
</div>
<div id="ContainerSiteTreeFilterDate" class="SearchCriteriaContainer" style="display:none">
<div id="TextSiteTreeFilterDate" class="SearchCriteria"><% _t('EDITEDSINCE','Edited Since') %>:</div>
<div id="InputSiteTreeFilterDate">$SiteTreeFilterDateField</div>
</div>
<div id='ContainerSiteTreeFilterClassName' class='SearchCriteriaContainer' style="display:none">
<div id="TextSiteTreeFilterClassName" class="SearchCriteria">Page type: </div>
<div id="InputSiteTreeFilterClassName">$SiteTreeFilterPageTypeField</div>
</div>
<% control SiteTreeFilterOptions %>
<div id="Container$Column" class="SearchCriteriaContainer" style="display:none">
<div id="Text$Column" class="SearchCriteria">$Title:</div>
<input id="Input$Column" name="$Column" class="SearchCriteria" />
</div>
<% end_control %>
<div id='SearchControls'>
<select id="SiteTreeFilterAddCriteria">
<option value=""><% _t('ADDSEARCHCRITERIA','Add Criteria') %></option>
<option value="SiteTreeFilterDate"><% _t('EDITEDSINCE','Edited Since') %></option>
<option value="SiteTreeFilterClassName">Page type</option>
<% control SiteTreeFilterOptions %>
<option value="$Column">$Title</option>
<% end_control %>
</select>
<div id="searchIndicator">&nbsp;</div>
<input type="submit" id="SiteTreeSearchClearButton" class="action" value="<% _t('CLEAR') %>" title="<% _t('CLEARTITLE','Clear the search and view all items') %>" />
<input type="submit" id="SiteTreeSearchButton" class="action" value="<% _t('SEARCH') %>" title="<% _t('SEARCHTITLE','Search through URL, Title, Menu Title, &amp; Content') %>" />
</div>
</div>
</form>
$SearchTreeForm
</div>
<div id="TreeActions-batchactions">