1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336 | 2×
2×
50×
50×
2×
264×
2×
42×
2×
2×
2×
14×
2×
2×
2×
28×
30×
30×
6×
2×
4×
18×
18×
16×
16×
16×
4×
28×
24×
24×
26×
26×
24×
24×
24×
2×
54×
4×
54×
2×
52×
52×
122×
122×
8×
8×
114×
106×
114×
44×
2×
10×
10×
2×
8×
8×
8×
6×
8×
8×
2×
212×
8×
204×
196×
196×
102×
196×
204×
204×
204×
2×
18×
12×
18×
2×
16×
16×
16×
16×
14×
12×
14×
14×
16×
2×
20×
20×
42×
42×
40×
20×
20×
| ( function () {
/**
* Contain and manage a list of {@link OO.EventEmitter} items.
*
* Aggregates and manages their events collectively.
*
* This mixin must be used in a class that also mixes in {@link OO.EventEmitter}.
*
* @abstract
* @class
*/
OO.EmitterList = function OoEmitterList() {
this.items = [];
this.aggregateItemEvents = {};
};
OO.initClass( OO.EmitterList );
/* Events */
/**
* Item has been added.
*
* @event OO.EmitterList#add
* @param {OO.EventEmitter} item Added item
* @param {number} index Index items were added at
*/
/**
* Item has been moved to a new index.
*
* @event OO.EmitterList#move
* @param {OO.EventEmitter} item Moved item
* @param {number} index Index item was moved to
* @param {number} oldIndex The original index the item was in
*/
/**
* Item has been removed.
*
* @event OO.EmitterList#remove
* @param {OO.EventEmitter} item Removed item
* @param {number} index Index the item was removed from
*/
/**
* The list has been cleared of items.
*
* @event OO.EmitterList#clear
*/
/* Methods */
/**
* Normalize requested index to fit into the bounds of the given array.
*
* @private
* @static
* @param {Array} arr Given array
* @param {number|undefined} index Requested index
* @return {number} Normalized index
*/
function normalizeArrayIndex( arr, index ) {
return ( index === undefined || index < 0 || index >= arr.length ) ?
arr.length :
index;
}
/**
* Get all items.
*
* @return {OO.EventEmitter[]} Items in the list
*/
OO.EmitterList.prototype.getItems = function () {
return this.items.slice( 0 );
};
/**
* Get the index of a specific item.
*
* @param {OO.EventEmitter} item Requested item
* @return {number} Index of the item
*/
OO.EmitterList.prototype.getItemIndex = function ( item ) {
return this.items.indexOf( item );
};
/**
* Get number of items.
*
* @return {number} Number of items in the list
*/
OO.EmitterList.prototype.getItemCount = function () {
return this.items.length;
};
/**
* Check if a list contains no items.
*
* @return {boolean} Group is empty
*/
OO.EmitterList.prototype.isEmpty = function () {
return !this.items.length;
};
/**
* Aggregate the events emitted by the group.
*
* When events are aggregated, the group will listen to all contained items for the event,
* and then emit the event under a new name. The new event will contain an additional leading
* parameter containing the item that emitted the original event. Other arguments emitted from
* the original event are passed through.
*
* @param {Object} events An object keyed by the name of the event that
* should be aggregated (e.g., ‘click’) and the value of the new name to use
* (e.g., ‘groupClick’). A `null` value will remove aggregated events.
* @throws {Error} If aggregation already exists
*/
OO.EmitterList.prototype.aggregate = function ( events ) {
let i, item;
for ( const itemEvent in events ) {
const groupEvent = events[ itemEvent ];
// Remove existing aggregated event
if ( Object.prototype.hasOwnProperty.call( this.aggregateItemEvents, itemEvent ) ) {
// Don't allow duplicate aggregations
if ( groupEvent ) {
throw new Error( 'Duplicate item event aggregation for ' + itemEvent );
}
// Remove event aggregation from existing items
for ( i = 0; i < this.items.length; i++ ) {
item = this.items[ i ];
if ( item.connect && item.disconnect ) {
const remove = {};
remove[ itemEvent ] = [ 'emit', this.aggregateItemEvents[ itemEvent ], item ];
item.disconnect( this, remove );
}
}
// Prevent future items from aggregating event
delete this.aggregateItemEvents[ itemEvent ];
}
// Add new aggregate event
if ( groupEvent ) {
// Make future items aggregate event
this.aggregateItemEvents[ itemEvent ] = groupEvent;
// Add event aggregation to existing items
for ( i = 0; i < this.items.length; i++ ) {
item = this.items[ i ];
if ( item.connect && item.disconnect ) {
const add = {};
add[ itemEvent ] = [ 'emit', groupEvent, item ];
item.connect( this, add );
}
}
}
}
};
/**
* Add items to the list.
*
* @param {OO.EventEmitter|OO.EventEmitter[]} items Item to add or
* an array of items to add
* @param {number} [index] Index to add items at. If no index is
* given, or if the index that is given is invalid, the item
* will be added at the end of the list.
* @return {OO.EmitterList}
* @fires OO.EmitterList#add
* @fires OO.EmitterList#move
*/
OO.EmitterList.prototype.addItems = function ( items, index ) {
if ( !Array.isArray( items ) ) {
items = [ items ];
}
if ( items.length === 0 ) {
return this;
}
index = normalizeArrayIndex( this.items, index );
for ( let i = 0; i < items.length; i++ ) {
const oldIndex = this.items.indexOf( items[ i ] );
if ( oldIndex !== -1 ) {
// Move item to new index
index = this.moveItem( items[ i ], index );
this.emit( 'move', items[ i ], index, oldIndex );
} else {
// insert item at index
index = this.insertItem( items[ i ], index );
this.emit( 'add', items[ i ], index );
}
index++;
}
return this;
};
/**
* Move an item from its current position to a new index.
*
* The item is expected to exist in the list. If it doesn't,
* the method will throw an exception.
*
* @private
* @param {OO.EventEmitter} item Items to add
* @param {number} newIndex Index to move the item to
* @return {number} The index the item was moved to
* @throws {Error} If item is not in the list
*/
OO.EmitterList.prototype.moveItem = function ( item, newIndex ) {
const existingIndex = this.items.indexOf( item );
if ( existingIndex === -1 ) {
throw new Error( 'Item cannot be moved, because it is not in the list.' );
}
newIndex = normalizeArrayIndex( this.items, newIndex );
// Remove the item from the current index
this.items.splice( existingIndex, 1 );
// If necessary, adjust new index after removal
if ( existingIndex < newIndex ) {
newIndex--;
}
// Move the item to the new index
this.items.splice( newIndex, 0, item );
return newIndex;
};
/**
* Utility method to insert an item into the list, and
* connect it to aggregate events.
*
* Don't call this directly unless you know what you're doing.
* Use {@link OO.EmitterList#addItems|addItems()} instead.
*
* This method can be extended in child classes to produce
* different behavior when an item is inserted. For example,
* inserted items may also be attached to the DOM or may
* interact with some other nodes in certain ways. Extending
* this method is allowed, but if overridden, the aggregation
* of events must be preserved, or behavior of emitted events
* will be broken.
*
* If you are extending this method, please make sure the
* parent method is called.
*
* @protected
* @param {OO.EventEmitter|Object} item Item to add
* @param {number} index Index to add items at
* @return {number} The index the item was added at
*/
OO.EmitterList.prototype.insertItem = function ( item, index ) {
// Throw an error if null or item is not an object.
if ( item === null || typeof item !== 'object' ) {
throw new Error( 'Expected object, but item is ' + typeof item );
}
// Add the item to event aggregation
if ( item.connect && item.disconnect ) {
const events = {};
for ( const event in this.aggregateItemEvents ) {
events[ event ] = [ 'emit', this.aggregateItemEvents[ event ], item ];
}
item.connect( this, events );
}
index = normalizeArrayIndex( this.items, index );
// Insert into items array
this.items.splice( index, 0, item );
return index;
};
/**
* Remove items.
*
* @param {OO.EventEmitter|OO.EventEmitter[]} items Items to remove
* @return {OO.EmitterList}
* @fires OO.EmitterList#remove
*/
OO.EmitterList.prototype.removeItems = function ( items ) {
if ( !Array.isArray( items ) ) {
items = [ items ];
}
if ( items.length === 0 ) {
return this;
}
// Remove specific items
for ( let i = 0; i < items.length; i++ ) {
const item = items[ i ];
const index = this.items.indexOf( item );
if ( index !== -1 ) {
if ( item.connect && item.disconnect ) {
// Disconnect all listeners from the item
item.disconnect( this );
}
this.items.splice( index, 1 );
this.emit( 'remove', item, index );
}
}
return this;
};
/**
* Clear all items.
*
* @return {OO.EmitterList}
* @fires OO.EmitterList#clear
*/
OO.EmitterList.prototype.clearItems = function () {
const cleared = this.items.splice( 0, this.items.length );
// Disconnect all items
for ( let i = 0; i < cleared.length; i++ ) {
const item = cleared[ i ];
if ( item.connect && item.disconnect ) {
item.disconnect( this );
}
}
this.emit( 'clear' );
return this;
};
}() );
|