Index: trunk/parsers/wikidom/lib/hype/es.DocumentModel.js |
— | — | @@ -0,0 +1,223 @@ |
| 2 | +/** |
| 3 | + * @class |
| 4 | + * @constructor |
| 5 | + */ |
| 6 | +es.DocumentModel = function() { |
| 7 | + this.data = []; |
| 8 | +}; |
| 9 | + |
| 10 | +/* |
| 11 | +// High-level operations |
| 12 | + |
| 13 | +es.DocumentModel.prototype.prepareInsertContent = function( offset, content ) { |
| 14 | + // retain ^ .. offset |
| 15 | + // insert content |
| 16 | + // retain offset .. $ |
| 17 | +}; |
| 18 | +es.DocumentModel.prototype.prepareRemoveContent = function( range ) { |
| 19 | + // retain ^ .. range.start |
| 20 | + // retain range.end .. $ |
| 21 | +}; |
| 22 | +es.DocumentModel.prototype.prepareAnnotateContent = function( range, annotations ) { |
| 23 | + // retain ^ .. range.start |
| 24 | + // start annotations |
| 25 | + // retain range.start .. range.end |
| 26 | + // end annotations |
| 27 | + // retain range.end .. $ |
| 28 | +}; |
| 29 | +es.DocumentModel.prototype.prepareInsertBlock = function( offset, type, attributes, content ) { |
| 30 | + // retain ^ .. offset |
| 31 | + // insert elementStart (type, attributes) |
| 32 | + // insert content |
| 33 | + // insert elementEnd (type, attributes) |
| 34 | + // retain offset .. $ |
| 35 | +}; |
| 36 | +es.DocumentModel.prototype.prepareRemoveBlock = function( offset ) { |
| 37 | + // retain ^ .. offset |
| 38 | + // retain findMatchingElementEnd( offset ) .. $ |
| 39 | +}; |
| 40 | +*/ |
| 41 | + |
| 42 | +// Low-level operations |
| 43 | + |
| 44 | +es.DocumentModel.newFromPlainObject = function( obj ) { |
| 45 | + |
| 46 | +}; |
| 47 | + |
| 48 | +es.DocumentModel.prototype.toPlainObject = function() { |
| 49 | + |
| 50 | +}; |
| 51 | + |
| 52 | +es.DocumentModel.prototype.insertContent = function( offset, content ) { |
| 53 | + this.data = this.data.slice( 0, offset ).concat( content ).concat( this.data.slice( offset ) ); |
| 54 | +}; |
| 55 | + |
| 56 | +es.DocumentModel.prototype.removeContent = function( range ) { |
| 57 | + this.data.splice( range.start, range.end - range.start ); |
| 58 | +}; |
| 59 | + |
| 60 | +es.DocumentModel.prototype.annotateContent = function( range, annotations ) { |
| 61 | + for ( var i = 0; i < annotations.length; i++ ) { |
| 62 | + var annotation = annotations[i]; |
| 63 | + if ( annotation.action = 'add' ) { |
| 64 | + // this.data[i][?] |
| 65 | + } else if ( annotation.action = 'remove' ) { |
| 66 | + // this.data[i][?] |
| 67 | + } |
| 68 | + } |
| 69 | +}; |
| 70 | + |
| 71 | +es.DocumentModel.prototype.insertElement = function( offset, element ) { |
| 72 | + this.data.splice( offset, 0, element ); |
| 73 | +}; |
| 74 | + |
| 75 | +es.DocumentModel.prototype.removeElement = function( offset ) { |
| 76 | + this.data.splice( offset, 1 ); |
| 77 | +}; |
| 78 | + |
| 79 | +es.DocumentModel.prototype.annotateElement = function( offset, annotations ) { |
| 80 | + for ( var i = 0; i < annotations.length; i++ ) { |
| 81 | + var annotation = annotations[i]; |
| 82 | + if ( annotation.action = 'add' ) { |
| 83 | + // this.data[i].annotations[?] |
| 84 | + } else if ( annotation.action = 'remove' ) { |
| 85 | + // this.data[i].annotations[?] |
| 86 | + } |
| 87 | + } |
| 88 | +}; |
| 89 | + |
| 90 | +/* |
| 91 | + * Example of content data |
| 92 | + * |
| 93 | + * Content data is an array made up of 3 kinds of values: |
| 94 | + * String: Plain text character |
| 95 | + * Array: Annotated character |
| 96 | + * Object: Opening or closing structural element |
| 97 | + */ |
| 98 | +var data = [ |
| 99 | + // 0 - Beginning of paragraph |
| 100 | + { 'type': 'paragraph' }, |
| 101 | + // 1 - Plain content |
| 102 | + 'a', |
| 103 | + // 2 - Annotated content |
| 104 | + ['b', { 'type': 'bold' }], |
| 105 | + // 3 - Annotated content |
| 106 | + ['c', { 'type': 'italic' }], |
| 107 | + // 4 - End of paragraph |
| 108 | + { 'type': '/paragraph' } |
| 109 | + // 5 - Beginning of table |
| 110 | + { 'type': 'table' }, |
| 111 | + // 6 - Beginning of row |
| 112 | + { 'type': 'row' }, |
| 113 | + // 7 - Beginning of cell |
| 114 | + { 'type': 'cell' }, |
| 115 | + // 8 - Beginning of paragraph |
| 116 | + { 'type': 'paragraph' }, |
| 117 | + // 9 - Plain content |
| 118 | + 'a', |
| 119 | + // 10 - End of paragraph |
| 120 | + { 'type': '/paragraph' }, |
| 121 | + // 11 - Beginning of list |
| 122 | + { 'type': 'list' }, |
| 123 | + // 12 - Beginning of bullet list item |
| 124 | + { 'type': 'item', 'styles': ['bullet'] }, |
| 125 | + // 13 - Plain content |
| 126 | + 'a', |
| 127 | + // 14 - End of item |
| 128 | + { 'type': '/item' }, |
| 129 | + // 15 - Beginning of nested bullet list item |
| 130 | + { 'type': 'item', 'styles': ['bullet', 'bullet'] }, |
| 131 | + // 16 - Plain content |
| 132 | + 'b', |
| 133 | + // 17 - End of item |
| 134 | + { 'type': '/item' }, |
| 135 | + // 18 - Beginning of numbered list item |
| 136 | + { 'type': 'item', 'styles': ['number'] }, |
| 137 | + // 19 - Plain content |
| 138 | + 'c', |
| 139 | + // 20 - End of item |
| 140 | + { 'type': '/item' }, |
| 141 | + // 21 - End of list |
| 142 | + { 'type': '/list' }, |
| 143 | + // 22 - End of cell |
| 144 | + { 'type': '/cell' } |
| 145 | + // 23 - End of row |
| 146 | + { 'type': '/row' } |
| 147 | + // 24 - End of table |
| 148 | + { 'type': '/table' } |
| 149 | + // 25 - Beginning of paragraph |
| 150 | + { 'type': 'paragraph' }, |
| 151 | + // 26 - Plain content |
| 152 | + 'a' |
| 153 | + // 27 - End of paragraph |
| 154 | + { 'type': '/paragraph' }, |
| 155 | +]; |
| 156 | + |
| 157 | +/* |
| 158 | + * Example of content tree |
| 159 | + * |
| 160 | + * Content trees are kept in sync with content data, providing a mapping between a structured user |
| 161 | + * interface and a flat content model. They are made up of nodes which have some common properties: |
| 162 | + * type: Symbolic name of a block or sub-block component |
| 163 | + * length: Number of elements in content data between the element start and end |
| 164 | + * items: Information about the content between the element start and end |
| 165 | + */ |
| 166 | +var tree = [ |
| 167 | + { |
| 168 | + 'type': 'paragraph', |
| 169 | + 'length': 5, |
| 170 | + //'content': ['a', ['b', { 'type': 'bold' }], ['c', { 'type': 'italic' }]], |
| 171 | + }, |
| 172 | + { |
| 173 | + 'type': 'table', |
| 174 | + 'length': 19, |
| 175 | + 'items': [ |
| 176 | + { |
| 177 | + 'type': 'row', |
| 178 | + 'length': 17, |
| 179 | + 'items': [ |
| 180 | + { |
| 181 | + 'type': 'cell', |
| 182 | + 'length': 15, |
| 183 | + 'items': { |
| 184 | + { |
| 185 | + 'type': 'paragraph', |
| 186 | + 'length': 3 |
| 187 | + //'content': ['a'] |
| 188 | + }, |
| 189 | + { |
| 190 | + 'type': 'list', |
| 191 | + 'length': 12, |
| 192 | + 'items': [ |
| 193 | + { |
| 194 | + 'type': 'item', |
| 195 | + 'styles': ['bullet'], |
| 196 | + 'length': 3, |
| 197 | + //'content': ['a'] |
| 198 | + }, |
| 199 | + { |
| 200 | + 'type': 'item', |
| 201 | + 'styles': ['bullet', 'bullet'], |
| 202 | + 'length': 3, |
| 203 | + //'content': ['b'] |
| 204 | + }, |
| 205 | + { |
| 206 | + 'type': 'item', |
| 207 | + 'styles': ['number'], |
| 208 | + 'length': 3, |
| 209 | + //'content': ['c'] |
| 210 | + } |
| 211 | + ] |
| 212 | + } |
| 213 | + } |
| 214 | + } |
| 215 | + ] |
| 216 | + } |
| 217 | + ] |
| 218 | + }, |
| 219 | + { |
| 220 | + 'type': 'paragraph', |
| 221 | + 'length': 3, |
| 222 | + //'content': ['a'] |
| 223 | + } |
| 224 | +]; |
Index: trunk/parsers/wikidom/lib/hype/es.js |
— | — | @@ -0,0 +1,96 @@ |
| 2 | +/** |
| 3 | + * EditSurface namespace. |
| 4 | + * |
| 5 | + * All classes and functions will be attached to this object to keep the global namespace clean. |
| 6 | + */ |
| 7 | +var es = {}; |
| 8 | + |
| 9 | +/* Functions */ |
| 10 | + |
| 11 | +/** |
| 12 | + * Extends a constructor with the prototype of another. |
| 13 | + * |
| 14 | + * When using this, it's required to include a call to the constructor of the parent class as the |
| 15 | + * first code in the child class's constructor. |
| 16 | + * |
| 17 | + * @example |
| 18 | + * // Define parent class |
| 19 | + * function Foo() { |
| 20 | + * // code here |
| 21 | + * } |
| 22 | + * // Define child class |
| 23 | + * function Bar() { |
| 24 | + * // Call parent constructor |
| 25 | + * Foo.call( this ); |
| 26 | + * } |
| 27 | + * // Extend prototype |
| 28 | + * extend( Bar, Foo ); |
| 29 | + * |
| 30 | + * @static |
| 31 | + * @method |
| 32 | + * @param dst {Function} Class to extend |
| 33 | + * @param src {Function} Base class to use methods from |
| 34 | + */ |
| 35 | +es.extend = function( dst, src ) { |
| 36 | + var base = new src(); |
| 37 | + for ( var method in base ) { |
| 38 | + if ( typeof base[method] === 'function' && !( method in dst.prototype ) ) { |
| 39 | + dst.prototype[method] = base[method]; |
| 40 | + } |
| 41 | + } |
| 42 | +}; |
| 43 | + |
| 44 | +/** |
| 45 | + * Recursively compares string and number property between two objects. |
| 46 | + * |
| 47 | + * A false result may be caused by property inequality or by properties in one object missing from |
| 48 | + * the other. An asymmetrical test may also be performed, which checks only that properties in the |
| 49 | + * first object are present in the second object, but not the inverse. |
| 50 | + * |
| 51 | + * @static |
| 52 | + * @method |
| 53 | + * @param a {Object} First object to compare |
| 54 | + * @param b {Object} Second object to compare |
| 55 | + * @param asymmetrical {Boolean} Whether to check only that b contains values from a |
| 56 | + * @returns {Boolean} If the objects contain the same values as each other |
| 57 | + */ |
| 58 | +es.compareObjects = function( a, b, asymmetrical ) { |
| 59 | + var aValue, bValue, aType, bType; |
| 60 | + var k; |
| 61 | + for ( k in a ) { |
| 62 | + aValue = a[k]; |
| 63 | + bValue = b[k]; |
| 64 | + aType = typeof aValue; |
| 65 | + bType = typeof bValue; |
| 66 | + if ( aType !== bType |
| 67 | + || ( ( aType === 'string' || aType === 'number' ) && aValue !== bValue ) |
| 68 | + || ( $.isPlainObject( aValue ) && !es.compareObjects( aValue, bValue ) ) ) { |
| 69 | + return false; |
| 70 | + } |
| 71 | + } |
| 72 | + // If the check is not asymmetrical, recursing with the arguments swapped will verify our result |
| 73 | + return asymmetrical ? true : es.compareObjects( b, a, true ); |
| 74 | +}; |
| 75 | + |
| 76 | +/** |
| 77 | + * Gets a recursive copy of an object's string, number and plain-object property. |
| 78 | + * |
| 79 | + * @static |
| 80 | + * @method |
| 81 | + * @param source {Object} Object to copy |
| 82 | + * @returns {Object} Copy of source object |
| 83 | + */ |
| 84 | +es.copyObject = function( source ) { |
| 85 | + var destination = {}; |
| 86 | + var key; |
| 87 | + for ( key in source ) { |
| 88 | + sourceValue = source[key]; |
| 89 | + sourceType = typeof sourceValue; |
| 90 | + if ( sourceType === 'string' || sourceType === 'number' ) { |
| 91 | + destination[key] = sourceValue; |
| 92 | + } else if ( $.isPlainObject( sourceValue ) ) { |
| 93 | + destination[key] = es.copyObject( sourceValue ); |
| 94 | + } |
| 95 | + } |
| 96 | + return destination; |
| 97 | +}; |
Index: trunk/parsers/wikidom/lib/hype/bases/es.ModelItem.js |
— | — | @@ -0,0 +1,125 @@ |
| 2 | +/** |
| 3 | + * Creates an es.ModelItem object. |
| 4 | + * |
| 5 | + * @class |
| 6 | + * @constructor |
| 7 | + * @extends {es.EventEmitter} |
| 8 | + * @property {es.ModelList} list Reference to list this item is in |
| 9 | + */ |
| 10 | +es.ModelItem = function() { |
| 11 | + es.EventEmitter.call( this ); |
| 12 | + this.list = null; |
| 13 | +}; |
| 14 | + |
| 15 | +/* Methods */ |
| 16 | + |
| 17 | +/** |
| 18 | + * Creates a view for this model. |
| 19 | + * |
| 20 | + * @method |
| 21 | + * @returns {es.ViewItem} New item view associated with this item model |
| 22 | + */ |
| 23 | +es.ModelItem.prototype.createView = function() { |
| 24 | + throw 'ModelItem.createView not implemented in this subclass:' + this.constructor; |
| 25 | +}; |
| 26 | + |
| 27 | +/** |
| 28 | + * Gets a reference to the list this item is in. |
| 29 | + * |
| 30 | + * @method |
| 31 | + * @returns {es.ModelList} Reference to this list this item is in |
| 32 | + */ |
| 33 | +es.ModelItem.prototype.getList = function() { |
| 34 | + return this.list; |
| 35 | +}; |
| 36 | + |
| 37 | +/** |
| 38 | + * Gets the index of this item within the list that it's in. |
| 39 | + * |
| 40 | + * @method |
| 41 | + * @returns {Integer} Index of item in it's list, -1 if not in a list |
| 42 | + */ |
| 43 | +es.ModelItem.prototype.getIndex = function() { |
| 44 | + if ( this.list ) { |
| 45 | + return this.list.indexOf( this ); |
| 46 | + } |
| 47 | + return -1; |
| 48 | +}; |
| 49 | + |
| 50 | +/** |
| 51 | + * Attaches item to a list. |
| 52 | + * |
| 53 | + * @method |
| 54 | + * @param {es.Container} list Container to attach to |
| 55 | + * @emits attach (list) |
| 56 | + */ |
| 57 | +es.ModelItem.prototype.attach = function( list ) { |
| 58 | + this.list = list; |
| 59 | + this.emit( 'attach', list ); |
| 60 | +}; |
| 61 | + |
| 62 | +/** |
| 63 | + * Detaches item from a list. |
| 64 | + * |
| 65 | + * @method |
| 66 | + * @emits detach (list) |
| 67 | + */ |
| 68 | +es.ModelItem.prototype.detach = function() { |
| 69 | + var list = this.list; |
| 70 | + this.list = null; |
| 71 | + this.emit( 'detach', list ); |
| 72 | +}; |
| 73 | + |
| 74 | +/** |
| 75 | + * Gets the previous item in list. |
| 76 | + * |
| 77 | + * @method |
| 78 | + * @returns {es.ModelItem} Previous item, or undefined if none exists |
| 79 | + */ |
| 80 | +es.ModelItem.prototype.getPreviousItem = function() { |
| 81 | + if ( this.list ) { |
| 82 | + return this.list[this.list.indexOf( this ) - 1]; |
| 83 | + } |
| 84 | +}; |
| 85 | + |
| 86 | +/** |
| 87 | + * Gets the next item in list. |
| 88 | + * |
| 89 | + * @method |
| 90 | + * @returns {Object} Next item, or undefined if none exists |
| 91 | + */ |
| 92 | +es.ModelItem.prototype.getNextItem = function() { |
| 93 | + if ( this.list ) { |
| 94 | + return this.list[this.list.indexOf( this ) + 1]; |
| 95 | + } |
| 96 | +}; |
| 97 | + |
| 98 | +/** |
| 99 | + * Checks if this item is the first in it's list. |
| 100 | + * |
| 101 | + * @method |
| 102 | + * @returns {Boolean} If item is in a list and is also the first item in that list |
| 103 | + */ |
| 104 | +es.ModelItem.prototype.isFirstItem = function() { |
| 105 | + if ( this.list ) { |
| 106 | + return this === this.list.getFirstItem(); |
| 107 | + } |
| 108 | + return false; |
| 109 | +}; |
| 110 | + |
| 111 | +/** |
| 112 | + * Checks if this item is the last in it's list. |
| 113 | + * |
| 114 | + * @method |
| 115 | + * @returns {Boolean} If item is in a list and is also the last item in that list |
| 116 | + */ |
| 117 | +es.ModelItem.prototype.isLastItem = function() { |
| 118 | + if ( this.list ) { |
| 119 | + return this === this.list.getLastItem(); |
| 120 | + } |
| 121 | + return false; |
| 122 | +}; |
| 123 | + |
| 124 | +/* Inheritance */ |
| 125 | + |
| 126 | +es.extend( es.ModelItem, es.EventEmitter ); |
Index: trunk/parsers/wikidom/lib/hype/bases/es.AggregateArray.js |
— | — | @@ -0,0 +1,117 @@ |
| 2 | +/** |
| 3 | + * Creates an es.AggregateArray object. |
| 4 | + * |
| 5 | + * A content series is an array of items which have a getLength method. |
| 6 | + */ |
| 7 | +es.AggregateArray = function( items ) { |
| 8 | + var items = $.isArray( items ) ? items : []; |
| 9 | + // Extend native array with method and properties of this |
| 10 | + return $.extend( items, this ); |
| 11 | +}; |
| 12 | + |
| 13 | +es.AggregateArray.prototype.lookup = function( offset ) { |
| 14 | + if ( this.length ) { |
| 15 | + var i = 0, |
| 16 | + length = this.length, |
| 17 | + left = 0, |
| 18 | + right; |
| 19 | + while ( i < length ) { |
| 20 | + right = left + this[i].getLength() + 1; |
| 21 | + if ( offset >= left && offset < right ) { |
| 22 | + return this[i]; |
| 23 | + } |
| 24 | + left = right; |
| 25 | + i++; |
| 26 | + } |
| 27 | + } |
| 28 | + return null; |
| 29 | +}; |
| 30 | + |
| 31 | +es.AggregateArray.prototype.rangeOf = function( item ) { |
| 32 | + if ( this.length ) { |
| 33 | + var i = 0, |
| 34 | + length = this.length, |
| 35 | + left = 0; |
| 36 | + while ( i < length ) { |
| 37 | + if ( this[i] === item ) { |
| 38 | + return new es.Range( left, left + this[i].getLength() ); |
| 39 | + } |
| 40 | + left += this[i].getLength() + 1; |
| 41 | + i++; |
| 42 | + } |
| 43 | + } |
| 44 | + return null; |
| 45 | +}; |
| 46 | + |
| 47 | +es.AggregateArray.prototype.getLengthOfItems = function() { |
| 48 | + var sum = 0; |
| 49 | + for ( var i = 0, length = this.length; i < length; i++ ) { |
| 50 | + sum += this[i].getLength(); |
| 51 | + } |
| 52 | + return Math.max( 0, sum + this.length - 1 ); |
| 53 | +}; |
| 54 | + |
| 55 | +es.AggregateArray.prototype.getCoverage = function( start, end ) { |
| 56 | + var result = { 'on': [], 'off': [] }, |
| 57 | + sum = 0, |
| 58 | + len; |
| 59 | + for ( var i = 0, length = this.length; i < length; i++ ) { |
| 60 | + len = this[i].getLength(); |
| 61 | + if ( sum >= start && sum + len < end ) { |
| 62 | + result.on.push( this[i] ); |
| 63 | + } else { |
| 64 | + result.off.push( this[i] ); |
| 65 | + } |
| 66 | + sum += len |
| 67 | + } |
| 68 | + return result; |
| 69 | +}; |
| 70 | + |
| 71 | +es.AggregateArray.prototype.select = function( start, end ) { |
| 72 | + // Support es.Range object as first argument |
| 73 | + if ( typeof start.from === 'number' && typeof start.to === 'number') { |
| 74 | + start.normalize(); |
| 75 | + end = start.end; |
| 76 | + start = start.start; |
| 77 | + } |
| 78 | + var items = []; |
| 79 | + if ( this.length ) { |
| 80 | + var i = 0, |
| 81 | + length = this.length, |
| 82 | + left = 0, |
| 83 | + right, |
| 84 | + inside = false, |
| 85 | + from, |
| 86 | + to; |
| 87 | + while ( i < length ) { |
| 88 | + right = left + this[i].getLength() + 1; |
| 89 | + if ( inside ) { |
| 90 | + // Append items until we reach the end |
| 91 | + from = 0; |
| 92 | + to = Math.min( right - left - 1, end - left ); |
| 93 | + |
| 94 | + if ( from !== to ) { |
| 95 | + items.push( { 'item': this[i], 'from': from, 'to': to } ); |
| 96 | + } |
| 97 | + if ( end >= left && end < right ) { |
| 98 | + break; |
| 99 | + } |
| 100 | + } else if ( start >= left && start < right ) { |
| 101 | + inside = true; |
| 102 | + // Append first item |
| 103 | + from = start - left; |
| 104 | + //to = Math.min( right - 1, end - left ); |
| 105 | + to = Math.min( right - left - 1, end - left ); |
| 106 | + if ( from !== to ) { |
| 107 | + items.push( { 'item': this[i], 'from': from, 'to': to } ); |
| 108 | + } |
| 109 | + if ( right >= end ) { |
| 110 | + break; |
| 111 | + } |
| 112 | + } |
| 113 | + left = right; |
| 114 | + i++; |
| 115 | + } |
| 116 | + } |
| 117 | + return items; |
| 118 | +}; |
Index: trunk/parsers/wikidom/lib/hype/bases/es.ViewList.js |
— | — | @@ -0,0 +1,132 @@ |
| 2 | +/** |
| 3 | + * Creates an es.ViewList object. |
| 4 | + * |
| 5 | + * View lists follow the operations performed on a model lists and keep a list of views, |
| 6 | + * each correlating to a model in the model list. |
| 7 | + * |
| 8 | + * This will override this.$ (important in case of multiple inheritance). |
| 9 | + * |
| 10 | + * @class |
| 11 | + * @constructor |
| 12 | + * @extends {es.EventEmitter} |
| 13 | + * @param model {es.ModelList} Model to observe |
| 14 | + * @param {jQuery} [$element=New DIV element] Element to use as a container |
| 15 | + * @property {es.ModelItem} model Model being observed |
| 16 | + * @property {jQuery} $ Container element |
| 17 | + */ |
| 18 | +es.ViewList = function( model, $element ) { |
| 19 | + var list = new es.AggregateArray(); |
| 20 | + es.EventEmitter.call( list ); |
| 21 | + |
| 22 | + // Extending this class will initialize it without any arguments, exiting early if no model |
| 23 | + // was given will prevent clogging up subclass prototypes with array methods |
| 24 | + if ( !model ) { |
| 25 | + return list; |
| 26 | + } |
| 27 | + |
| 28 | + list.model = model; |
| 29 | + list.$ = $element || $( '<div/>' ); |
| 30 | + |
| 31 | + // Reusable function for passing update events upstream |
| 32 | + this.emitUpdate = function() { |
| 33 | + list.emit( 'update' ); |
| 34 | + }; |
| 35 | + |
| 36 | + // Observe and mimic changes on model |
| 37 | + model.addListenerMethods( list, { |
| 38 | + 'push': 'onPush', |
| 39 | + 'unshift': 'onUnshift', |
| 40 | + 'pop': 'onPop', |
| 41 | + 'shift': 'onShift', |
| 42 | + 'splice': 'onSplice', |
| 43 | + 'sort': 'onSort', |
| 44 | + 'reverse': 'onReverse' |
| 45 | + } ); |
| 46 | + |
| 47 | + // Append existing model items |
| 48 | + for ( var i = 0; i < model.length; i++ ) { |
| 49 | + this.onPush( model[i] ); |
| 50 | + } |
| 51 | + |
| 52 | + // Extend native array with method and properties of this |
| 53 | + return $.extend( list, this ); |
| 54 | +}; |
| 55 | + |
| 56 | +es.ViewList.onPush = function( itemModel ) { |
| 57 | + var itemView = itemModel.createView(); |
| 58 | + itemView.attach( this ); |
| 59 | + itemView.on( 'update', this.emitUpdate ); |
| 60 | + this.push( itemView ); |
| 61 | + this.$.append( itemView.$ ); |
| 62 | + this.emit( 'push', itemView ); |
| 63 | + this.emit( 'update' ); |
| 64 | +}; |
| 65 | + |
| 66 | +es.ViewList.onUnshift = function( itemModel ) { |
| 67 | + var itemView = itemModel.createView(); |
| 68 | + itemView.attach( this ); |
| 69 | + itemView.on( 'update', this.emitUpdate ); |
| 70 | + this.unshift( itemView ); |
| 71 | + this.$.prepend( itemView.$ ); |
| 72 | + this.emit( 'unshift', itemView ); |
| 73 | + this.emit( 'update' ); |
| 74 | +}; |
| 75 | + |
| 76 | +es.ViewList.onPop = function() { |
| 77 | + var itemView = this.pop(); |
| 78 | + itemView.detach(); |
| 79 | + itemView.removeEventListener( 'update', this.emitUpdate ); |
| 80 | + itemView.$.detach(); |
| 81 | + this.emit( 'pop' ); |
| 82 | + this.emit( 'update' ); |
| 83 | +}; |
| 84 | + |
| 85 | +es.ViewList.onShift = function() { |
| 86 | + var itemView = this.shift(); |
| 87 | + itemView.detach(); |
| 88 | + itemView.removeEventListener( 'update', this.emitUpdate ); |
| 89 | + itemView.$.detach(); |
| 90 | + this.emit( 'shift' ); |
| 91 | + this.emit( 'update' ); |
| 92 | +}; |
| 93 | + |
| 94 | +es.ViewList.onSplice = function( index, howmany ) { |
| 95 | + var args = Array.prototype.slice( arguments, 0 ), |
| 96 | + added = args.slice( 2 ), |
| 97 | + removed = this.splice.apply( this, args ); |
| 98 | + this.$.children().slice( index, index + howmany ).detach(); |
| 99 | + var $added = $.map( added, function( itemView ) { |
| 100 | + return itemView.$; |
| 101 | + } ); |
| 102 | + this.$.children().get( index ).after( $added ); |
| 103 | + this.emit.apply( ['splice'].concat( args ) ); |
| 104 | + this.emit( 'update' ); |
| 105 | +}; |
| 106 | + |
| 107 | +es.ViewList.onSort = function() { |
| 108 | + for ( var i = 0; i < this.model.length; i++ ) { |
| 109 | + for ( var j = 0; j < this.length; j++ ) { |
| 110 | + if ( this[j].getModel() === this.model[i] ) { |
| 111 | + var itemView = this[j]; |
| 112 | + this.splice( j, 1 ); |
| 113 | + this.push( itemView ); |
| 114 | + this.$.append( itemView.$ ); |
| 115 | + } |
| 116 | + } |
| 117 | + } |
| 118 | + this.emit( 'sort' ); |
| 119 | + this.emit( 'update' ); |
| 120 | +}; |
| 121 | + |
| 122 | +es.ViewList.onReverse = function() { |
| 123 | + this.reverse(); |
| 124 | + this.$.children().each( function() { |
| 125 | + $(this).prependTo( $(this).parent() ); |
| 126 | + } ); |
| 127 | + this.emit( 'reverse' ); |
| 128 | + this.emit( 'update' ); |
| 129 | +}; |
| 130 | + |
| 131 | +/* Inheritance */ |
| 132 | + |
| 133 | +es.extend( es.ViewList, es.EventEmitter ); |
Index: trunk/parsers/wikidom/lib/hype/bases/es.ViewItem.js |
— | — | @@ -0,0 +1,76 @@ |
| 2 | +/** |
| 3 | + * Generic synchronized Object/Element container item. |
| 4 | + * This will override this.$ (important in case of multiple inheritance). |
| 5 | + * |
| 6 | + * @class |
| 7 | + * @constructor |
| 8 | + * @extends {es.EventEmitter} |
| 9 | + * @param {jQuery} $element jQuery object to use |
| 10 | + * @property {jQuery} $ Container element |
| 11 | + */ |
| 12 | +es.ViewItem = function( model, $element ) { |
| 13 | + es.EventEmitter.call( this ); |
| 14 | + this.model = model; |
| 15 | + this.$ = $element || $( '<div/>' ); |
| 16 | + this.list = null; |
| 17 | +}; |
| 18 | + |
| 19 | +/** |
| 20 | + * Gets a reference to the model this item observes. |
| 21 | + * |
| 22 | + * @method |
| 23 | + * @returns {es.ModelList} Reference to the model this item observes |
| 24 | + */ |
| 25 | +es.ViewItem.prototype.getModel = function() { |
| 26 | + return this.model; |
| 27 | +}; |
| 28 | + |
| 29 | +/** |
| 30 | + * Gets a reference to the list this item is in. |
| 31 | + * |
| 32 | + * @method |
| 33 | + * @returns {es.ModelList} Reference to this list this item is in |
| 34 | + */ |
| 35 | +es.ViewItem.prototype.getList = function() { |
| 36 | + return this.list; |
| 37 | +}; |
| 38 | + |
| 39 | +/** |
| 40 | + * Gets the index of this item within it's list. |
| 41 | + * |
| 42 | + * This method simply delegates to the model. |
| 43 | + * |
| 44 | + * @method |
| 45 | + * @returns {Integer} Index of item in it's container |
| 46 | + */ |
| 47 | +es.ViewItem.prototype.getIndex = function() { |
| 48 | + return this.model.getIndex(); |
| 49 | +}; |
| 50 | + |
| 51 | +/** |
| 52 | + * Attaches item to a list. |
| 53 | + * |
| 54 | + * @method |
| 55 | + * @param {es.Container} list Container to attach to |
| 56 | + * @emits attach (list) |
| 57 | + */ |
| 58 | +es.ViewItem.prototype.attach = function( list ) { |
| 59 | + this.list = list; |
| 60 | + this.emit( 'attach', list ); |
| 61 | +}; |
| 62 | + |
| 63 | +/** |
| 64 | + * Detaches item from a list. |
| 65 | + * |
| 66 | + * @method |
| 67 | + * @emits detach (list) |
| 68 | + */ |
| 69 | +es.ViewItem.prototype.detach = function() { |
| 70 | + var list = this.list; |
| 71 | + this.list = null; |
| 72 | + this.emit( 'detach', list ); |
| 73 | +}; |
| 74 | + |
| 75 | +/* Inheritance */ |
| 76 | + |
| 77 | +es.extend( es.ViewItem, es.EventEmitter ); |
\ No newline at end of file |
Index: trunk/parsers/wikidom/lib/hype/bases/es.EventEmitter.js |
— | — | @@ -0,0 +1,168 @@ |
| 2 | +/** |
| 3 | + * Event emitter. |
| 4 | + * |
| 5 | + * @class |
| 6 | + * @constructor |
| 7 | + * @property events {Object} |
| 8 | + */ |
| 9 | +es.EventEmitter = function() { |
| 10 | + this.events = {}; |
| 11 | +} |
| 12 | + |
| 13 | +/* Methods */ |
| 14 | + |
| 15 | +/** |
| 16 | + * Emits an event. |
| 17 | + * |
| 18 | + * @method |
| 19 | + * @param type {String} Type of event |
| 20 | + * @param args {Mixed} First in a list of variadic arguments passed to event handler (optional) |
| 21 | + * @returns {Boolean} If event was handled by at least one listener |
| 22 | + */ |
| 23 | +es.EventEmitter.prototype.emit = function( type ) { |
| 24 | + if ( type === 'error' && !( 'error' in this.events ) ) { |
| 25 | + throw 'Missing error handler error.'; |
| 26 | + } |
| 27 | + if ( !( type in this.events ) ) { |
| 28 | + return false; |
| 29 | + } |
| 30 | + var listeners = this.events[type].slice(); |
| 31 | + var args = Array.prototype.slice.call( arguments, 1 ); |
| 32 | + for ( var i = 0; i < listeners.length; i++ ) { |
| 33 | + listeners[i].apply( this, args ); |
| 34 | + } |
| 35 | + return true; |
| 36 | +}; |
| 37 | + |
| 38 | +/** |
| 39 | + * Adds a listener to events of a specific type. |
| 40 | + * |
| 41 | + * @method |
| 42 | + * @param type {String} Type of event to listen to |
| 43 | + * @param listener {Function} Listener to call when event occurs |
| 44 | + * @returns {es.EventEmitter} This object |
| 45 | + * @throws "Invalid listener error" if listener argument is not a function |
| 46 | + */ |
| 47 | +es.EventEmitter.prototype.addListener = function( type, listener ) { |
| 48 | + if ( typeof listener !== 'function' ) { |
| 49 | + throw 'Invalid listener error. Function expected.'; |
| 50 | + } |
| 51 | + this.emit( 'newListener', type, listener ); |
| 52 | + if ( type in this.events ) { |
| 53 | + this.events[type].push( listener ); |
| 54 | + } else { |
| 55 | + this.events[type] = [listener]; |
| 56 | + } |
| 57 | + return this; |
| 58 | +}; |
| 59 | + |
| 60 | +/** |
| 61 | + * Add multiple listeners at once. |
| 62 | + * |
| 63 | + * @method |
| 64 | + * @param listeners {Object} List of event/callback pairs |
| 65 | + * @returns {es.EventEmitter} This object |
| 66 | + */ |
| 67 | +es.EventEmitter.prototype.addListeners = function( listeners ) { |
| 68 | + for ( var event in listeners ) { |
| 69 | + this.addListener( event, listeners[event] ); |
| 70 | + } |
| 71 | + return this; |
| 72 | +}; |
| 73 | + |
| 74 | +/** |
| 75 | + * Add multiple listeners, each mapped to a method on a target object. |
| 76 | + * |
| 77 | + * @method |
| 78 | + * @param target {Object} Object to call methods on when events occur |
| 79 | + * @param map {Object} List of event/method name pairs |
| 80 | + * @returns {es.EventEmitter} This object |
| 81 | + */ |
| 82 | +es.EventEmitter.prototype.addListenerMethods = function( target, map ) { |
| 83 | + for ( var event in map ) { |
| 84 | + this.addListener( event, function() { |
| 85 | + target[map[event]].apply( target, Array.prototype.slice( arguments, 1 ) ) ); |
| 86 | + } ); |
| 87 | + } |
| 88 | + return this; |
| 89 | +}; |
| 90 | + |
| 91 | +/** |
| 92 | + * Alias for addListener |
| 93 | + * |
| 94 | + * @method |
| 95 | + */ |
| 96 | +es.EventEmitter.prototype.on = es.EventEmitter.prototype.addListener; |
| 97 | + |
| 98 | +/** |
| 99 | + * Adds a one-time listener to a specific event. |
| 100 | + * |
| 101 | + * @method |
| 102 | + * @param type {String} Type of event to listen to |
| 103 | + * @param listener {Function} Listener to call when event occurs |
| 104 | + * @returns {es.EventEmitter} This object |
| 105 | + */ |
| 106 | +es.EventEmitter.prototype.once = function( type, listener ) { |
| 107 | + var eventEmitter = this; |
| 108 | + return this.addListener( type, function listenerWrapper() { |
| 109 | + eventEmitter.removeListener( type, listenerWrapper ); |
| 110 | + listener.apply( eventEmitter, arguments ); |
| 111 | + } ); |
| 112 | +}; |
| 113 | + |
| 114 | +/** |
| 115 | + * Removes a specific listener from a specific event. |
| 116 | + * |
| 117 | + * @method |
| 118 | + * @param type {String} Type of event to remove listener from |
| 119 | + * @param listener {Function} Listener to remove |
| 120 | + * @returns {es.EventEmitter} This object |
| 121 | + * @throws "Invalid listener error" if listener argument is not a function |
| 122 | + */ |
| 123 | +es.EventEmitter.prototype.removeListener = function( type, listener ) { |
| 124 | + if ( typeof listener !== 'function' ) { |
| 125 | + throw 'Invalid listener error. Function expected.'; |
| 126 | + } |
| 127 | + if ( !( type in this.events ) || !this.events[type].length ) { |
| 128 | + return this; |
| 129 | + } |
| 130 | + var handlers = this.events[type]; |
| 131 | + if ( handlers.length == 1 && handlers[0] === listener ) { |
| 132 | + delete this.events[type]; |
| 133 | + } else { |
| 134 | + var i = handlers.indexOf( listener ); |
| 135 | + if ( i < 0 ) { |
| 136 | + return this; |
| 137 | + } |
| 138 | + handlers.splice( i, 1 ); |
| 139 | + if ( handlers.length == 0 ) { |
| 140 | + delete this.events[type]; |
| 141 | + } |
| 142 | + } |
| 143 | + return this; |
| 144 | +}; |
| 145 | + |
| 146 | +/** |
| 147 | + * Removes all listeners from a specific event. |
| 148 | + * |
| 149 | + * @method |
| 150 | + * @param type {String} Type of event to remove listeners from |
| 151 | + * @returns {es.EventEmitter} This object |
| 152 | + */ |
| 153 | +es.EventEmitter.prototype.removeAllListeners = function( type ) { |
| 154 | + if ( type in this.events ) { |
| 155 | + delete this.events[type]; |
| 156 | + } |
| 157 | + return this; |
| 158 | +}; |
| 159 | + |
| 160 | +/** |
| 161 | + * Gets a list of listeners attached to a specific event. |
| 162 | + * |
| 163 | + * @method |
| 164 | + * @param type {String} Type of event to get listeners for |
| 165 | + * @returns {Array} List of listeners to an event |
| 166 | + */ |
| 167 | +es.EventEmitter.prototype.listeners = function( type ) { |
| 168 | + return type in this.events ? this.events[type] : []; |
| 169 | +}; |
Index: trunk/parsers/wikidom/lib/hype/bases/es.ModelList.js |
— | — | @@ -0,0 +1,182 @@ |
| 2 | +/** |
| 3 | + * Creates an es.ModelList object. |
| 4 | + * |
| 5 | + * es.ModelList extends native JavaScript Array objects, without changing Array.prototype by |
| 6 | + * dynamically extending an array literal with the methods of es.ModelList. |
| 7 | + * |
| 8 | + * Child objects must extend es.ModelItem. |
| 9 | + * |
| 10 | + * @class |
| 11 | + * @constructor |
| 12 | + * @extends {Array} |
| 13 | + * @extends {es.EventEmitter} |
| 14 | + */ |
| 15 | +es.ModelList = function( items ) { |
| 16 | + var items = new AggregateArray( items ); |
| 17 | + es.EventEmitter.call( items ); |
| 18 | + |
| 19 | + // Reusable function for passing update events upstream |
| 20 | + items.emitUpdate = function() { |
| 21 | + items.emit( 'update' ); |
| 22 | + }; |
| 23 | + |
| 24 | + // Extend native array with method and properties of this |
| 25 | + return $.extend( items, this ); |
| 26 | +}; |
| 27 | + |
| 28 | +/* Methods */ |
| 29 | + |
| 30 | +/** |
| 31 | + * Gets the first item in the list. |
| 32 | + * |
| 33 | + * @method |
| 34 | + * @returns {es.ModelItem|undefined} First item in the list, or undefined if none exists |
| 35 | + */ |
| 36 | +es.ModelList.prototype.getFirstItem = function() { |
| 37 | + return this[0]; |
| 38 | +}; |
| 39 | + |
| 40 | +/** |
| 41 | + * Gets the last item in the list. |
| 42 | + * |
| 43 | + * @method |
| 44 | + * @returns {es.ModelItem|undefined} last item in the list, or undefined if none exists |
| 45 | + */ |
| 46 | +es.ModelList.prototype.getLastItem = function() { |
| 47 | + return this[this.length - 1]; |
| 48 | +}; |
| 49 | + |
| 50 | +/** |
| 51 | + * Adds an item to the end of the list. |
| 52 | + * |
| 53 | + * @method |
| 54 | + * @param {es.ModelItem} item Item to add |
| 55 | + * @returns {Integer} New length of list |
| 56 | + * @emits push (item) |
| 57 | + * @emits update |
| 58 | + */ |
| 59 | +es.ModelList.prototype.push = function( item ) { |
| 60 | + item.attach( this ); |
| 61 | + item.on( 'update', this.relayUpdate ); |
| 62 | + Array.prototype.push.call( this, item ); |
| 63 | + this.emit( 'push', item ); |
| 64 | + this.emit( 'update' ); |
| 65 | + return this.length; |
| 66 | +}; |
| 67 | + |
| 68 | +/** |
| 69 | + * Adds an item to the beginning of the list. |
| 70 | + * |
| 71 | + * @method |
| 72 | + * @param {es.ModelItem} item Item to add |
| 73 | + * @returns {Integer} New length of list |
| 74 | + * @emits unshift (item) |
| 75 | + * @emits update |
| 76 | + */ |
| 77 | +es.ModelList.prototype.unshift = function( item ) { |
| 78 | + item.attach( this ); |
| 79 | + item.on( 'update', this.relayUpdate ); |
| 80 | + Array.prototype.unshift.call( this, item ); |
| 81 | + this.emit( 'unshift', item ); |
| 82 | + this.emit( 'update' ); |
| 83 | + return this.length; |
| 84 | +}; |
| 85 | + |
| 86 | +/** |
| 87 | + * Removes an item from the end of the list. |
| 88 | + * |
| 89 | + * @method |
| 90 | + * @returns {Integer} Removed item |
| 91 | + * @emits pop |
| 92 | + * @emits update |
| 93 | + */ |
| 94 | +es.ModelList.prototype.pop = function() { |
| 95 | + if ( this.length ) { |
| 96 | + var item = this[this.length - 1]; |
| 97 | + item.detach(); |
| 98 | + item.removeListener( 'update', this.relayUpdate ); |
| 99 | + Array.prototype.pop.call( this, item ); |
| 100 | + this.emit( 'pop' ); |
| 101 | + this.emit( 'update' ); |
| 102 | + return item; |
| 103 | + } |
| 104 | +}; |
| 105 | + |
| 106 | +/** |
| 107 | + * Removes an item from the beginning of the list. |
| 108 | + * |
| 109 | + * @method |
| 110 | + * @returns {Integer} Removed item |
| 111 | + * @emits shift |
| 112 | + * @emits update |
| 113 | + */ |
| 114 | +es.ModelList.prototype.shift = function() { |
| 115 | + if ( this.length ) { |
| 116 | + var item = this[0]; |
| 117 | + item.detach(); |
| 118 | + item.removeListener( 'update', this.relayUpdate ); |
| 119 | + Array.prototype.shift.call( this, item ); |
| 120 | + this.emit( 'shift' ); |
| 121 | + this.emit( 'update' ); |
| 122 | + return item; |
| 123 | + } |
| 124 | +}; |
| 125 | + |
| 126 | +/** |
| 127 | + * Removes an item from the beginning of the list. |
| 128 | + * |
| 129 | + * @method |
| 130 | + * @param {Integer} index Index to remove and or insert items |
| 131 | + * @param {Integer} howmany Number of items to remove |
| 132 | + * @param {es.ModelItem} [...] Variadic list of items to insert |
| 133 | + * @returns {es.ModelItem[]} Removed items |
| 134 | + * @emits splice (index, howmany, [...]) |
| 135 | + * @emits update |
| 136 | + */ |
| 137 | +es.ModelList.prototype.splice = function( index, howmany ) { |
| 138 | + var args = Array.prototype.slice.call( arguments, 0 ); |
| 139 | + if ( args.length >= 3 ) { |
| 140 | + for ( var i = 2; i < args.length; i++ ) { |
| 141 | + args[i].attach( this ); |
| 142 | + } |
| 143 | + } |
| 144 | + var removed = Array.prototype.splice.apply( this, args ); |
| 145 | + for ( var i = 0; i < removed.length; i++ ) { |
| 146 | + removed[i].detach(); |
| 147 | + removed[i].removeListener( 'update', this.relayUpdate ); |
| 148 | + } |
| 149 | + this.emit.apply( ['splice'].concat( args ) ); |
| 150 | + this.emit( 'update' ); |
| 151 | + return removed; |
| 152 | +}; |
| 153 | + |
| 154 | +/** |
| 155 | + * Sorts items in list. |
| 156 | + * |
| 157 | + * @method |
| 158 | + * @param {Function} sortfunc Function to use when sorting |
| 159 | + * @emits sort |
| 160 | + * @emits update |
| 161 | + */ |
| 162 | +es.ModelList.prototype.sort = function( sortfunc ) { |
| 163 | + this.emit( 'sort' ); |
| 164 | + this.emit( 'update' ); |
| 165 | + Array.prototype.reverse.call( this ); |
| 166 | +}; |
| 167 | + |
| 168 | +/** |
| 169 | + * Reverses the order of the list. |
| 170 | + * |
| 171 | + * @method |
| 172 | + * @emits reverse |
| 173 | + * @emits update |
| 174 | + */ |
| 175 | +es.ModelList.prototype.reverse = function() { |
| 176 | + this.emit( 'reverse' ); |
| 177 | + this.emit( 'update' ); |
| 178 | + Array.prototype.reverse.call( this ); |
| 179 | +}; |
| 180 | + |
| 181 | +/* Inheritance */ |
| 182 | + |
| 183 | +es.extend( es.ModelList, es.EventEmitter ); |