From 9b03d82aba91dda1b7eb7e52134a547d641dc3fb Mon Sep 17 00:00:00 2001 From: Thomas Date: Tue, 17 Nov 2009 09:36:51 +0100 Subject: [PATCH 01/44] Add support for non-tabbed forms (temporary in a new herited class). --- src/phorms.php | 12 +++++++- src/phorms_ext.php | 71 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+), 1 deletion(-) create mode 100644 src/phorms_ext.php diff --git a/src/phorms.php b/src/phorms.php index f374d44..c515141 100644 --- a/src/phorms.php +++ b/src/phorms.php @@ -315,6 +315,16 @@ public function getIterator() return new ArrayIterator($this->fields); } + /** + * Returns an the fields' array. + * @return Array + * @author Thomas Lété + **/ + public function getFields() + { + return $this->fields; + } + /** * Returns the form's opening HTML tag. * @param string $target the form target ($_SERVER['PHP_SELF'] by default) @@ -456,4 +466,4 @@ public function as_table() } } -?> \ No newline at end of file +?> diff --git a/src/phorms_ext.php b/src/phorms_ext.php new file mode 100644 index 0000000..5650bcb --- /dev/null +++ b/src/phorms_ext.php @@ -0,0 +1,71 @@ +as_labels(); + } + + /** + * Returns the form fields as a series of paragraphs. + * @return string the HTML form + * @author Thomas Lété + **/ + public function as_labels() + { + $elts = array(); + foreach ($this->getFields() as $name => $field) + { + $label = $field->label(); + if ($label !== '') + $elts[] = sprintf('

%s%s

', str_replace('label()), $field); + else + $elts[] = strval($field); + } + return implode($elts); + } +} + +abstract class FieldsetPhormExt extends Phorm +{ + public function __construct($method=Phorm::GET, $multi_part=false, $data=array()) + { + parent::__construct($method, $multi_part, $data); + $this->define_fieldsets(); + } + /** + * Returns the form fields as a series of paragraphs. + * @return string the HTML form + * @author Thomas Lété + **/ + public function as_labels() + { + $elts = array(); + foreach ($this->fieldsets as $fieldset) + { + $elts[] = sprintf('
%s', $fieldset->label); + foreach ($fieldset->field_names as $field_name) { + $field = $this->$field_name; + $label = $field->label(); + + if ($label !== '') + $elts[] = sprintf('

%s%s

', str_replace(''; + } + return implode($elts, "\n"); + } +} +?> From fffda62902e03c55b3accf297183e87bc8d76c37 Mon Sep 17 00:00:00 2001 From: Thomas Date: Tue, 17 Nov 2009 19:57:00 +0100 Subject: [PATCH 02/44] New example file to demonstrate PhormsExt. --- examples/comment_form_ext.php | 180 ++++++++++++++++++++++++++++++++++ 1 file changed, 180 insertions(+) create mode 100644 examples/comment_form_ext.php diff --git a/examples/comment_form_ext.php b/examples/comment_form_ext.php new file mode 100644 index 0000000..af890bc --- /dev/null +++ b/examples/comment_form_ext.php @@ -0,0 +1,180 @@ +post_id = new HiddenField(array('required')); + $this->first_name = new TextField("First name", 25, 255, array('required')); + $this->last_name = new TextField("Last name", 25, 255, array('required')); + $this->email = new EmailField("Email address", 25, 255, array('required')); + $this->url = new URLField("Home page", 25, 255); + $this->number = new IntegerField("Favorite number", 7, array('required')); + $this->message = new LargeTextField('Message', 5, 40, array('required')); + $this->notify = new BooleanField('Reply notification'); + + // Add some help text + $this->notify->set_help_text('Email me when my comment receives a response.'); + $this->email->set_help_text('We will never give out your email address.'); + } + + public function report() + { + var_dump( $this->cleaned_data() ); + } +} + +// Set up the form +$post_id = 42; +$form = new CommentForm(Phorm::POST, false, array('post_id'=>$post_id, 'notify'=>true)); + +// Check form validity +$valid = $form->is_valid(); + +?> + + + + open() ?> +

Add a comment

+ has_errors() ): ?> +

Please correct the following errors.

+ + +

+ + +

+ close() ?> + +

Raw POST data:

+ + +
+ + is_bound() && $valid): ?> +

Processed and cleaned form data:

+ report() ?> + has_errors()): ?> +

Errors:

+ get_errors()); ?> + +

The form is unbound.

+ + + + + From c1da8df7a1c6263f66f191383e0ccf8e1d303587 Mon Sep 17 00:00:00 2001 From: Thomas Date: Tue, 17 Nov 2009 22:25:45 +0100 Subject: [PATCH 03/44] Add integration with "Really Easy Field Validation" (based on prototypejs) for real-time and client-side validation (example updated). --- examples/comment_form_ext.php | 22 +- src/fields.php | 84 +- src/javascript/fabtabulous.js | 50 + src/javascript/index.html | 196 ++ src/javascript/scriptaculous/CHANGELOG | 962 +++++++ src/javascript/scriptaculous/MIT-LICENSE | 20 + src/javascript/scriptaculous/README | 57 + src/javascript/scriptaculous/lib/prototype.js | 2426 +++++++++++++++++ src/javascript/scriptaculous/src/builder.js | 131 + src/javascript/scriptaculous/src/controls.js | 835 ++++++ src/javascript/scriptaculous/src/dragdrop.js | 944 +++++++ src/javascript/scriptaculous/src/effects.js | 1091 ++++++++ .../scriptaculous/src/scriptaculous.js | 51 + src/javascript/scriptaculous/src/slider.js | 278 ++ src/javascript/scriptaculous/src/unittest.js | 564 ++++ src/javascript/style.css | 92 + src/javascript/validation.js | 280 ++ 17 files changed, 8078 insertions(+), 5 deletions(-) create mode 100644 src/javascript/fabtabulous.js create mode 100644 src/javascript/index.html create mode 100644 src/javascript/scriptaculous/CHANGELOG create mode 100644 src/javascript/scriptaculous/MIT-LICENSE create mode 100644 src/javascript/scriptaculous/README create mode 100644 src/javascript/scriptaculous/lib/prototype.js create mode 100644 src/javascript/scriptaculous/src/builder.js create mode 100644 src/javascript/scriptaculous/src/controls.js create mode 100644 src/javascript/scriptaculous/src/dragdrop.js create mode 100644 src/javascript/scriptaculous/src/effects.js create mode 100644 src/javascript/scriptaculous/src/scriptaculous.js create mode 100644 src/javascript/scriptaculous/src/slider.js create mode 100644 src/javascript/scriptaculous/src/unittest.js create mode 100644 src/javascript/style.css create mode 100644 src/javascript/validation.js diff --git a/examples/comment_form_ext.php b/examples/comment_form_ext.php index af890bc..2db13a7 100644 --- a/examples/comment_form_ext.php +++ b/examples/comment_form_ext.php @@ -24,6 +24,7 @@ protected function define_fields() $this->number = new IntegerField("Favorite number", 7, array('required')); $this->message = new LargeTextField('Message', 5, 40, array('required')); $this->notify = new BooleanField('Reply notification'); + $this->date = new DateTimeField('Date', array('required')); // Add some help text $this->notify->set_help_text('Email me when my comment receives a response.'); @@ -44,12 +45,12 @@ public function report() $valid = $form->is_valid(); ?> + + + + open() ?> @@ -174,6 +185,9 @@ public function report()

The form is unbound.

+ diff --git a/src/fields.php b/src/fields.php index dd97a4f..452cba3 100644 --- a/src/fields.php +++ b/src/fields.php @@ -86,6 +86,18 @@ abstract class PhormField **/ public function __construct($label, array $validators=array(), array $attributes=array()) { + if(in_array('required', $validators)) + { + if(!isset($attributes['class'])) + { + $attributes['class'] = 'required'; + } + else + { + $attributes['class'] .= ' required'; + } + } + $this->label = (string)$label; $this->attributes = $attributes; $this->validators = $validators; @@ -735,6 +747,14 @@ class IntegerField extends PhormField public function __construct($label, $max_digits, array $validators=array(), array $attributes=array()) { $attributes['size'] = 20; + if(!isset($attributes['class'])) + { + $attributes['class'] = 'validate-digits'; + } + else + { + $attributes['class'] .= ' validate-digits'; + } parent::__construct($label, $validators, $attributes); $this->max_digits = $max_digits; } @@ -798,6 +818,15 @@ class DecimalField extends PhormField **/ public function __construct($label, $precision, array $validators=array(), array $attributes=array()) { + if(!isset($attributes['class'])) + { + $attributes['class'] = 'validate-number'; + } + else + { + $attributes['class'] .= ' validate-number'; + } + $attributes['size'] = 20; parent::__construct($label, $validators, $attributes); $this->precision = $precision; @@ -1004,6 +1033,28 @@ public function import_value($value) **/ class URLField extends TextField { + /** + * @author Thomas Lété + * @param string $label the field's text label + * @param int $size the field's size attribute + * @param int $max_length the maximum size in characters + * @param array $validators a list of callbacks to validate the field data + * @param array $attributes a list of key/value pairs representing HTML attributes + **/ + public function __construct($label, $size, $max_length, array $validators=array(), array $attributes=array()) + { + if(!isset($attributes['class'])) + { + $attributes['class'] = 'validate-url'; + } + else + { + $attributes['class'] .= ' validate-url'; + } + + parent::__construct($label, $size, $max_length, $validators, $attributes); + } + /** * Prepares the value by inserting http:// to the beginning if missing. * @author Jeff Ober @@ -1042,6 +1093,28 @@ public function validate($value) **/ class EmailField extends TextField { + /** + * @author Thomas Lété + * @param string $label the field's text label + * @param int $size the field's size attribute + * @param int $max_length the maximum size in characters + * @param array $validators a list of callbacks to validate the field data + * @param array $attributes a list of key/value pairs representing HTML attributes + **/ + public function __construct($label, $size, $max_length, array $validators=array(), array $attributes=array()) + { + if(!isset($attributes['class'])) + { + $attributes['class'] = 'validate-email'; + } + else + { + $attributes['class'] .= ' validate-email'; + } + + parent::__construct($label, $size, $max_length, $validators, $attributes); + } + /** * Validates that the value is a valid email address. * @author Jeff Ober @@ -1077,6 +1150,15 @@ class DateTimeField extends TextField **/ public function __construct($label, array $validators=array(), array $attributes=array()) { + if(!isset($attributes['class'])) + { + $attributes['class'] = 'validate-date'; + } + else + { + $attributes['class'] .= ' validate-date'; + } + parent::__construct($label, 25, 100, $validators, $attributes); } @@ -1331,4 +1413,4 @@ public function get_widget() } } -?> \ No newline at end of file +?> diff --git a/src/javascript/fabtabulous.js b/src/javascript/fabtabulous.js new file mode 100644 index 0000000..f2155ae --- /dev/null +++ b/src/javascript/fabtabulous.js @@ -0,0 +1,50 @@ +/* + * Fabtabulous! Simple tabs using Prototype + * http://tetlaw.id.au/view/blog/fabtabulous-simple-tabs-using-prototype/ + * Andrew Tetlaw + * version 1.1 2006-05-06 + * http://creativecommons.org/licenses/by-sa/2.5/ + */ +var Fabtabs = Class.create(); + +Fabtabs.prototype = { + initialize : function(element) { + this.element = $(element); + var options = Object.extend({}, arguments[1] || {}); + this.menu = $A(this.element.getElementsByTagName('a')); + this.show(this.getInitialTab()); + this.menu.each(this.setupTab.bind(this)); + }, + setupTab : function(elm) { + Event.observe(elm,'click',this.activate.bindAsEventListener(this),false) + }, + activate : function(ev) { + var elm = Event.findElement(ev, "a"); + //Event.stop(ev); + this.show(elm); + this.menu.without(elm).each(this.hide.bind(this)); + }, + hide : function(elm) { + $(elm).removeClassName('active-tab'); + $(this.tabID(elm)).removeClassName('active-tab-body'); + }, + show : function(elm) { + $(elm).addClassName('active-tab'); + $(this.tabID(elm)).addClassName('active-tab-body'); + + }, + tabID : function(elm) { + return elm.href.match(/#(\w.+)/)[1]; + }, + getInitialTab : function() { + if(document.location.href.match(/#(\w.+)/)) { + var loc = RegExp.$1; + var elm = this.menu.find(function(value) { return value.href.match(/#(\w.+)/)[1] == loc; }); + return elm || this.menu.first(); + } else { + return this.menu.first(); + } + } +} + +Event.observe(window,'load',function(){ new Fabtabs('tabs'); },false); diff --git a/src/javascript/index.html b/src/javascript/index.html new file mode 100644 index 0000000..d3d4c6f --- /dev/null +++ b/src/javascript/index.html @@ -0,0 +1,196 @@ + + + + + + + + + + + +
+

Return to the article

+

Really Easy Field validation with Prototype

+ ' + + '' + + '' + + '' + + '
StatusTestMessage
'; + this.logsummary = $('logsummary') + this.loglines = $('loglines'); + }, + _toHTML: function(txt) { + return txt.escapeHTML().replace(/\n/g,"
"); + }, + addLinksToResults: function(){ + $$("tr.failed .nameCell").each( function(td){ // todo: limit to children of this.log + td.title = "Run only this test" + Event.observe(td, 'click', function(){ window.location.search = "?tests=" + td.innerHTML;}); + }); + $$("tr.passed .nameCell").each( function(td){ // todo: limit to children of this.log + td.title = "Run all tests" + Event.observe(td, 'click', function(){ window.location.search = "";}); + }); + } +} + +Test.Unit.Runner = Class.create(); +Test.Unit.Runner.prototype = { + initialize: function(testcases) { + this.options = Object.extend({ + testLog: 'testlog' + }, arguments[1] || {}); + this.options.resultsURL = this.parseResultsURLQueryParameter(); + this.options.tests = this.parseTestsQueryParameter(); + if (this.options.testLog) { + this.options.testLog = $(this.options.testLog) || null; + } + if(this.options.tests) { + this.tests = []; + for(var i = 0; i < this.options.tests.length; i++) { + if(/^test/.test(this.options.tests[i])) { + this.tests.push(new Test.Unit.Testcase(this.options.tests[i], testcases[this.options.tests[i]], testcases["setup"], testcases["teardown"])); + } + } + } else { + if (this.options.test) { + this.tests = [new Test.Unit.Testcase(this.options.test, testcases[this.options.test], testcases["setup"], testcases["teardown"])]; + } else { + this.tests = []; + for(var testcase in testcases) { + if(/^test/.test(testcase)) { + this.tests.push( + new Test.Unit.Testcase( + this.options.context ? ' -> ' + this.options.titles[testcase] : testcase, + testcases[testcase], testcases["setup"], testcases["teardown"] + )); + } + } + } + } + this.currentTest = 0; + this.logger = new Test.Unit.Logger(this.options.testLog); + setTimeout(this.runTests.bind(this), 1000); + }, + parseResultsURLQueryParameter: function() { + return window.location.search.parseQuery()["resultsURL"]; + }, + parseTestsQueryParameter: function(){ + if (window.location.search.parseQuery()["tests"]){ + return window.location.search.parseQuery()["tests"].split(','); + }; + }, + // Returns: + // "ERROR" if there was an error, + // "FAILURE" if there was a failure, or + // "SUCCESS" if there was neither + getResult: function() { + var hasFailure = false; + for(var i=0;i 0) { + return "ERROR"; + } + if (this.tests[i].failures > 0) { + hasFailure = true; + } + } + if (hasFailure) { + return "FAILURE"; + } else { + return "SUCCESS"; + } + }, + postResults: function() { + if (this.options.resultsURL) { + new Ajax.Request(this.options.resultsURL, + { method: 'get', parameters: 'result=' + this.getResult(), asynchronous: false }); + } + }, + runTests: function() { + var test = this.tests[this.currentTest]; + if (!test) { + // finished! + this.postResults(); + this.logger.summary(this.summary()); + return; + } + if(!test.isWaiting) { + this.logger.start(test.name); + } + test.run(); + if(test.isWaiting) { + this.logger.message("Waiting for " + test.timeToWait + "ms"); + setTimeout(this.runTests.bind(this), test.timeToWait || 1000); + } else { + this.logger.finish(test.status(), test.summary()); + this.currentTest++; + // tail recursive, hopefully the browser will skip the stackframe + this.runTests(); + } + }, + summary: function() { + var assertions = 0; + var failures = 0; + var errors = 0; + var messages = []; + for(var i=0;i 0) return 'failed'; + if (this.errors > 0) return 'error'; + return 'passed'; + }, + assert: function(expression) { + var message = arguments[1] || 'assert: got "' + Test.Unit.inspect(expression) + '"'; + try { expression ? this.pass() : + this.fail(message); } + catch(e) { this.error(e); } + }, + assertEqual: function(expected, actual) { + var message = arguments[2] || "assertEqual"; + try { (expected == actual) ? this.pass() : + this.fail(message + ': expected "' + Test.Unit.inspect(expected) + + '", actual "' + Test.Unit.inspect(actual) + '"'); } + catch(e) { this.error(e); } + }, + assertInspect: function(expected, actual) { + var message = arguments[2] || "assertInspect"; + try { (expected == actual.inspect()) ? this.pass() : + this.fail(message + ': expected "' + Test.Unit.inspect(expected) + + '", actual "' + Test.Unit.inspect(actual) + '"'); } + catch(e) { this.error(e); } + }, + assertEnumEqual: function(expected, actual) { + var message = arguments[2] || "assertEnumEqual"; + try { $A(expected).length == $A(actual).length && + expected.zip(actual).all(function(pair) { return pair[0] == pair[1] }) ? + this.pass() : this.fail(message + ': expected ' + Test.Unit.inspect(expected) + + ', actual ' + Test.Unit.inspect(actual)); } + catch(e) { this.error(e); } + }, + assertNotEqual: function(expected, actual) { + var message = arguments[2] || "assertNotEqual"; + try { (expected != actual) ? this.pass() : + this.fail(message + ': got "' + Test.Unit.inspect(actual) + '"'); } + catch(e) { this.error(e); } + }, + assertIdentical: function(expected, actual) { + var message = arguments[2] || "assertIdentical"; + try { (expected === actual) ? this.pass() : + this.fail(message + ': expected "' + Test.Unit.inspect(expected) + + '", actual "' + Test.Unit.inspect(actual) + '"'); } + catch(e) { this.error(e); } + }, + assertNotIdentical: function(expected, actual) { + var message = arguments[2] || "assertNotIdentical"; + try { !(expected === actual) ? this.pass() : + this.fail(message + ': expected "' + Test.Unit.inspect(expected) + + '", actual "' + Test.Unit.inspect(actual) + '"'); } + catch(e) { this.error(e); } + }, + assertNull: function(obj) { + var message = arguments[1] || 'assertNull' + try { (obj==null) ? this.pass() : + this.fail(message + ': got "' + Test.Unit.inspect(obj) + '"'); } + catch(e) { this.error(e); } + }, + assertMatch: function(expected, actual) { + var message = arguments[2] || 'assertMatch'; + var regex = new RegExp(expected); + try { (regex.exec(actual)) ? this.pass() : + this.fail(message + ' : regex: "' + Test.Unit.inspect(expected) + ' did not match: ' + Test.Unit.inspect(actual) + '"'); } + catch(e) { this.error(e); } + }, + assertHidden: function(element) { + var message = arguments[1] || 'assertHidden'; + this.assertEqual("none", element.style.display, message); + }, + assertNotNull: function(object) { + var message = arguments[1] || 'assertNotNull'; + this.assert(object != null, message); + }, + assertType: function(expected, actual) { + var message = arguments[2] || 'assertType'; + try { + (actual.constructor == expected) ? this.pass() : + this.fail(message + ': expected "' + Test.Unit.inspect(expected) + + '", actual "' + (actual.constructor) + '"'); } + catch(e) { this.error(e); } + }, + assertNotOfType: function(expected, actual) { + var message = arguments[2] || 'assertNotOfType'; + try { + (actual.constructor != expected) ? this.pass() : + this.fail(message + ': expected "' + Test.Unit.inspect(expected) + + '", actual "' + (actual.constructor) + '"'); } + catch(e) { this.error(e); } + }, + assertInstanceOf: function(expected, actual) { + var message = arguments[2] || 'assertInstanceOf'; + try { + (actual instanceof expected) ? this.pass() : + this.fail(message + ": object was not an instance of the expected type"); } + catch(e) { this.error(e); } + }, + assertNotInstanceOf: function(expected, actual) { + var message = arguments[2] || 'assertNotInstanceOf'; + try { + !(actual instanceof expected) ? this.pass() : + this.fail(message + ": object was an instance of the not expected type"); } + catch(e) { this.error(e); } + }, + assertRespondsTo: function(method, obj) { + var message = arguments[2] || 'assertRespondsTo'; + try { + (obj[method] && typeof obj[method] == 'function') ? this.pass() : + this.fail(message + ": object doesn't respond to [" + method + "]"); } + catch(e) { this.error(e); } + }, + assertReturnsTrue: function(method, obj) { + var message = arguments[2] || 'assertReturnsTrue'; + try { + var m = obj[method]; + if(!m) m = obj['is'+method.charAt(0).toUpperCase()+method.slice(1)]; + m() ? this.pass() : + this.fail(message + ": method returned false"); } + catch(e) { this.error(e); } + }, + assertReturnsFalse: function(method, obj) { + var message = arguments[2] || 'assertReturnsFalse'; + try { + var m = obj[method]; + if(!m) m = obj['is'+method.charAt(0).toUpperCase()+method.slice(1)]; + !m() ? this.pass() : + this.fail(message + ": method returned true"); } + catch(e) { this.error(e); } + }, + assertRaise: function(exceptionName, method) { + var message = arguments[2] || 'assertRaise'; + try { + method(); + this.fail(message + ": exception expected but none was raised"); } + catch(e) { + ((exceptionName == null) || (e.name==exceptionName)) ? this.pass() : this.error(e); + } + }, + assertElementsMatch: function() { + var expressions = $A(arguments), elements = $A(expressions.shift()); + if (elements.length != expressions.length) { + this.fail('assertElementsMatch: size mismatch: ' + elements.length + ' elements, ' + expressions.length + ' expressions'); + return false; + } + elements.zip(expressions).all(function(pair, index) { + var element = $(pair.first()), expression = pair.last(); + if (element.match(expression)) return true; + this.fail('assertElementsMatch: (in index ' + index + ') expected ' + expression.inspect() + ' but got ' + element.inspect()); + }.bind(this)) && this.pass(); + }, + assertElementMatches: function(element, expression) { + this.assertElementsMatch([element], expression); + }, + benchmark: function(operation, iterations) { + var startAt = new Date(); + (iterations || 1).times(operation); + var timeTaken = ((new Date())-startAt); + this.info((arguments[2] || 'Operation') + ' finished ' + + iterations + ' iterations in ' + (timeTaken/1000)+'s' ); + return timeTaken; + }, + _isVisible: function(element) { + element = $(element); + if(!element.parentNode) return true; + this.assertNotNull(element); + if(element.style && Element.getStyle(element, 'display') == 'none') + return false; + + return this._isVisible(element.parentNode); + }, + assertNotVisible: function(element) { + this.assert(!this._isVisible(element), Test.Unit.inspect(element) + " was not hidden and didn't have a hidden parent either. " + ("" || arguments[1])); + }, + assertVisible: function(element) { + this.assert(this._isVisible(element), Test.Unit.inspect(element) + " was not visible. " + ("" || arguments[1])); + }, + benchmark: function(operation, iterations) { + var startAt = new Date(); + (iterations || 1).times(operation); + var timeTaken = ((new Date())-startAt); + this.info((arguments[2] || 'Operation') + ' finished ' + + iterations + ' iterations in ' + (timeTaken/1000)+'s' ); + return timeTaken; + } +} + +Test.Unit.Testcase = Class.create(); +Object.extend(Object.extend(Test.Unit.Testcase.prototype, Test.Unit.Assertions.prototype), { + initialize: function(name, test, setup, teardown) { + Test.Unit.Assertions.prototype.initialize.bind(this)(); + this.name = name; + + if(typeof test == 'string') { + test = test.gsub(/(\.should[^\(]+\()/,'#{0}this,'); + test = test.gsub(/(\.should[^\(]+)\(this,\)/,'#{1}(this)'); + this.test = function() { + eval('with(this){'+test+'}'); + } + } else { + this.test = test || function() {}; + } + + this.setup = setup || function() {}; + this.teardown = teardown || function() {}; + this.isWaiting = false; + this.timeToWait = 1000; + }, + wait: function(time, nextPart) { + this.isWaiting = true; + this.test = nextPart; + this.timeToWait = time; + }, + run: function() { + try { + try { + if (!this.isWaiting) this.setup.bind(this)(); + this.isWaiting = false; + this.test.bind(this)(); + } finally { + if(!this.isWaiting) { + this.teardown.bind(this)(); + } + } + } + catch(e) { this.error(e); } + } +}); + +// *EXPERIMENTAL* BDD-style testing to please non-technical folk +// This draws many ideas from RSpec http://rspec.rubyforge.org/ + +Test.setupBDDExtensionMethods = function(){ + var METHODMAP = { + shouldEqual: 'assertEqual', + shouldNotEqual: 'assertNotEqual', + shouldEqualEnum: 'assertEnumEqual', + shouldBeA: 'assertType', + shouldNotBeA: 'assertNotOfType', + shouldBeAn: 'assertType', + shouldNotBeAn: 'assertNotOfType', + shouldBeNull: 'assertNull', + shouldNotBeNull: 'assertNotNull', + + shouldBe: 'assertReturnsTrue', + shouldNotBe: 'assertReturnsFalse', + shouldRespondTo: 'assertRespondsTo' + }; + Test.BDDMethods = {}; + for(m in METHODMAP) { + Test.BDDMethods[m] = eval( + 'function(){'+ + 'var args = $A(arguments);'+ + 'var scope = args.shift();'+ + 'scope.'+METHODMAP[m]+'.apply(scope,(args || []).concat([this])); }'); + } + [Array.prototype, String.prototype, Number.prototype].each( + function(p){ Object.extend(p, Test.BDDMethods) } + ); +} + +Test.context = function(name, spec, log){ + Test.setupBDDExtensionMethods(); + + var compiledSpec = {}; + var titles = {}; + for(specName in spec) { + switch(specName){ + case "setup": + case "teardown": + compiledSpec[specName] = spec[specName]; + break; + default: + var testName = 'test'+specName.gsub(/\s+/,'-').camelize(); + var body = spec[specName].toString().split('\n').slice(1); + if(/^\{/.test(body[0])) body = body.slice(1); + body.pop(); + body = body.map(function(statement){ + return statement.strip() + }); + compiledSpec[testName] = body.join('\n'); + titles[testName] = specName; + } + } + new Test.Unit.Runner(compiledSpec, { titles: titles, testLog: log || 'testlog', context: name }); +}; \ No newline at end of file diff --git a/src/javascript/style.css b/src/javascript/style.css new file mode 100644 index 0000000..8f32fe0 --- /dev/null +++ b/src/javascript/style.css @@ -0,0 +1,92 @@ +body { + color: #333; + padding: 0 30px; + font-family: Arial, Helvetica, sans-serif; + font-size: 76%; +} + +.panel { + clear: both; + display: none; + border: 3px solid #CCC; + padding: 1em; +} +.panel.active-tab-body { + display: block; +} +#tabs { + list-style: none; +} + +#tabs li { + float: left; +} + +#tabs a { + float: left; + padding: 5px 8px; + margin-left: 6px; + background-color: #F2F2F2; + text-decoration: none; + color: #999999; +} + +#tabs a.active-tab { + background-color: #CCC; + border-top: 3px solid #999; + padding-top: 3px; + color: #000; +} +input.disabled { + border: 1px solid #F2F2F2; + background-color: #F2F2F2; +} + +input.required, textarea.required { + border: 1px solid #00A8E6; +} +input.validation-failed, textarea.validation-failed { + border: 1px solid #FF3300; + color : #FF3300; +} +input.validation-passed, textarea.validation-passed { + border: 1px solid #00CC00; + color : #000; +} + +.validation-advice { + margin: 5px 0; + padding: 5px; + background-color: #FF3300; + color : #FFF; + font-weight: bold; +} + +.custom-advice { + margin: 5px 0; + padding: 5px; + background-color: #C8AA00; + color : #FFF; + font-weight: bold; +} + +fieldset { + padding: 1em; + margin-bottom: 0.5em; +} + +label { + font-weight: bold; +} +.form-row { + clear: both; + padding: 0.5em; +} + +.field-label { + +} + +.field-widget { + +} \ No newline at end of file diff --git a/src/javascript/validation.js b/src/javascript/validation.js new file mode 100644 index 0000000..378ba3b --- /dev/null +++ b/src/javascript/validation.js @@ -0,0 +1,280 @@ +/* +* Really easy field validation with Prototype +* http://tetlaw.id.au/view/javascript/really-easy-field-validation +* Andrew Tetlaw +* Version 1.5.4.1 (2007-01-05) +* +* Copyright (c) 2007 Andrew Tetlaw +* Permission is hereby granted, free of charge, to any person +* obtaining a copy of this software and associated documentation +* files (the "Software"), to deal in the Software without +* restriction, including without limitation the rights to use, copy, +* modify, merge, publish, distribute, sublicense, and/or sell copies +* of the Software, and to permit persons to whom the Software is +* furnished to do so, subject to the following conditions: +* +* The above copyright notice and this permission notice shall be +* included in all copies or substantial portions of the Software. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +* BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +* ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +* SOFTWARE. +* +*/ +var Validator = Class.create(); + +Validator.prototype = { + initialize : function(className, error, test, options) { + if(typeof test == 'function'){ + this.options = $H(options); + this._test = test; + } else { + this.options = $H(test); + this._test = function(){return true}; + } + this.error = error || 'Validation failed.'; + this.className = className; + }, + test : function(v, elm) { + return (this._test(v,elm) && this.options.all(function(p){ + return Validator.methods[p.key] ? Validator.methods[p.key](v,elm,p.value) : true; + })); + } +} +Validator.methods = { + pattern : function(v,elm,opt) {return Validation.get('IsEmpty').test(v) || opt.test(v)}, + minLength : function(v,elm,opt) {return v.length >= opt}, + maxLength : function(v,elm,opt) {return v.length <= opt}, + min : function(v,elm,opt) {return v >= parseFloat(opt)}, + max : function(v,elm,opt) {return v <= parseFloat(opt)}, + notOneOf : function(v,elm,opt) {return $A(opt).all(function(value) { + return v != value; + })}, + oneOf : function(v,elm,opt) {return $A(opt).any(function(value) { + return v == value; + })}, + is : function(v,elm,opt) {return v == opt}, + isNot : function(v,elm,opt) {return v != opt}, + equalToField : function(v,elm,opt) {return v == $F(opt)}, + notEqualToField : function(v,elm,opt) {return v != $F(opt)}, + include : function(v,elm,opt) {return $A(opt).all(function(value) { + return Validation.get(value).test(v,elm); + })} +} + +var Validation = Class.create(); + +Validation.prototype = { + initialize : function(form, options){ + this.options = Object.extend({ + onSubmit : true, + stopOnFirst : false, + immediate : false, + focusOnError : true, + useTitles : false, + onFormValidate : function(result, form) {}, + onElementValidate : function(result, elm) {} + }, options || {}); + this.form = $(form); + if(this.options.onSubmit) Event.observe(this.form,'submit',this.onSubmit.bind(this),false); + if(this.options.immediate) { + var useTitles = this.options.useTitles; + var callback = this.options.onElementValidate; + Form.getElements(this.form).each(function(input) { // Thanks Mike! + Event.observe(input, 'blur', function(ev) { Validation.validate(Event.element(ev),{useTitle : useTitles, onElementValidate : callback}); }); + }); + } + }, + onSubmit : function(ev){ + if(!this.validate()) Event.stop(ev); + }, + validate : function() { + var result = false; + var useTitles = this.options.useTitles; + var callback = this.options.onElementValidate; + if(this.options.stopOnFirst) { + result = Form.getElements(this.form).all(function(elm) { return Validation.validate(elm,{useTitle : useTitles, onElementValidate : callback}); }); + } else { + result = Form.getElements(this.form).collect(function(elm) { return Validation.validate(elm,{useTitle : useTitles, onElementValidate : callback}); }).all(); + } + if(!result && this.options.focusOnError) { + Form.getElements(this.form).findAll(function(elm){return $(elm).hasClassName('validation-failed')}).first().focus() + } + this.options.onFormValidate(result, this.form); + return result; + }, + reset : function() { + Form.getElements(this.form).each(Validation.reset); + } +} + +Object.extend(Validation, { + validate : function(elm, options){ + options = Object.extend({ + useTitle : false, + onElementValidate : function(result, elm) {} + }, options || {}); + elm = $(elm); + var cn = elm.classNames(); + return result = cn.all(function(value) { + var test = Validation.test(value,elm,options.useTitle); + options.onElementValidate(test, elm); + return test; + }); + }, + test : function(name, elm, useTitle) { + var v = Validation.get(name); + var prop = '__advice'+name.camelize(); + try { + if(Validation.isVisible(elm) && !v.test($F(elm), elm)) { + if(!elm[prop]) { + var advice = Validation.getAdvice(name, elm); + if(advice == null) { + var errorMsg = useTitle ? ((elm && elm.title) ? elm.title : v.error) : v.error; + advice = '' + switch (elm.type.toLowerCase()) { + case 'checkbox': + case 'radio': + var p = elm.parentNode; + if(p) { + new Insertion.Bottom(p, advice); + } else { + new Insertion.After(elm, advice); + } + break; + default: + new Insertion.After(elm, advice); + } + advice = Validation.getAdvice(name, elm); + } + if(typeof Effect == 'undefined') { + advice.style.display = 'block'; + } else { + new Effect.Appear(advice, {duration : 1 }); + } + } + elm[prop] = true; + elm.removeClassName('validation-passed'); + elm.addClassName('validation-failed'); + return false; + } else { + var advice = Validation.getAdvice(name, elm); + if(advice != null) advice.hide(); + elm[prop] = ''; + elm.removeClassName('validation-failed'); + elm.addClassName('validation-passed'); + return true; + } + } catch(e) { + throw(e) + } + }, + isVisible : function(elm) { + while(elm.tagName != 'BODY') { + if(!$(elm).visible()) return false; + elm = elm.parentNode; + } + return true; + }, + getAdvice : function(name, elm) { + return $('advice-' + name + '-' + Validation.getElmID(elm)) || $('advice-' + Validation.getElmID(elm)); + }, + getElmID : function(elm) { + return elm.id ? elm.id : elm.name; + }, + reset : function(elm) { + elm = $(elm); + var cn = elm.classNames(); + cn.each(function(value) { + var prop = '__advice'+value.camelize(); + if(elm[prop]) { + var advice = Validation.getAdvice(value, elm); + advice.hide(); + elm[prop] = ''; + } + elm.removeClassName('validation-failed'); + elm.removeClassName('validation-passed'); + }); + }, + add : function(className, error, test, options) { + var nv = {}; + nv[className] = new Validator(className, error, test, options); + Object.extend(Validation.methods, nv); + }, + addAllThese : function(validators) { + var nv = {}; + $A(validators).each(function(value) { + nv[value[0]] = new Validator(value[0], value[1], value[2], (value.length > 3 ? value[3] : {})); + }); + Object.extend(Validation.methods, nv); + }, + get : function(name) { + return Validation.methods[name] ? Validation.methods[name] : Validation.methods['_LikeNoIDIEverSaw_']; + }, + methods : { + '_LikeNoIDIEverSaw_' : new Validator('_LikeNoIDIEverSaw_','',{}) + } +}); + +Validation.add('IsEmpty', '', function(v) { + return ((v == null) || (v.length == 0)); // || /^\s+$/.test(v)); + }); + +Validation.addAllThese([ + ['required', 'This is a required field.', function(v) { + return !Validation.get('IsEmpty').test(v); + }], + ['validate-number', 'Please enter a valid number in this field.', function(v) { + return Validation.get('IsEmpty').test(v) || (!isNaN(v) && !/^\s+$/.test(v)); + }], + ['validate-digits', 'Please use numbers only in this field. please avoid spaces or other characters such as dots or commas.', function(v) { + return Validation.get('IsEmpty').test(v) || !/[^\d]/.test(v); + }], + ['validate-alpha', 'Please use letters only (a-z) in this field.', function (v) { + return Validation.get('IsEmpty').test(v) || /^[a-zA-Z]+$/.test(v) + }], + ['validate-alphanum', 'Please use only letters (a-z) or numbers (0-9) only in this field. No spaces or other characters are allowed.', function(v) { + return Validation.get('IsEmpty').test(v) || !/\W/.test(v) + }], + ['validate-date', 'Please enter a valid date.', function(v) { + var test = new Date(v); + return Validation.get('IsEmpty').test(v) || !isNaN(test); + }], + ['validate-email', 'Please enter a valid email address. For example fred@domain.com .', function (v) { + return Validation.get('IsEmpty').test(v) || /\w{1,}[@][\w\-]{1,}([.]([\w\-]{1,})){1,3}$/.test(v) + }], + ['validate-url', 'Please enter a valid URL.', function (v) { + return Validation.get('IsEmpty').test(v) || /^(http|https|ftp):\/\/(([A-Z0-9][A-Z0-9_-]*)(\.[A-Z0-9][A-Z0-9_-]*)+)(:(\d+))?\/?/i.test(v) + }], + ['validate-date-au', 'Please use this date format: dd/mm/yyyy. For example 17/03/2006 for the 17th of March, 2006.', function(v) { + if(Validation.get('IsEmpty').test(v)) return true; + var regex = /^(\d{2})\/(\d{2})\/(\d{4})$/; + if(!regex.test(v)) return false; + var d = new Date(v.replace(regex, '$2/$1/$3')); + return ( parseInt(RegExp.$2, 10) == (1+d.getMonth()) ) && + (parseInt(RegExp.$1, 10) == d.getDate()) && + (parseInt(RegExp.$3, 10) == d.getFullYear() ); + }], + ['validate-currency-dollar', 'Please enter a valid $ amount. For example $100.00 .', function(v) { + // [$]1[##][,###]+[.##] + // [$]1###+[.##] + // [$]0.## + // [$].## + return Validation.get('IsEmpty').test(v) || /^\$?\-?([1-9]{1}[0-9]{0,2}(\,[0-9]{3})*(\.[0-9]{0,2})?|[1-9]{1}\d*(\.[0-9]{0,2})?|0(\.[0-9]{0,2})?|(\.[0-9]{1,2})?)$/.test(v) + }], + ['validate-selection', 'Please make a selection', function(v,elm){ + return elm.options ? elm.selectedIndex > 0 : !Validation.get('IsEmpty').test(v); + }], + ['validate-one-required', 'Please select one of the above options.', function (v,elm) { + var p = elm.parentNode; + var options = p.getElementsByTagName('INPUT'); + return $A(options).any(function(elm) { + return $F(elm); + }); + }] +]); \ No newline at end of file From d33b31706cf9c018de867800aa8c07d3d28022a5 Mon Sep 17 00:00:00 2001 From: Thomas Date: Wed, 18 Nov 2009 17:03:01 +0100 Subject: [PATCH 04/44] Correct support for fieldsets and add an example. (+ minor doc fixes) --- build_doc.sh | 0 examples/comment_form_ext.php | 7 +- examples/profile_form.php | 4 +- examples/profile_form_ext.php | 201 ++++++++++++++++++++++++++++++++++ src/javascript/fabtabulous.js | 50 --------- src/javascript/index.html | 196 --------------------------------- src/phorms_ext.php | 15 ++- 7 files changed, 223 insertions(+), 250 deletions(-) mode change 100644 => 100755 build_doc.sh create mode 100644 examples/profile_form_ext.php delete mode 100644 src/javascript/fabtabulous.js delete mode 100644 src/javascript/index.html diff --git a/build_doc.sh b/build_doc.sh old mode 100644 new mode 100755 diff --git a/examples/comment_form_ext.php b/examples/comment_form_ext.php index 2db13a7..6af7c00 100644 --- a/examples/comment_form_ext.php +++ b/examples/comment_form_ext.php @@ -47,6 +47,7 @@ public function report() ?> + @@ -101,9 +102,13 @@ public function report() form input, form select { margin-left: 1%; - width: 58%; border: #CCC 1px solid; } + + form input[type="text"], form select + { + width: 58%; + } form .form_input_day_month { diff --git a/examples/profile_form.php b/examples/profile_form.php index a894c51..9751f31 100644 --- a/examples/profile_form.php +++ b/examples/profile_form.php @@ -10,7 +10,7 @@ function required($value) throw new ValidationError('This field is required.'); } -class ProfileForm extends FieldsetPhorm +class ProfileForm extends FieldsetPhormExt { protected function define_fields() { @@ -96,4 +96,4 @@ public function report()

The form is unbound.

- \ No newline at end of file + diff --git a/examples/profile_form_ext.php b/examples/profile_form_ext.php new file mode 100644 index 0000000..802d217 --- /dev/null +++ b/examples/profile_form_ext.php @@ -0,0 +1,201 @@ +user_id = new HiddenField(array('required')); + $this->first_name = new TextField("First name", 25, 255, array('required')); + $this->last_name = new TextField("Last name", 25, 255, array('required')); + $this->email = new EmailField("Email address", 25, 255, array('required')); + $this->url = new URLField("Home page", 25, 255); + $this->bio = new LargeTextField('Bio', 5, 40, array('required')); + + // Add some help text + $this->email->set_help_text('We will never give out your email address.'); + } + + protected function define_fieldsets() + { + $this->fieldsets = array(new Fieldset('name', 'Name', array('user_id', + 'first_name', + 'last_name')), + new Fieldset('extra', 'Extra', array('email', + 'url', + 'bio'))); + } + + public function report() + { + var_dump( $this->cleaned_data() ); + } +} + +// Set up the form +$post_id = 42; +$form = new ProfileForm(Phorm::POST, false, array('post_id'=>$post_id)); + +// Check form validity +$valid = $form->is_valid(); + +?> + + + + + + + + + + open() ?> +

Add a comment

+ has_errors() ): ?> +

Please correct the following errors.

+ + +

+ + +

+ close() ?> + +

Raw POST data:

+ + +
+ + is_bound() && $valid): ?> +

Processed and cleaned form data:

+ report() ?> + has_errors()): ?> +

Errors:

+ get_errors()); ?> + +

The form is unbound.

+ + + diff --git a/src/javascript/fabtabulous.js b/src/javascript/fabtabulous.js deleted file mode 100644 index f2155ae..0000000 --- a/src/javascript/fabtabulous.js +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Fabtabulous! Simple tabs using Prototype - * http://tetlaw.id.au/view/blog/fabtabulous-simple-tabs-using-prototype/ - * Andrew Tetlaw - * version 1.1 2006-05-06 - * http://creativecommons.org/licenses/by-sa/2.5/ - */ -var Fabtabs = Class.create(); - -Fabtabs.prototype = { - initialize : function(element) { - this.element = $(element); - var options = Object.extend({}, arguments[1] || {}); - this.menu = $A(this.element.getElementsByTagName('a')); - this.show(this.getInitialTab()); - this.menu.each(this.setupTab.bind(this)); - }, - setupTab : function(elm) { - Event.observe(elm,'click',this.activate.bindAsEventListener(this),false) - }, - activate : function(ev) { - var elm = Event.findElement(ev, "a"); - //Event.stop(ev); - this.show(elm); - this.menu.without(elm).each(this.hide.bind(this)); - }, - hide : function(elm) { - $(elm).removeClassName('active-tab'); - $(this.tabID(elm)).removeClassName('active-tab-body'); - }, - show : function(elm) { - $(elm).addClassName('active-tab'); - $(this.tabID(elm)).addClassName('active-tab-body'); - - }, - tabID : function(elm) { - return elm.href.match(/#(\w.+)/)[1]; - }, - getInitialTab : function() { - if(document.location.href.match(/#(\w.+)/)) { - var loc = RegExp.$1; - var elm = this.menu.find(function(value) { return value.href.match(/#(\w.+)/)[1] == loc; }); - return elm || this.menu.first(); - } else { - return this.menu.first(); - } - } -} - -Event.observe(window,'load',function(){ new Fabtabs('tabs'); },false); diff --git a/src/javascript/index.html b/src/javascript/index.html deleted file mode 100644 index d3d4c6f..0000000 --- a/src/javascript/index.html +++ /dev/null @@ -1,196 +0,0 @@ - - - - - - - - - - - -
-

Return to the article

-

Really Easy Field validation with Prototype

-