Smarty resources - template from string
Smarty templates are usually stored in a template file, but in certain cases this may not be desirable. Another possibility for storing the templates is database and the documentation contains nice example . But, in a structured Php code, the code directly interacting with the database will be in another layer than the code using the template for displaing the results. Moreover, the template from the database should be loaded together with other related data, to make the database query more efficient. Bellow is an example of a display-layer class that uses templates already fetched from the database and together with other data stored in a data-layer classes. I was to some extend inspired by this smarty-related post. I apologise for possible errors, the example is derived from a working real code, but it is a simplified and not tested version.
/**
* Display class outputs all categories starting at certain node
*
* @uses Categories_Data_Categories
* @package Categories
* @subpackage Views
* @copyright WebDnes.cz
* @author Vaclav Mach
*/
class Categories_Views_List {
private $DataCategories;
private $smarty;
private static $mktime;
private static $template;
/**
* @param mixed $smarty ... an instance of Smarty object
*/
public function __construct($tree, $id_parent=0, $smarty) {
$this->DataCategories=new Categories_Data_Categories($tree, $id_parent);
$this->smarty=$smarty;
/**
* here we register static function of this display class as the smarty resources
* note that the functions are public to be accesible for smarty
*/
$smarty->register_resource("text", array("Categories_Views_List::text_get_template",
"Categories_Views_List::text_get_timestamp",
"Categories_Views_List::text_get_secure",
"Categories_Views_List::text_get_trusted"));
$this->template_article=$this->get_template_article($tree);
}
/**
* iterates the collection of data classes
* and sends them one by one to the display function
*/
public function DisplayAll() {
$ret="";
while ($DataCategory = $this->DataCategories->GetCategoryClass()) {
$ret.= $this->DisplayCategory($DataCategory);
}
return $ret;
}
/**
* this is the actual diplay function, important for this example
*
* @param Categories_Data_Category $DataCategory is a data class containing all the necessary data
* template, templatemodification time, and textual data e.g. category title and description are stored in the same table
*/
private function DisplayCategory(Categories_Data_Category $DataCategory) {
/**
* every piece of data, this means every data class may contain its own template
* the template is assigned to static variable to
* make it accessible for the registered resource functions
*/
self::$template=$DataCategory->Gettemplate();
self::$mktime=$DataCategory->GetLastModifiedTime();
// and this is the textual data for the template
$title=$DataCategory->GetTitle();
$description=$DataCategory->GetDescription();
$this->smarty->assign("cat_title", $DataCategory->GetTitle());
$this->smarty->assign("cat_description", $DataCategory->GetDescription());
// or shorter way
$this->smarty->assign("cat_img", $DataCategory->GetImage());
/**
* the name of the template is unique for each data class
* i do know smarty enough to be sure this is necessary for e.g. catching
* appending .tpl is just custom
*/
$templatename="cat".$DataCategory->GetId().".tpl";
/**
* in this call, smarty uses the static resources functions bellow and assigns
* variables to the template
* note that the template was specified just few lines above and is therefore specofic
* for the data in every data class in the DataCategories collection
*/
$ret= $this->smarty->fetch("text:$templatename");
return $ret;
}
public static function text_get_template($tpl_name, &$tpl_source, &$smarty)
{
/**
* uses the static variable assigned in the DisplayCategory function
*/
if(!empty(self::$template)) {
$tpl_source=self::$template;
return true;
}
return false;
}
public static function text_get_timestamp($tpl_name, &$tpl_timestamp, &$smarty)
{
/**
* uses the static variable assigned in the DisplayCategory function
*/
if(!empty(self::$mktime)) {
$tpl_timestamp=self::$mktime;
return true;
}
return false;
}
public static function text_get_secure($tpl_name, &$smarty)
{
// assume all templates are secure
return true;
}
public static function text_get_trusted($tpl_name, &$smarty)
{
// not used for templates
}
}
?>
TinyMCE editor and EXT library - a troubled marriage
TinyMCE, together with the FCKeditor, is perhaps the best WYSIWYG editor available. EXT 3, the recent version of EXT, is a cool (but for certain licences paid) javascript library. EXT comes with its own WYSIWYG editor, but it is no match for TinyMCE. Naturally, one would like to use these two together - TinyMCE for text input, EXT for creating trees, datagrids, dragging and a whole range of dynamic effects. To make life a bit more complicated, TinyMCE and EXT do not go along very well. So far I have noticed two issues:
1. TinyMCE converted textarea + EXT drag and drop = the text disappears
2. EXT TabPanel prevents TinyMCE init
The first problem is in fact not directly connected to Ext.dd . The textarea was, together with other elements, embedded in a parent DIV and this DIV was moved manually by calling InsertBefore in the "notify drop" function. Nevertheless, the text disappeared. The solution (I do not claim it is the best or the only way how to handle this problem) is
removing the particular textarea from TinyMce just before the textarea is moved around and adding it back after it is safely placed in the new position.
Thus the set of commands is like this:
notifyDrop: function(dd, e, data) {
tinyMCE.execCommand('mceRemoveControl', false, TextAreaId);
Ext.fly('idDivMoved').insertBefore(SomeElement);
tinyMCE.execCommand('mceAddControl', false, TextAreaId);
}
The second problem concerns the loading order, TinyMCE instance cannot be created before the particular Tab exist. Fortunately, the problem is well known and solved.
The code below is slightly different from the original and shows where to place the tinyMCE.init and how to select the tab using the id of element it is rendered to ('Atab' in this example)
var tabs = new Ext.TabPanel({
renderTo: 'tabs',
width:780,
activeTab: 0,
frame:true,
defaults:{autoHeight: true},
items:[
{contentEl:'Atab', title: 'A'},
{contentEl:'Btab', title: 'B'}
],
listeners: {
tabchange : function(e) {
var act = e.getActiveTab();
if (act.contentEl == 'Atab' && descMceLoaded == false){
tinyMCE.init({
mode : "textareas",
theme : "simple",
language : "cs"
});
descMceLoaded = true;
}
}
}
})
Site Search Google Ajax API
This is a working example of Google Ajax Search API with custom form. The intended usage of the code is for running your own fully customised site restricted search with the help of Google API. Do not wish to read the full article? Just grab the code at the end of this post, replace words "your-API-key-there" with real Google API key and change "webdnes.cz" to the url of your website
I have been considering the possible solutions for implementing the full site search for the websites based on the CMS WebDnes. Suprisingly, although this is a general problem, I was unable to Google out a solution directly fitting my needs. Anyway, what are the possibilities?
Searching over a database.
It seems like a nice solution, but it makes sense mostly in cases when we can rely on a consistent structure of individual items, e.g. for eshops. But for a flexible CMS it means relying on a suboptimal MySQL full text search performance. So this is not the way.
So called parasite forms, e.g Google
Apparently, this solution is very easy to implement. But I have certain doubts that website owners would enjoy search results routed OUTSIDE the website and interlaced with the ads of their competitors. Thus big NO again.
Google Search AJAX API
This solution feels really better. After a minor effort, I was able to google out a nice
example. After a little tweaking, the code worked approximately in the way I intended. If you copy paste the following code, it should work. Just add the proper API key and do not overlook the line options.setRoot, indicating the element to be filled by the results should go.
<script src="http://www.google.com/jsapi?key=vas-api-key-patri-sem" type="text/javascript"></script>
<script language="Javascript" type="text/javascript">
//<![CDATA[
google.load("search", "1", {"language" : "cs"});
function OnLoad() {
// Create a search control
var searchControl = new google.search.SearchControl();
searchControl.setResultSetSize(google.search.Search.LARGE_RESULTSET);
// site restricted web search with custom label
// and class suffix
var siteSearch = new google.search.WebSearch();
siteSearch.setUserDefinedLabel("Webdnes.cz");
siteSearch.setUserDefinedClassSuffix("siteSearch");
siteSearch.setSiteRestriction("webdnes.cz");
options = new google.search.SearcherOptions();
options.setExpandMode(google.search.SearchControl.EXPAND_MODE_OPEN);
options.setRoot(document.getElementById("search"));
searchControl.addSearcher(siteSearch, options);
// tell the searcher to draw itself and tell it where to attach
searchControl.draw(document.getElementById("searchcontrol"));
}
google.setOnLoadCallback(OnLoad);
//]]>
</script>
<div id="searchcontrol">Loading...</div>
<div id="search"></div>
Custom form using Google Search Ajax API
There are still some minor problems with the code above.
- Google will create a nice form, but there are no tools for customising its appearance
- Google logo - well I personally like it but would not the website owners mind displying the log even in cases where no search was performed
- Finally we can use options.setRoot to specify the element used for displaying the results. Sometimes, it would be convenient to replace the page content itself with the search results. Unfortunately, if we setRoot to the div element used to display the content of the page, it is set to zero, even if no search was made
Unfortunately it appears that the only way to get rid of these nuisances is to us the the crude search API. Originally I thought that it will be pretty easy to google out a working example, but it was not the case. The only code I found was this one and it did not work properly - for every search term, the same set of results was returned. Not encouraging. Soon I gave up attempts to fix this code and decided to make my own using Google AJAX search API documentation and the image search example. The code below should work properly, just enter your API key. You can see the code in action there
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>My Google AJAX Search API Application</title>
<style type="text/css">
.qw2 {color:#ff0000;font-weight:600;}
.qw3 {color:#aa0000;font-weight:300;}
</style>
</head>
<body>
<script src="http://www.google.com/jsapi?key=your-API-key-there" type="text/javascript"></script>
<script language="Javascript" type="text/javascript">
//<![CDATA[
/*
* The SearchControl manages searchers and draws a UI for them. However,
* searchers can be used by themselves without the SearchControl. This is
* called using a "Raw Searcher". When doing this, you must handle and draw
* the search results manually.
*/
google.load('search', '1');
var webSearch;
var gSearch;
function addPaginationLinks() {
// The cursor object has all things to do with pagination
var cursor = webSearch.cursor;
var curPage = cursor.currentPageIndex; // check what page the app is on
var pagesDiv = document.createElement('div');
for (var i = 0; i < cursor.pages.length; i++) {
var page = cursor.pages[i];
if (curPage == i) { // if we are on the curPage, then don't make a link
var label = document.createTextNode(' ' + page.label + ' ');
pagesDiv.appendChild(label);
} else {
// If we aren't on the current page, then we want a link to this page.
// So we create a link that calls the gotoPage() method on the searcher.
var link = document.createElement('a');
link.href = 'javascript:webSearch.gotoPage('+i+');';
link.innerHTML = page.label;
link.style.marginRight = '2px';
pagesDiv.appendChild(link);
}
}
var contentDiv = document.getElementById('content');
contentDiv.appendChild(pagesDiv);
}
function searchComplete() {
// Check that we got results
var contentDiv = document.getElementById('content');
contentDiv.innerHTML = '';
if (webSearch.results && webSearch.results.length > 0) {
// Grab our content div, clear it.
// Loop through our results, printing them to the page.
var results = webSearch.results;
for (var i = 0; i < results.length; i++) {
var result = results[i];
var resContainer = document.createElement('div');
resContainer.className='gw1';
var title = document.createElement('div');
title.className='gw2';
titulek='<a href="' + result.url + '">' + result.title + '</a>';
title.innerHTML = titulek;
var content = document.createElement('div');
content.className='gw3';
content.innerHTML = result.content;
/*
var newImg = document.createElement('img');
// There is also a result.url property which has the escaped version
newImg.src = result.tbUrl;
*/
resContainer.appendChild(title);
resContainer.appendChild(content);
// imgContainer.appendChild(newImg);
// Put our title + image in the content
contentDiv.appendChild(resContainer);
}
// Now add the paging links so the user can see more results.
addPaginationLinks(webSearch);
// setResultSetSize
var b= gSearch.getBranding();
contentDiv.insertBefore(b, contentDiv.firstChild);
}
else {
contentDiv.innerHTML = 'no results';
}
}
function OnLoad() {
gSearch= google.search.Search;
// Our ImageSearch instance.
// imageSearch = new google.search.ImageSearch();
webSearch = new google.search.WebSearch();
// Restrict to extra large images only
webSearch.setSiteRestriction('www.webdnes.cz');
// Here we set a callback so that anytime a search is executed, it will call
// the searchComplete function and pass it our WebSearch searcher.
webSearch.setResultSetSize(gSearch.LARGE_RESULTSET);
//searchControl.setResultSetSize(google.search.Search.LARGE_RESULTSET);
//gSearch.setNoResultsString(gSearch.NO_RESULTS_DEFAULT_STRING);
webSearch.setSearchCompleteCallback(this, searchComplete, null);
var query = document.getElementById("q");
webSearch.execute(query.value);
}
//google.setOnLoadCallback(OnLoad);
//]]> </script>
<form onSubmit="OnLoad();return false;">
<input type="text" id="q">
<input type="submit" value="vyhledat" name="cmdSubmit">
</form>
<div id="content">original content of this page</div>
