diff --git a/.project b/.project new file mode 100644 index 0000000..f17759c --- /dev/null +++ b/.project @@ -0,0 +1,12 @@ + + + Source Code + + + + + + + com.aptana.projects.webnature + + diff --git a/Chapter01/1.01 - The flexibility of JavaScript.js b/Chapter01/1.01 - The flexibility of JavaScript.js new file mode 100644 index 0000000..e9eaaaf --- /dev/null +++ b/Chapter01/1.01 - The flexibility of JavaScript.js @@ -0,0 +1,89 @@ +/* below is five method to do the same thing */ +/* a. Start and stop animations using functions. */ + +function startAnimation() { + ... +} + +function stopAnimation() { + ... +} + + + +/* b. Anim class. */ + +var Anim = function() { + ... +}; +Anim.prototype.start = function() { + ... +}; +Anim.prototype.stop = function() { + ... +}; + +/* Usage. */ + +var myAnim = new Anim(); +myAnim.start(); +... +myAnim.stop(); + + + +/* c. Anim class, with a slightly different syntax for declaring methods. */ + +var Anim = function() { + ... +}; +Anim.prototype = { + start: function() { + ... + }, + stop: function() { + ... + } +}; + + + +/* d. Add a method to the Function class that can be used to declare methods. */ + +Function.prototype.method = function(name, fn) { + this.prototype[name] = fn; +}; + +/* Anim class, with methods created using a convenience method. */ + +var Anim = function() { + ... +}; +Anim.method('start', function() { + ... +}); +Anim.method('stop', function() { + ... +}); + + + +/* e. This version allows the calls to be chained. */ + +Function.prototype.method = function(name, fn) { + this.prototype[name] = fn; + return this; +}; + +/* Anim class, with methods created using a convenience method and chaining. */ + +var Anim = function() { + ... +}; +Anim. + method('start', function() { + ... + }). + method('stop', function() { + ... + }); diff --git a/Chapter01/1.02 - Functions as first-class objects.js b/Chapter01/1.02 - Functions as first-class objects.js new file mode 100644 index 0000000..be8ee58 --- /dev/null +++ b/Chapter01/1.02 - Functions as first-class objects.js @@ -0,0 +1,39 @@ +/* An anonymous function, executed immediately. */ + +(function() { + var foo = 10; + var bar = 2; + alert(foo * bar); +})(); + + +/* An anonymous function with arguments. */ + +(function(foo, bar) { + alert(foo * bar); +})(10, 2); + + +/* An anonymous function that returns a value. */ + +var baz = (function(foo, bar) { + return foo * bar; +})(10, 2); + +// baz will equal 20. + + +/* An anonymous function used as a closure. */ + +var baz; + +(function() { + var foo = 10; + var bar = 2; + baz = function() { + return foo * bar; + }; +})(); + +baz(); // baz can access foo and bar, even though is it executed outside of the + // anonymous function. diff --git a/Chapter01/1.03 - The mutability of objects.js b/Chapter01/1.03 - The mutability of objects.js new file mode 100644 index 0000000..0c08807 --- /dev/null +++ b/Chapter01/1.03 - The mutability of objects.js @@ -0,0 +1,38 @@ +function displayError(message) { + displayError.numTimesExecuted++; + alert(message); +}; +displayError.numTimesExecuted = 0; + + +/* Class Person. */ + +function Person(name, age) { + this.name = name; + this.age = age; +} +Person.prototype = { + getName: function() { + return this.name; + }, + getAge: function() { + return this.age; + } +} + +/* Instantiate the class. */ + +var alice = new Person('Alice', 93); +var bill = new Person('Bill', 30); + +/* Modify the class. */ + +Person.prototype.getGreeting = function() { + return 'Hi ' + this.getName() + '!'; +}; + +/* Modify a specific instance. */ + +alice.displayGreeting = function() { + alert(this.getGreeting()); +} diff --git a/Chapter02/2.01 - Describing interfaces with comments.js b/Chapter02/2.01 - Describing interfaces with comments.js new file mode 100644 index 0000000..c02546d --- /dev/null +++ b/Chapter02/2.01 - Describing interfaces with comments.js @@ -0,0 +1,35 @@ +/* + +interface Composite { + function add(child); + function remove(child); + function getChild(index); +} + +interface FormItem { + function save(); +} + +*/ + +var CompositeForm = function(id, method, action) { // implements Composite, FormItem + ... +}; + +// Implement the Composite interface. + +CompositeForm.prototype.add = function(child) { + ... +}; +CompositeForm.prototype.remove = function(child) { + ... +}; +CompositeForm.prototype.getChild = function(index) { + ... +}; + +// Implement the FormItem interface. + +CompositeForm.prototype.save = function() { + ... +}; diff --git a/Chapter02/2.02 - Emulating interfaces with attribute checking.js b/Chapter02/2.02 - Emulating interfaces with attribute checking.js new file mode 100644 index 0000000..1725c12 --- /dev/null +++ b/Chapter02/2.02 - Emulating interfaces with attribute checking.js @@ -0,0 +1,49 @@ +/* + +interface Composite { + function add(child); + function remove(child); + function getChild(index); +} + +interface FormItem { + function save(); +} + +*/ + +var CompositeForm = function(id, method, action) { + this.implementsInterfaces = ['Composite', 'FormItem']; + ... +}; + +... + +function addForm(formInstance) { + if(!implements(formInstance, 'Composite', 'FormItem')) { + throw new Error("Object does not implement a required interface."); + } + ... +} + +// The implements function, which checks to see if an object declares that it +// implements the required interfaces. + +function implements(object) { + for(var i = 1; i < arguments.length; i++) { // Looping through all arguments + // after the first one. + var interfaceName = arguments[i]; + var interfaceFound = false; + for(var j = 0; j < object.implementsInterfaces.length; j++) { + if(object.implementsInterfaces[j] == interfaceName) { + interfaceFound = true; + break; + } + } + + if(!interfaceFound) { + return false; // An interface was not found. + } + } + return true; // All interfaces were found. +} diff --git a/Chapter02/2.03 - Emulating interfaces with duck typing.js b/Chapter02/2.03 - Emulating interfaces with duck typing.js new file mode 100644 index 0000000..28db04a --- /dev/null +++ b/Chapter02/2.03 - Emulating interfaces with duck typing.js @@ -0,0 +1,18 @@ +// Interfaces. + +var Composite = new Interface('Composite', ['add', 'remove', 'getChild']); +var FormItem = new Interface('FormItem', ['save']); + +// CompositeForm class + +var CompositeForm = function(id, method, action) { + ... +}; + +... + +function addForm(formInstance) { + ensureImplements(formInstance, Composite, FormItem); + // This function will throw an error if a required method is not implemented. + ... +} diff --git a/Chapter02/2.04 - The interface implementation for this book.js b/Chapter02/2.04 - The interface implementation for this book.js new file mode 100644 index 0000000..9df3439 --- /dev/null +++ b/Chapter02/2.04 - The interface implementation for this book.js @@ -0,0 +1,20 @@ +// Interfaces. + +var Composite = new Interface('Composite', ['add', 'remove', 'getChild']); +var FormItem = new Interface('FormItem', ['save']); + +// CompositeForm class + +var CompositeForm = function(id, method, action) { // implements Composite, FormItem + ... +}; + +... + +function addForm(formInstance) { + Interface.ensureImplements(formInstance, Composite, FormItem); + // This function will throw an error if a required method is not implemented, + // halting execution of the function. + // All code beneath this line will be executed only if the checks pass. + ... +} diff --git a/Chapter02/2.05 - The Interface class.js b/Chapter02/2.05 - The Interface class.js new file mode 100644 index 0000000..145c786 --- /dev/null +++ b/Chapter02/2.05 - The Interface class.js @@ -0,0 +1,44 @@ +// Constructor. + +var Interface = function(name, methods) { + if(arguments.length != 2) { + throw new Error("Interface constructor called with " + arguments.length + + "arguments, but expected exactly 2."); + } + + this.name = name; + this.methods = []; + for(var i = 0, len = methods.length; i < len; i++) { + if(typeof methods[i] !== 'string') { + throw new Error("Interface constructor expects method names to be " + + "passed in as a string."); + } + this.methods.push(methods[i]); + } +}; + +// Static class method. + +Interface.ensureImplements = function(object) { + if(arguments.length < 2) { + throw new Error("Function Interface.ensureImplements called with " + + arguments.length + "arguments, but expected at least 2."); + } + + for(var i = 1, len = arguments.length; i < len; i++) { + var interface = arguments[i]; + if(interface.constructor !== Interface) { + throw new Error("Function Interface.ensureImplements expects arguments " + + "two and above to be instances of Interface."); + } + + for(var j = 0, methodsLen = interface.methods.length; j < methodsLen; j++) { + var method = interface.methods[j]; + if(!object[method] || typeof object[method] !== 'function') { + throw new Error("Function Interface.ensureImplements: object " + + "does not implement the " + interface.name + + " interface. Method " + method + " was not found."); + } + } + } +}; diff --git a/Chapter02/2.06 - When to use the Interface class.js b/Chapter02/2.06 - When to use the Interface class.js new file mode 100644 index 0000000..5f741bb --- /dev/null +++ b/Chapter02/2.06 - When to use the Interface class.js @@ -0,0 +1,9 @@ +var DynamicMap = new Interface('DynamicMap', ['centerOnPoint', 'zoom', 'draw']); + +function displayRoute(mapInstance) { + Interface.ensureImplements(mapInstace, DynamicMap); + mapInstance.centerOnPoint(12, 34); + mapInstance.zoom(5); + mapInstance.draw(); + ... +} diff --git a/Chapter02/2.07 - An example illustrating the use of the Interface class.js b/Chapter02/2.07 - An example illustrating the use of the Interface class.js new file mode 100644 index 0000000..3ec0247 --- /dev/null +++ b/Chapter02/2.07 - An example illustrating the use of the Interface class.js @@ -0,0 +1,47 @@ +// ResultFormatter class, before we implement interface checking. + +var ResultFormatter = function(resultsObject) { + if(!(resultsObject instanceOf TestResult)) { + throw new Error('ResultsFormatter: constructor requires an instance ' + + 'of TestResult as an argument.'); + } + this.resultsObject = resultsObject; +}; + +ResultFormatter.prototype.renderResults = function() { + var dateOfTest = this.resultsObject.getDate(); + var resultsArray = this.resultsObject.getResults(); + + var resultsContainer = document.createElement('div'); + + var resultsHeader = document.createElement('h3'); + resultsHeader.innerHTML = 'Test Results from ' + dateOfTest.toUTCString(); + resultsContainer.appendChild(resultsHeader); + + var resultsList = document.createElement('ul'); + resultsContainer.appendChild(resultsList); + + for(var i = 0, len = resultsArray.length; i < len; i++) { + var listItem = document.createElement('li'); + listItem.innerHTML = resultsArray[i]; + resultsList.appendChild(listItem); + } + + return resultsContainer; +}; + + +// ResultSet Interface. + +var ResultSet = new Interface('ResultSet', ['getDate', 'getResults']); + +// ResultFormatter class, after adding Interface checking. + +var ResultFormatter = function(resultsObject) { + Interface.ensureImplements(resultsObject, ResultSet); + this.resultsObject = resultsObject; +}; + +ResultFormatter.prototype.renderResults = function() { + ... +}; diff --git a/Chapter03/3.01 - Book example class.js b/Chapter03/3.01 - Book example class.js new file mode 100644 index 0000000..44d8c1e --- /dev/null +++ b/Chapter03/3.01 - Book example class.js @@ -0,0 +1,3 @@ +// Book(isbn, title, author) +var theHobbit = new Book('0-395-07122-4', 'The Hobbit', 'J. R. R. Tolkein'); +theHobbit.display(); // Outputs the data by creating and populating an HTML element. diff --git a/Chapter03/3.02 - Fully exposed object.js b/Chapter03/3.02 - Fully exposed object.js new file mode 100644 index 0000000..229e24f --- /dev/null +++ b/Chapter03/3.02 - Fully exposed object.js @@ -0,0 +1,114 @@ +var Book = function(isbn, title, author) { + if(isbn == undefined) throw new Error('Book constructor requires an isbn.'); + this.isbn = isbn; + this.title = title || 'No title specified'; + this.author = author || 'No author specified'; +} + +Book.prototype.display = function() { + ... +}; + + +/* With ISBN check. */ + +var Book = function(isbn, title, author) { + if(!this.checkIsbn(isbn)) throw new Error('Book: Invalid ISBN.'); + this.isbn = isbn; + this.title = title || 'No title specified'; + this.author = author || 'No author specified'; +} + +Book.prototype = { + checkIsbn: function(isbn) { + if(isbn == undefined || typeof isbn != 'string') { + return false; + } + + isbn = isbn.replace(/-/. ''); // Remove dashes. + if(isbn.length != 10 && isbn.length != 13) { + return false; + } + + var sum = 0; + if(isbn.length === 10) { // 10 digit ISBN. + If(!isbn.match(\^\d{9}\)) { // Ensure characters 1 through 9 are digits. + return false; + } + + for(var i = 0; i < 9; i++) { + sum += isbn.charAt(i) * (10 - i); + } + var checksum = sum % 11; + if(checksum === 10) checksum = 'X'; + if(isbn.charAt(9) != checksum) { + return false; + } + } + else { // 13 digit ISBN. + if(!isbn.match(\^\d{12}\)) { // Ensure characters 1 through 12 are digits. + return false; + } + + for(var i = 0; i < 12; i++) { + sum += isbn.charAt(i) * ((i % 2 === 0) ? 1 : 3); + } + var checksum = sum % 10; + if(isbn.charAt(12) != checksum) { + return false; + } + } + + return true; // All tests passed. + }, + + display: function() { + ... + } +}; + + +/* Publication interface. */ + +var Publication = new Interface('Publication', ['getIsbn', 'setIsbn', 'getTitle', + 'setTitle', 'getAuthor', 'setAuthor', 'display']); + + +/* With mutators and accessors. */ + +var Book = function(isbn, title, author) { // implements Publication + this.setIsbn(isbn); + this.setTitle(title); + this.setAuthor(author); +} + +Book.prototype = { + checkIsbn: function(isbn) { + ... + }, + getIsbn: function() { + return this.isbn; + }, + setIsbn: function(isbn) { + if(!this.checkIsbn(isbn)) throw new Error('Book: Invalid ISBN.'); + this.isbn = isbn; + }, + + getTitle: function() { + return this.title; + }, + setTitle: function(title) { + this.title = title || 'No title specified'; + }, + + getAuthor: function() { + return this.author; + }, + setAuthor: function(author) { + this.author = author || 'No author specified'; + }, + + display: function() { + ... + } +}; diff --git a/Chapter03/3.03 - Private methods with underscores.js b/Chapter03/3.03 - Private methods with underscores.js new file mode 100644 index 0000000..db4403e --- /dev/null +++ b/Chapter03/3.03 - Private methods with underscores.js @@ -0,0 +1,36 @@ +var Book = function(isbn, title, author) { // implements Publication + this.setIsbn(isbn); + this.setTitle(title); + this.setAuthor(author); +} + +Book.prototype = { + _checkIsbn: function(isbn) { + ... + }, + getIsbn: function() { + return this._isbn; + }, + setIsbn: function(isbn) { + if(!this._checkIsbn(isbn)) throw new Error('Book: Invalid ISBN.'); + this._isbn = isbn; + }, + + getTitle: function() { + return this._title; + }, + setTitle: function(title) { + this._title = title || 'No title specified'; + }, + + getAuthor: function() { + return this._author; + }, + setAuthor: function(author) { + this._author = author || 'No author specified'; + }, + + display: function() { + ... + } +}; diff --git a/Chapter03/3.04 - Scope, nested functions, and closures.js b/Chapter03/3.04 - Scope, nested functions, and closures.js new file mode 100644 index 0000000..5f9a3a5 --- /dev/null +++ b/Chapter03/3.04 - Scope, nested functions, and closures.js @@ -0,0 +1,32 @@ +function foo() { + var a = 10; + + function bar() { + a *= 2; + } + + bar(); + return a; +} + + + + +function foo() { + var a = 10; + + function bar() { + a *= 2; + return a; + } + + return bar; +} + +var baz = foo(); // baz is now a reference to function bar. +baz(); // returns 20. +baz(); // returns 40. +baz(); // returns 80. + +var blat = foo(); // blat is another reference to bar. +blat(); // returns 20, because a new copy of a is being used. diff --git a/Chapter03/3.05 - Private methods with closures.js b/Chapter03/3.05 - Private methods with closures.js new file mode 100644 index 0000000..b7586c5 --- /dev/null +++ b/Chapter03/3.05 - Private methods with closures.js @@ -0,0 +1,45 @@ +var Book = function(newIsbn, newTitle, newAuthor) { // implements Publication + + // Private attributes. + var isbn, title, author; + + // Private method. + function checkIsbn(isbn) { + ... + } + + // Privileged methods. + this.getIsbn = function() { + return isbn; + }; + this.setIsbn = function(newIsbn) { + if(!checkIsbn(newIsbn)) throw new Error('Book: Invalid ISBN.'); + isbn = newIsbn; + }; + + this.getTitle = function() { + return title; + }; + this.setTitle = function(newTitle) { + title = newTitle || 'No title specified'; + }; + + this.getAuthor = function() { + return author; + }; + this.setAuthor = function(newAuthor) { + author = newAuthor || 'No author specified'; + }; + + // Constructor code. + this.setIsbn(newIsbn); + this.setTitle(newTitle); + this.setAuthor(newAuthor); +}; + +// Public, non-privileged methods. +Book.prototype = { + display: function() { + ... + } +}; diff --git a/Chapter03/3.06 - Static members.js b/Chapter03/3.06 - Static members.js new file mode 100644 index 0000000..3487329 --- /dev/null +++ b/Chapter03/3.06 - Static members.js @@ -0,0 +1,62 @@ +var Book = (function() { + + // Private static attributes. + var numOfBooks = 0; + + // Private static method. + function checkIsbn(isbn) { + ... + } + + // Return the constructor. + return function(newIsbn, newTitle, newAuthor) { // implements Publication + + // Private attributes. + var isbn, title, author; + + // Privileged methods. + this.getIsbn = function() { + return isbn; + }; + this.setIsbn = function(newIsbn) { + if(!checkIsbn(newIsbn)) throw new Error('Book: Invalid ISBN.'); + isbn = newIsbn; + }; + + this.getTitle = function() { + return title; + }; + this.setTitle = function(newTitle) { + title = newTitle || 'No title specified'; + }; + + this.getAuthor = function() { + return author; + }; + this.setAuthor = function(newAuthor) { + author = newAuthor || 'No author specified'; + }; + + // Constructor code. + numOfBooks++; // Keep track of how many Books have been instantiated + // with the private static attribute. + if(numOfBooks > 50) throw new Error('Book: Only 50 instances of Book can be ' + + 'created.'); + + this.setIsbn(newIsbn); + this.setTitle(newTitle); + this.setAuthor(newAuthor); + } +})(); + +// Public static method. +Book.convertToTitleCase = function(inputString) { + ... +}; + +// Public, non-privileged methods. +Book.prototype = { + display: function() { + ... + } +}; diff --git a/Chapter03/3.07 - Constants.js b/Chapter03/3.07 - Constants.js new file mode 100644 index 0000000..53a1732 --- /dev/null +++ b/Chapter03/3.07 - Constants.js @@ -0,0 +1,46 @@ +var Class = (function() { + + // Constants (created as private static attributes). + var UPPER_BOUND = 100; + + // Privileged static method. + this.getUPPER_BOUND() { + return UPPER_BOUND; + } + + ... + + // Return the constructor. + return function(constructorArgument) { + ... + } +})(); + + +/* Grouping constants together. */ + +var Class = (function() { + + // Private static attributes. + var constants = { + UPPER_BOUND: 100, + LOWER_BOUND: -100 + } + + // Privileged static method. + this.getConstant(name) { + return constants[name]; + } + + ... + + // Return the constructor. + return function(constructorArgument) { + ... + } +})(); + + +/* Usage. */ + +Class.getConstant('UPPER_BOUND'); diff --git a/Chapter04/4.01 - Classical inheritance.js b/Chapter04/4.01 - Classical inheritance.js new file mode 100644 index 0000000..2a52c92 --- /dev/null +++ b/Chapter04/4.01 - Classical inheritance.js @@ -0,0 +1,12 @@ +/* Class Person. */ + +function Person(name) { + this.name = name; +} + +Person.prototype.getName = function() { + return this.name; +} + +var reader = new Person('John Smith'); +reader.getName(); diff --git a/Chapter04/4.02 - The prototype chain.js b/Chapter04/4.02 - The prototype chain.js new file mode 100644 index 0000000..f061fd4 --- /dev/null +++ b/Chapter04/4.02 - The prototype chain.js @@ -0,0 +1,19 @@ +/* Class Author. */ + +function Author(name, books) { + Person.call(this, name); // Call the superclass' constructor in the scope of this. + this.books = books; // Add an attribute to Author. +} + +Author.prototype = new Person(); // Set up the prototype chain. +Author.prototype.constructor = Author; // Set the constructor attribute to Author. +Author.prototype.getBooks = function() { // Add a method to Author. + return this.books; +}; + +var author = []; +author[0] = new Author('Dustin Diaz', ['JavaScript Design Patterns']); +author[1] = new Author('Ross Harmes', ['JavaScript Design Patterns']); + +author[1].getName(); +author[1].getBooks(); diff --git a/Chapter04/4.03 - The extend function.js b/Chapter04/4.03 - The extend function.js new file mode 100644 index 0000000..417c946 --- /dev/null +++ b/Chapter04/4.03 - The extend function.js @@ -0,0 +1,65 @@ +/* Extend function. */ + +function extend(subClass, superClass) { + var F = function() {}; + F.prototype = superClass.prototype; + subClass.prototype = new F(); + subClass.prototype.constructor = subClass; +} + + +/* Class Person. */ + +function Person(name) { + this.name = name; +} + +Person.prototype.getName = function() { + return this.name; +} + +/* Class Author. */ + +function Author(name, books) { + Person.call(this, name); + this.books = books; +} +extend(Author, Person); + +Author.prototype.getBooks = function() { + return this.books; +}; + + + +/* Extend function, improved. */ + +function extend(subClass, superClass) { + var F = function() {}; + F.prototype = superClass.prototype; + subClass.prototype = new F(); + subClass.prototype.constructor = subClass; + + subClass.superclass = superClass.prototype; + if(superClass.prototype.constructor == Object.prototype.constructor) { + superClass.prototype.constructor = superClass; + } +} + + +/* Class Author. */ + +function Author(name, books) { + Author.superclass.constructor.call(this, name); + this.books = books; +} +extend(Author, Person); + +Author.prototype.getBooks = function() { + return this.books; +}; + +Author.prototype.getName = function() { + var name = Author.superclass.getName.call(this); + return name + ', Author of ' + this.getBooks().join(', '); +}; diff --git a/Chapter04/4.04 - Prototypal inheritance.js b/Chapter04/4.04 - Prototypal inheritance.js new file mode 100644 index 0000000..0eb4e4f --- /dev/null +++ b/Chapter04/4.04 - Prototypal inheritance.js @@ -0,0 +1,34 @@ +/* Person Prototype Object. */ + +var Person = { + name: 'default name', + getName: function() { + return this.name; + } +}; + +var reader = clone(Person); +alert(reader.getName()); // This will output 'default name'. +reader.name = 'John Smith'; +alert(reader.getName()); // This will now output 'John Smith'. + +/* Author Prototype Object. */ + +var Author = clone(Person); +Author.books = []; // Default value. +Author.getBooks = function() { + return this.books; +} + +var author = []; + +author[0] = clone(Author); +author[0].name = 'Dustin Diaz'; +author[0].books = ['JavaScript Design Patterns']; + +author[1] = clone(Author); +author[1].name = 'Ross Harmes'; +author[1].books = ['JavaScript Design Patterns']; + +author[1].getName(); +author[1].getBooks(); diff --git a/Chapter04/4.05 - Asymmetrical reading and writing.js b/Chapter04/4.05 - Asymmetrical reading and writing.js new file mode 100644 index 0000000..11a95dd --- /dev/null +++ b/Chapter04/4.05 - Asymmetrical reading and writing.js @@ -0,0 +1,54 @@ +var authorClone = clone(Author); +alert(authorClone.name); // Linked to the primative Person.name, which is the + // string 'default name'. +authorClone.name = 'new name'; // A new primative is created and added to the + // authorClone object itself. +alert(authorClone.name); // Now linked to the primative authorClone.name, which + // is the string 'new name'. + +authorClone.books.push('new book'); // authorClone.books is linked to the array + // Author.books. We just modified the + // prototype object's default value, and all + // other objects that link to it will now + // have a new default value there. +authorClone.books = []; // A new array is created and added to the authorClone + // object itself. +authorClone.books.push('new book'); // We are now modifying that new array. + +var CompoundObject = { + string1: 'default value', + childObject: { + bool: true, + num: 10 + } +} + +var compoundObjectClone = clone(CompoundObject); + +// Bad! Changes the value of CompoundObject.childObject.num. +compoundObjectClone.childObject.num = 5; + +// Better. Creates a new object, but compoundObject must know the structure +// of that object, and the defaults. This makes CompoundObject and +// compoundObjectClone tightly coupled. +compoundObjectClone.childObject = { + bool: true, + num: 5 +}; + +// Best approach. Uses a method to create a new object, with the same structure and +// defaults as the original. + +var CompoundObject = {}; +CompoundObject.string1 = 'default value', +CompoundObject.createChildObject = function() { + return { + bool: true, + num: 10 + } +}; +CompoundObject.childObject = CompoundObject.createChildObject(); + +var compoundObjectClone = clone(CompoundObject); +compoundObjectClone.childObject = CompoundObject.createChildObject(); +compoundObjectClone.childObject.num = 5; diff --git a/Chapter04/4.06 - The clone function.js b/Chapter04/4.06 - The clone function.js new file mode 100644 index 0000000..867c65c --- /dev/null +++ b/Chapter04/4.06 - The clone function.js @@ -0,0 +1,7 @@ +/* Clone function. */ + +function clone(object) { + function F() {} + F.prototype = object; + return new F; +} diff --git a/Chapter04/4.07 - Mixin classes.js b/Chapter04/4.07 - Mixin classes.js new file mode 100644 index 0000000..35814ac --- /dev/null +++ b/Chapter04/4.07 - Mixin classes.js @@ -0,0 +1,17 @@ +/* Mixin class. */ + +var Mixin = function() {}; +Mixin.prototype = { + serialize: function() { + var output = []; + for(key in this) { + output.push(key + ': ' + this[key]); + } + return output.join(', '); + } +}; + +augment(Author, Mixin); + +var author = new Author('Ross Harmes', ['JavaScript Design Patterns']); +var serializedString = author.serialize(); diff --git a/Chapter04/4.08 - The augment function.js b/Chapter04/4.08 - The augment function.js new file mode 100644 index 0000000..4d5948b --- /dev/null +++ b/Chapter04/4.08 - The augment function.js @@ -0,0 +1,26 @@ +/* Augment function. */ + +function augment(receivingClass, givingClass) { + for(methodName in givingClass.prototype) { + if(!receivingClass.prototype[methodName]) { + receivingClass.prototype[methodName] = givingClass.prototype[methodName]; + } + } +} + +/* Augment function, improved. */ + +function augment(receivingClass, givingClass) { + if(arguments[2]) { // Only give certain methods. + for(var i = 2, len = arguments.length; i < len; i++) { + receivingClass.prototype[arguments[i]] = givingClass.prototype[arguments[i]]; + } + } + else { // Give all methods. + for(methodName in givingClass.prototype) { + if(!receivingClass.prototype[methodName]) { + receivingClass.prototype[methodName] = givingClass.prototype[methodName]; + } + } + } +} diff --git a/Chapter04/4.09 - Edit-in-place example, classical.js b/Chapter04/4.09 - Edit-in-place example, classical.js new file mode 100644 index 0000000..c0c47b1 --- /dev/null +++ b/Chapter04/4.09 - Edit-in-place example, classical.js @@ -0,0 +1,134 @@ +/* EditInPlaceField class. */ + +function EditInPlaceField(id, parent, value) { + this.id = id; + this.value = value || 'default value'; + this.parentElement = parent; + + this.createElements(this.id); + this.attachEvents(); +}; + +EditInPlaceField.prototype = { + createElements: function(id) { + this.containerElement = document.createElement('div'); + this.parentElement.appendChild(this.containerElement); + + this.staticElement = document.createElement('span'); + this.containerElement.appendChild(this.staticElement); + this.staticElement.innerHTML = this.value; + + this.fieldElement = document.createElement('input'); + this.fieldElement.type = 'text'; + this.fieldElement.value = this.value; + this.containerElement.appendChild(this.fieldElement); + + this.saveButton = document.createElement('input'); + this.saveButton.type = 'button'; + this.saveButton.value = 'Save'; + this.containerElement.appendChild(this.saveButton); + + this.cancelButton = document.createElement('input'); + this.cancelButton.type = 'button'; + this.cancelButton.value = 'Cancel'; + this.containerElement.appendChild(this.cancelButton); + + this.convertToText(); + }, + attachEvents: function() { + var that = this; + addEvent(this.staticElement, 'click', function() { that.convertToEditable(); }); + addEvent(this.saveButton, 'click', function() { that.save(); }); + addEvent(this.cancelButton, 'click', function() { that.cancel(); }); + }, + + convertToEditable: function() { + this.staticElement.style.display = 'none'; + this.fieldElement.style.display = 'inline'; + this.saveButton.style.display = 'inline'; + this.cancelButton.style.display = 'inline'; + + this.setValue(this.value); + }, + save: function() { + this.value = this.getValue(); + var that = this; + var callback = { + success: function() { that.convertToText(); }, + failure: function() { alert('Error saving value.'); } + }; + ajaxRequest('GET', 'save.php?id=' + this.id + '&value=' + this.value, callback); + }, + cancel: function() { + this.convertToText(); + }, + convertToText: function() { + this.fieldElement.style.display = 'none'; + this.saveButton.style.display = 'none'; + this.cancelButton.style.display = 'none'; + this.staticElement.style.display = 'inline'; + + this.setValue(this.value); + }, + + setValue: function(value) { + this.fieldElement.value = value; + this.staticElement.innerHTML = value; + }, + getValue: function() { + return this.fieldElement.value; + } +}; +To create a field, instantiate the class: +var titleClassical = new EditInPlaceField('titleClassical', $('doc'), 'Title Here'); +var currentTitleText = titleClassical.getValue(); + +/* EditInPlaceArea class. */ + +function EditInPlaceArea(id, parent, value) { + EditInPlaceArea.superclass.constructor.call(this, id, parent, value); +}; +extend(EditInPlaceArea, EditInPlaceField); + +// Override certain methods. + +EditInPlaceArea.prototype.createElements = function(id) { + this.containerElement = document.createElement('div'); + this.parentElement.appendChild(this.containerElement); + + this.staticElement = document.createElement('p'); + this.containerElement.appendChild(this.staticElement); + this.staticElement.innerHTML = this.value; + + this.fieldElement = document.createElement('textarea'); + this.fieldElement.value = this.value; + this.containerElement.appendChild(this.fieldElement); + + this.saveButton = document.createElement('input'); + this.saveButton.type = 'button'; + this.saveButton.value = 'Save'; + this.containerElement.appendChild(this.saveButton); + + this.cancelButton = document.createElement('input'); + this.cancelButton.type = 'button'; + this.cancelButton.value = 'Cancel'; + this.containerElement.appendChild(this.cancelButton); + + this.convertToText(); +}; +EditInPlaceArea.prototype.convertToEditable = function() { + this.staticElement.style.display = 'none'; + this.fieldElement.style.display = 'block'; + this.saveButton.style.display = 'inline'; + this.cancelButton.style.display = 'inline'; + + this.setValue(this.value); +}; +EditInPlaceArea.prototype.convertToText = function() { + this.fieldElement.style.display = 'none'; + this.saveButton.style.display = 'none'; + this.cancelButton.style.display = 'none'; + this.staticElement.style.display = 'block'; + + this.setValue(this.value); +}; diff --git a/Chapter04/4.10 - Edit-in-place example, prototypal.js b/Chapter04/4.10 - Edit-in-place example, prototypal.js new file mode 100644 index 0000000..f4bd2c8 --- /dev/null +++ b/Chapter04/4.10 - Edit-in-place example, prototypal.js @@ -0,0 +1,131 @@ +/* EditInPlaceField object. */ + +var EditInPlaceField = { + configure: function(id, parent, value) { + this.id = id; + this.value = value || 'default value'; + this.parentElement = parent; + + this.createElements(this.id); + this.attachEvents(); + }, + createElements: function(id) { + this.containerElement = document.createElement('div'); + this.parentElement.appendChild(this.containerElement); + + this.staticElement = document.createElement('span'); + this.containerElement.appendChild(this.staticElement); + this.staticElement.innerHTML = this.value; + + this.fieldElement = document.createElement('input'); + this.fieldElement.type = 'text'; + this.fieldElement.value = this.value; + this.containerElement.appendChild(this.fieldElement); + + this.saveButton = document.createElement('input'); + this.saveButton.type = 'button'; + this.saveButton.value = 'Save'; + this.containerElement.appendChild(this.saveButton); + + this.cancelButton = document.createElement('input'); + this.cancelButton.type = 'button'; + this.cancelButton.value = 'Cancel'; + this.containerElement.appendChild(this.cancelButton); + + this.convertToText(); + }, + attachEvents: function() { + var that = this; + addEvent(this.staticElement, 'click', function() { that.convertToEditable(); }); + addEvent(this.saveButton, 'click', function() { that.save(); }); + addEvent(this.cancelButton, 'click', function() { that.cancel(); }); + }, + + convertToEditable: function() { + this.staticElement.style.display = 'none'; + this.fieldElement.style.display = 'inline'; + this.saveButton.style.display = 'inline'; + this.cancelButton.style.display = 'inline'; + + this.setValue(this.value); + }, + save: function() { + this.value = this.getValue(); + var that = this; + var callback = { + success: function() { that.convertToText(); }, + failure: function() { alert('Error saving value.'); } + }; + ajaxRequest('GET', 'save.php?id=' + this.id + '&value=' + this.value, callback); + }, + cancel: function() { + this.convertToText(); + }, + convertToText: function() { + this.fieldElement.style.display = 'none'; + this.saveButton.style.display = 'none'; + this.cancelButton.style.display = 'none'; + this.staticElement.style.display = 'inline'; + + this.setValue(this.value); + }, + + setValue: function(value) { + this.fieldElement.value = value; + this.staticElement.innerHTML = value; + }, + getValue: function() { + return this.fieldElement.value; + } +}; + +var titlePrototypal = clone(EditInPlaceField); +titlePrototypal.configure(' titlePrototypal ', $('doc'), 'Title Here'); +var currentTitleText = titlePrototypal.getValue(); + +/* EditInPlaceArea object. */ + +var EditInPlaceArea = clone(EditInPlaceField); + +// Override certain methods. + +EditInPlaceArea.createElements = function(id) { + this.containerElement = document.createElement('div'); + this.parentElement.appendChild(this.containerElement); + + this.staticElement = document.createElement('p'); + this.containerElement.appendChild(this.staticElement); + this.staticElement.innerHTML = this.value; + + this.fieldElement = document.createElement('textarea'); + this.fieldElement.value = this.value; + this.containerElement.appendChild(this.fieldElement); + + this.saveButton = document.createElement('input'); + this.saveButton.type = 'button'; + this.saveButton.value = 'Save'; + this.containerElement.appendChild(this.saveButton); + + this.cancelButton = document.createElement('input'); + this.cancelButton.type = 'button'; + this.cancelButton.value = 'Cancel'; + this.containerElement.appendChild(this.cancelButton); + + this.convertToText(); +}; +EditInPlaceArea.convertToEditable = function() { + this.staticElement.style.display = 'none'; + this.fieldElement.style.display = 'block'; + this.saveButton.style.display = 'inline'; + this.cancelButton.style.display = 'inline'; + + this.setValue(this.value); +}; +EditInPlaceArea.convertToText = function() { + this.fieldElement.style.display = 'none'; + this.saveButton.style.display = 'none'; + this.cancelButton.style.display = 'none'; + this.staticElement.style.display = 'block'; + + this.setValue(this.value); +}; diff --git a/Chapter04/4.11 - Edit-in-place example, mixin.js b/Chapter04/4.11 - Edit-in-place example, mixin.js new file mode 100644 index 0000000..2b5642f --- /dev/null +++ b/Chapter04/4.11 - Edit-in-place example, mixin.js @@ -0,0 +1,141 @@ +/* Mixin class for the edit-in-place methods. */ + +var EditInPlaceMixin = function() {}; +EditInPlaceMixin.prototype = { + createElements: function(id) { + this.containerElement = document.createElement('div'); + this.parentElement.appendChild(this.containerElement); + + this.staticElement = document.createElement('span'); + this.containerElement.appendChild(this.staticElement); + this.staticElement.innerHTML = this.value; + + this.fieldElement = document.createElement('input'); + this.fieldElement.type = 'text'; + this.fieldElement.value = this.value; + this.containerElement.appendChild(this.fieldElement); + + this.saveButton = document.createElement('input'); + this.saveButton.type = 'button'; + this.saveButton.value = 'Save'; + this.containerElement.appendChild(this.saveButton); + + this.cancelButton = document.createElement('input'); + this.cancelButton.type = 'button'; + this.cancelButton.value = 'Cancel'; + this.containerElement.appendChild(this.cancelButton); + + this.convertToText(); + }, + attachEvents: function() { + var that = this; + addEvent(this.staticElement, 'click', function() { that.convertToEditable(); }); + addEvent(this.saveButton, 'click', function() { that.save(); }); + addEvent(this.cancelButton, 'click', function() { that.cancel(); }); + }, + + convertToEditable: function() { + this.staticElement.style.display = 'none'; + this.fieldElement.style.display = 'inline'; + this.saveButton.style.display = 'inline'; + this.cancelButton.style.display = 'inline'; + + this.setValue(this.value); + }, + save: function() { + this.value = this.getValue(); + var that = this; + var callback = { + success: function() { that.convertToText(); }, + failure: function() { alert('Error saving value.'); } + }; + ajaxRequest('GET', 'save.php?id=' + this.id + '&value=' + this.value, callback); + }, + cancel: function() { + this.convertToText(); + }, + convertToText: function() { + this.fieldElement.style.display = 'none'; + this.saveButton.style.display = 'none'; + this.cancelButton.style.display = 'none'; + this.staticElement.style.display = 'inline'; + + this.setValue(this.value); + }, + + setValue: function(value) { + this.fieldElement.value = value; + this.staticElement.innerHTML = value; + }, + getValue: function() { + return this.fieldElement.value; + } +}; + +/* EditInPlaceField class. */ + +function EditInPlaceField(id, parent, value) { + this.id = id; + this.value = value || 'default value'; + this.parentElement = parent; + + this.createElements(this.id); + this.attachEvents(); +}; +augment(EditInPlaceField, EditInPlaceMixin); + +/* EditInPlaceArea class. */ + +function EditInPlaceArea(id, parent, value) { + this.id = id; + this.value = value || 'default value'; + this.parentElement = parent; + + this.createElements(this.id); + this.attachEvents(); +}; + +// Add certain methods so that augment won't include them. + +EditInPlaceArea.prototype.createElements = function(id) { + this.containerElement = document.createElement('div'); + this.parentElement.appendChild(this.containerElement); + + this.staticElement = document.createElement('p'); + this.containerElement.appendChild(this.staticElement); + this.staticElement.innerHTML = this.value; + + this.fieldElement = document.createElement('textarea'); + this.fieldElement.value = this.value; + this.containerElement.appendChild(this.fieldElement); + + this.saveButton = document.createElement('input'); + this.saveButton.type = 'button'; + this.saveButton.value = 'Save'; + this.containerElement.appendChild(this.saveButton); + + this.cancelButton = document.createElement('input'); + this.cancelButton.type = 'button'; + this.cancelButton.value = 'Cancel'; + this.containerElement.appendChild(this.cancelButton); + + this.convertToText(); +}; +EditInPlaceArea.prototype.convertToEditable = function() { + this.staticElement.style.display = 'none'; + this.fieldElement.style.display = 'block'; + this.saveButton.style.display = 'inline'; + this.cancelButton.style.display = 'inline'; + + this.setValue(this.value); +}; +EditInPlaceArea.prototype.convertToText = function() { + this.fieldElement.style.display = 'none'; + this.saveButton.style.display = 'none'; + this.cancelButton.style.display = 'none'; + this.staticElement.style.display = 'block'; + + this.setValue(this.value); +}; + +augment(EditInPlaceArea, EditInPlaceMixin); diff --git a/Chapter05/5.01 - Basic structure of the singleton.js b/Chapter05/5.01 - Basic structure of the singleton.js new file mode 100644 index 0000000..99300fc --- /dev/null +++ b/Chapter05/5.01 - Basic structure of the singleton.js @@ -0,0 +1,17 @@ +/* Basic Singleton. */ + +var Singleton = { + attribute1: true, + attribute2: 10, + + method1: function() { + + }, + method2: function(arg) { + + } +}; + +Singleton.attribute1 = false; +var total = Singleton.attribute2 + 5; +var result = Singleton.method1(); diff --git a/Chapter05/5.02 - Namespacing.js b/Chapter05/5.02 - Namespacing.js new file mode 100644 index 0000000..9528873 --- /dev/null +++ b/Chapter05/5.02 - Namespacing.js @@ -0,0 +1,42 @@ +/* Declared globally. */ + +function findProduct(id) { + ... +} + +... + +// Later in your page, another programmer adds... +var resetProduct = $('reset-product-button'); +var findProduct = $('find-product-button'); // The findProduct function just got + // overwritten. + + +/* Using a namespace. */ + +var MyNamespace = { + findProduct: function(id) { + ... + }, + // Other methods can go here as well. +} +... + +// Later in your page, another programmer adds... +var resetProduct = $('reset-product-button'); +var findProduct = $('find-product-button'); // Nothing was overwritten. + +/* GiantCorp namespace. */ +var GiantCorp = {}; + +GiantCorp.Common = { + // A singleton with common methods used by all objects and modules. +}; + +GiantCorp.ErrorCodes = { + // An object literal used to store data. +}; + +GiantCorp.PageHandler = { + // A singleton with page specific methods and attributes. +}; diff --git a/Chapter05/5.03 - Wrappers for page specific code.js b/Chapter05/5.03 - Wrappers for page specific code.js new file mode 100644 index 0000000..e9141c2 --- /dev/null +++ b/Chapter05/5.03 - Wrappers for page specific code.js @@ -0,0 +1,76 @@ +/* Generic Page Object. */ + +Namespace.PageName = { + + // Page constants. + CONSTANT_1: true, + CONSTANT_2: 10, + + // Page methods. + method1: function() { + + }, + method2: function() { + + }, + + // Initialization method. + init: function() { + + } +} + +// Invoke the initialization method after the page loads. +addLoadEvent(Namespace.PageName.init); + + +var GiantCorp = window.GiantCorp || {}; + +/* RegPage singleton, page handler object. */ + +GiantCorp.RegPage = { + + // Constants. + FORM_ID: 'reg-form', + OUTPUT_ID: 'reg-results', + + // Form handling methods. + handleSubmit: function(e) { + e.preventDefault(); // Stop the normal form submission. + + var data = {}; + var inputs = GiantCorp.RegPage.formEl.getElementsByTagName('input'); + + // Collect the values of the input fields in the form. + for(var i = 0, len = inputs.length; i < len; i++) { + data[inputs[i].name] = inputs[i].value; + } + + // Send the form values back to the server. + GiantCorp.RegPage.sendRegistration(data); + }, + sendRegistration: function(data) { + // Make an XHR request and call displayResult() when the response is + // received. + ... + }, + displayResult: function(response) { + // Output the response directly into the output element. We are + // assuming the server will send back formatted HTML. + GiantCorp.RegPage.outputEl.innerHTML = response; + }, + + // Initialization method. + init: function() { + // Get the form and output elements. + GiantCorp.RegPage.formEl = $(GiantCorp.RegPage.FORM_ID); + GiantCorp.RegPage.outputEl = $(GiantCorp.RegPage.OUTPUT_ID); + + // Hijack the form submission. + addEvent(GiantCorp.RegPage.formEl, 'submit', GiantCorp.RegPage.handleSubmit); + } +}; + +// Invoke the initialization method after the page loads. +addLoadEvent(GiantCorp.RegPage.init); + diff --git a/Chapter05/5.04 - Private methods with underscores.js b/Chapter05/5.04 - Private methods with underscores.js new file mode 100644 index 0000000..ed64082 --- /dev/null +++ b/Chapter05/5.04 - Private methods with underscores.js @@ -0,0 +1,20 @@ +/* DataParser singleton, converts character delimited strings into arrays. */ + +GiantCorp.DataParser = { + // Private methods. + _stripWhitespace: function(str) { + return str.replace(/\s+/, ''); + }, + _stringSplit: function(str, delimiter) { + return str.split(delimiter); + }, + + // Public method. + stringToArray: function(str, delimiter, stripWS) { + if(stripWS) { + str = this._stripWhitespace(str); + } + var outputArray = this._stringSplit(str, delimiter); + return outputArray; + } +}; diff --git a/Chapter05/5.05 - Private methods with closures.js b/Chapter05/5.05 - Private methods with closures.js new file mode 100644 index 0000000..e62bd29 --- /dev/null +++ b/Chapter05/5.05 - Private methods with closures.js @@ -0,0 +1,52 @@ +/* Singleton as an Object Literal. */ + +MyNamespace.Singleton = {}; + +/* Singleton with Private Members, step 1. */ + +MyNamespace.Singleton = (function() { + return {}; +})(); + +/* Singleton with Private Members, step 2. */ + +MyNamespace.Singleton = (function() { + return { // Public members. + publicAttribute1: true, + publicAttribute2: 10, + + publicMethod1: function() { + ... + }, + publicMethod2: function(args) { + ... + } + }; +})(); + +/* Singleton with Private Members, step 3. */ + +MyNamespace.Singleton = (function() { + // Private members. + var privateAttribute1 = false; + var privateAttribute2 = [1, 2, 3]; + + function privateMethod1() { + ... + } + function privateMethod2(args) { + ... + } + + return { // Public members. + publicAttribute1: true, + publicAttribute2: 10, + + publicMethod1: function() { + ... + }, + publicMethod2: function(args) { + ... + } + }; +})(); diff --git a/Chapter05/5.06 - Comparing the two techniques.js b/Chapter05/5.06 - Comparing the two techniques.js new file mode 100644 index 0000000..bd9b901 --- /dev/null +++ b/Chapter05/5.06 - Comparing the two techniques.js @@ -0,0 +1,29 @@ +/* DataParser singleton, converts character delimited strings into arrays. */ +/* Now using true private methods. */ + +GiantCorp.DataParser = (function() { + // Private attributes. + var whitespaceRegex = /\s+/; + + // Private methods. + function stripWhitespace(str) { + return str.replace(whitespaceRegex, ''); + } + function stringSplit(str, delimiter) { + return str.split(delimiter); + } + + // Everything returned in the object literal is public, but can access the + // members in the closure created above. + return { + // Public method. + stringToArray: function(str, delimiter, stripWS) { + if(stripWS) { + str = stripWhitespace(str); + } + var outputArray = stringSplit(str, delimiter); + return outputArray; + } + }; +})(); // Invoke the function and assign the returned object literal to + // GiantCorp.DataParser. diff --git a/Chapter05/5.07 - Lazy instantiation.js b/Chapter05/5.07 - Lazy instantiation.js new file mode 100644 index 0000000..a20bbcf --- /dev/null +++ b/Chapter05/5.07 - Lazy instantiation.js @@ -0,0 +1,92 @@ +/* Singleton with Private Members, step 3. */ + +MyNamespace.Singleton = (function() { + // Private members. + var privateAttribute1 = false; + var privateAttribute2 = [1, 2, 3]; + + function privateMethod1() { + ... + } + function privateMethod2(args) { + ... + } + + return { // Public members. + publicAttribute1: true, + publicAttribute2: 10, + + publicMethod1: function() { + ... + }, + publicMethod2: function(args) { + ... + } + }; +})(); + +/* General skeleton for a lazy loading singleton, step 1. */ + +MyNamespace.Singleton = (function() { + + function constructor() { // All of the normal singleton code goes here. + // Private members. + var privateAttribute1 = false; + var privateAttribute2 = [1, 2, 3]; + + function privateMethod1() { + ... + } + function privateMethod2(args) { + ... + } + + return { // Public members. + publicAttribute1: true, + publicAttribute2: 10, + + publicMethod1: function() { + ... + }, + publicMethod2: function(args) { + ... + } + } + } + +})(); + +/* General skeleton for a lazy loading singleton, step 2. */ + +MyNamespace.Singleton = (function() { + + function constructor() { // All of the normal singleton code goes here. + ... + } + + return { + getInstance: function() { + // Control code goes here. + } + } +})(); + +/* General skeleton for a lazy loading singleton, step 3. */ + +MyNamespace.Singleton = (function() { + + var uniqueInstance; // Private attribute that holds the single instance. + + function constructor() { // All of the normal singleton code goes here. + ... + } + + return { + getInstance: function() { + if(!uniqueInstance) { // Instantiate only if the instance doesn't exist. + uniqueInstance = constructor(); + } + return uniqueInstance; + } + } +})(); diff --git a/Chapter05/5.08 - Branching.js b/Chapter05/5.08 - Branching.js new file mode 100644 index 0000000..e525aa1 --- /dev/null +++ b/Chapter05/5.08 - Branching.js @@ -0,0 +1,22 @@ +/* Branching Singleton (skeleton). */ + +MyNamespace.Singleton = (function() { + var objectA = { + method1: function() { + ... + }, + method2: function() { + ... + } + }; + var objectB = { + method1: function() { + ... + }, + method2: function() { + ... + } + }; + + return (someCondition) ? objectA : objectB; +})(); diff --git a/Chapter05/5.09 - Creating XHR objects with branching.js b/Chapter05/5.09 - Creating XHR objects with branching.js new file mode 100644 index 0000000..3a73153 --- /dev/null +++ b/Chapter05/5.09 - Creating XHR objects with branching.js @@ -0,0 +1,67 @@ +/* SimpleXhrFactory singleton, step 1. */ + +var SimpleXhrFactory = (function() { + + // The three branches. + var standard = { + createXhrObject: function() { + return new XMLHttpRequest(); + } + }; + var activeXNew = { + createXhrObject: function() { + return new ActiveXObject('Msxml2.XMLHTTP'); + } + }; + var activeXOld = { + createXhrObject: function() { + return new ActiveXObject('Microsoft.XMLHTTP'); + } + }; + +})(); + +/* SimpleXhrFactory singleton, step 2. */ + +var SimpleXhrFactory = (function() { + + // The three branches. + var standard = { + createXhrObject: function() { + return new XMLHttpRequest(); + } + }; + var activeXNew = { + createXhrObject: function() { + return new ActiveXObject('Msxml2.XMLHTTP'); + } + }; + var activeXOld = { + createXhrObject: function() { + return new ActiveXObject('Microsoft.XMLHTTP'); + } + }; + + // To assign the branch, try each method; return whatever doesn't fail. + var testObject; + try { + testObject = standard.createXhrObject(); + return standard; // Return this if no error was thrown. + } + catch(e) { + try { + testObject = activeXNew.createXhrObject(); + return activeXNew; // Return this if no error was thrown. + } + catch(e) { + try { + testObject = activeXOld.createXhrObject(); + return activeXOld; // Return this if no error was thrown. + } + catch(e) { + throw new Error('No XHR object found in this environment.'); + } + } + } + +})(); diff --git a/Chapter06/6.01 - Introduction to chaining.js b/Chapter06/6.01 - Introduction to chaining.js new file mode 100644 index 0000000..aa35cd0 --- /dev/null +++ b/Chapter06/6.01 - Introduction to chaining.js @@ -0,0 +1,10 @@ +// Without chaining: +addEvent($('example'), 'click', function() { + setStyle(this, 'color', 'green'); + show(this); +}); + +// With chaining: +$('example').addEvent('click', function() { + $(this).setStyle('color', 'green').show(); +}); diff --git a/Chapter06/6.02 - The structure of the chain.js b/Chapter06/6.02 - The structure of the chain.js new file mode 100644 index 0000000..5e16bc3 --- /dev/null +++ b/Chapter06/6.02 - The structure of the chain.js @@ -0,0 +1,91 @@ +function $() { + var elements = []; + for (var i = 0, len = arguments.length; i < len; ++i) { + var element = arguments[i]; + if (typeof element == 'string') { + element = document.getElementById(element); + } + if (arguments.length == 1) { + return element; + } + elements.push(element); + } + return elements; +} + + + +(function() { + // Use a private class. + function _$(els) { + this.elements = []; + for (var i = 0, len = els.length; i < len; ++i) { + var element = els[i]; + if (typeof element == 'string') { + element = document.getElementById(element); + } + this.elements.push(element); + } + } + // The public interface remains the same. + window.$ = function() { + return new _$(arguments); + }; +})(); + + + +(function() { + function _$(els) { + // ... + } + _$.prototype = { + each: function(fn) { + for ( var i = 0, len = this.elements.length; i < len; ++i ) { + fn.call(this, this.elements[i]); + } + return this; + }, + setStyle: function(prop, val) { + this.each(function(el) { + el.style[prop] = val; + }); + return this; + }, + show: function() { + var that = this; + this.each(function(el) { + that.setStyle('display', 'block'); + }); + return this; + }, + addEvent: function(type, fn) { + var add = function(el) { + if (window.addEventListener) { + el.addEventListener(type, fn, false); + } + else if (window.attachEvent) { + el.attachEvent('on'+type, fn); + } + }; + this.each(function(el) { + add(el); + }); + return this; + } + }; + window.$ = function() { + return new _$(arguments); + }; +})(); + + +/* Usage. */ + +$(window).addEvent('load', function() { + $('test-1', 'test-2').show(). + setStyle('color', 'red'). + addEvent('click', function(e) { + $(this).setStyle('color', 'green'); + }); +}); diff --git a/Chapter06/6.03 - Building a chainable JavaScript library.js b/Chapter06/6.03 - Building a chainable JavaScript library.js new file mode 100644 index 0000000..7c2e2f2 --- /dev/null +++ b/Chapter06/6.03 - Building a chainable JavaScript library.js @@ -0,0 +1,95 @@ +// Include syntactic sugar to help the development of our interface. +Function.prototype.method = function(name, fn) { + this.prototype[name] = fn; + return this; +}; +(function() { + function _$(els) { + // ... + } + /* + Events + * addEvent + * getEvent + */ + _$.method('addEvent', function(type, fn) { + // ... + }).method('getEvent', function(e) { + // ... + }). + /* + DOM + * addClass + * removeClass + * replaceClass + * hasClass + * getStyle + * setStyle + */ + method('addClass', function(className) { + // ... + }).method('removeClass', function(className) { + // ... + }).method('replaceClass', function(oldClass, newClass) { + // ... + }).method('hasClass', function(className) { + // ... + }).method('getStyle', function(prop) { + // ... + }).method('setStyle', function(prop, val) { + // ... + }). + /* + AJAX + * load. Fetches an HTML fragment from a URL and inserts it into an element. + */ + method('load', function(uri, method) { + // ... + }); + window.$ = function() { + return new _$(arguments); + }); +})(); + +Function.prototype.method = function(name, fn) { + // ... +}; +(function() { + function _$(els) { + // ... + } + _$.method('addEvent', function(type, fn) { + // ... + }) + // ... + + window.installHelper = function(scope, interface) { + scope[interface] = function() { + return new _$(arguments); + } + }; +})(); + + +/* Usage. */ + +installHelper(window, '$'); + +$('example').show(); + + +/* Another usage example. */ + +// Define a namespace without overwriting it if it already exists. +window.com = window.com || {}; +com.example = com.example || {}; +com.example.util = com.example.util || {}; + +installHelper(com.example.util, 'get'); + +(function() { + var get = com.example.util.get; + get('example').addEvent('click', function(e) { + get(this).addClass('hello'); + }); +})(); diff --git a/Chapter06/6.04 - Using callbacks.js b/Chapter06/6.04 - Using callbacks.js new file mode 100644 index 0000000..d2cd40e --- /dev/null +++ b/Chapter06/6.04 - Using callbacks.js @@ -0,0 +1,40 @@ +// Accessor without function callbacks: returning requested data in accessors. +window.API = window.API || {}; +API.prototype = function() { + var name = 'Hello world'; + // Privileged mutator method. + setName: function(newName) { + name = newName; + return this; + }, + // Privileged accessor method. + getName: function() { + return name; + } +}(); + +// Implementation code. +var o = new API; +console.log(o.getName()); // Displays 'Hello world'. +console.log(o.setName('Meow').getName()); // Displays 'Meow'. + +// Accessor with function callbacks. +window.API2 = window.API2 || {}; +API2.prototype = function() { + var name = 'Hello world'; + // Privileged mutator method. + setName: function(newName) { + name = newName; + return this; + }, + // Privileged accessor method. + getName: function(callback) { + callback.call(this, name); + return this; + } +}(); + +// Implementation code. +var o2 = new API2; +o2.getName(console.log).setName('Meow').getName(console.log); +// Displays 'Hello world' and then 'Meow'. diff --git a/Chapter07/7.01 - The simple factory.js b/Chapter07/7.01 - The simple factory.js new file mode 100644 index 0000000..57ffebc --- /dev/null +++ b/Chapter07/7.01 - The simple factory.js @@ -0,0 +1,122 @@ +/* BicycleShop class. */ + +var BicycleShop = function() {}; +BicycleShop.prototype = { + sellBicycle: function(model) { + var bicycle; + + switch(model) { + case 'The Speedster': + bicycle = new Speedster(); + break; + case 'The Lowrider': + bicycle = new Lowrider(); + break; + case 'The Comfort Cruiser': + default: + bicycle = new ComfortCruiser(); + } + Interface.ensureImplements(bicycle, Bicycle); + + bicycle.assemble(); + bicycle.wash(); + + return bicycle; + } +}; + + +/* The Bicycle interface. */ + +var Bicycle = new Interface('Bicycle', ['assemble', 'wash', 'ride', 'repair']); + +/* Speedster class. */ + +var Speedster = function() { // implements Bicycle + ... +}; +Speedster.prototype = { + assemble: function() { + ... + }, + wash: function() { + ... + }, + ride: function() { + ... + }, + repair: function() { + ... + } +}; + + +/* Usage. */ + +var californiaCruisers = new BicycleShop(); +var yourNewBike = californiaCruisers.sellBicycle('The Speedster'); + + + +/* BicycleFactory namespace. */ + +var BicycleFactory = { + createBicycle: function(model) { + var bicycle; + + switch(model) { + case 'The Speedster': + bicycle = new Speedster(); + break; + case 'The Lowrider': + bicycle = new Lowrider(); + break; + case 'The Comfort Cruiser': + default: + bicycle = new ComfortCruiser(); + } + + Interface.ensureImplements(bicycle, Bicycle); + return bicycle; + } +}; + +/* BicycleShop class, improved. */ + +var BicycleShop = function() {}; +BicycleShop.prototype = { + sellBicycle: function(model) { + var bicycle = BicycleFactory.createBicycle(model); + + bicycle.assemble(); + bicycle.wash(); + + return bicycle; + } +}; + +/* BicycleFactory namespace, with more models. */ + +var BicycleFactory = { + createBicycle: function(model) { + var bicycle; + + switch(model) { + case 'The Speedster': + bicycle = new Speedster(); + break; + case 'The Lowrider': + bicycle = new Lowrider(); + break; + case 'The Flatlander': + bicycle = new Flatlander(); + break; + case 'The Comfort Cruiser': + default: + bicycle = new ComfortCruiser(); + } + + Interface.ensureImplements(bicycle, Bicycle); + return bicycle; + } +}; diff --git a/Chapter07/7.02 - The factory pattern.js b/Chapter07/7.02 - The factory pattern.js new file mode 100644 index 0000000..38e9f82 --- /dev/null +++ b/Chapter07/7.02 - The factory pattern.js @@ -0,0 +1,77 @@ +/* BicycleShop class (abstract). */ + +var BicycleShop = function() {}; +BicycleShop.prototype = { + sellBicycle: function(model) { + var bicycle = this.createBicycle(model); + + bicycle.assemble(); + bicycle.wash(); + + return bicycle; + }, + createBicycle: function(model) { + throw new Error('Unsupported operation on an abstract class.'); + } +}; + +/* AcmeBicycleShop class. */ + +var AcmeBicycleShop = function() {}; +extend(AcmeBicycleShop, BicycleShop); +AcmeBicycleShop.prototype.createBicycle = function(model) { + var bicycle; + + switch(model) { + case 'The Speedster': + bicycle = new AcmeSpeedster(); + break; + case 'The Lowrider': + bicycle = new AcmeLowrider(); + break; + case 'The Flatlander': + bicycle = new AcmeFlatlander(); + break; + case 'The Comfort Cruiser': + default: + bicycle = new AcmeComfortCruiser(); + } + + Interface.ensureImplements(bicycle, Bicycle); + return bicycle; +}; + +/* GeneralProductsBicycleShop class. */ + +var GeneralProductsBicycleShop = function() {}; +extend(GeneralProductsBicycleShop, BicycleShop); +GeneralProductsBicycleShop.prototype.createBicycle = function(model) { + var bicycle; + + switch(model) { + case 'The Speedster': + bicycle = new GeneralProductsSpeedster(); + break; + case 'The Lowrider': + bicycle = new GeneralProductsLowrider(); + break; + case 'The Flatlander': + bicycle = new GeneralProductsFlatlander(); + break; + case 'The Comfort Cruiser': + default: + bicycle = new GeneralProductsComfortCruiser(); + } + + Interface.ensureImplements(bicycle, Bicycle); + return bicycle; +}; + + +/* Usage. */ + +var alecsCruisers = new AcmeBicycleShop(); +var yourNewBike = alecsCruisers.sellBicycle('The Lowrider'); + +var bobsCruisers = new GeneralProductsBicycleShop(); +var yourSecondNewBike = bobsCruisers.sellBicycle('The Lowrider'); diff --git a/Chapter07/7.03 - XHR factory example.js b/Chapter07/7.03 - XHR factory example.js new file mode 100644 index 0000000..a1b1a7b --- /dev/null +++ b/Chapter07/7.03 - XHR factory example.js @@ -0,0 +1,52 @@ +/* AjaxHandler interface. */ + +var AjaxHandler = new Interface('AjaxHandler', ['request', 'createXhrObject']); + +/* SimpleHandler class. */ + +var SimpleHandler = function() {}; // implements AjaxHandler +SimpleHandler.prototype = { + request: function(method, url, callback, postVars) { + var xhr = this.createXhrObject(); + xhr.onreadystatechange = function() { + if(xhr.readyState !== 4) return; + (xhr.status === 200) ? + callback.success(xhr.responseText, xhr.responseXML) : + callback.failure(xhr.status); + }; + xhr.open(method, url, true); + if(method !== 'POST') postVars = null; + xhr.send(postVars); + }, + createXhrObject: function() { // Factory method. + var methods = [ + function() { return new XMLHttpRequest(); }, + function() { return new ActiveXObject('Msxml2.XMLHTTP'); }, + function() { return new ActiveXObject('Microsoft.XMLHTTP'); } + ]; + + for(var i = 0, len = methods.length; i < len; i++) { + try { + methods[i](); + } + catch(e) { + continue; + } + // If we reach this point, method[i] worked. + this.createXhrObject = methods[i]; // Memoize the method. + return methods[i]; + } + + // If we reach this point, none of the methods worked. + throw new Error('SimpleHandler: Could not create an XHR object.'); + } +}; + +/* Usage. */ + +var myHandler = new SimpleHandler(); +var callback = { + success: function(responseText) { alert('Success: ' + responseText); }, + failure: function(statusCode) { alert('Failure: ' + statusCode); } +}; +myHandler.request('GET', 'script.php', callback); diff --git a/Chapter07/7.04 - Specialized connection objects.js b/Chapter07/7.04 - Specialized connection objects.js new file mode 100644 index 0000000..2f69700 --- /dev/null +++ b/Chapter07/7.04 - Specialized connection objects.js @@ -0,0 +1,76 @@ +/* QueuedHandler class. */ + +var QueuedHandler = function() { // implements AjaxHandler + this.queue = []; + this.requestInProgress = false; + this.retryDelay = 5; // In seconds. +}; +extend(QueuedHandler, SimpleHandler); +QueuedHandler.prototype.request = function(method, url, callback, postVars, + override) { + if(this.requestInProgress && !override) { + this.queue.push({ + method: method, + url: url, + callback: callback, + postVars: postVars + }); + } + else { + this.requestInProgress = true; + var xhr = this.createXhrObject(); + var that = this; + xhr.onreadystatechange = function() { + if(xhr.readyState !== 4) return; + if(xhr.status === 200) { + callback.success(xhr.responseText, xhr.responseXML); + that.advanceQueue(); + } + else { + callback.failure(xhr.status); + setTimeout(function() { that.request(method, url, callback, postVars); }, + that.retryDelay * 1000); + } + }; + xhr.open(method, url, true); + if(method !== 'POST') postVars = null; + xhr.send(postVars); + } +}; +QueuedHandler.prototype.advanceQueue = function() { + if(this.queue.length === 0) { + this.requestInProgress = false; + return; + } + var req = this.queue.shift(); + this.request(req.method, req.url, req.callback, req.postVars, true); +}; + + +/* OfflineHandler class. */ + +var OfflineHandler = function() { // implements AjaxHandler + this.storedRequests = []; +}; +extend(OfflineHandler, SimpleHandler); +OfflineHandler.prototype.request = function(method, url, callback, postVars) { + if(XhrManager.isOffline()) { // Store the requests until we are online. + this.storedRequests.push({ + method: method, + url: url, + callback: callback, + postVars: postVars + }); + } + else { // Call SimpleHandler's request method if we are online. + this.flushStoredRequests(); + OfflineHandler.superclass.request(method, url, callback, postVars); + } +}; +OfflineHandler.prototype.flushStoredRequests = function() { + for(var i = 0, len = storedRequests.length; i < len; i++) { + var req = storedRequests[i]; + OfflineHandler.superclass.request(req.method, req.url, req.callback, + req.postVars); + } +}; diff --git a/Chapter07/7.05 - Choosing connection objects at run-time.js b/Chapter07/7.05 - Choosing connection objects at run-time.js new file mode 100644 index 0000000..031d275 --- /dev/null +++ b/Chapter07/7.05 - Choosing connection objects at run-time.js @@ -0,0 +1,36 @@ +/* XhrManager singleton. */ + +var XhrManager = { + createXhrHandler: function() { + var xhr; + if(this.isOffline()) { + xhr = new OfflineHandler(); + } + else if(this.isHighLatency()) { + xhr = new QueuedHandler(); + } + else { + xhr = new SimpleHandler() + } + + Interface.ensureImplements(xhr, AjaxHandler); + return xhr + }, + isOffline: function() { // Do a quick request with SimpleHandler and see if + ... // it succeeds. + }, + isHighLatency: function() { // Do a series of requests with SimpleHandler and + ... // time the responses. Best done once, as a + // branching function. + } +}; + + +/* Usage. */ + +var myHandler = XhrManager.createXhrHandler(); +var callback = { + success: function(responseText) { alert('Success: ' + responseText); }, + failure: function(statusCode) { alert('Failure: ' + statusCode); } +}; +myHandler.request('GET', 'script.php', callback); diff --git a/Chapter07/7.06 - RSS reader example.js b/Chapter07/7.06 - RSS reader example.js new file mode 100644 index 0000000..2cd5408 --- /dev/null +++ b/Chapter07/7.06 - RSS reader example.js @@ -0,0 +1,92 @@ +/* DisplayModule interface. */ + +var DisplayModule = new Interface('DisplayModule', ['append', 'remove', 'clear']); + +/* ListDisplay class. */ + +var ListDisplay = function(id, parent) { // implements DisplayModule + this.list = document.createElement('ul'); + this.list.id = id; + parent.appendChild(this.list); +}; +ListDisplay.prototype = { + append: function(text) { + var newEl = document.createElement('li'); + this.list.appendChild(newEl); + newEl.innerHTML = text; + return newEl; + }, + remove: function(el) { + this.list.removeChild(el); + }, + clear: function() { + this.list.innerHTML = ''; + } +}; + +/* Configuration object. */ + +var conf = { + id: 'cnn-top-stories', + feedUrl: 'http://rss.cnn.com/rss/cnn_topstories.rss', + updateInterval: 60, // In seconds. + parent: $('feed-readers') +}; + +/* FeedReader class. */ + +var FeedReader = function(display, xhrHandler, conf) { + this.display = display; + this.xhrHandler = xhrHandler; + this.conf = conf; + + this.startUpdates(); +}; +FeedReader.prototype = { + fetchFeed: function() { + var that = this; + var callback = { + success: function(text, xml) { that.parseFeed(text, xml); }, + failure: function(status) { that.showError(status); } + }; + this.xhrHandler.request('GET', 'feedProxy.php?feed=' + this.conf.feedUrl, + callback); + }, + parseFeed: function(responseText, responseXML) { + this.display.clear(); + var items = responseXML.getElementsByTagName('item'); + for(var i = 0, len = items.length; i < len; i++) { + var title = items[i].getElementsByTagName('title')[0]; + var link = items[i].getElementsByTagName('link')[0]; + this.display.append('' + + title.firstChild.data + ''); + } + }, + showError: function(statusCode) { + this.display.clear(); + this.display.append('Error fetching feed.'); + }, + stopUpdates: function() { + clearInterval(this.interval); + }, + startUpdates: function() { + this.fetchFeed(); + var that = this; + this.interval = setInterval(function() { that.fetchFeed(); }, + this.conf.updateInterval * 1000); + } +}; + +/* FeedManager namespace. */ + +var FeedManager = { + createFeedReader: function(conf) { + var displayModule = new ListDisplay(conf.id + '-display', conf.parent); + Interface.ensureImplements(displayModule, DisplayModule); + + var xhrHandler = XhrManager.createXhrHandler(); + Interface.ensureImplements(xhrHandler, AjaxHandler); + + return new FeedReader(displayModule, xhrHandler, conf); + } +}; diff --git a/Chapter08/8.01 - Event listener example.js b/Chapter08/8.01 - Event listener example.js new file mode 100644 index 0000000..1345986 --- /dev/null +++ b/Chapter08/8.01 - Event listener example.js @@ -0,0 +1,23 @@ +addEvent(element, 'click', getBeerById); +function getBeerById(e) { + var id = this.id; + asyncRequest('GET', 'beer.uri?id=' + id, function(resp) { + // Callback response. + console.log('Requested Beer: ' + resp.responseText); + }); +} + +function getBeerById(id, callback) { + // Make request for beer by ID, then return the beer data. + asyncRequest('GET', 'beer.uri?id=' + id, function(resp) { + // callback response + callback(resp.responseText); + }); +} + +addEvent(element, 'click', getBeerByIdBridge); +function getBeerByIdBridge (e) { + getBeerById(this.id, function(beer) { + console.log('Requested Beer: '+beer); + }); +} diff --git a/Chapter08/8.02 - Other examples of bridges.js b/Chapter08/8.02 - Other examples of bridges.js new file mode 100644 index 0000000..7cfafe0 --- /dev/null +++ b/Chapter08/8.02 - Other examples of bridges.js @@ -0,0 +1,9 @@ +var Public = function() { + var secret = 3; + this.privilegedGetter = function() { + return secret; + }; +}; + +var o = new Public; +var data = o.privilegedGetter(); diff --git a/Chapter08/8.03 - Bridging multiple classes together.js b/Chapter08/8.03 - Bridging multiple classes together.js new file mode 100644 index 0000000..1cb288c --- /dev/null +++ b/Chapter08/8.03 - Bridging multiple classes together.js @@ -0,0 +1,13 @@ +var Class1 = function(a, b, c) { + this.a = a; + this.b = b; + this.c = c; +} +var Class2 = function(d) { + this.d = d; +}; + +var BridgeClass = function(a, b, c, d) { + this.one = new Class1(a, b, c); + this.two = new Class2(d); +}; diff --git a/Chapter08/8.04 - Building an XHR connection queue.js b/Chapter08/8.04 - Building an XHR connection queue.js new file mode 100644 index 0000000..4436cc0 --- /dev/null +++ b/Chapter08/8.04 - Building an XHR connection queue.js @@ -0,0 +1,225 @@ +var asyncRequest = (function() { + function handleReadyState(o, callback) { + var poll = window.setInterval( + function() { + if (o && o.readyState == 4) { + window.clearInterval(poll); + if (callback) { + callback(o); + } + } + }, + 50 + ); + } + var getXHR = function() { + var http; + try { + http = new XMLHttpRequest; + getXHR = function() { + return new XMLHttpRequest; + }; + } + catch(e) { + var msxml = [ + 'MSXML2.XMLHTTP.3.0', + 'MSXML2.XMLHTTP', + 'Microsoft.XMLHTTP' + ]; + for (var I = 0, len = msxml.length; i < len; ++i) { + try { + http = new ActiveXObject(msxml[i]); + getXHR = function() { + return new ActiveXObject(msxml[i]); + }; + break; + } + catch(e) {} + } + } + return http; + }; + return function(method, uri, callback, postData) { + var http = getXHR(); + http.open(method, uri, true); + handleReadyState(http, callback); + http.send(postData || null); + return http; + }; +})(); + + +Function.prototype.method = function(name, fn) { + this.prototype[name] = fn; + return this; +}; + + +// From the Mozilla Developer Center website at http://developer.mozilla.org/en/docs/New_in_JavaScript_1.6#Array_extras. + +if ( !Array.prototype.forEach ) { + Array.method('forEach', function(fn, thisObj) { + var scope = thisObj || window; + for ( var i = 0, len = this.length; i < len; ++i ) { + fn.call(scope, this[i], i, this); + } + }); +} + +if ( !Array.prototype.filter ) { + Array.method('filter', function(fn, thisObj) { + var scope = thisObj || window; + var a = []; + for ( var i = 0, len = this.length; i < len; ++i ) { + if ( !fn.call(scope, this[i], i, this) ) { + continue; + } + a.push(this[i]); + } + return a; + }); +} + + +window.DED = window.DED || {}; +DED.util = DED.util || {}; +DED.util.Observer = function() { + this.fns = []; +} +DED.util.Observer.prototype = { + subscribe: function(fn) { + this.fns.push(fn); + }, + unsubscribe: function(fn) { + this.fns = this.fns.filter( + function(el) { + if ( el !== fn ) { + return el; + } + } + ); + }, + fire: function(o) { + this.fns.forEach( + function(el) { + el(o); + } + ); + } +}; + + +DED.Queue = function() { + // Queued requests. + this.queue = []; + + // Observable Objects that can notify the client of interesting moments + // on each DED.Queue instance. + this.onComplete = new DED.util.Observer; + this.onFailure = new DED.util.Observer; + this.onFlush = new DED.util.Observer; + + // Core properties that set up a frontend queueing system. + this.retryCount = 3; + this.currentRetry = 0; + this.paused = false; + this.timeout = 5000; + this.conn = {}; + this.timer = {}; +}; + +DED.Queue. + method('flush', function() { + if (!this.queue.length > 0) { + return; + } + if (this.paused) { + this.paused = false; + return; + } + var that = this; + this.currentRetry++; + var abort = function() { + that.conn.abort(); + if (that.currentRetry == that.retryCount) { + that.onFailure.fire(); + that.currentRetry = 0; + } else { + that.flush(); + } + }; + this.timer = window.setTimeout(abort, this.timeout); + var callback = function(o) { + window.clearTimeout(that.timer); + that.currentRetry = 0; + that.queue.shift(); + that.onFlush.fire(o.responseText); + if (that.queue.length == 0) { + that.onComplete.fire(); + return; + } + // recursive call to flush + that.flush(); + }; + this.conn = asyncRequest( + this.queue[0]['method'], + this.queue[0]['uri'], + callback, + this.queue[0]['params'] + ); + }). + method('setRetryCount', function(count) { + this.retryCount = count; + }). + method('setTimeout', function(time) { + this.timeout = time; + }). + method('add', function(o) { + this.queue.push(o); + }). + method('pause', function() { + this.paused = true; + }). + method('dequeue', function() { + this.queue.pop(); + }). + method('clear', function() { + this.queue = []; + }); + + +/* Usage. */ + +var q = new DED.Queue; +// Reset our retry count to be higher for slow connections. +q.setRetryCount(5); +// Decrease timeout limit because we still want fast connections to benefit. +q.setTimeout(1000); +// Add two slots. +q.add({ + method: 'GET', + uri: '/path/to/file.php?ajax=true' +}); +q.add({ + method: 'GET', + uri: '/path/to/file.php?ajax=true&woe=me' +}); +// Flush the queue. +q.flush(); +// Pause the queue, retaining the requests. +q.pause(); +// Clear our queue and start fresh. +q.clear(); +// Add two requests. +q.add({ + method: 'GET', + uri: '/path/to/file.php?ajax=true' +}); +q.add({ + method: 'GET', + uri: '/path/to/file.php?ajax=true&woe=me' +}); +// Remove the last request from the queue. +q.dequeue(); +// Flush the queue again. +q.flush(); diff --git a/Chapter08/8.05 - XHR connection queue example page.html b/Chapter08/8.05 - XHR connection queue example page.html new file mode 100644 index 0000000..0bc1766 --- /dev/null +++ b/Chapter08/8.05 - XHR connection queue example page.html @@ -0,0 +1,131 @@ + + + + + Ajax Connection Queue + + + + + + +
+

Ajax Connection Queue

+
+
+

Add Requests to Queue

+ +
+

Other Queue Actions

+ +
+

Results:

+
+
+
+ + diff --git a/Chapter08/8.06 - Where have bridges been used_.js b/Chapter08/8.06 - Where have bridges been used_.js new file mode 100644 index 0000000..af8613e --- /dev/null +++ b/Chapter08/8.06 - Where have bridges been used_.js @@ -0,0 +1,18 @@ +// Original function. + +var addRequest = function(request) { + var data = request.split('-')[1]; + // etc... +}; + +// Function de-coupled. + +var addRequest = function(data) { + // etc... +}; + +// Bridge + +var addRequestFromClick = function(request) { + addRequest(request.split(‘-‘)[0]); +}; diff --git a/Chapter09/9.01 - Form validation.js b/Chapter09/9.01 - Form validation.js new file mode 100644 index 0000000..bcae735 --- /dev/null +++ b/Chapter09/9.01 - Form validation.js @@ -0,0 +1,153 @@ +/* Interfaces. */ + +var Composite = new Interface('Composite', ['add', 'remove', 'getChild']); +var FormItem = new Interface('FormItem', ['save']); + +/* CompositeForm class. */ + +var CompositeForm = function(id, method, action) { // implements Composite, FormItem + this.formComponents = []; + + this.element = document.createElement('form'); + this.element.id = id; + this.element.method = method || 'POST'; + this.element.action = action || '#'; +}; + +CompositeForm.prototype.add = function(child) { + Interface.ensureImplements(child, Composite, FormItem); + this.formComponents.push(child); + this.element.appendChild(child.getElement()); +}; + +CompositeForm.prototype.remove = function(child) { + for(var i = 0, len = this.formComponents.length; i < len; i++) { + if(this.formComponents[i] === child) { + this.formComponents.splice(i, 1); // Remove one element from the array at + // position i. + break; + } + } +}; + +CompositeForm.prototype.getChild = function(i) { + return this.formComponents[i]; +}; + +CompositeForm.prototype.save = function() { + for(var i = 0, len = this.formComponents.length; i < len; i++) { + this.formComponents[i].save(); + } +}; + +CompositeForm.prototype.getElement = function() { + return this.element; +}; + +/* Field class, abstract. */ + +var Field = function(id) { // implements Composite, FormItem + this.id = id; + this.element; +}; + +Field.prototype.add = function() {}; +Field.prototype.remove = function() {}; +Field.prototype.getChild = function() {}; + +Field.prototype.save = function() { + setCookie(this.id, this.getValue); +}; + +Field.prototype.getElement = function() { + return this.element; +}; + +Field.prototype.getValue = function() { + throw new Error('Unsupported operation on the class Field.'); +}; + +/* InputField class. */ + +var InputField = function(id, label) { // implements Composite, FormItem + Field.call(this, id); + + this.input = document.createElement('input'); + this.input.id = id; + + this.label = document.createElement('label'); + var labelTextNode = document.createTextNode(label); + this.label.appendChild(labelTextNode); + + this.element = document.createElement('div'); + this.element.className = 'input-field'; + this.element.appendChild(this.label); + this.element.appendChild(this.input); +}; +extend(InputField, Field); // Inherit from Field. + +InputField.prototype.getValue = function() { + return this.input.value; +}; + +/* TextareaField class. */ + +var TextareaField = function(id, label) { // implements Composite, FormItem + Field.call(this, id); + + this.textarea = document.createElement('textarea'); + this.textarea.id = id; + + this.label = document.createElement('label'); + var labelTextNode = document.createTextNode(label); + this.label.appendChild(labelTextNode); + + this.element = document.createElement('div'); + this.element.className = 'input-field'; + this.element.appendChild(this.label); + this.element.appendChild(this.textarea); +}; +extend(TextareaField, Field); // Inherit from Field. + +TextareaField.prototype.getValue = function() { + return this.textarea.value; +}; + +/* SelectField class. */ + +var SelectField = function(id, label) { // implements Composite, FormItem + Field.call(this, id); + + this.select = document.createElement('select'); + this.select.id = id; + + this.label = document.createElement('label'); + var labelTextNode = document.createTextNode(label); + this.label.appendChild(labelTextNode); + + this.element = document.createElement('div'); + this.element.className = 'input-field'; + this.element.appendChild(this.label); + this.element.appendChild(this.select); +}; +extend(SelectField, Field); // Inherit from Field. + +SelectField.prototype.getValue = function() { + return this.select.options[this.select.selectedIndex].value; +}; + + +/* Usage. */ + +var contactForm = new CompositeForm('contact-form', 'POST', 'contact.php'); + +contactForm.add(new InputField('first-name', 'First Name')); +contactForm.add(new InputField('last-name', 'Last Name')); +contactForm.add(new InputField('address', 'Address')); +contactForm.add(new InputField('city', 'City')); +contactForm.add(new SelectField('state', 'State', stateArray)); // var stateArray = + [{'al', 'Alabama'}, ...] +contactForm.add(new InputField('zip', 'Zip')); +contactForm.add(new TextareaField('comments', 'Comments')); + +addEvent(window, 'unload', contactForm.save); diff --git a/Chapter09/9.02 - Adding operations to FormItem.js b/Chapter09/9.02 - Adding operations to FormItem.js new file mode 100644 index 0000000..119c4d6 --- /dev/null +++ b/Chapter09/9.02 - Adding operations to FormItem.js @@ -0,0 +1,13 @@ +var FormItem = new Interface('FormItem', ['save', 'restore']); + +Field.prototype.restore = function() { + this.element.value = getCookie(this.id); +}; + +CompositeForm.prototype.restore = function() { + for(var i = 0, len = this.formComponents.length; i < len; i++) { + this.formComponents[i].restore(); + } +}; + +addEvent(window, 'load', contactForm.restore); diff --git a/Chapter09/9.03 - Adding classes to the hierarchy.js b/Chapter09/9.03 - Adding classes to the hierarchy.js new file mode 100644 index 0000000..4f7525f --- /dev/null +++ b/Chapter09/9.03 - Adding classes to the hierarchy.js @@ -0,0 +1,79 @@ +/* CompositeFieldset class. */ + +var CompositeFieldset = function(id, legendText) { // implements Composite, FormItem + this.components = {}; + + this.element = document.createElement('fieldset'); + this.element.id = id; + + if(legendText) { // Create a legend if the optional second + // argument is set. + this.legend = document.createElement('legend'); + this.legend.appendChild(document.createTextNode(legendText); + this.element.appendChild(this.legend); + } +}; + +CompositeFieldset.prototype.add = function(child) { + Interface.ensureImplements(child, Composite, FormItem); + this.components[child.getElement().id] = child; + this.element.appendChild(child.getElement()); +}; + +CompositeFieldset.prototype.remove = function(child) { + delete this.components[child.getElement().id]; +}; + +CompositeFieldset.prototype.getChild = function(id) { + if(this.components[id] != undefined) { + return this.components[id]; + } + else { + return null; + } +}; + +CompositeFieldset.prototype.save = function() { + for(var id in this.components) { + if(!this.components.hasOwnProperty(id)) continue; + this.components[id].save(); + } +}; + +CompositeFieldset.prototype.restore = function() { + for(var id in this.components) { + if(!this.components.hasOwnProperty(id)) continue; + this.components[id].restore(); + } +}; + +CompositeFieldset.prototype.getElement = function() { + return this.element; +}; + + +/* Usage. */ + +var contactForm = new CompositeForm('contact-form', 'POST', 'contact.php'); + +var nameFieldset = new CompositeFieldset('name-fieldset'); +nameFieldset.add(new InputField('first-name', 'First Name')); +nameFieldset.add(new InputField('last-name', 'Last Name')); +contactForm.add(nameFieldset); + +var addressFieldset = new CompositeFieldset('address-fieldset'); +addressFieldset.add(new InputField('address', 'Address')); +addressFieldset.add(new InputField('city', 'City')); +addressFieldset.add(new SelectField('state', 'State', stateArray)); +addressFieldset.add(new InputField('zip', 'Zip')); +contactForm.add(addressFieldset); + +contactForm.add(new TextareaField('comments', 'Comments')); + +body.appendChild(contactForm.getElement()); + +addEvent(window, 'unload', contactForm.save); +addEvent(window, 'load', contactForm.restore); + +addEvent('save-button', 'click', nameFieldset.save); +addEvent('restore-button', 'click', nameFieldset.restore); diff --git a/Chapter09/9.04 - Image gallery example.js b/Chapter09/9.04 - Image gallery example.js new file mode 100644 index 0000000..6431154 --- /dev/null +++ b/Chapter09/9.04 - Image gallery example.js @@ -0,0 +1,110 @@ +// Interfaces. + +var Composite = new Interface('Composite', ['add', 'remove', 'getChild']); +var GalleryItem = new Interface('GalleryItem', ['hide', 'show']); + + +// DynamicGallery class. + +var DynamicGallery = function(id) { // implements Composite, GalleryItem + this.children = []; + + this.element = document.createElement('div'); + this.element.id = id; + this.element.className = 'dynamic-gallery'; +} + +DynamicGallery.prototype = { + + // Implement the Composite interface. + + add: function(child) { + Interface.ensureImplements(child, Composite, GalleryItem); + this.children.push(child); + this.element.appendChild(child.getElement()); + }, + remove: function(child) { + for(var node, i = 0; node = this.getChild(i); i++) { + if(node == child) { + this.formComponents[i].splice(i, 1); + break; + } + } + this.element.removeChild(child.getElement()); + }, + getChild: function(i) { + return this.children[i]; + }, + + // Implement the GalleryItem interface. + + hide: function() { + for(var node, i = 0; node = this.getChild(i); i++) { + node.hide(); + } + this.element.style.display = 'none'; + }, + show: function() { + this.element.style.display = 'block'; + for(var node, i = 0; node = this.getChild(i); i++) { + node.show(); + } + }, + + // Helper methods. + + getElement: function() { + return this.element; + } +}; + +// GalleryImage class. + +var GalleryImage = function(src) { // implements Composite, GalleryItem + this.element = document.createElement('img'); + this.element.className = 'gallery-image'; + this.element.src = src; +} + +GalleryImage.prototype = { + + // Implement the Composite interface. + + add: function() {}, // This is a leaf node, so we don't + remove: function() {}, // implement these methods, we just + getChild: function() {}, // define them. + + // Implement the GalleryItem interface. + + hide: function() { + this.element.style.display = 'none'; + }, + show: function() { + this.element.style.display = ''; // Restore the display attribute to its + // previous setting. + }, + + // Helper methods. + + getElement: function() { + return this.element; + } +}; + +// Usage. + +var topGallery = new DynamicGallery('top-gallery'); + +topGallery.add(new GalleryImage('/img/image-1.jpg')); +topGallery.add(new GalleryImage('/img/image-2.jpg')); +topGallery.add(new GalleryImage('/img/image-3.jpg')); + +var vacationPhotos = new DynamicGallery('vacation-photos'); + +for(var i = 0; i < 30; i++) { + vacationPhotos.add(new GalleryImage('/img/vac/image-' + i + '.jpg')); +} + +topGallery.add(vacationPhotos); +topGallery.show(); // Show the main gallery, +vacationPhotos.hide(); // but hide the vacation gallery. diff --git a/Chapter10/10.01 - Some facades you probably already know about.js b/Chapter10/10.01 - Some facades you probably already know about.js new file mode 100644 index 0000000..e5f6197 --- /dev/null +++ b/Chapter10/10.01 - Some facades you probably already know about.js @@ -0,0 +1,11 @@ +function addEvent(el, type, fn) { + if (window.addEventListener) { + el.addEventListener(type, fn, false); + } + else if (window.attachEvent) { + el.attachEvent('on' + type, fn); + } + else { + el['on' + type] = fn; + } +} diff --git a/Chapter10/10.02 - Facades as convenience methods.js b/Chapter10/10.02 - Facades as convenience methods.js new file mode 100644 index 0000000..fdef3be --- /dev/null +++ b/Chapter10/10.02 - Facades as convenience methods.js @@ -0,0 +1,41 @@ +function a(x) { + // do stuff here... +} +function b(y) { + // do stuff here... +} +function ab(x, y) { + a(x); + b(y); +} + + + +var DED = window.DED || {}; +DED.util = { + stopPropagation: function(e) { + if (ev.stopPropagation) { + // W3 interface + e.stopPropagation(); + } + else { + // IE's interface + e.cancelBubble = true; + } + }, + preventDefault: function(e) { + if (e.preventDefault) { + // W3 interface + e.preventDefault(); + } + else { + // IE's interface + e.returnValue = false; + } + }, + /* our convenience method */ + stopEvent: function(e) { + DED.util.stopPropagation(e); + DED.util.preventDefault(e); + } +}; diff --git a/Chapter10/10.03 - Setting styles on HTML elements.js b/Chapter10/10.03 - Setting styles on HTML elements.js new file mode 100644 index 0000000..923d3e5 --- /dev/null +++ b/Chapter10/10.03 - Setting styles on HTML elements.js @@ -0,0 +1,48 @@ +var element = document.getElementById('content'); +element.style.color = 'red'; + +element.style.fontSize = '16px'; + +var element1 = document.getElementById('foo'); +element1.style.color = 'red'; + +var element2 = document.getElementById('bar'); +element2.style.color = 'red'; + +var element3 = document.getElementById('baz'); +element3.style.color = 'red'; + + + + +setStyle(['foo', 'bar', 'baz'], 'color', 'red'); + +function setStyle(elements, prop, val) { + for (var i = 0, len = elements.length-1; I < len; ++i) { + document.getElementById(elements[i]).style[prop] = val; + } +} + +setStyle(['foo'], 'position', 'absolute'); +setStyle(['foo'], 'top', '50px'); +setStyle(['foo'], 'left', '300px'); + +setCSS(['foo'], { + position: 'absolute', + top: '50px', + left: '300px' +}); + +function setCSS(el, styles) { + for ( var prop in styles ) { + if (!styles.hasOwnProperty(prop)) continue; + setStyle(el, prop, styles[prop]); + } +} + +setCSS(['foo', 'bar', 'baz'], { + color: 'white', + background: 'black', + fontSize: '16px', + fontFamily: 'georgia, times, serif' +}); diff --git a/Chapter10/10.04 - Creating an event utility.js b/Chapter10/10.04 - Creating an event utility.js new file mode 100644 index 0000000..204efc6 --- /dev/null +++ b/Chapter10/10.04 - Creating an event utility.js @@ -0,0 +1,35 @@ +DED.util.Event = { + getEvent: function(e) { + return e || window.event; + }, + getTarget: function(e) { + return e.target || e.srcElement; + }, + stopPropagation: function(e) { + if (e.stopPropagation) { + e.stopPropagation(); + } + else { + e.cancelBubble = true; + } + }, + preventDefault: function(e) { + if (e.preventDefault) { + e.preventDefault(); + } + else { + e.returnValue = false; + } + }, + stopEvent: function(e) { + this.stopPropagation(e); + this.preventDefault(e); + } +}; + +addEvent($('example'), 'click', function(e) { + // Who clicked me. + console.log(DED.util.Event.getTarget(e)); + // Stop propgating and prevent the default action. + DED.util.Event.stopEvent(e); +}); diff --git a/Chapter11/11.01 - Characteristics of an adapter.js b/Chapter11/11.01 - Characteristics of an adapter.js new file mode 100644 index 0000000..e3326af --- /dev/null +++ b/Chapter11/11.01 - Characteristics of an adapter.js @@ -0,0 +1,16 @@ +var clientObject = { + string1: 'foo', + string2: 'bar', + string3: 'baz' +}; +function interfaceMethod(str1, str2, str3) { + ... +} + +function clientToInterfaceAdapter(o) { + interfaceMethod(o.string1, o.string2, o.string3); +} + +/* Usage. */ + +clientToInterfaceAdapter(clientObject); diff --git a/Chapter11/11.02 - Adapting one library to another.js b/Chapter11/11.02 - Adapting one library to another.js new file mode 100644 index 0000000..5974970 --- /dev/null +++ b/Chapter11/11.02 - Adapting one library to another.js @@ -0,0 +1,42 @@ +// Prototype $ function. +function $() { + var elements = new Array(); + for(var i = 0; i < arguments.length; i++) { + var element = arguments[i]; + if(typeof element == 'string') + element = document.getElementById(element); + if(arguments.length == 1) + return element; + elements.push(element); + } + return elements; +} + +/* YUI get method. */ +YAHOO.util.Dom.get = function(el) { + if(YAHOO.lang.isString(el)) { + return document.getElementById(el); + } + if(YAHOO.lang.isArray(el)) { + var c = []; + for(var i = 0, len = el.length; i < len; ++i) { + c[c.length] = Y.Dom.get(el[i]); + } + return c; + } + if(el) { + return el; + } + return null; +}; + +function PrototypeToYUIAdapter() { + return YAHOO.util.Dom.get(arguments); +} +function YUIToPrototypeAdapter(el) { + return $.apply(window, el); +} + +$ = PrototypeToYUIAdapter; +or vice-versa, for those who are migrating from YUI to Prototype: +YAHOO.util.Dom.get = YUIToPrototypeAdapter; diff --git a/Chapter11/11.03 - Adapting an email API.html b/Chapter11/11.03 - Adapting an email API.html new file mode 100644 index 0000000..eb7ccab --- /dev/null +++ b/Chapter11/11.03 - Adapting an email API.html @@ -0,0 +1,170 @@ + + + + Mail API Demonstration + + + + + + +
+

Email Application Interface

+ +
+
+ + diff --git a/Chapter11/11.04 - More on adapting an email API.js b/Chapter11/11.04 - More on adapting an email API.js new file mode 100644 index 0000000..950214e --- /dev/null +++ b/Chapter11/11.04 - More on adapting an email API.js @@ -0,0 +1,30 @@ +var substitutionObject = { + name: "world" + place: "Google" +}; +var text = 'Hello {name}, welcome to {place}'; +var replacedText = DED.util.substitute(text, substitutionObject); +console.log(replacedText); +// produces "Hello world, welcome to Google" + + +fooMail.getMail(function(text) { + $('message-pane').innerHTML = text; +}); + +var dedMailtoFooMailAdapter = {}; +dedMailtoFooMailAdapter.getMail = function(id, callback) { + dedMail.getMail(id, function(resp) { + var resp = eval('('+resp+')'); + var details = '

From: {from}
'; + details += 'Sent: {date}

'; + details += '

Message:
'; + details += '{message}

'; + callback(DED.util.substitute(details, resp)); + }); +}; +// Other methods needed to adapt dedMail to the fooMail interface. +... + +// Assign the adapter to the fooMail variable. +fooMail = dedMailtoFooMailAdapter; diff --git a/Chapter12/12.01 - Structure of the decorator.js b/Chapter12/12.01 - Structure of the decorator.js new file mode 100644 index 0000000..eea26e3 --- /dev/null +++ b/Chapter12/12.01 - Structure of the decorator.js @@ -0,0 +1,92 @@ +/* The Bicycle interface. */ + +var Bicycle = new Interface('Bicycle', ['assemble', 'wash', 'ride', 'repair', + 'getPrice']); + +/* The AcmeComfortCruiser class. */ + +var AcmeComfortCruiser = function() { // implements Bicycle + ... +}; +AcmeComfortCruiser.prototype = { + assemble: function() { + ... + }, + wash: function() { + ... + }, + ride: function() { + ... + }, + repair: function() { + ... + }, + getPrice: function() { + return 399.00; + } +}; + +/* The BicycleDecorator abstract decorator class. */ + +var BicycleDecorator = function(bicycle) { // implements Bicycle + Interface.ensureImplements(bicycle, Bicycle); + this.bicycle = bicycle; +} +BicycleDecorator.prototype = { + assemble: function() { + return this.bicycle.assemble(); + }, + wash: function() { + return this.bicycle.wash(); + }, + ride: function() { + return this.bicycle.ride(); + }, + repair: function() { + return this.bicycle.repair(); + }, + getPrice: function() { + return this.bicycle.getPrice(); + } +}; + +/* HeadlightDecorator class. */ + +var HeadlightDecorator = function(bicycle) { // implements Bicycle + this.superclass.constructor(bicycle); // Call the superclass's constructor. +} +extend(HeadlightDecorator, BicycleDecorator); // Extend the superclass. +HeadlightDecorator.prototype.assemble = function() { + return this.bicycle.assemble() + ' Attach headlight to handlebars.'; +}; +HeadlightDecorator.prototype.getPrice = function() { + return this.bicycle.getPrice() + 15.00; +}; + + +/* TaillightDecorator class. */ + +var TaillightDecorator = function(bicycle) { // implements Bicycle + this.superclass.constructor(bicycle); // Call the superclass's constructor. +} +extend(TaillightDecorator, BicycleDecorator); // Extend the superclass. +TaillightDecorator.prototype.assemble = function() { + return this.bicycle.assemble() + ' Attach taillight to the seat post.'; +}; +TaillightDecorator.prototype.getPrice = function() { + return this.bicycle.getPrice() + 9.00; +}; + + +/* Usage. */ + +var myBicycle = new AcmeComfortCruiser(); // Instantiate the bicycle. +alert(myBicycle.getPrice()); // Returns 399.00 + +myBicycle = new TaillightDecorator(myBicycle); // Decorate the bicycle object + // with a taillight. +alert(myBicycle.getPrice()); // Now returns 408.00 + +myBicycle = new HeadlightDecorator(myBicycle); // Decorate the bicycle object + // again, now with a headlight. +alert(myBicycle.getPrice()); // Now returns 423.00 diff --git a/Chapter12/12.02 - In what ways can a decorator modify its component.js b/Chapter12/12.02 - In what ways can a decorator modify its component.js new file mode 100644 index 0000000..2b13636 --- /dev/null +++ b/Chapter12/12.02 - In what ways can a decorator modify its component.js @@ -0,0 +1,170 @@ +HeadlightDecorator.prototype.getPrice = function() { + return this.bicycle.getPrice() + 15.00; +}; + +var myBicycle = new AcmeComfortCruiser(); // Instantiate the bicycle. +alert(myBicycle.getPrice()); // Returns 399.00 + +myBicycle = new HeadlightDecorator(myBicycle); // Decorate the bicycle object + // with the first headlight. +myBicycle = new HeadlightDecorator(myBicycle); // Decorate the bicycle object + // with the second headlight. +myBicycle = new TaillightDecorator(myBicycle); // Decorate the bicycle object + // with a taillight. +alert(myBicycle.getPrice()); // Now returns 438.00 + + +/* FrameColorDecorator class. */ + +var FrameColorDecorator = function(bicycle, frameColor) { // implements Bicycle + this.superclass.constructor(bicycle); // Call the superclass's constructor. + this.frameColor = frameColor; +} +extend(FrameColorDecorator, BicycleDecorator); // Extend the superclass. +FrameColorDecorator.prototype.assemble = function() { + return 'Paint the frame ' + this.frameColor + ' and allow it to dry. ' + + this.bicycle.assemble(); +}; +FrameColorDecorator.prototype.getPrice = function() { + return this.bicycle.getPrice() + 30.00; +}; + +var myBicycle = new AcmeComfortCruiser(); // Instantiate the bicycle. +myBicycle = new FrameColorDecorator(myBicycle, 'red'); // Decorate the bicycle + // object with the frame color. +myBicycle = new HeadlightDecorator(myBicycle); // Decorate the bicycle object + // with the first headlight. +myBicycle = new HeadlightDecorator(myBicycle); // Decorate the bicycle object + // with the second headlight. +myBicycle = new TaillightDecorator(myBicycle); // Decorate the bicycle object + // with a taillight. +alert(myBicycle.assemble()); +/* Returns: + "Paint the frame red and allow it to dry. (Full instructions for assembling + the bike itself go here) Attach headlight to handlebars. Attach headlight + to handlebars. Attach taillight to the seat post." +*/ + + + + +/* LifetimeWarrantyDecorator class. */ + +var LifetimeWarrantyDecorator = function(bicycle) { // implements Bicycle + this.superclass.constructor(bicycle); // Call the superclass's constructor. +} +extend(LifetimeWarrantyDecorator, BicycleDecorator); // Extend the superclass. +LifetimeWarrantyDecorator.prototype.repair = function() { + return 'This bicycle is covered by a lifetime warranty. Please take it to ' + + 'an authorized Acme Repair Center.'; +}; +LifetimeWarrantyDecorator.prototype.getPrice = function() { + return this.bicycle.getPrice() + 199.00; +}; + +/* TimedWarrantyDecorator class. */ + +var TimedWarrantyDecorator = function(bicycle, coverageLengthInYears) { + // implements Bicycle + this.superclass.constructor(bicycle); // Call the superclass's constructor. + this.coverageLength = coverageLengthInYears; + this.expDate = new Date(); + var coverageLengthInMs = this.coverageLength * 365 * 24 * 60 * 60 * 1000; + expDate.setTime(expDate.getTime() + coverageLengthInMs); +} +extend(TimedWarrantyDecorator, BicycleDecorator); // Extend the superclass. +TimedWarrantyDecorator.prototype.repair = function() { + var repairInstructions; + var currentDate = new Date(); + if(currentDate < expDate) { + repairInstructions = 'This bicycle is currently covered by a warranty. ' + + 'Please take it to an authorized Acme Repair Center.'; + } + else { + repairInstructions = this.bicycle.repair(); + } + return repairInstructions; +}; +TimedWarrantyDecorator.prototype.getPrice = function() { + return this.bicycle.getPrice() + (40.00 * this.coverageLength); +}; + + + +/* BellDecorator class. */ + +var BellDecorator = function(bicycle) { // implements Bicycle + this.superclass.constructor(bicycle); // Call the superclass's constrcutor. +} +extend(BellDecorator, BicycleDecorator); // Extend the superclass. +BellDecorator.prototype.assemble = function() { + return this.bicycle.assemble() + ' Attach bell to handlebars.'; +}; +BellDecorator.prototype.getPrice = function() { + return this.bicycle.getPrice() + 6.00; +}; +BellDecorator.prototype.ringBell = function() { + return 'Bell rung.'; +}; + +var myBicycle = new AcmeComfortCruiser(); // Instantiate the bicycle. +myBicycle = new BellDecorator(myBicycle); // Decorate the bicycle object + // with a bell. +alert(myBicycle.ringBell()); // Returns 'Bell rung.' + +var myBicycle = new AcmeComfortCruiser(); // Instantiate the bicycle. +myBicycle = new BellDecorator(myBicycle); // Decorate the bicycle object + // with a bell. +myBicycle = new HeadlightDecorator(myBicycle); // Decorate the bicycle object + // with a headlight. +alert(myBicycle.ringBell()); // Method not found. + + + +/* The BicycleDecorator abstract decorator class, improved. */ + +var BicycleDecorator = function(bicycle) { // implements Bicycle + this.bicycle = bicycle; + this.interface = Bicycle; + + // Loop through all of the attributes of this.bicycle and create pass-through + // methods for any methods that aren't currently implemented. + outerloop: for(var key in this.bicycle) { + // Ensure that the property is a function. + if(typeof this.bicycle[key] !== 'function') { + continue outerloop; + } + + // Ensure that the method isn't in the interface. + for(var i = 0, len = this.interface.methods.length; i < len; i++) { + if(key === this.interface.methods[i]) { + continue outerloop; + } + } + + // Add the new method. + var that = this; + (function(methodName) { + that[methodName] = function() { + return that.bicycle[methodName](); + }; + })(key); + } +} +BicycleDecorator.prototype = { + assemble: function() { + return this.bicycle.assemble(); + }, + wash: function() { + return this.bicycle.wash(); + }, + ride: function() { + return this.bicycle.ride(); + }, + repair: function() { + return this.bicycle.repair(); + }, + getPrice: function() { + return this.bicycle.getPrice(); + } +}; diff --git a/Chapter12/12.03 - The role of the factory.js b/Chapter12/12.03 - The role of the factory.js new file mode 100644 index 0000000..2640fde --- /dev/null +++ b/Chapter12/12.03 - The role of the factory.js @@ -0,0 +1,81 @@ +/* Original AcmeBicycleShop factory class. */ + +var AcmeBicycleShop = function() {}; +extend(AcmeBicycleShop, BicycleShop); +AcmeBicycleShop.prototype.createBicycle = function(model) { + var bicycle; + + switch(model) { + case 'The Speedster': + bicycle = new AcmeSpeedster(); + break; + case 'The Lowrider': + bicycle = new AcmeLowrider(); + break; + case 'The Flatlander': + bicycle = new AcmeFlatlander(); + break; + case 'The Comfort Cruiser': + default: + bicycle = new AcmeComfortCruiser(); + } + + Interface.ensureImplements(bicycle, Bicycle); + return bicycle; +}; + +/* AcmeBicycleShop factory class, with decorators. */ + +var AcmeBicycleShop = function() {}; +extend(AcmeBicycleShop, BicycleShop); +AcmeBicycleShop.prototype.createBicycle = function(model, options) { + // Instantiate the bicycle object. + var bicycle = new AcmeBicycleShop.models[model](); + + // Iterate through the options and instantiate decorators. + for(var i = 0, len = options.length; i < len; i++) { + var decorator = AcmeBicycleShop.options[options[i].name]; + if(typeof decorator !== 'function') { + throw new Error('Decorator ' + options[i].name + ' not found.'); + } + var argument = options[i].arg; + bicycle = new decorator(bicycle, argument); + } + + // Check the interface and return the finished object. + Interface.ensureImplements(bicycle, Bicycle); + return bicycle; +}; + +// Model name to class name mapping. +AcmeBicycleShop.models = { + 'The Speedster': AcmeSpeedster, + 'The Lowrider': AcmeLowrider, + 'The Flatlander': AcmeFlatlander, + 'The Comfort Cruiser': AcmeComfortCruiser +}; + +// Option name to decorator class name mapping. +AcmeBicycleShop.options = { + 'headlight': HeadlightDecorator, + 'taillight': TaillightDecorator, + 'bell': BellDecorator, + 'basket': BasketDecorator, + 'color': FrameColorDecorator, + 'lifetime warranty': LifetimeWarrantyDecorator, + 'timed warranty': TimedWarrantyDecorator +}; + +var myBicycle = new AcmeSpeedster(); +myBicycle = new FrameColorDecorator(myBicycle, 'blue'); +myBicycle = new HeadlightDecorator(myBicycle); +myBicycle = new TaillightDecorator(myBicycle); +myBicycle = new TimedWarrantyDecorator(myBicycle, 2); + +var alecsCruisers = new AcmeBicycleShop(); +var myBicycle = alecsCruisers.createBicycle('The Speedster', [ + { name: 'color', arg: 'blue' }, + { name: 'headlight' }, + { name: 'taillight' }, + { name: 'timed warranty', arg: 2 } +]); diff --git a/Chapter12/12.04 - Function decorators.js b/Chapter12/12.04 - Function decorators.js new file mode 100644 index 0000000..e57aa46 --- /dev/null +++ b/Chapter12/12.04 - Function decorators.js @@ -0,0 +1,22 @@ +function upperCaseDecorator(func) { + return function() { + return func.apply(this, arguments).toUpperCase(); + } +} + +function getDate() { + return (new Date()).toString(); +} +getDateCaps = upperCaseDecorator(getDate); + +alert(getDate()); // Returns Wed Sep 26 2007 20:11:02 GMT-0700 (PDT) +alert(getDateCaps()); // Returns WED SEP 26 2007 20:11:02 GMT-0700 (PDT) + +BellDecorator.prototype.ringBellLoudly = + upperCaseDecorator(BellDecorator.prototype.ringBell); + +var myBicycle = new AcmeComfortCruiser(); +myBicycle = new BellDecorator(myBicycle); + +alert(myBicycle.ringBell()); // Returns 'Bell rung.' +alert(myBicycle.ringBellLoudly()); // Returns 'BELL RUNG.' diff --git a/Chapter12/12.05 - Method profiler.js b/Chapter12/12.05 - Method profiler.js new file mode 100644 index 0000000..a7f3cea --- /dev/null +++ b/Chapter12/12.05 - Method profiler.js @@ -0,0 +1,84 @@ +/* ListBuilder class. */ + +var ListBuilder = function(parent, listLength) { + this.parentEl = $(parent); + this.listLength = listLength; +}; +ListBuilder.prototype = { + buildList: function() { + var list = document.createElement('ol'); + this.parentEl.appendChild(list); + + for(var i = 0; i < this.listLength; i++) { + var item = document.createElement('li'); + list.appendChild(item); + } + } +}; + +/* SimpleProfiler class. */ + +var SimpleProfiler = function(component) { + this.component = component; +}; +SimpleProfiler.prototype = { + buildList: function() { + var startTime = new Date(); + this.component.buildList(); + var elapsedTime = (new Date()).getTime() - startTime.getTime(); + console.log('buildList: ' + elapsedTime + ' ms'); + } +}; + +/* Usage. */ + +var list = new ListBuilder('list-container', 5000); // Instantiate the object. +list = new SimpleProfiler(list); // Wrap the object in the decorator. +list.buildList(); // Creates the list and displays "buildList: 298 ms". + + + +/* MethodProfiler class. */ + +var MethodProfiler = function(component) { + this.component = component; + this.timers = {}; + + for(var key in this.component) { + // Ensure that the property is a function. + if(typeof this.component[key] !== 'function') { + continue; + } + + // Add the method. + var that = this; + (function(methodName) { + that[methodName] = function() { + that.startTimer(methodName); + var returnValue = that.component[methodName].apply(that.component, + arguments); + that.displayTime(methodName, that.getElapsedTime(methodName)); + return returnValue; + }; + })(key); } +}; +MethodProfiler.prototype = { + startTimer: function(methodName) { + this.timers[methodName] = (new Date()).getTime(); + }, + getElapsedTime: function(methodName) { + return (new Date()).getTime() - this.timers[methodName]; + }, + displayTime: function(methodName, time) { + console.log(methodName + ': ' + time + ' ms'); + } +}; + +/* Usage. */ + +var list = new ListBuilder('list-container', 5000); +list = new MethodProfiler(list); +list.buildList('ol'); // Displays "buildList: 301 ms". +list.buildList('ul'); // Displays "buildList: 287 ms". +list.removeLists('ul'); // Displays "removeLists: 10 ms". +list.removeLists('ol'); // Displays "removeLists: 12 ms". diff --git a/Chapter13/13.01 - Car registration example.js b/Chapter13/13.01 - Car registration example.js new file mode 100644 index 0000000..a1b240e --- /dev/null +++ b/Chapter13/13.01 - Car registration example.js @@ -0,0 +1,109 @@ +/* Car class, un-optimized. */ + +var Car = function(make, model, year, owner, tag, renewDate) { + this.make = make; + this.model = model; + this.year = year; + this.owner = owner; + this.tag = tag; + this.renewDate = renewDate; +}; +Car.prototype = { + getMake: function() { + return this.make; + }, + getModel: function() { + return this.model; + }, + getYear: function() { + return this.year; + }, + + transferOwnership: function(newOwner, newTag, newRenewDate) { + this.owner = newOwner; + this.tag = newTag; + this.renewDate = newRenewDate; + }, + renewRegistration: function(newRenewDate) { + this.renewDate = newRenewDate; + }, + isRegistrationCurrent: function() { + var today = new Date(); + return today.getTime() < Date.parse(this.renewDate); + } +}; + + /* Car class, optimized as a flyweight. */ + +var Car = function(make, model, year) { + this.make = make; + this.model = model; + this.year = year; +}; +Car.prototype = { + getMake: function() { + return this.make; + }, + getModel: function() { + return this.model; + }, + getYear: function() { + return this.year; + } +}; + +/* CarFactory singleton. */ + +var CarFactory = (function() { + + var createdCars = {}; + + return { + createCar: function(make, model, year) { + // Check to see if this particular combination has been created before. + if(createdCars[make + '-' + model + '-' + year]) { + return createdCars[make + '-' + model + '-' + year]; + } + // Otherwise create a new instance and save it. + else { + var car = new Car(make, model, year); + createdCars[make + '-' + model + '-' + year] = car; + return car; + } + } + }; +})(); + +/* CarRecordManager singleton. */ + +var CarRecordManager = (function() { + + var carRecordDatabase = {}; + + return { + // Add a new car record into the city's system. + addCarRecord: function(make, model, year, owner, tag, renewDate) { + var car = CarFactory.createCar(make, model, year); + carRecordDatabase[tag] = { + owner: owner, + renewDate: renewDate, + car: car + }; + }, + + // Methods previously contained in the Car class. + transferOwnership: function(tag, newOwner, newTag, newRenewDate) { + var record = carRecordDatabase[tag]; + record.owner = newOwner; + record.tag = newTag; + record.renewDate = newRenewDate; + }, + renewRegistration: function(tag, newRenewDate) { + carRecordDatabase[tag].renewDate = newRenewDate; + }, + isRegistrationCurrent: function(tag) { + var today = new Date(); + return today.getTime() < Date.parse(carRecordDatabase[tag].renewDate); + } + }; +})(); diff --git a/Chapter13/13.02 - Web calendar example.js b/Chapter13/13.02 - Web calendar example.js new file mode 100644 index 0000000..17e65db --- /dev/null +++ b/Chapter13/13.02 - Web calendar example.js @@ -0,0 +1,103 @@ +/* CalendarItem interface. */ + +var CalendarItem = new Interface('CalendarItem', ['display']); + +/* CalendarYear class, a composite. */ + +var CalendarYear = function(year, parent) { // implements CalendarItem + this.year = year; + this.element = document.createElement('div'); + this.element.style.display = 'none'; + parent.appendChild(this.element); + + function isLeapYear(y) { + return (y > 0) && !(y % 4) && ((y % 100) || !(y % 400)); + } + + this.months = []; + // The number of days in each month. + this.numDays = [31, isLeapYear(this.year) ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, + 31, 30, 31]; + for(var i = 0, len = 12; i < len; i++) { + this.months[i] = new CalendarMonth(i, this.numDays[i], this.element); + } +); +CalendarYear.prototype = { + display: function() { + for(var i = 0, len = this.months.length; i < len; i++) { + this.months[i].display(); // Pass the call down to the next level. + } + this.element.style.display = 'block'; + } +}; + +/* CalendarMonth class, a composite. */ + +var CalendarMonth = function(monthNum, numDays, parent) { // implements CalendarItem + this.monthNum = monthNum; + this.element = document.createElement('div'); + this.element.style.display = 'none'; + parent.appendChild(this.element); + + this.days = []; + for(var i = 0, len = numDays; i < len; i++) { + this.days[i] = new CalendarDay(i, this.element); + } +); +CalendarMonth.prototype = { + display: function() { + for(var i = 0, len = this.days.length; i < len; i++) { + this.days[i].display(); // Pass the call down to the next level. + } + this.element.style.display = 'block'; + } +}; + +/* CalendarDay class, a leaf (unoptimized). */ + +var CalendarDay = function(date, parent) { // implements CalendarItem + this.date = date; + this.element = document.createElement('div'); + this.element.style.display = 'none'; + parent.appendChild(this.element); +}; +CalendarDay.prototype = { + display: function() { + this.element.style.display = 'block'; + this.element.innerHTML = this.date; + } +}; + + +/* CalendarDay class, a flyweight leaf (optimized). */ + +var CalendarDay = function() {}; // implements CalendarItem +CalendarDay.prototype = { + display: function(date, parent) { + var element = document.createElement('div'); + parent.appendChild(element); + element.innerHTML = date; + } +}; + +/* CalendarMonth class, a composite (optimized). */ + +var CalendarMonth = function(monthNum, numDays, parent) { // implements CalendarItem + this.monthNum = monthNum; + this.element = document.createElement('div'); + this.element.style.display = 'none'; + parent.appendChild(this.element); + + this.days = []; + for(var i = 0, len = numDays; i < len; i++) { + this.days[i] = calendarDay; + } +); +CalendarMonth.prototype = { + display: function() { + for(var i = 0, len = this.days.length; i < len; i++) { + this.days[i].display(i, this.element); + } + this.element.style.display = 'block'; + } +}; diff --git a/Chapter13/13.03 - Tooltip example.js b/Chapter13/13.03 - Tooltip example.js new file mode 100644 index 0000000..5cd0c24 --- /dev/null +++ b/Chapter13/13.03 - Tooltip example.js @@ -0,0 +1,127 @@ +/* Tooltip class, un-optimized. */ + +var Tooltip = function(targetElement, text) { + this.target = targetElement; + this.text = text; + this.delayTimeout = null; + this.delay = 1500; // in milliseconds. + + // Create the HTML. + this.element = document.createElement('div'); + this.element.style.display = 'none'; + this.element.style.position = 'absolute'; + this.element.className = 'tooltip'; + document.getElementsByTagName('body')[0].appendChild(this.element); + this.element.innerHTML = this.text; + + // Attach the events. + var that = this; // Correcting the scope. + addEvent(this.target, 'mouseover', function(e) { that.startDelay(e); }); + addEvent(this.target, 'mouseout', function(e) { that.hide(); }); +}; +Tooltip.prototype = { + startDelay: function(e) { + if(this.delayTimeout == null) { + var that = this; + var x = e.clientX; + var y = e.clientY; + this.delayTimeout = setTimeout(function() { + that.show(x, y); + }, this.delay); + } + }, + show: function(x, y) { + clearTimeout(this.delayTimeout); + this.delayTimeout = null; + this.element.style.left = (x) + 'px'; + this.element.style.top = (y + 20) + 'px'; + this.element.style.display = 'block'; + }, + hide: function() { + clearTimeout(this.delayTimeout); + this.delayTimeout = null; + this.element.style.display = 'none'; + } +}; + +/* Tooltip usage. */ + +var linkElement = $('link-id'); +var tt = new Tooltip(linkElement, 'Lorem ipsum...'); + + + +/* Tooltip class, as a flyweight. */ + +var Tooltip = function() { + this.delayTimeout = null; + this.delay = 1500; // in milliseconds. + + // Create the HTML. + this.element = document.createElement('div'); + this.element.style.display = 'none'; + this.element.style.position = 'absolute'; + this.element.className = 'tooltip'; + document.getElementsByTagName('body')[0].appendChild(this.element); +}; +Tooltip.prototype = { + startDelay: function(e, text) { + if(this.delayTimeout == null) { + var that = this; + var x = e.clientX; + var y = e.clientY; + this.delayTimeout = setTimeout(function() { + that.show(x, y, text); + }, this.delay); + } + }, + show: function(x, y, text) { + clearTimeout(this.delayTimeout); + this.delayTimeout = null; + this.element.innerHTML = text; + this.element.style.left = (x) + 'px'; + this.element.style.top = (y + 20) + 'px'; + this.element.style.display = 'block'; + }, + hide: function() { + clearTimeout(this.delayTimeout); + this.delayTimeout = null; + this.element.style.display = 'none'; + } +}; + +/* TooltipManager singleton, a flyweight factory and manager. */ + +var TooltipManager = (function() { + var storedInstance = null; + + /* Tooltip class, as a flyweight. */ + + var Tooltip = function() { + ... + }; + Tooltip.prototype = { + ... + }; + + return { + addTooltip: function(targetElement, text) { + // Get the tooltip object. + var tt = this.getTooltip(); + + // Attach the events. + addEvent(targetElement, 'mouseover', function(e) { tt.startDelay(e, text); }); + addEvent(targetElement, 'mouseout', function(e) { tt.hide(); }); + }, + getTooltip: function() { + if(storedInstance == null) { + storedInstance = new Tooltip(); + } + return storedInstance; + } + }; +})(); + +/* Tooltip usage. */ + +TooltipManager.addTooltip($('link-id'), 'Lorem ipsum...'); diff --git a/Chapter13/13.04 - Storing instances for later reuse.js b/Chapter13/13.04 - Storing instances for later reuse.js new file mode 100644 index 0000000..927007c --- /dev/null +++ b/Chapter13/13.04 - Storing instances for later reuse.js @@ -0,0 +1,49 @@ +/* DisplayModule interface. */ + +var DisplayModule = new Interface('DisplayModule', ['show', 'hide', 'state']); + +/* DialogBox class. */ + +var DialogBox = function() { // implements DisplayModule + ... +}; +DialogBox.prototype = { + show: function(header, body, footer) { // Sets the content and shows the + ... // dialog box. + }, + hide: function() { // Hides the dialog box. + ... + }, + state: function() { // Returns 'visible' or 'hidden'; + ... + } +}; + +/* DialogBoxManager singleton. */ + +var DialogBoxManager = (function() { + var created = []; // Stores created instances. + + return { + displayDialogBox: function(header, body, footer) { + var inUse = this.numberInUse(); // Find the number currently in use. + if(inUse > created.length) { + created.push(this.createDialogBox()); // Augment it if need be. + } + created[inUse].show(header, body, footer); // Show the dialog box. + }, + createDialogBox: function() { // Factory method. + var db = new DialogBox(); + return db; + }, + numberInUse: function() { + var inUse = 0; + for(var i = 0, len = created.length; i < len; i++) { + if(created[i].state() === 'visible') { + inUse++; + } + } + return inUse; + } + }; +})(); diff --git a/Chapter14/14.01 - PublicLibrary class from Chapter 3.js b/Chapter14/14.01 - PublicLibrary class from Chapter 3.js new file mode 100644 index 0000000..86cdcdf --- /dev/null +++ b/Chapter14/14.01 - PublicLibrary class from Chapter 3.js @@ -0,0 +1,56 @@ +/* From chapter 3. */ + +var Publication = new Interface('Publication', ['getIsbn', 'setIsbn', 'getTitle', + 'setTitle', 'getAuthor', 'setAuthor', 'display']); +var Book = function(isbn, title, author) { ... } // implements Publication + +/* Library interface. */ + +var Library = new Interface('Library', ['findBooks', 'checkoutBook', 'returnBook']); + +/* PublicLibrary class. */ + +var PublicLibrary = function(books) { // implements Library + this.catalog = {}; + for(var i = 0, len = books.length; i < len; i++) { + this.catalog[books[i].getIsbn()] = { book: books[i], available: true }; + } +}; +PublicLibrary.prototype = { + findBooks: function(searchString) { + var results = []; + for(var isbn in this.catalog) { + if(!this.catalog.hasOwnProperty(isbn)) continue; + if(searchString.match(this.catalog[isbn].getTitle()) || + searchString.match(this.catalog[isbn].getAuthor())) { + results.push(this.catalog[isbn]); + } + } + return results; + }, + checkoutBook: function(book) { + var isbn = book.getIsbn(); + if(this.catalog[isbn]) { + if(this.catalog[isbn].available) { + this.catalog[isbn].available = false; + return this.catalog[isbn]; + } + else { + throw new Error('PublicLibrary: book ' + book.getTitle() + + ' is not currently available.'); + } + } + else { + throw new Error('PublicLibrary: book ' + book.getTitle() + ' not found.'); + } + }, + returnBook: function(book) { + var isbn = book.getIsbn(); + if(this.catalog[isbn]) { + this.catalog[isbn].available = true; + } + else { + throw new Error('PublicLibrary: book ' + book.getTitle() + ' not found.'); + } + } +}; diff --git a/Chapter14/14.02 - PublicLibraryProxy class.js b/Chapter14/14.02 - PublicLibraryProxy class.js new file mode 100644 index 0000000..827dabb --- /dev/null +++ b/Chapter14/14.02 - PublicLibraryProxy class.js @@ -0,0 +1,16 @@ +/* PublicLibraryProxy class, a useless proxy. */ + +var PublicLibraryProxy = function(catalog) { // implements Library + this.library = new PublicLibrary(catalog); +}; +PublicLibraryProxy.prototype = { + findBooks: function(searchString) { + return this.library.findBooks(searchString); + }, + checkoutBook: function(book) { + return this.library.checkoutBook(book); + }, + returnBook: function(book) { + return this.library.returnBook(book); + } +}; diff --git a/Chapter14/14.03 - PublicLibraryVirtualProxy class.js b/Chapter14/14.03 - PublicLibraryVirtualProxy class.js new file mode 100644 index 0000000..03e35d3 --- /dev/null +++ b/Chapter14/14.03 - PublicLibraryVirtualProxy class.js @@ -0,0 +1,25 @@ +/* PublicLibraryVirtualProxy class. */ + +var PublicLibraryVirtualProxy = function(catalog) { // implements Library + this.library = null; + this.catalog = catalog; // Store the argument to the constructor. +}; +PublicLibraryVirtualProxy.prototype = { + _initializeLibrary: function() { + if(this.library === null) { + this.library = new PublicLibrary(this.catalog); + } + }, + findBooks: function(searchString) { + this._initializeLibrary(); + return this.library.findBooks(searchString); + }, + checkoutBook: function(book) { + this._initializeLibrary(); + return this.library.checkoutBook(book); + }, + returnBook: function(book) { + this._initializeLibrary(); + return this.library.returnBook(book); + } +}; diff --git a/Chapter14/14.04 - Page statistics example.js b/Chapter14/14.04 - Page statistics example.js new file mode 100644 index 0000000..aaa563f --- /dev/null +++ b/Chapter14/14.04 - Page statistics example.js @@ -0,0 +1,107 @@ +/* Manually making the calls. */ + +var xhrHandler = XhrManager.createXhrHandler(); + +/* Get the pageview statistics. */ + +var callback = { + success: function(responseText) { + var stats = eval('(' + responseText + ')'); // Parse the JSON data. + displayPageviews(stats); // Display the stats on the page. + }, + failure: function(statusCode) { + throw new Error('Asynchronous request for stats failed.'); + } +}; +xhrHandler.request('GET', '/stats/getPageviews/?page=index.html', callback); + +/* Get the browser statistics. */ + +var callback = { + success: function(responseText) { + var stats = eval('(' + responseText + ')'); // Parse the JSON data. + displayBrowserShare(stats); // Display the stats on the page. + }, + failure: function(statusCode) { + throw new Error('Asynchronous request for stats failed.'); + } +}; +xhrHandler.request('GET', '/stats/getBrowserShare/?page=index.html', callback); + + + +/* Using a remote proxy. */ + +/* PageStats interface. */ + +var PageStats = new Interface('PageStats', ['getPageviews', 'getUniques', + 'getBrowserShare', 'getTopSearchTerms', 'getMostVisitedPages']); + +/* StatsProxy singleton. */ + +var StatsProxy = function() { // implements PageStats + + /* Private attributes. */ + + var xhrHandler = XhrManager.createXhrHandler(); + var urls = { + pageviews: '/stats/getPageviews/', + uniques: '/stats/getUniques/', + browserShare: '/stats/getBrowserShare/', + topSearchTerms: '/stats/getTopSearchTerms/', + mostVisitedPages: '/stats/getMostVisitedPages/' + }; + + /* Private methods. */ + + function xhrFailure() { + throw new Error('StatsProxy: Asynchronous request for stats failed.'); + } + + function fetchData(url, dataCallback, startDate, endDate, page) { + var callback = { + success: function(responseText) { + var stats = eval('(' + responseText + ')'); + dataCallback(stats); + }, + failure: xhrFailure + }; + + var getVars = []; + if(startDate != undefined) { + getVars.push('startDate=' + encodeURI(startDate)); + } + if(endDate != undefined) { + getVars.push('endDate=' + encodeURI(endDate)); + } + if(page != undefined) { + getVars.push('page=' + page); + } + + if(getVars.length > 0) { + url = url + '?' + getVars.join('&'); + } + + xhrHandler.request('GET', url, callback); + } + + /* Public methods. */ + + return { + getPageviews: function(callback, startDate, endDate, page) { + fetchData(urls.pageviews, callback, startDate, endDate, page); + }, + getUniques: function(callback, startDate, endDate, page) { + fetchData(urls.uniques, callback, startDate, endDate, page); + }, + getBrowserShare: function(callback, startDate, endDate, page) { + fetchData(urls.browserShare, callback, startDate, endDate, page); + }, + getTopSearchTerms: function(callback, startDate, endDate, page) { + fetchData(urls.topSearchTerms, callback, startDate, endDate, page); + }, + getMostVisitedPages: function(callback, startDate, endDate) { + fetchData(urls.mostVisitedPages, callback, startDate, endDate); + } + }; +}(); diff --git a/Chapter14/14.05 - General pattern for wrapping a web service.js b/Chapter14/14.05 - General pattern for wrapping a web service.js new file mode 100644 index 0000000..9d9a7f0 --- /dev/null +++ b/Chapter14/14.05 - General pattern for wrapping a web service.js @@ -0,0 +1,77 @@ +/* WebserviceProxy class */ + +var WebserviceProxy = function() { + this.xhrHandler = XhrManager.createXhrHandler(); +}; +WebserviceProxy.prototype = { + _xhrFailure: function(statusCode) { + throw new Error('StatsProxy: Asynchronous request for stats failed.'); + }, + _fetchData: function(url, dataCallback, getVars) { + var that = this; + var callback = { + success: function(responseText) { + var obj = eval('(' + responseText + ')'); + dataCallback(obj); + }, + failure: that._xhrFailure + }; + + var getVarArray = []; + for(varName in getVars) { + getVarArray.push(varName + '=' + getVars[varName]); + } + if(getVarArray.length > 0) { + url = url + '?' + getVarArray.join('&'); + } + + xhrHandler.request('GET', url, callback); + } +}; + +/* StatsProxy class, using WebserviceProxy. */ + +var StatsProxy = function() {}; // implements PageStats +extend(StatsProxy, WebserviceProxy); + +/* Implement the needed methods. */ + +StatsProxy.prototype.getPageviews = function(callback, startDate, endDate, + page) { + this._fetchData('/stats/getPageviews/', callback, { + 'startDate': startDate, + 'endDate': endDate, + 'page': page + }); +}; +StatsProxy.prototype.getUniques = function(callback, startDate, endDate, + page) { + this._fetchData('/stats/getUniques/', callback, { + 'startDate': startDate, + 'endDate': endDate, + 'page': page + }); +}; +StatsProxy.prototype.getBrowserShare = function(callback, startDate, endDate, + page) { + this._fetchData('/stats/getBrowserShare/', callback, { + 'startDate': startDate, + 'endDate': endDate, + 'page': page + }); +}; +StatsProxy.prototype.getTopSearchTerms = function(callback, startDate, + endDate, page) { + this._fetchData('/stats/getTopSearchTerms/', callback, { + 'startDate': startDate, + 'endDate': endDate, + 'page': page + }); +}; +StatsProxy.prototype.getMostVisitedPages = function(callback, startDate, + endDate) { + this._fetchData('/stats/getMostVisitedPages/', callback, { + 'startDate': startDate, + 'endDate': endDate + }); +}; diff --git a/Chapter14/14.06 - Directory lookup example.js b/Chapter14/14.06 - Directory lookup example.js new file mode 100644 index 0000000..562b817 --- /dev/null +++ b/Chapter14/14.06 - Directory lookup example.js @@ -0,0 +1,109 @@ +/* Directory interface. */ + +var Directory = new Interface('Directory', ['showPage']); + +/* PersonnelDirectory class, the Real Subject */ + +var PersonnelDirectory = function(parent) { // implements Directory + this.xhrHandler = XhrManager.createXhrHandler(); + this.parent = parent; + this.data = null; + this.currentPage = null; + + var that = this; + var callback = { + success: that._configure, + failure: function() { + throw new Error('PersonnelDirectory: failure in data retrieval.'); + } + } + xhrHandler.request('GET', 'directoryData.php', callback); +}; +PersonnelDirectory.prototype = { + _configure: function(responseText) { + this.data = eval('(' + reponseText + ')'); + ... + this.currentPage = 'a'; + }, + showPage: function(page) { + $('page-' + this.currentPage).style.display = 'none'; + $('page-' + page).style.display = 'block'; + this.currentPage = page; + } +}; + + +/* DirectoryProxy class, just the outline. */ + +var DirectoryProxy = function(parent) { // implements Directory + +}; +DirectoryProxy.prototype = { + showPage: function(page) { + + } +}; + +/* DirectoryProxy class, as a useless proxy. */ + +var DirectoryProxy = function(parent) { // implements Directory + this.directory = new PersonnelDirectory(parent); +}; +DirectoryProxy.prototype = { + showPage: function(page) { + return this.directory.showPage(page); + } +}; + +/* DirectoryProxy class, as a virtual proxy. */ + +var DirectoryProxy = function(parent) { // implements Directory + this.parent = parent; + this.directory = null; + var that = this; + addEvent(parent, 'mouseover', that._initialize); // Initialization trigger. +}; +DirectoryProxy.prototype = { + _initialize: function() { + this.directory = new PersonnelDirectory(this.parent); + }, + showPage: function(page) { + return this.directory.showPage(page); + } +}; + +/* DirectoryProxy class, with loading message. */ + +var DirectoryProxy = function(parent) { // implements Directory + this.parent = parent; + this.directory = null; + this.warning = null; + this.interval = null; + this.initialized = false; + var that = this; + addEvent(parent, 'mouseover', that._initialize); // Initialization trigger. +}; +DirectoryProxy.prototype = { + _initialize: function() { + this.warning = document.createElement('div'); + this.parent.appendChild(this.warning); + this.warning.innerHTML = 'The company directory is loading...'; + + this.directory = new PersonnelDirectory(this.parent); + var that = this; + this.interval = setInterval(that._checkInitialization, 100); + }, + _checkInitialization: function() { + if(this.directory.currentPage != null) { + clearInterval(this.interval); + this.initialized = true; + this.parent.removeChild(this.warning); + } + }, + showPage: function(page) { + if(!this.initialized) { + return; + } + return this.directory.showPage(page); + } +}; diff --git a/Chapter14/14.07 - General pattern for creating a virtual proxy.js b/Chapter14/14.07 - General pattern for creating a virtual proxy.js new file mode 100644 index 0000000..d1558f9 --- /dev/null +++ b/Chapter14/14.07 - General pattern for creating a virtual proxy.js @@ -0,0 +1,89 @@ +/* DynamicProxy abstract class, incomplete. */ + +var DynamicProxy = function() { + this.args = arguments; + this.initialized = false; +}; +DynamicProxy.prototype = { + _initialize: function() { + this.subject = {}; // Instantiate the class. + this.class.apply(this.subject, this.args); + this.subject.__proto__ = this.class.prototype; + + var that = this; + this.interval = setInterval(function() { that._checkInitialization(); }, 100); + }, + _checkInitialization: function() { + if(this._isInitialized()) { + clearInterval(this.interval); + this.initialized = true; + } + }, + _isInitialized: function() { // Must be implemented in the subclass. + throw new Error('Unsupported operation on an abstract class.'); + } +}; + +/* DynamicProxy abstract class, complete. */ + +var DynamicProxy = function() { + this.args = arguments; + this.initialized = false; + + if(typeof this.class != 'function') { + throw new Error('DynamicProxy: the class attribute must be set before ' + + 'calling the super-class constructor.'); + } + + // Create the methods needed to implement the same interface. + for(var key in this.class.prototype) { + // Ensure that the property is a function. + if(typeof this.class.prototype[key] !== 'function') { + continue; + } + + // Add the method. + var that = this; + (function(methodName) { + that[methodName] = function() { + if(!that.initialized) { + return + } + return that.subject[methodName].apply(that.subject, arguments); + }; + })(key); + } +}; +DynamicProxy.prototype = { + _initialize: function() { + this.subject = {}; // Instantiate the class. + this.class.apply(this.subject, this.args); + this.subject.__proto__ = this.class.prototype; + + var that = this; + this.interval = setInterval(function() { that._checkInitialization(); }, 100); + }, + _checkInitialization: function() { + if(this._isInitialized()) { + clearInterval(this.interval); + this.initialized = true; + } + }, + _isInitialized: function() { // Must be implemented in the subclass. + throw new Error('Unsupported operation on an abstract class.'); + } +}; + +/* TestProxy class. */ + +var TestProxy = function() { + this.class = TestClass; + var that = this; + addEvent($('test-link'), 'click', function() { that._initialize(); }); + // Initialization trigger. + TestProxy.superclass.constructor.apply(this, arguments); +}; +extend(TestProxy, DynamicProxy); +TestProxy.prototype._isInitialized = function() { + ... // Initialization condition goes here. +}; diff --git a/Chapter15/15.01 - Sellsian approach.js b/Chapter15/15.01 - Sellsian approach.js new file mode 100644 index 0000000..0cdce8f --- /dev/null +++ b/Chapter15/15.01 - Sellsian approach.js @@ -0,0 +1,27 @@ +/* From http://pluralsight.com/blogs/dbox/archive/2007/01/24/45864.aspx */ + +/* + * Publishers are in charge of "publishing" i.e. creating the event. + * They're also in charge of "notifying" (firing the event). +*/ +var Publisher = new Observable; + +/* + * Subscribers basically... "subscribe" (or listen). + * Once they've been "notified" their callback functions are invoked. +*/ +var Subscriber = function(news) { + // news delivered directly to my front porch +}; +Publisher.subscribeCustomer(Subscriber); + +/* + * Deliver a paper: + * sends out the news to all subscribers. +*/ +Publisher.deliver('extre, extre, read all about it'); + +/* + * That customer forgot to pay his bill. +*/ +Publisher.unSubscribeCustomer(Subscriber); diff --git a/Chapter15/15.02 - Newspapers and subscribers.js b/Chapter15/15.02 - Newspapers and subscribers.js new file mode 100644 index 0000000..a83f421 --- /dev/null +++ b/Chapter15/15.02 - Newspapers and subscribers.js @@ -0,0 +1,60 @@ +/* + * Newspaper Vendors + * setup as new Publisher objects +*/ +var NewYorkTimes = new Publisher; +var AustinHerald = new Publisher; +var SfChronicle = new Publisher; + + +/* + * People who like to read + * (Subscribers) + * + * Each subscriber is set up as a callback method. + * They all inherit from the Function prototype Object. +*/ +var Joe = function(from) { + console.log('Delivery from '+from+' to Joe'); +}; +var Lindsay = function(from) { + console.log('Delivery from '+from+' to Lindsay'); +}; +var Quadaras = function(from) { + console.log('Delivery from '+from+' to Quadaras '); +}; + +/* + * Here we allow them to subscribe to newspapers + * which are the Publisher objects. + * In this case Joe subscribes to the NY Times and + * the Chronicle. Lindsay subscribes to NY Times + * Austin Herald and Chronicle. And the Quadaras + * respectfully subscribe to the Herald and the Chronicle +*/ +Joe. + subscribe(NewYorkTimes). + subscribe(SfChronicle); + +Lindsay. + subscribe(AustinHerald). + subscribe(SfChronicle). + subscribe(NewYorkTimes); + +Quadaras. + subscribe(AustinHerald). + subscribe(SfChronicle); + +/* + * Then at any given time in our application, our publishers can send + * off data for the subscribers to consume and react to. +*/ +NewYorkTimes. + deliver('Here is your paper! Direct from the Big apple'); +AustinHerald. + deliver('News'). + deliver('Reviews'). + deliver('Coupons'); +SfChronicle. + deliver('The weather is still chilly'). + deliver('Hi Mom! I\'m writing a book'); diff --git a/Chapter15/15.03 - Building an observer API.js b/Chapter15/15.03 - Building an observer API.js new file mode 100644 index 0000000..8f28415 --- /dev/null +++ b/Chapter15/15.03 - Building an observer API.js @@ -0,0 +1,50 @@ +function Publisher() { + this.subscribers = []; +} + +Publisher.prototype.deliver = function(data) { + this.subscribers.forEach( + function(fn) { + fn(data); + } + ); + return this; +}; + +Function.prototype.subscribe = function(publisher) { + var that = this; + var alreadyExists = publisher.subscribers.some( + function(el) { + if ( el === that ) { + return; + } + } + ); + if ( !alreadyExists ) { + publisher.subscribers.push(this); + } + return this; +}; + +Function.prototype.unsubscribe = function(publisher) { + var that = this; + publisher.subscribers = publisher.subscribers.filter( + function(el) { + if ( el !== that ) { + return el; + } + } + ); + return this; +}; + +var publisherObject = new Publisher; + +var observerObject = function(data) { + // process data + console.log(data); + // unsubscribe from this publisher + arguments.callee.unsubscribe(publisherObject); +}; + +observerObject.subscribe(publisherObject); diff --git a/Chapter15/15.04 - Animation example.js b/Chapter15/15.04 - Animation example.js new file mode 100644 index 0000000..6f40826 --- /dev/null +++ b/Chapter15/15.04 - Animation example.js @@ -0,0 +1,35 @@ +// Publisher API +var Animation = function(o) { + this.onStart = new Publisher, + this.onComplete = new Publisher, + this.onTween = new Publisher; +}; +Animation. + method('fly', function() { + // begin animation + this.onStart.deliver(); + for ( ... ) { // loop through frames + // deliver frame number + this.onTween.deliver(i); + } + // end animation + this.onComplete.deliver(); + }); + +// setup an account with the animation manager +var Superman = new Animation({...config properties...}); + +// Begin implementing subscribers +var putOnCape = function(i) { }; +var takeOffCape = function(i) { }; + +putOnCape.subscribe(Superman.onStart); +takeOffCape.subscribe(Superman.onComplete); + + +// fly can be called anywhere +Superman.fly(); +// for instance: +addEvent(element, 'click', function() { + Superman.fly(); +}); diff --git a/Chapter15/15.05 - Event listeners are also observers.js b/Chapter15/15.05 - Event listeners are also observers.js new file mode 100644 index 0000000..2673e5d --- /dev/null +++ b/Chapter15/15.05 - Event listeners are also observers.js @@ -0,0 +1,25 @@ +// example using listeners +var element = document.getElementById(‘a’); +var fn1 = function(e) { + // handle click +}; +var fn2 = function(e) { + // do other stuff with click +}; + +addEvent(element, ‘click’, fn1); +addEvent(element, ‘click’, fn2); + + + +// example using handlers +var element = document.getElementById(‘b’); +var fn1 = function(e) { + // handle click +}; +var fn2 = function(e) { + // do other stuff with click +}; + +element.onclick = fn1; +element.onclick = fn2; diff --git a/Chapter16/16.01 - StopAd and StartAd classes.js b/Chapter16/16.01 - StopAd and StartAd classes.js new file mode 100644 index 0000000..7fb6cd6 --- /dev/null +++ b/Chapter16/16.01 - StopAd and StartAd classes.js @@ -0,0 +1,35 @@ +/* AdCommand interface. */ + +var AdCommand = new Interface('AdCommand', ['execute']); + +/* StopAd command class. */ + +var StopAd = function(adObject) { // implements AdCommand + this.ad = adObject; +}; +StopAd.prototype.execute = function() { + this.ad.stop(); +}; + +/* StartAd command class. */ + +var StartAd = function(adObject) { // implements AdCommand + this.ad = adObject; +}; +StartAd.prototype.execute = function() { + this.ad.start(); +}; + + +/* Implementation code. */ + +var ads = getAds(); +for(var i = 0, len = ads.length; i < len; i++) { + // Create command objects for starting and stopping the ad. + var startCommand = new StartAd(ads[i]); + var stopCommand = new StopAd(ads[i]); + + // Create the UI elements that will execute the command on click. + new UiButton('Start ' + ads[i].name, startCommand); + new UiButton('Stop ' + ads[i].name, stopCommand); +} diff --git a/Chapter16/16.02 - Commands using closures.js b/Chapter16/16.02 - Commands using closures.js new file mode 100644 index 0000000..8f6d6ec --- /dev/null +++ b/Chapter16/16.02 - Commands using closures.js @@ -0,0 +1,20 @@ +/* Commands using closures. */ + +function makeStart(adObject) { + return function() { + adObject.start(); + }; +} +function makeStop(adObject) { + return function() { + adObject.stop(); + }; +} + +/* Implementation code. */ + +var startCommand = makeStart(ads[i]); +var stopCommand = makeStop(ads[i]); + +startCommand(); // Execute the functions directly instead of calling a method. +stopCommand(); diff --git a/Chapter16/16.03 - Using interfaces with the command pattern.js b/Chapter16/16.03 - Using interfaces with the command pattern.js new file mode 100644 index 0000000..81df463 --- /dev/null +++ b/Chapter16/16.03 - Using interfaces with the command pattern.js @@ -0,0 +1,19 @@ +/* Command interface. */ + +var Command = new Interface('Command', ['execute']); + +/* Checking the interface of a command object. */ + +// Ensure that the execute operation is defined. If not, a descriptive exception +// will be thrown. +Interface.ensureImplements(someCommand, Command); + +// If no exception is thrown, you can safely invoke the execute operation. +someCommand.execute(); + + +/* Checking command functions. */ + +if(typeof someCommand != 'function') { + throw new Error('Command isn't a function'); +} diff --git a/Chapter16/16.04 - Types of commands.js b/Chapter16/16.04 - Types of commands.js new file mode 100644 index 0000000..d9801e5 --- /dev/null +++ b/Chapter16/16.04 - Types of commands.js @@ -0,0 +1,47 @@ +/* SimpleCommand, a loosely coupled, simple command class. */ + +var SimpleCommand = function(receiver) { // implements Command + this.receiver = receiver; +}; +SimpleCommand.prototype.execute = function() { + this.receiver.action(); +}; + +/* ComplexCommand, a tightly coupled, complex command class. */ + +var ComplexCommand = function() { // implements Command + this.logger = new Logger(); + this.xhrHandler = XhrManager.createXhrHandler(); + this.parameters = {}; +}; +ComplexCommand.prototype = { + setParameter: function(key, value) { + this.parameters[key] = value; + }, + execute: function() { + this.logger.log('Executing command'); + var postArray = []; + for(var key in this.parameters) { + postArray.push(key + '=' + this.parameters[key]); + } + var postString = postArray.join('&'); + this.xhrHandler.request( + 'POST', + 'script.php', + function() {}, + postString + ); + } +}; + +/* GreyAreaCommand, somewhere between simple and complex. */ + +var GreyAreaCommand = function(recevier) { // implements Command + this.logger = new Logger(); + this.receiver = receiver; +}; +GreyAreaCommand.prototype.execute = function() { + this.logger.log('Executing command'); + this.receiver.prepareAction(); + this.receiver.action(); +}; diff --git a/Chapter16/16.05 - Menu commands.js b/Chapter16/16.05 - Menu commands.js new file mode 100644 index 0000000..e5ec383 --- /dev/null +++ b/Chapter16/16.05 - Menu commands.js @@ -0,0 +1,181 @@ +/* Command, Composite and MenuObject interfaces. */ + +var Command = new Interface('Command', ['execute']); +var Composite = new Interface('Composite', ['add', 'remove', 'getChild', + 'getElement']); +var MenuObject = new Interface('MenuObject', ['show']); + +/* MenuBar class, a composite. */ + +var MenuBar = function() { // implements Composite, MenuObject + this.menus = {}; + this.element = document.createElement('ul'); + this.element.style.display = 'none'; +}; +MenuBar.prototype = { + add: function(menuObject) { + Interface.ensureImplements(menuObject, Composite, MenuObject); + this.menus[menuObject.name] = menuObject; + this.element.appendChild(this.menus[menuObject.name].getElement()); + }, + remove: function(name) { + delete this.menus[name]; + }, + getChild: function(name) { + return this.menus[name]; + }, + getElement: function() { + return this.element; + }, + + show: function() { + this.element.style.display = 'block'; + for(name in this.menus) { // Pass the call down the composite. + this.menus[name].show(); + } + } +}; + +/* Menu class, a composite. */ + +var Menu = function(name) { // implements Composite, MenuObject + this.name = name; + this.items = {}; + this.element = document.createElement('li'); + this.element.innerHTML = this.name; + this.element.style.display = 'none'; + this.container = document.createElement('ul'); + this.element.appendChild(this.container); +}; +Menu.prototype = { + add: function(menuItemObject) { + Interface.ensureImplements(menuItemObject, Composite, MenuObject); + this.items[menuItemObject.name] = menuItemObject; + this.container.appendChild(this.items[menuItemObject.name].getElement()); + }, + remove: function(name) { + delete this.items[name]; + }, + getChild: function(name) { + return this.items[name]; + }, + getElement: function() { + return this.element; + }, + + show: function() { + this.element.style.display = 'block'; + for(name in this.items) { // Pass the call down the composite. + this.items[name].show(); + } + } +}; + +/* MenuItem class, a leaf. */ + +var MenuItem = function(name, command) { // implements Composite, MenuObject + Interface.ensureImplements(command, Command); + this.name = name; + this.element = document.createElement('li'); + this.element.style.display = 'none'; + this.anchor = document.createElement('a'); + this.anchor.href = '#'; // To make it clickable. + this.element.appendChild(this.anchor); + this.anchor.innerHTML = this.name; + + addEvent(this.anchor, 'click', function(e) { // Invoke the command on click. + e.preventDefault(); + command.execute(); + }); +}; +MenuItem.prototype = { + add: function() {}, + remove: function() {}, + getChild: function() {}, + getElement: function() { + return this.element; + }, + + show: function() { + this.element.style.display = 'block'; + } +}; + + +/* MenuCommand class, a command object. */ + +var MenuCommand = function(action) { // implements Command + this.action = action; +}; +MenuCommand.prototype.execute = function() { + this.action(); +}; + + +/* Implementation code. */ + +/* Receiver objects, instantiated from existing classes. */ +var fileActions = new FileActions(); +var editActions = new EditActions(); +var insertActions = new InsertActions(); +var helpActions = new HelpActions(); + +/* Create the menu bar. */ +var appMenuBar = new MenuBar(); + +/* The File menu. */ +var fileMenu = new Menu('File'); + +var openCommand = new MenuCommand(fileActions.open); +var closeCommand = new MenuCommand(fileActions.close); +var saveCommand = new MenuCommand(fileActions.save); +var saveAsCommand = new MenuCommand(fileActions.saveAs); + +fileMenu.add(new MenuItem('Open', openCommand)); +fileMenu.add(new MenuItem('Close', closeCommand)); +fileMenu.add(new MenuItem('Save', saveCommand)); +fileMenu.add(new MenuItem('Save As...', saveAsCommand)); + +appMenuBar.add(fileMenu); + +/* The Edit menu. */ +var editMenu = new Menu('Edit'); + +var cutCommand = new MenuCommand(editActions.cut); +var copyCommand = new MenuCommand(editActions.copy); +var pasteCommand = new MenuCommand(editActions.paste); +var deleteCommand = new MenuCommand(editActions.delete); + +editMenu.add(new MenuItem('Cut', cutCommand)); +editMenu.add(new MenuItem('Copy', copyCommand)); +editMenu.add(new MenuItem('Paste', pasteCommand)); +editMenu.add(new MenuItem('Delete', deleteCommand)); + +appMenuBar.add(editMenu); + +/* The Insert menu. */ +var insertMenu = new Menu('Insert'); + +var textBlockCommand = new MenuCommand(insertActions.textBlock); +insertMenu.add(new MenuItem('Text Block', textBlockCommand)); + +appMenuBar.add(insertMenu); + +/* The Help menu. */ +var helpMenu = new Menu('Help'); + +var showHelpCommand = new MenuCommand(helpActions.showHelp); +helpMenu.add(new MenuItem('Show Help', showHelpCommand)); + +appMenuBar.add(helpMenu); + +/* Build the menu bar. */ +document.getElementsByTagName('body')[0].appendChild(appMenuBar.getElement()); +appMenuBar.show(); + + +/* Adding more menu items later on. */ + +var imageCommand = new MenuCommand(insertActions.image); +insertMenu.add(new MenuItem('Image', imageCommand)); + diff --git a/Chapter16/16.06 - Undo with reversible commands.js b/Chapter16/16.06 - Undo with reversible commands.js new file mode 100644 index 0000000..0e8bf92 --- /dev/null +++ b/Chapter16/16.06 - Undo with reversible commands.js @@ -0,0 +1,137 @@ +/* ReversibleCommand interface. */ + +var ReversibleCommand = new Interface('ReversibleCommand', ['execute', 'undo']); + +/* Movement commands. */ + +var MoveUp = function(cursor) { // implements ReversibleCommand + this.cursor = cursor; +}; +MoveUp.prototype = { + execute: function() { + cursor.move(0, -10); + }, + undo: function() { + cursor.move(0, 10); + } +}; + +var MoveDown = function(cursor) { // implements ReversibleCommand + this.cursor = cursor; +}; +MoveDown.prototype = { + execute: function() { + cursor.move(0, 10); + }, + undo: function() { + cursor.move(0, -10); + } +}; + +var MoveLeft = function(cursor) { // implements ReversibleCommand + this.cursor = cursor; +}; +MoveLeft.prototype = { + execute: function() { + cursor.move(-10, 0); + }, + undo: function() { + cursor.move(10, 0); + } +}; + +var MoveRight = function(cursor) { // implements ReversibleCommand + this.cursor = cursor; +}; +MoveRight.prototype = { + execute: function() { + cursor.move(10, 0); + }, + undo: function() { + cursor.move(-10, 0); + } +}; + +/* Cursor class. */ + +var Cursor = function(width, height, parent) { + this.width = width; + this.height = height; + this.position = { x: width / 2, y: height / 2 }; + + this.canvas = document.createElement('canvas'); + this.canvas.width = this.width; + this.canvas.height = this.height; + parent.appendChild(this.canvas); + + this.ctx = this.canvas.getContext('2d'); + this.ctx.fillStyle = '#cc0000'; + this.move(0, 0); +}; +Cursor.prototype.move = function(x, y) { + this.position.x += x; + this.position.y += y; + + this.ctx.clearRect(0, 0, this.width, this.height); + this.ctx.fillRect(this.position.x, this.position.y, 3, 3); +}; + +/* UndoDecorator class. */ + +var UndoDecorator = function(command, undoStack) { // implements ReversibleCommand + this.command = command; + this.undoStack = undoStack; +}; +UndoDecorator.prototype = { + execute: function() { + this.undoStack.push(this.command); + this.command.execute(); + }, + undo: function() { + this.command.undo(); + } +}; + +/* CommandButton class. */ + +var CommandButton = function(label, command, parent) { + Interface.ensureImplements(command, ReversibleCommand); + this.element = document.createElement('button'); + this.element.innerHTML = label; + parent.appendChild(this.element); + + addEvent(this.element, 'click', function() { + command.execute(); + }); +}; + +/* UndoButton class. */ + +var UndoButton = function(label, parent, undoStack) { + this.element = document.createElement('button'); + this.element.innerHTML = label; + parent.appendChild(this.element); + + addEvent(this.element, 'click', function() { + if(undoStack.length === 0) return; + var lastCommand = undoStack.pop(); + lastCommand.undo(); + }); +}; + +/* Implementation code. */ + +var body = document.getElementsByTagName('body')[0]; +var cursor = new Cursor(400, 400, body); +var undoStack = []; + +var upCommand = new UndoDecorator(new MoveUp(cursor), undoStack); +var downCommand = new UndoDecorator(new MoveDown(cursor), undoStack); +var leftCommand = new UndoDecorator(new MoveLeft(cursor), undoStack); +var rightCommand = new UndoDecorator(new MoveRight(cursor), undoStack); + +var upButton = new CommandButton('Up', upCommand, body); +var downButton = new CommandButton('Down', downCommand, body); +var leftButton = new CommandButton('Left', leftCommand, body); +var rightButton = new CommandButton('Right', rightCommand, body); +var undoButton = new UndoButton('Undo', body, undoStack); diff --git a/Chapter16/16.07 - Undo with command logging.js b/Chapter16/16.07 - Undo with command logging.js new file mode 100644 index 0000000..b27b4d4 --- /dev/null +++ b/Chapter16/16.07 - Undo with command logging.js @@ -0,0 +1,81 @@ +/* Movement commands. */ + +var MoveUp = function(cursor) { // implements Command + this.cursor = cursor; +}; +MoveUp.prototype = { + execute: function() { + cursor.move(0, -10); + } +}; + +/* Cursor class, with an internal command stack. */ + +var Cursor = function(width, height, parent) { + this.width = width; + this.height = height; + this.commandStack = []; + + this.canvas = document.createElement('canvas'); + this.canvas.width = this.width; + this.canvas.height = this.height; + parent.appendChild(this.canvas); + + this.ctx = this.canvas.getContext('2d'); + this.ctx.strokeStyle = '#cc0000'; + this.move(0, 0); +}; +Cursor.prototype = { + move: function(x, y) { + var that = this; + this.commandStack.push(function() { that.lineTo(x, y); }); + this.executeCommands(); + }, + lineTo: function(x, y) { + this.position.x += x; + this.position.y += y; + this.ctx.lineTo(this.position.x, this.position.y); + }, + executeCommands: function() { + this.position = { x: this.width / 2, y: this.height / 2 }; + this.ctx.clearRect(0, 0, this.width, this.height); // Clear the canvas. + this.ctx.beginPath(); + this.ctx.moveTo(this.position.x, this.position.y); + for(var i = 0, len = this.commandStack.length; i < len; i++) { + this.commandStack[i](); + } + this.ctx.stroke(); + }, + undo: function() { + this.commandStack.pop(); + this.executeCommands(); + } +}; + +/* UndoButton class. */ + +var UndoButton = function(label, parent, cursor) { + this.element = document.createElement('button'); + this.element.innerHTML = label; + parent.appendChild(this.element); + + addEvent(this.element, 'click', function() { + cursor.undo(); + }); +}; + +/* Implementation code. */ + +var body = document.getElementsByTagName('body')[0]; +var cursor = new Cursor(400, 400, body); + +var upCommand = new MoveUp(cursor); +var downCommand = new MoveDown(cursor); +var leftCommand = new MoveLeft(cursor); +var rightCommand = new MoveRight(cursor); + +var upButton = new CommandButton('Up', upCommand, body); +var downButton = new CommandButton('Down', downCommand, body); +var leftButton = new CommandButton('Left', leftCommand, body); +var rightButton = new CommandButton('Right', rightCommand, body); +var undoButton = new UndoButton('Undo', body, cursor); diff --git a/Chapter17/17.01 - PublicLibrary class.js b/Chapter17/17.01 - PublicLibrary class.js new file mode 100644 index 0000000..2c0e05f --- /dev/null +++ b/Chapter17/17.01 - PublicLibrary class.js @@ -0,0 +1,65 @@ +/* Interfaces. */ + +var Publication = new Interface('Publication', ['getIsbn', 'setIsbn', 'getTitle', + 'setTitle', 'getAuthor', 'setAuthor', 'getGenres', 'setGenres', 'display']); +var Library = new Interface('Library', [‘addBook’, 'findBooks', 'checkoutBook', + 'returnBook']); +var Catalog = new Interface('Catalog', ['handleFilingRequest', 'findBooks', + 'setSuccessor']); + +/* Book class. */ + +var Book = function(isbn, title, author, genres) { // implements Publication + ... +} + + +/* PublicLibrary class. */ + +var PublicLibrary = function(books) { // implements Library + this.catalog = {}; + for(var i = 0, len = books.length; i < len; i++) { + this.addBook(books[i]); + } +}; +PublicLibrary.prototype = { + findBooks: function(searchString) { + var results = []; + for(var isbn in this.catalog) { + if(!this.catalog.hasOwnProperty(isbn)) continue; + if(this.catalog[isbn].getTitle().match(searchString) || + this.catalog[isbn].getAuthor().match(searchString)) { + results.push(this.catalog[isbn]); + } + } + return results; + }, + checkoutBook: function(book) { + var isbn = book.getIsbn(); + if(this.catalog[isbn]) { + if(this.catalog[isbn].available) { + this.catalog[isbn].available = false; + return this.catalog[isbn]; + } + else { + throw new Error('PublicLibrary: book ' + book.getTitle() + + ' is not currently available.'); + } + } + else { + throw new Error('PublicLibrary: book ' + book.getTitle() + ' not found.'); + } + }, + returnBook: function(book) { + var isbn = book.getIsbn(); + if(this.catalog[isbn]) { + this.catalog[isbn].available = true; + } + else { + throw new Error('PublicLibrary: book ' + book.getTitle() + ' not found.'); + } + }, + addBook: function(newBook) { + this.catalog[newBook.getIsbn()] = { book: newBook, available: true }; + } +}; diff --git a/Chapter17/17.02 - PublicLibrary class with hard-coded catalogs.js b/Chapter17/17.02 - PublicLibrary class with hard-coded catalogs.js new file mode 100644 index 0000000..8d9fd4b --- /dev/null +++ b/Chapter17/17.02 - PublicLibrary class with hard-coded catalogs.js @@ -0,0 +1,30 @@ +/* PublicLibrary class, with hard-coded catalogs for genre. */ + +var PublicLibrary = function(books) { // implements Library + this.catalog = {}; + this.biographyCatalog = new BiographyCatalog(); + this.fantasyCatalog = new FantasyCatalog(); + this.mysteryCatalog = new MysteryCatalog(); + this.nonFictionCatalog = new NonFictionCatalog(); + this.sciFiCatalog = new SciFiCatalog(); + + for(var i = 0, len = books.length; i < len; i++) { + this.addBook(books[i]); + } +}; +PublicLibrary.prototype = { + findBooks: function(searchString) { ... }, + checkoutBook: function(book) { ... }, + returnBook: function(book) { ... }, + addBook: function(newBook) { + // Always add the book to the main catalog. + this.catalog[newBook.getIsbn()] = { book: newBook, available: true }; + + // Try to add the book to each genre catalog. + this.biographyCatalog.handleFilingRequest(newBook); + this.fantasyCatalog.handleFilingRequest(newBook); + this.mysteryCatalog.handleFilingRequest(newBook); + this.nonFictionCatalog.handleFilingRequest(newBook); + this.sciFiCatalog.handleFilingRequest(newBook); + } +}; diff --git a/Chapter17/17.03 - PublicLibrary class with chain of responsibility catalogs.js b/Chapter17/17.03 - PublicLibrary class with chain of responsibility catalogs.js new file mode 100644 index 0000000..9e692d8 --- /dev/null +++ b/Chapter17/17.03 - PublicLibrary class with chain of responsibility catalogs.js @@ -0,0 +1,47 @@ +/* PublicLibrary class, with genre catalogs in a chain of responsibility. */ + +var PublicLibrary = function(books, firstGenreCatalog) { // implements Library + this.catalog = {}; + this.firstGenreCatalog = firstGenreCatalog; + + for(var i = 0, len = books.length; i < len; i++) { + this.addBook(books[i]); + } +}; +PublicLibrary.prototype = { + findBooks: function(searchString) { ... }, + checkoutBook: function(book) { ... }, + returnBook: function(book) { ... }, + addBook: function(newBook) { + // Always add the book to the main catalog. + this.catalog[newBook.getIsbn()] = { book: newBook, available: true }; + + // Try to add the book to each genre catalog. + this.firstGenreCatalog.handleFilingRequest(newBook); + } +}; + + +// ----------------------------------------------------------------------------- +// Usage example. +// ----------------------------------------------------------------------------- + + // Instantiate the catalogs. +var biographyCatalog = new BiographyCatalog(); +var fantasyCatalog = new FantasyCatalog(); +var mysteryCatalog = new MysteryCatalog(); +var nonFictionCatalog = new NonFictionCatalog(); +var sciFiCatalog = new SciFiCatalog(); + +// Set the links in the chain. +biographyCatalog.setSuccessor(fantasyCatalog); +fantasyCatalog.setSuccessor(mysteryCatalog); +mysteryCatalog.setSuccessor(nonFictionCatalog); +nonFictionCatalog.setSuccessor(sciFiCatalog); + +// Give the first link in the chain as an argument to the constructor. +var myLibrary = new PublicLibrary(books, biographyCatalog); + +// You can add links to the chain whenever you like. +var historyCatalog = new HistoryCatalog(); +sciFiCatalog.setSuccessor(historyCatalog); diff --git a/Chapter17/17.04 - GenreCatalog and SciFiCatalog classes.js b/Chapter17/17.04 - GenreCatalog and SciFiCatalog classes.js new file mode 100644 index 0000000..9cea14f --- /dev/null +++ b/Chapter17/17.04 - GenreCatalog and SciFiCatalog classes.js @@ -0,0 +1,51 @@ +/* GenreCatalog class, used as a superclass for specific catalog classes. */ + +var GenreCatalog = function() { // implements Catalog + this.successor = null; + this.catalog = []; +}; +GenreCatalog.prototype = { + _bookMatchesCriteria: function(book) { + return false; // Default implementation; this method will be overriden in + // the subclasses. + } + handleFilingRequest: function(book) { + // Check to see if the book belongs in this catagory. + if(this._bookMatchesCriteria(book)) { + this.catalog.push(book); + } + // Pass the request on to the next link. + if(this.successor) { + this.successor.handleFilingRequest(book); + } + }, + findBooks: function(request) { + if(this.successor) { + return this.successor.findBooks(request); + } + }, + setSuccessor: function(successor) { + if(Interface.ensureImplements(successor, Catalog) { + this.successor = successor; + } + } +}; + + +/* SciFiCatalog class. */ + +var SciFiCatalog = function() {}; // implements Catalog +extend(SciFiCatalog, GenreCatalog); +SciFiCatalog.prototype._bookMatchesCriteria = function(book) { + var genres = book.getGenres(); + if(book.getTitle().match(/space/i)) { + return true; + } + for(var i = 0, len = genres.length; i < len; i++) { + var genre = genres[i].toLowerCase(); + if(genres === 'sci-fi' || genres === 'scifi' || genres === 'science fiction') { + return true; + } + } + return false; +}; diff --git a/Chapter17/17.05 - The findBooks method.js b/Chapter17/17.05 - The findBooks method.js new file mode 100644 index 0000000..7182ad1 --- /dev/null +++ b/Chapter17/17.05 - The findBooks method.js @@ -0,0 +1,96 @@ +/* PublicLibrary class. */ + +var PublicLibrary = function(books) { // implements Library + ... +}; +PublicLibrary.prototype = { + findBooks: function(searchString, genres) { + // If the optional genres argument is given, search for books only in + // those genres. Use the chain of responsibility to perform the search. + if(typeof genres === 'array' && genres.length > 0) { + var requestObject = { + searchString: searchString, + genres: genres, + results: [] + }; + var responseObject = this.firstGenreCatalog.findBooks(requestObject); + return responseObject.results; + } + // Otherwise, search through all books. + else { + var results = []; + for(var isbn in this.catalog) { + if(!this.catalog.hasOwnProperty(isbn)) continue; + if(this.catalog[isbn].getTitle().match(searchString) || + this.catalog[isbn].getAuthor().match(searchString)) { + results.push(this.catalog[isbn]); + } + } + return results; + } + }, + checkoutBook: function(book) { ... }, + returnBook: function(book) { ... }, + addBook: function(newBook) { ... } +}; + +/* GenreCatalog class, used as a superclass for specific catalog classes. */ + +var GenreCatalog = function() { // implements Catalog + this.successor = null; + this.catalog = []; + this.genreNames = []; +}; +GenreCatalog.prototype = { + _bookMatchesCriteria: function(book) { ... } + handleFilingRequest: function(book) { ... }, + findBooks: function(request) { + var found = false; + for(var i = 0, len = request.genres.length; i < len; i++) { + for(var j = 0, nameLen = this.genreNames.length; j < nameLen; j++) { + if(this.genreNames[j] === request.genres[i]) { + found = true; // This link in the chain should handle + // the request. + break; + } + } + } + + if(found) { // Search through this catalog for books that match the search + // string and aren't already in the results. + outerloop: for(var i = 0, len = this.catalog.length; i < len; i++) { + var book = this.catalog[i]; + if(book.getTitle().match(searchString) || + book.getAuthor().match(searchString)) { + for(var j = 0, requestLen = request.results.length; j < requestLen; j++) { + if(request.results[j].getIsbn() === book.getIsbn()) { + continue outerloop; // The book is already in the results; skip it. + } + } + request.results.push(book); // The book matches and doesn't already + // appear in the results. Add it. + } + } + } + + // Continue to pass the request down the chain if the successor is set. + if(this.successor) { + return this.successor.findBooks(request); + } + // Otherwise, we have reached the end of the chain. Return the request + // object back up the chain. + else { + return request; + } + }, + setSuccessor: function(successor) { ... } +}; + + +/* SciFiCatalog class. */ + +var SciFiCatalog = function() { // implements Catalog + this.genreNames = ['sci-fi', 'scifi', 'science fiction']; +}; +extend(SciFiCatalog, GenreCatalog); +SciFiCatalog.prototype._bookMatchesCriteria = function(book) { ... }; diff --git a/Chapter17/17.06 - DynamicGallery class from Chapter 9.js b/Chapter17/17.06 - DynamicGallery class from Chapter 9.js new file mode 100644 index 0000000..1bad7b7 --- /dev/null +++ b/Chapter17/17.06 - DynamicGallery class from Chapter 9.js @@ -0,0 +1,73 @@ +/* Interfaces. */ + +var Composite = new Interface('Composite', ['add', 'remove', 'getChild']); +var GalleryItem = new Interface('GalleryItem', ['hide', 'show']); + +/* DynamicGallery class. */ + +var DynamicGallery = function(id) { // implements Composite, GalleryItem + this.children = []; + this.element = document.createElement('div'); + this.element.id = id; + this.element.className = 'dynamic-gallery'; +} +DynamicGallery.prototype = { + add: function(child) { + Interface.ensureImplements(child, Composite, GalleryItem); + this.children.push(child); + this.element.appendChild(child.getElement()); + }, + remove: function(child) { + for(var node, i = 0; node = this.getChild(i); i++) { + if(node == child) { + this.formComponents[i].splice(i, 1); + break; + } + } + this.element.removeChild(child.getElement()); + }, + getChild: function(i) { + return this.children[i]; + }, + + hide: function() { + for(var node, i = 0; node = this.getChild(i); i++) { + node.hide(); + } + this.element.style.display = 'none'; + }, + show: function() { + this.element.style.display = ''; + for(var node, i = 0; node = this.getChild(i); i++) { + node.show(); + } + }, + + getElement: function() { + return this.element; + } +}; + +/* GalleryImage class. */ + +var GalleryImage = function(src) { // implements Composite, GalleryItem + this.element = document.createElement('img'); + this.element.className = 'gallery-image'; + this.element.src = src; +} +GalleryImage.prototype = { + add: function() {}, // This is a leaf node, so we don't + remove: function() {}, // implement these methods, we just + getChild: function() {}, // define them. + + hide: function() { + this.element.style.display = 'none'; + }, + show: function() { + this.element.style.display = ''; + }, + + getElement: function() { + return this.element; + } +}; diff --git a/Chapter17/17.07 - DynamicGallery class with optimization.js b/Chapter17/17.07 - DynamicGallery class with optimization.js new file mode 100644 index 0000000..9868009 --- /dev/null +++ b/Chapter17/17.07 - DynamicGallery class with optimization.js @@ -0,0 +1,15 @@ +/* DynamicGallery class. */ + +var DynamicGallery = function(id) { // implements Composite, GalleryItem + ... +} +DynamicGallery.prototype = { + add: function(child) { ... }, + remove: function(child) { ... }, + getChild: function(i) { ... }, + hide: function() { + this.element.style.display = 'none'; + }, + show: function() { ... }, + getElement: function() { ... } +}; diff --git a/Chapter17/17.08 - DynamicGallery class with tags.js b/Chapter17/17.08 - DynamicGallery class with tags.js new file mode 100644 index 0000000..f7d7223 --- /dev/null +++ b/Chapter17/17.08 - DynamicGallery class with tags.js @@ -0,0 +1,76 @@ +/* Interfaces. */ + +var Composite = new Interface('Composite', ['add', 'remove', 'getChild', + 'getAllLeaves']); +var GalleryItem = new Interface('GalleryItem', ['hide', 'show', 'addTag', + 'getPhotosWithTag']); + +/* DynamicGallery class. */ + +var DynamicGallery = function(id) { // implements Composite, GalleryItem + this.children = []; + this.tags = []; + this.element = document.createElement('div'); + this.element.id = id; + this.element.className = 'dynamic-gallery'; +} +DynamicGallery.prototype = { + ... + addTag: function(tag) { + this.tags.push(tag); + for(var node, i = 0; node = this.getChild(i); i++) { + node.addTag(tag); + } + }, + getAllLeaves: function() { + var leaves = []; + for(var node, i = 0; node = this.getChild(i); i++) { + leaves.concat(node.getAllLeaves()); + } + return leaves; + }, + getPhotosWithTag: function(tag) { + // First search in this object's tags; if the tag is found here, we can stop + // the search and just return all the leaf nodes. + for(var i = 0, len = this.tags.length; i < len; i++) { + if(this.tags[i] === tag) { + return this.getAllLeaves(); + } + } + + // If the tag isn't found in this object's tags, pass the request down + // the hierarchy. + for(var results = [], node, i = 0; node = this.getChild(i); i++) { + results.concat(node.getPhotosWithTag(tag)); + } + return results; + }, + ... +}; + +/* GalleryImage class. */ + +var GalleryImage = function(src) { // implements Composite, GalleryItem + this.element = document.createElement('img'); + this.element.className = 'gallery-image'; + this.element.src = src; + this.tags = []; +} +GalleryImage.prototype = { + ... + addTag: function(tag) { + this.tags.push(tag); + }, + getAllLeaves: function() { // Just return this. + return [this]; + }, + getPhotosWithTag: function(tag) { + for(var i = 0, len = this.tags.length; i < len; i++) { + if(this.tags[i] === tag) { + return [this]; + } + } + return []; // Return an empty array if no matches were found. + }, + ... +}; diff --git a/Introduction/Interface.js b/Introduction/Interface.js new file mode 100644 index 0000000..aabef9e --- /dev/null +++ b/Introduction/Interface.js @@ -0,0 +1,72 @@ +// Constructor. + +var Interface = function(name, methods) { + if(arguments.length != 2) { + throw new Error("Interface constructor called with " + arguments.length + + "arguments, but expected exactly 2."); + } + + this.name = name; + this.methods = []; + for(var i = 0, len = methods.length; i < len; i++) { + if(typeof methods[i] !== 'string') { + throw new Error("Interface constructor expects method names to be " + + "passed in as a string."); + } + this.methods.push(methods[i]); + } +}; + +// Static class method. + +Interface.ensureImplements = function(object) { + if(arguments.length < 2) { + throw new Error("Function Interface.ensureImplements called with " + + arguments.length + "arguments, but expected at least 2."); + } + + for(var i = 1, len = arguments.length; i < len; i++) { + var interface = arguments[i]; + if(interface.constructor !== Interface) { + throw new Error("Function Interface.ensureImplements expects arguments " + + "two and above to be instances of Interface."); + } + + for(var j = 0, methodsLen = interface.methods.length; j < methodsLen; j++) { + var method = interface.methods[j]; + if(!object[method] || typeof object[method] !== 'function') { + throw new Error("Function Interface.ensureImplements: object " + + "does not implement the " + interface.name + + " interface. Method " + method + " was not found."); + } + } + } +}; + + +/* + + +// Example usage: + +// Interfaces. + +var Composite = new Interface('Composite', ['add', 'remove', 'getChild']); +var FormItem = new Interface('FormItem', ['save']); + +// CompositeForm class + +var CompositeForm = function(id, method, action) { // implements Composite, FormItem + ... +}; + +... + +function addForm(formInstance) { + Interface.ensureImplements(formInstance, Composite, FormItem); + // This function will throw an error if a required method is not implemented, halting execution. + // All code beneath this line will be executed only if the checks pass. + ... +} + +*/ diff --git a/Introduction/Library.js b/Introduction/Library.js new file mode 100644 index 0000000..c3a26ac --- /dev/null +++ b/Introduction/Library.js @@ -0,0 +1,167 @@ +/* Reference Article: http://www.dustindiaz.com/top-ten-javascript/ */ + +/* addEvent: simplified event attachment */ +function addEvent( obj, type, fn ) { + if (obj.addEventListener) { + obj.addEventListener( type, fn, false ); + EventCache.add(obj, type, fn); + } + else if (obj.attachEvent) { + obj["e"+type+fn] = fn; + obj[type+fn] = function() { obj["e"+type+fn]( window.event ); } + obj.attachEvent( "on"+type, obj[type+fn] ); + EventCache.add(obj, type, fn); + } + else { + obj["on"+type] = obj["e"+type+fn]; + } +} + +var EventCache = function(){ + var listEvents = []; + return { + listEvents : listEvents, + add : function(node, sEventName, fHandler){ + listEvents.push(arguments); + }, + flush : function(){ + var i, item; + for(i = listEvents.length - 1; i >= 0; i = i - 1){ + item = listEvents[i]; + if(item[0].removeEventListener){ + item[0].removeEventListener(item[1], item[2], item[3]); + }; + if(item[1].substring(0, 2) != "on"){ + item[1] = "on" + item[1]; + }; + if(item[0].detachEvent){ + item[0].detachEvent(item[1], item[2]); + }; + item[0][item[1]] = null; + }; + } + }; +}(); +addEvent(window,'unload',EventCache.flush); + +/* window 'load' attachment */ +function addLoadEvent(func) { + var oldonload = window.onload; + if (typeof window.onload != 'function') { + window.onload = func; + } + else { + window.onload = function() { + oldonload(); + func(); + } + } +} + +/* grab Elements from the DOM by className */ +function getElementsByClass(searchClass,node,tag) { + var classElements = new Array(); + if ( node == null ) + node = document; + if ( tag == null ) + tag = '*'; + var els = node.getElementsByTagName(tag); + var elsLen = els.length; + var pattern = new RegExp("(^|\\s)"+searchClass+"(\\s|$)"); + for (i = 0, j = 0; i < elsLen; i++) { + if ( pattern.test(els[i].className) ) { + classElements[j] = els[i]; + j++; + } + } + return classElements; +} + + +/* insert an element after a particular node */ +function insertAfter(parent, node, referenceNode) { + parent.insertBefore(node, referenceNode.nextSibling); +} + + +/* get, set, and delete cookies */ +function getCookie( name ) { + var start = document.cookie.indexOf( name + "=" ); + var len = start + name.length + 1; + if ( ( !start ) && ( name != document.cookie.substring( 0, name.length ) ) ) { + return null; + } + if ( start == -1 ) return null; + var end = document.cookie.indexOf( ";", len ); + if ( end == -1 ) end = document.cookie.length; + return unescape( document.cookie.substring( len, end ) ); +} + +function setCookie( name, value, expires, path, domain, secure ) { + var today = new Date(); + today.setTime( today.getTime() ); + if ( expires ) { + expires = expires * 1000 * 60 * 60 * 24; + } + var expires_date = new Date( today.getTime() + (expires) ); + document.cookie = name+"="+escape( value ) + + ( ( expires ) ? ";expires="+expires_date.toGMTString() : "" ) + + ( ( path ) ? ";path=" + path : "" ) + + ( ( domain ) ? ";domain=" + domain : "" ) + + ( ( secure ) ? ";secure" : "" ); +} + +function deleteCookie( name, path, domain ) { + if ( getCookie( name ) ) document.cookie = name + "=" + + ( ( path ) ? ";path=" + path : "") + + ( ( domain ) ? ";domain=" + domain : "" ) + + ";expires=Thu, 01-Jan-1970 00:00:01 GMT"; +} + +/* quick getElement reference */ +function $() { + var elements = new Array(); + for (var i = 0; i < arguments.length; i++) { + var element = arguments[i]; + if (typeof element == 'string') + element = document.getElementById(element); + if (arguments.length == 1) + return element; + elements.push(element); + } + return elements; +} + +/* Object-oriented Helper functions. */ +function clone(object) { + function F() {} + F.prototype = object; + return new F; +} + +function extend(subClass, superClass) { + var F = function() {}; + F.prototype = superClass.prototype; + subClass.prototype = new F(); + subClass.prototype.constructor = subClass; + + subClass.superclass = superClass.prototype; + if(superClass.prototype.constructor == Object.prototype.constructor) { + superClass.prototype.constructor = superClass; + } +} + +function augment(receivingClass, givingClass) { + if(arguments[2]) { // Only give certain methods. + for(var i = 2, len = arguments.length; i < len; i++) { + receivingClass.prototype[arguments[i]] = givingClass.prototype[arguments[i]]; + } + } + else { // Give all methods. + for(methodName in givingClass.prototype) { + if(!receivingClass.prototype[methodName]) { + receivingClass.prototype[methodName] = givingClass.prototype[methodName]; + } + } + } +} \ No newline at end of file diff --git a/README.txt b/README.txt new file mode 100644 index 0000000..eaa0d12 --- /dev/null +++ b/README.txt @@ -0,0 +1,17 @@ +-------------------------------------------------------------------------------- +Version 1.0.0 +-------------------------------------------------------------------------------- + +This folder contains the code examples from the book "Pro JavaScript Design +Patterns" by Ross Harmes and Dustin Diaz. + +The latest version of this code can always be downloaded at: + +http://jsdesignpatterns.com/code.zip + +and at the Apress website: + +http://apress.com/book/view/159059908x + +Questions and corrections can be sent to ross@jsdesignpatterns.com and +dustin@jsdesignpatterns.com. \ No newline at end of file