![]() |
|
Snippets |
|
Hi!
I am using a lot of hand written jscript codes due to the required behavior not found in any javascript framework. I've created my Objects in distinct files, although some of them are quite small ( <1kb ). So to make it fast, I've written a filter class that actually parses the files found in the /js directories having *.js suffix. It does not only parse them together but removes whitespaces, comments, indentations so the sum filesize gets a bit compressed. The filter also includes the final file into the response so there is no need to include any *.js ( inside /js ) in a template.
<?php class JavascriptParser extends sfFilter { public function execute($filterChain) { $filterChain->execute(); $fp = fopen( "js/compiled.js", "w" ); JavascriptParser::getJSFiles( "js", $fp ); fclose ( $fp ); $response = $this->getContext()->getResponse(); $content = $response->getContent(); if (false !== ($pos = strpos($content, '</head>'))) { $html = "<script type='text/javascript' src='/js/compiled.js'></script>"; if ($html) { $response->setContent(substr($content, 0, $pos).$html.substr($content, $pos)); } } } public function getJSFiles( $dir, &$fp ) { $hDir = opendir( $dir ); while( ( $filename = readdir( $hDir ) ) !== false ) { if ( is_dir( $filename ) ) { } else if ( is_dir( $dir."/".$filename ) ) JavascriptParser::getJSFiles( $dir."/".$filename, $fp ); else if ( strpos( $filename, ".js" ) !== false && $filename != "compiled.js" ) { $tmpFile = fopen( $dir."/".$filename, "r" ); $data = fread( $tmpFile, filesize( $dir."/".$filename ) ); $data = preg_replace( "'\/\*.*?\*\/'si", "", $data ); $data = preg_replace( "'//.*?\n'si", "", $data ); $data = preg_replace( "'[ \t]+'", " ", $data ); fwrite( $fp, " \n".$data ); fclose( $tmpFile ); } } closedir( $hDir ); } } ?>
The same should be done with *.css files, since they are even smaller and loading many small files takes much more time then loading one big.
Best Regards
For ergonomic and graphism reasons, I sometimes prefer to use link rather than buttons and/or button rather than links...
To do so, I created this small function :
<?php # File : JsHelper.php // use of "normal" Javascript Helper use_helper('Javascript'); /*! * Create a link that submit the closest parent form * * @param $string string : text to display * @param $options mixed : options to be pass to the tag * @return string : HTML code for the link */ function link_to_submit( $string, $options = null ) { $func = ";var a=this.ancestors();for(var i in a){if(a[i].tagName=='FORM'){try{a[i].onsubmit();} catch(err){a[i].submit();}break;}}"; return link_to_function( $string, $func, $options ); } /*! * Create a button that act as a link * * @param $string string : text to display in the button * @param $url string : URL of the link * @param $options mixed : options to be pass to the tag * @return string : HTML code for the link */ function button_to_link( $string, $url, $options = null ) { $url = url_for($url); $func = ";document.location = '$url';return false;"; if ( is_string($options) ) $options.= " onclick=$func"; else $options['onclick'] = $func; return tag('button', $options ).htmlspecialchars($string).'</button>'; }
Then, you simply use it as a normal link_to function :
<? use_helper('Js') ?> <?=form_tag('module/action')?> <?=input_tag('text')?> <?=link_to_submit('Validate')?> <?=link_to('Cancel','module/index')?> </form>
Or :
<? use_helper('Js') ?> <?=form_tag('module/action')?> <?=input_tag('text')?> <?=submit_tag('Validate')?> <?=button_to_link('Cancel','module/index')?> </form>
In these two examples, links/buttons of the form are graphicly consistents. For a better user experience ;)
PS : works as well with AJAX forms form_remote_tag()
PPS : Don't forget that thanks to CSS, you can make a button looking like a link (et vice-versa).
PPPS : Don't forget that buttons and links doesn't have the same meaning for search engine bots.
PPPPS : Don't forget that this rely on the fact that user is using javascript!!!
These two js functions do the job while using jQuery with admin double list
function double_list_move(src, dest) { var L = $(src).children(); $(L).each(function(i){ if(L[i].selected){ $(dest).append('<option value="'+$(L[i]).val()+'">'+ $(L[i]).text()+'</option>'); $(L[i]).remove(); } }); } function double_list_submit() { var sel = $("form").find("select.sf_admin_multiple-selected"); var C = $(sel).children(); $(C).each( function(i){ if(!C[i].selected) C[i].selected=true; } ); }
Enjoy it.
format a phone number with 7, 10, or 11 digits. also can convert phone number letters to numbers
lib/helpers/PhoneHelper.php
<?php function format_phone($phone = '', $convert = false, $trim = true) { // If we have not entered a phone number just return empty if (empty($phone)) { return ''; } // Strip out any extra characters that we do not need only keep letters and numbers $phone = preg_replace("/[^0-9A-Za-z]/", "", $phone); // Do we want to convert phone numbers with letters to their number equivalent? // Samples are: 1-800-TERMINIX, 1-800-FLOWERS, 1-800-Petmeds if ($convert == true) { $replace = array('2'=>array('a','b','c'), '3'=>array('d','e','f'), '4'=>array('g','h','i'), '5'=>array('j','k','l'), '6'=>array('m','n','o'), '7'=>array('p','q','r','s'), '8'=>array('t','u','v'), '9'=>array('w','x','y','z')); // Replace each letter with a number // Notice this is case insensitive with the str_ireplace instead of str_replace foreach($replace as $digit=>$letters) { $phone = str_ireplace($letters, $digit, $phone); } } // If we have a number longer than 11 digits cut the string down to only 11 // This is also only ran if we want to limit only to 11 characters if ($trim == true && strlen($phone)>11) { $phone = substr($phone, 0, 11); } // Perform phone number formatting here if (strlen($phone) == 7) { return preg_replace("/([0-9a-zA-Z]{3})([0-9a-zA-Z]{4})/", "$1-$2", $phone); } elseif (strlen($phone) == 10) { return preg_replace("/([0-9a-zA-Z]{3})([0-9a-zA-Z]{3})([0-9a-zA-Z]{4})/", "($1) $2-$3", $phone); } elseif (strlen($phone) == 11) { return preg_replace("/([0-9a-zA-Z]{1})([0-9a-zA-Z]{3})([0-9a-zA-Z]{3})([0-9a-zA-Z]{4})/", "$1($2) $3-$4", $phone); } // Return original phone if not 7, 10 or 11 digits long return $phone; }
A symfony helper for the stickman labs accordion http://www.stickmanlabs.com/accordion/
hopefully plugin oneday
<?php use_helper('Tag', 'Javascript'); function accordion($container, $options = array()){ $response = sfContext::getInstance()->getResponse(); $response->addJavascript(sfConfig::get('sf_prototype_web_dir').'/js/prototype'); $response->addJavascript(sfConfig::get('sf_prototype_web_dir').'/js/effects'); $response->addJavascript('accordion', 'last'); $options = _parse_attributes($options); $on_load = (isset($options['on_load']) && $options['on_load'] == false) ? false : true; if(isset($options['use_stylesheet']) && $options['use_stylesheet']==true) $response->addStylesheet('accordion'); $output= ''; //onLoad $output .= $on_load ? "Event.observe(window, 'load', function() {" : ''; //new accordion $output .= "var accordion_$container = new accordion ('$container', "; $accordion_options = array(); //speed if (isset($options['resize_speed'])) $accordion_options['resizeSpeed'] = $options['resize_speed']; //classes if(isset($options['toggle']) || isset($options['toggle_active']) || isset($options['content'])){ if(isset($options['toggle'])) $accordion_options['classNames']['toggle'] = "'{$options['toggle']}'"; if(isset($options['toggle_active'])) $accordion_options['classNames']['toggleActive'] = "'{$options['toggle_active']}'"; if(isset($options['content'])) $accordion_options['classNames']['content'] = "'{$options['content']}'"; $accordion_options['classNames'] = _options_for_javascript_no_sort($accordion_options['classNames']); } //size if(isset($options['height']) || isset($options['width'])){ if(isset($options['height'])) $accordion_options['defaultSize']['height'] = $options['height']; if(isset($options['width'])) $accordion_options['defaultSize']['width'] = $options['width']; $accordion_options['defaultSize'] = _options_for_javascript_no_sort($accordion_options['defaultSize']); } //direction if (isset($options['direction'])) $accordion_options['direction'] = "'{$options['direction']}'"; //event if (isset($options['on_event'])) $accordion_options['onEvent'] = "'{$options['on_event']}'"; $output .= _options_for_javascript_no_sort($accordion_options); //new accordion end $output .= ");"; if(isset($options['activate'])){ $number = $options['activate']; $output .= "accordion_$container.activate($$('#$container .{$options['toggle']}')[$number]);"; } //onLoad $output .= $on_load ? "});" : ''; return javascript_tag($output); } function _options_for_javascript_no_sort($options) { $opts = array(); foreach ($options as $key => $value) { $opts[] = "$key:$value"; } return '{'.join(', ', $opts).'}'; }
This JavaScript code registers a change detection mechanism in every form field and notifies the user about unsaved changes. No changes need to be applied to existing modules/actions. Additionaly, the TinyMCE helper can be changed in order to detect changes there as well.
Only requirement: the links for leaving the page need to be in a container with id "header". This can of course be changed.
Add this to the head of the page (or to an external js-file):
var changesDetected = false; /** * Registers a change detection mechanism that notifies users about unsaved changes whenever they click on a link. */ function registerChangeDetection() { /** * Notifies user about unsaved changes */ function notifyAboutChanges(e) { if(changesDetected){ //My choice: modal dialog using modalbox (http://www.wildbit.com/labs/modalbox/) //Modalbox.show('<div class=\'warning\'><p>Before continuing, you need to save you changes.</p> <input type=\'button\' value=\'Ignore changes\' onclick=\'changesDetected=false;Modalbox.hide()\' style=\'color: #999\' /> <input type=\'button\' value=\'OK\' onclick=\'Modalbox.hide()\' /></div>', {title: 'Warning', width: 300}); //Alternative: alert('Before continuing, you need to save your changes.'); return false; } } /* Add change detection to every form field */ if(document.forms.sf_admin_edit_form != null) { var elements = Form.getElements(document.forms.sf_admin_edit_form); elements.each(function(item) { item.onchange = function(e) { changesDetected = true; } }); } /* Add an onclick handler to every link in the container with id "header" */ var links = $$('#header a'); links.each(function(item) { if(!(item.onclick instanceof Function)) { //Avoid overwriting existing onclick handlers item.onclick = notifyAboutChanges; } }); }
Register the change detection in the body tag:
<body onload="registerChangeDetection()">
I also modified the sfRichTextEditorTinyMCE helper in order to use TinyMCE 3 (currently beta). Here is the code relevant to change detection to be put into TinyMCE.init({...}) (Go to http://wiki.moxiecode.com/index.php/TinyMCE:API/tinymce.Editor/onChange for more information):
setup: function(ed) { var i = 0; ed.onChange.add(function(ed, l) { if(i == 0) i++ //Ignore the first change else changesDetected = true; }); }
A real client-side validation with some nice features, without Ajax tricks provided by sfYzAjaxValidationPlugin may be easily done using jsValidator (http://sourceforge.net/projects/jsformutils/) and little helper that follows.
<?php /** * Generates JavaScript code to validate a form using * jsValidator as client-side validation engine (see http://sourceforge.net/projects/jsformutils/) * * @param string $targetForm id attribute of the form to be validated * @param mixed $options associative array of options * @param string $action action to validated, defaults to current * * Options are: * stopOnFirstError: boolean, default: false * labelMessageDelimiter: string, default: ' : ', * messageSeparator: string, default: "\n", * messageHeader: string, default: 'These fields are invalid:\n---\n' + * highlightErrors: boolean, whether to mark erroneous fields * errorElementClass: string, CSS class name to be applied to wrong fields * highlightLabels: boolean, whether to mark fields or fields' labels */ function generate_validator($targetForm, $options, $action) { $NL = "\n"; $funcPrefix = 'validate_'; $labelsKey = 'labels'; $fieldsKey = 'fields'; $jsCode = ''; $paramHolder = sfContext::getInstance()->getRequest()->getParameterHolder(); $rulesFilePath = sfConfig::get('sf_app_module_dir').'/'.$paramHolder->get('module').'/'. sfConfig::get('sf_app_module_validate_dir_name').'/'; // Load rules from YAML file. if (file_exists($rulesFilePath.$paramHolder->get('action').'.yml')) $rules = sfYaml::load($rulesFilePath.$paramHolder->get('action').'.yml'); else $rules = sfYaml::load($rulesFilePath.$action.'.yml'); // Generate jsValidator compliant rules. $jsRules = array(); foreach ($rules['fields'] as $fieldId => $validationRule) { foreach ($validationRule as $validator=>$rule) { // Remove server-side sfCallbackValidator. if ($validator == 'sfCallbackValidator') { unset($validationRule[$validator]); continue; } // Map Symfony validators to jsValidator. $jsvalidator = preg_replace('/^sf(\w+Validator)$/', 'js\\1', $validator); if ($jsvalidator != $validator) { $validationRule[$jsvalidator] = $validationRule[$validator]; unset($validationRule[$validator]); } } $jsRules[] = array_merge ( array ('field' => $fieldId, 'label' => $rules['labels'][$fieldId]), $validationRule ); } // Generate final JavaScript code. $jsCode .= 'function '.$funcPrefix.$targetForm.'()'.$NL; $jsCode .= '{'.$NL. ' var options = '.json_encode($options).';'.$NL. ' var rules = '.json_encode($jsRules).';'.$NL. ' var jsv = new jsValidator();'.$NL. ' jsv.SetOptions(options);'.$NL.$NL. ' if (!jsv.Validate(rules))'.$NL. ' {'.$NL. ' alert(jsv.GetErrorMessage());'.$NL. ' return false;'.$NL. ' }'.$NL. ' return true;'.$NL. '}'.$NL; return javascript_tag($jsCode); } ?>
However, I need to clear this out. Besides placing this code into jsValidatorHelper.php either in modules' lib directory or symfony's one, we need to call it properly in the corresponding view.
First of all, include the helper (use JavaScript as well, jsValidator depends on it)
<?php use_helper('JavaScript', 'jsValidator') ?>
Validation is performed based on the same simple rule mechanism that Symfony provides. The only difference is that JavaScript validator needs these rules to be in JSON format and it needs some more options, that configure it's behavior.
<?php // Here we set validation options. // For more information please refer to documentation of jsValidator $options = array( 'stopOnFirstError' => false, 'labelMessageDelimiter' => ' : ', 'messageSeparator' => "\n", 'messageHeader' => "These fields are invalid:\n---\n", 'highlightErrors' => true, 'errorElementClass' => 'errClass', 'highlightLabels' => true ); // Output auto-generated JavaScript code. echo generate_validator('editComment', $options, 'update'); ?>
We also have to set up form to point to our validator before submitting. Note, that callback in onsubmit is named by concatenating "validate_" and form's id attribute.
<?php echo form_tag('comment/update', array('id'=>'editComment','onsubmit' => 'return validate_editComment()')) ?>
Each field must be somehow identified in the resulting error message. We achieve this by adding some extra information to <action>.yml configuration file.
# define labels for erroneous fields
labels:
<field_id>: <field_label_text>
There is a limitation to validation *.yml file structure. The syntax should be something like this:
# define labels for erroneous fields labels: author: Author email: E-mail body: Body fields: author: required: msg: The name field cannot be left blank email: sfEmailValidator: email_error: The email address is not valid. body: required: msg: The text field cannot be left blank fillin: enabled: on
And finally, don't forget to place jsValidator into /web/js folder, and include it in view.yml
<actionTemplate>: javascripts: [jsvalidator]
Feel free to modify the snippet code and validator to achieve best results!
In order to delay a page redirect with several seconds, I wrote this simple helper
<?php use_helper('Javascript'); /** * Adds javascript code to delay a page redirect * * @param string 'module/action' or '@rule' of the action (same argument as url_for()) * @param int time of delay in seconds. Default = 5 * @return JavaScript tag for delayed page redirect */ function delayed_redirect($internal_uri, $time = 5) { sfContext::getInstance()->getResponse()->addJavascript(sfConfig::get('sf_prototype_web_dir').'/js/prototype'); $code = 'new PeriodicalExecuter(function() { location.href=\''.url_for($internal_uri).'\';}, '.$time.')'; return javascript_tag($code); }
See http://www.symfony-project.com/book/1_0/07-Inside-the-View-Layer#Adding%20Your%20Own%20Helpers for information on how to add your own helper
creates an InplaceSelect field. A text is as normal text shown, when you click on it, it transforms to a select field. After selecting a value the select field transforms back to normal text. The data shown in the select field is requested from the server.
Save this helper in your_project/lib/helper/AjaxInplaceSelectHelper.php:
<?php /** * AjaxInplaceSelectHelper. * * @package symfony * @subpackage helper * @author Thomas Eigner <webmaster@flyingfinger.de> */ /** * wrapper for InplaceSelect. * @param string $name The id of surrounding span * @param string $value The default text for the span * @param string $url The url to receive the update * @param string $json The url to fetch the list to show in the select field * @param int $selectedId The id of the element to select when select is shown first time * @param array $spanOptions The span tag options. (size, class, etc...) * @param array $inplaceOptions The options for the inplaceSelect (callback, etc...) * * @return string A span with the text, and InplaceSelect javascript tags */ use_helper('JavaScript'); function input_in_place_select_tag($name, $value, $url, $json, $selectedId, $spanOptions = array(), $inplaceOptions = array()) { $context = sfContext::getInstance(); $response = $context->getResponse(); $response->addJavascript(sfConfig::get('sf_prototype_web_dir').'/js/builder'); $response->addJavascript(sfConfig::get('sf_prototype_web_dir').'/js/prototype'); $response->addJavascript(sfConfig::get('sf_prototype_web_dir').'/js/effects'); $response->addJavascript('/js/inplaceselect'); $content = content_tag('span', $value, array('id' => $name)); $js_options = array(); if (isset($inplaceOptions ['callback'])) { $js_options['callback'] = $inplaceOptions['callback']; } else { $js_options['callback'] = "function(value, text) { return '".$name."_id='+value+'&".$name."_value='+text; }"; } if (isset($inplaceOptions ['onFailure'])) { $js_options['onFailure'] = $inplaceOptions['onFailure']; } if (isset($inplaceOptions ['onSuccess'])) { $js_options['onSuccess'] = $inplaceOptions['onSuccess']; } if (isset($inplaceOptions ['highlightcolor'])) { $js_options['highlightcolor'] = "'".addslashes($inplaceOptions['highlightcolor'])."'"; } if (isset($inplaceOptions ['highlightendcolor'])) { $js_options['highlightendcolor'] = "'".addslashes($inplaceOptions['highlightendcolor'])."'"; } if (isset($inplaceOptions ['savingText'])) { $js_options['savingText'] = "'".addslashes($inplaceOptions['savingText'])."'"; } if (isset($inplaceOptions ['cancelText'])) { $js_options['cancelText'] = "'".addslashes($inplaceOptions['cancelText'])."'"; } if (isset($inplaceOptions ['savingClassName'])) { $js_options['savingClassName'] = "'".addslashes($inplaceOptions['savingClassName'])."'"; } if (isset($inplaceOptions ['clickToEditText'])) { $js_options['clickToEditText'] = "'".addslashes($inplaceOptions['clickToEditText'])."'"; } if (isset($inplaceOptions ['cancelLink'])) { $js_options['cancelLink'] = ($inplaceOptions['cancelLink']) ? "true" : "false"; } $javascript = "new Ajax.InPlaceSelect('" . $name . "', '" . url_for($url) . "', '" . url_for($json) . "', " . $selectedId . ", " . _options_for_javascript($js_options) . " );"; return $content . javascript_tag($javascript); }
You also need an additional JS-File in your_project/lib/helper/AjaxInplaceSelectHelper.php:
/*
- inplaceselect.js -
Creates a <select> control in place of the html element with the id
specified. It functions similar to "Ajax.InPlaceEditor" but instead
of an <input> control, it creates a <select> control with a list of
<options> from which to choose. The parameters 'values' and 'labels'
are arrays (of the same length) from which the <options> are defined.
- Syntax -
new Ajax.InPlaceSelect('id', 'url', 'json', 'selected', { options });
- Example -
new Ajax.InPlaceSelect('someId', 'someURL', 'otherURL', 1,
{ callback: function(value, text) { return 'newval='+value+'&newtxt='+text; } } );
- Options('default value') -
- hightlightcolor("#FFFF99"): initial color (mouseover)
- hightlightendcolor("#FFFFFF"): final color (mouseover)
- onFailure(function(transport) {}): Called if failure occurs sending changes
- onSuccess(function(transport) {}): Called on sending changes successfully
- callback(function(value, text) { return 'newval='+value+'&newtxt='+text; }): function to
send additional parameters with the requests
- cancelText("cancel"): Text for the cancel link
- clickToEditText("Click to edit"): Tooltip for the text
- cancelLink(true): Should the cancel link be shown
Original JS-Script is from http://dev.rubyonrails.org/ticket/2667
*/
Ajax.InPlaceSelect = Class.create();
Ajax.InPlaceSelect.prototype =
{
initialize:function(element,url,json,selected,options)
{
this.element = $(element);
this.url = url;
this.json = json;
this.selected = selected;
this.values = new Array();
this.labels = new Array();
this.options = Object.extend(
{
highlightcolor: "#FFFF99",
highlightendcolor: "#FFFFFF",
onFailure: function(transport) {},
onSuccess: function(transport) {},
callback: function(value, text)
{
return 'newval='+value+'&newtxt='+text;
},
cancelText: "cancel",
clickToEditText: "Click to edit",
cancelLink: true,
},
options || {}
);
this.originalBackground = Element.getStyle(this.element, 'background-color');
if (!this.originalBackground)
{
this.originalBackground = "transparent";
}
this.element.title = this.options.clickToEditText;
this.ondblclickListener = this.enterEditMode.bindAsEventListener(this);
this.mouseoverListener = this.enterHover.bindAsEventListener(this);
this.mouseoutListener = this.leaveHoverNormal.bindAsEventListener(this);
Event.observe(this.element, 'click', this.ondblclickListener);
Event.observe(this.element, 'mouseover', this.mouseoverListener);
Event.observe(this.element, 'mouseout', this.mouseoutListener);
},
enterEditMode: function(evt)
{
if (this.saving) return;
if (this.editing) return;
this.editing = true;
new Ajax.Request(
this.json,
{
parameters: this.options.callback('', ''),
onSuccess:this.finishEnterEditMode.bind(this)
}
);
return false;
},
finishEnterEditMode: function(response)
{
var newData = eval(response.responseText);
this.values = new Array();
this.labels = new Array();
var i = 0;
var toSelect = 0;
for (var value in newData)
{
this.values.push(value);
if (value == this.selected) toSelect = i;
this.labels.push(newData[value]);
i++;
}
Element.hide(this.element);
this.createControls();
this.element.parentNode.insertBefore(this.menu, this.element);
this.menu.focus();
if (this.options.cancelLink)
{
this.element.parentNode.insertBefore(this.cancelButton, this.element);
}
this.menu.selectedIndex = toSelect;
return false;
},
createControls: function()
{
var options = new Array();
for (var i=0;i<this.values.length;i++)
options[i] = Builder.node('option', {value:this.values[i]}, this.labels[i]);
this.menu = Builder.node('select', options);
this.menu.onchange = this.onChange.bind(this);
this.menu.onblur = this.onCancel.bind(this);
for (var i=0;i<this.values.length;i++)
if (this.labels[i]==this.element.innerHTML)
{
this.menu.selectedIndex=i;
continue;
}
if (this.options.cancelLink)
{
this.cancelButton = Builder.node('a', this.options.cancelText);
this.cancelButton.onclick = this.onCancel.bind(this);
}
},
onCancel: function()
{
this.cleanUp();
this.leaveEditMode();
return false;
},
onChange: function()
{
var value = this.values[this.menu.selectedIndex];
var text = this.labels[this.menu.selectedIndex];
this.selected = value;
this.onLoading(text);
new Ajax.Request(
this.url, Object.extend(
{
parameters: this.options.callback(value, text),
onComplete: this.onComplete.bind(this),
onFailure: this.onFailure.bind(this)
},
this.options.ajaxOptions
)
);
},
onLoading: function(text)
{
this.saving = true;
this.removeControls();
this.leaveHover();
this.showSaving(text);
},
removeControls:function()
{
if(this.menu)
{
if (this.menu.parentNode) Element.remove(this.menu);
this.menu = null;
}
if (this.cancelButton)
{
if (this.cancelButton.parentNode) Element.remove(this.cancelButton);
this.cancelButton = null;
}
},
showSaving:function(text)
{
this.oldInnerHTML = this.element.innerHTML;
this.element.innerHTML = text;
this.element.style.backgroundColor = this.options.highlightcolor;
Element.show(this.element);
},
onComplete: function(transport)
{
this.options.onSuccess(transport);
this.cleanUp();
},
cleanUp: function()
{
this.leaveEditMode();
new Effect.Highlight(
this.element,
{
startcolor: this.options.highlightcolor,
endcolor: this.options.highlightendcolor,
restorecolor: this.originalBackground
}
);
},
onFailure: function(transport)
{
this.options.onFailure(transport);
if (this.oldInnerHTML)
{
this.element.innerHTML = this.oldInnerHTML;
this.oldInnerHTML = null;
}
return false;
},
enterHover: function()
{
if (this.saving) return;
this.element.style.backgroundColor = this.options.highlightcolor;
if (this.effect) { this.effect.cancel(); }
Element.addClassName(this.element, this.options.hoverClassName)
},
leaveHoverNormal: function()
{
if (this.saving) return;
this.element.style.backgroundColor = this.originalBackground;
},
leaveHover: function()
{
if (this.options.backgroundColor)
{
this.element.style.backgroundColor = this.oldBackground;
}
Element.removeClassName(this.element, this.options.hoverClassName)
if (this.saving) return;
this.effect = new Effect.Highlight(
this.element,
{
startcolor: this.options.highlightcolor,
endcolor: this.options.highlightendcolor,
restorecolor: this.originalBackground
}
);
},
leaveEditMode:function(transport)
{
this.removeControls();
this.leaveHover();
Element.show(this.element);
this.editing = false;
this.saving = false;
this.oldInnerHTML = null;
}
}
Now you need 3 actions
First one to build your complete HTML page:
...
executeShow()
{
$this->article = Article::retrieveByPk(1);
}
...
and the according template showSuccess.php:
<?php use_helper('AjaxInplaceSelect') ?> <h1><?php echo $article->getName() ?></h1> <strong>author:</strong> <?php echo input_in_place_select_tag( 'author_dom_id', $article->getAuthor()->getName(), 'module/updateChanges', 'module/jsonUpdate', $article->getAuthor()->getId(), array(), array('callback' => 'function(value, text) { return \'author_id=\'+value+\'&article='.$article; }')); ?>
Then the second action will generate the data to show in select field when clicking on the author. The data is passed to the InplaceSelect as JSON code:
...
executeJsonUpdate()
{
$authors = Author::doSelect(new Criteria());
$data = array():
foreach($authors as $author)
{
$data[$author->getId()]=$author->getName();
}
$this->renderText('('.json_encode($data).')');
}
...
And finally still the action that is receiving the change and saving it
...
executeUpdateChanges()
{
$newAuthorId = $this->getRequestParameter('author_id');
$articleId = $this->getRequestParameter('article');
$article = Article::retrieveByPk($articleId);
$article->setAuthorId($newAuthorId );
$article->save();
$this->renderText('');
}
...
That's it! Now the InplaceSelect should work fine. After clicking an option of the select field, it transforms back to normal text which show now the selected option. Instead of only pass over the text of the option, of course you can specify something else. For example if you normally show beside the author name also the number of books written by him, you do not need to show all this information also in the select field in order to still have this information for a new selected author. Therefore you can define a onSuccess function when calling the helper in the showSuccess template:
<?php use_helper('AjaxInplaceSelect') ?> <h1><?php echo $article->getName() ?></h1> <strong>author:</strong> <?php echo input_in_place_select_tag( 'author_dom_id', $article->getAuthor()->getName() . ' [' . $article->getAuthor()->getNumberOfBooks() . ' Book(s)]', 'module/updateChanges', 'module/jsonUpdate', $article->getAuthor()->getId(), array(), array( 'onSuccess' => 'onSuccess' => 'function(transporation) { $(\'author_dom_id\').innerHTML = transporation.responseText; }', 'callback' => 'function(value, text) { return \'author_id=\'+value+\'&article='.$article; }') ); ?>
and when saving the changes render the more detailed Author info:
...
executeUpdateChanges()
{
$newAuthorId = $this->getRequestParameter('author_id');
$articleId = $this->getRequestParameter('article');
$article = Article::retrieveByPk($articleId);
$article->setAuthorId($newAuthorId );
$article->save();
$this->renderText($article->getAuthor()->getName() . ' [' . $article->getAuthor()->getNumberOfBooks() . ' Book(s)]');
}
...
There are still some more option which you can use, check helper the source code.
Enjoy the InplaceSelect!
I've found it annoying to set the show and hide commands on each remote request. So I've used this to define it globally:
/* * Defines the indicator globally */ Ajax.Responders.register({ onCreate: function() { Element.show('indicator'); }, onComplete: function() { Element.hide('indicator'); } });
If you want to hide the Web Debug Details/Menus but also want to have quick access when you need it.
Then this could help you. Just copy this in a js file or print it in your layout:
/* * HIDE WEB DEBUG DETAILS ON LAUNCH */ Event.observe(window, 'load', function(){ // check if web debug is on if ($('sfWebDebugBar')) sfWebDebugToggleMenu(); });
I've found this to be a pretty good solution for the dynamic page state v. bookmarking conflict that prevents users from bookmarking a specific state of your dynamic page. Typically, if a general user bookmarks your page after spending some time interacting with its dynamic features, she will be disappointed to find her bookmark doesn't reflect the state of the page when she bookmarked it.
If your Javascript is built in a way that you can hijack the window's onLoad handler to initialize a custom state, based what fragment might be in the URI, you just might find this helper function useful.
Save this to your project or application's /lib/helper folder.
<?php require_once(sfConfig::get('sf_symfony_lib_dir') . '/helper/JavascriptHelper.php'); /** * An alternative to the sf default link_to_function. * * Adds logic to onclick's concat'd "; return false;" so it only shows up if * the value of the href option doesn't include a #fragment. If no fragment is * embedded in the href it is set to "javascript:void(0)" a la Google. * * With the "; return false;" absent, the fragment will show up in the * browser's address bar, and will be included if the user copies the link or * bookmarks the current state of your dynamic page. You can then add a bit of * logic to your window initialization Javascript to detect any fragments in * the URL and adjust the onLoad state accordingly. * * @author Kris Wallsmith <kris [dot] wallsmith [at] gmail [dot] com> * @version tested on symfony 1.0.3 * @see link_to_function() * * @param string $name * @param string $function * @param mixed $options * * @return string */ function my_link_to_function($name, $function, $options = array()) { $options = _parse_attributes($options); $has_href = isset($options['href']); if(!isset($options['href'])) { $options['href'] = 'javascript:void(0)'; } $options['onclick'] = $function; if(!$has_href || strpos($options['href'], '#') === false) { $options['onclick'] .= '; return false;'; } return content_tag('a', $name, $options); }
If you're like me and prefer to code your own Unobtusive Javascript, you'll probably find this snippet handy. It will search through your project's /web/js folder for any .js files that match the current Symfony action.
For example, the snippet/new action would look for /web/js/snippet/snippet.js and /web/js/snippet/new.js, and add them to the response if either exist.
You can also define an optional subfolder within /web/js where you want the filter to look (i.e. "backend").
<?php /** * Looks for Javascript files based on current action/module and adds them to * the current response object. * * Add this filter to the end of your filter chain in filters.yml, after the * execution filter: * * <code> * rendering: ~ * web_debug: ~ * security: ~ * * # generally, you will want to insert your own filters here * * cache: ~ * common: ~ * flash: ~ * execution: ~ * * auto_javascript_include: * class: myAutoJavascriptIncludeFilter * </code> * * @package Automatic Javascript Include (AJI) * @subpackage filter * @author Kris Wallsmith <kris [dot] wallsmith [at] gmail [dot] com> * @version SVN: $Id$ * @copyright Have at it ... */ class myAutoJavascriptIncludeFilter extends sfFilter { /** * Include external Javascript files based on last action called. * * This kicks in on the way back down the filter chain, so we're sure to * catch the last action. Looks through the web/js folder for files that * match the naming syntax and adds them to the response. * * You can specify a subfolder of the web/js folder for the filter to * search in app.yml. * * @author Kris Wallsmith <kris [dot] wallsmith [at] gmail [dot] com> * @see sfConfig::get('app_aji_subfolder') * @param sfFilterChain $filterChain */ public function execute($filterChain) { $filterChain->execute(); $sf_context = sfContext::getInstance(); $sf_response = $sf_context->getResponse(); $sf_web_dir = sfConfig::get('sf_web_dir'); $module = $sf_context->getModuleName(); $action = $sf_context->getActionName(); $sub_folder = sfConfig::get('app_aji_subfolder'); if($sub_folder && $sub_folder{0} != '/') $sub_folder = '/' . $sub_folder; $fmt = '/js%s/%s/%s.js'; $mod_mod_js = sprintf($fmt, $sub_folder, $module, $module); $mod_act_js = sprintf($fmt, $sub_folder, $module, $action); if(file_exists($sf_web_dir . $mod_mod_js)) $sf_response->addJavascript($mod_mod_js); if(file_exists($sf_web_dir . $mod_act_js)) $sf_response->addJavascript($mod_act_js); } } ?>
This helper creates a div tag tag into which links can call other actions and dynamically display its output via AJAX. When JS is not enabled, the links reload the page with GET parameters and allow this helper to load the output.
What do you think of this code? First of all, has it already been done before? And do you think it will actually be useful? How well does it conform to the MVC structure?
Actual code:
<?php /** * This file includes functions that assist in making AJAX degradable * * These small helper functions add in some of the basic features that allow for degradable * JS. The idea is to dynamically load contents when JS is available and let the page reload with * appropriate html when JS is not. * @author Yining Zhao * @package YZ_Helpers * @subpackage AJAX_Degredation * @since 1/16/2007 */ /** * This class