r88618 MediaWiki - Code Review archive

Repository:MediaWiki
Revision:r88617‎ | r88618 | r88619 >
Date:23:16, 22 May 2011
Author:krinkle
Status:ok
Tags:
Comment:
Copy of jquery.suggestions in UploadWizard is outdated, using core module instead.
> > (bug 29099 / bug 26306) Cleanup extensions that include jquery (and common plugins) themselves
Modified paths:
  • /trunk/extensions/UploadWizard/UploadWizardHooks.php (modified) (history)
  • /trunk/extensions/UploadWizard/resources/jquery/jquery.suggestions.css (deleted) (history)
  • /trunk/extensions/UploadWizard/resources/jquery/jquery.suggestions.js (deleted) (history)

Diff [purge]

Index: trunk/extensions/UploadWizard/UploadWizardHooks.php
@@ -20,10 +20,11 @@
2121 'jquery.ui.dialog',
2222 'jquery.ui.datepicker',
2323 'jquery.ui.progressbar',
 24+ 'jquery.suggestions',
2425 'jquery.ui.widget',
2526 'mediawiki.language',
2627 'mediawiki.util',
27 - 'ext.uploadwizard.mediawiki.language.parser'
 28+ 'ext.uploadwizard.mediawiki.language.parser',
2829 ),
2930 'scripts' => array(
3031
@@ -35,7 +36,6 @@
3637 'resources/jquery/jquery.autocomplete.js',
3738 'resources/jquery/jquery.spinner.js',
3839 'resources/jquery/jquery.autoEllipsis.js',
39 - 'resources/jquery/jquery.suggestions.js',
4040 'resources/jquery/jquery.removeCtrl.js',
4141 'resources/jquery/jquery.pubsub.js',
4242
@@ -87,7 +87,6 @@
8888 ),
8989 'styles' => array(
9090 'resources/jquery/jquery.tipsy.css',
91 - 'resources/jquery/jquery.suggestions.css',
9291 'resources/uploadWizard.css',
9392 'resources/jquery/jquery.arrowSteps.css',
9493 'resources/jquery/jquery.mwCoolCats.css',
Index: trunk/extensions/UploadWizard/resources/jquery/jquery.suggestions.css
@@ -1,68 +0,0 @@
2 -/* suggestions plugin */
3 -
4 -.suggestions {
5 - overflow: hidden;
6 - position: absolute;
7 - top: 0px;
8 - left: 0px;
9 - width: 0px;
10 - border: none;
11 - z-index: 99;
12 - padding: 0;
13 - margin: -1px 0 0 0;
14 -}
15 -.suggestions-special {
16 - position: relative;
17 - background-color: Window;
18 - font-size: 0.8em;
19 - cursor: pointer;
20 - border: solid 1px #aaaaaa;
21 - padding: 0;
22 - margin: 0;
23 - margin-top: -2px;
24 - display: none;
25 - padding: 0.25em 0.25em;
26 - line-height: 1.25em;
27 -}
28 -.suggestions-results {
29 - background-color: white;
30 - background-color: Window;
31 - font-size: 0.8em;
32 - cursor: pointer;
33 - border: solid 1px #aaaaaa;
34 - padding: 0;
35 - margin: 0;
36 -}
37 -.suggestions-result {
38 - color: black;
39 - color: WindowText;
40 - margin: 0;
41 - line-height: 1.5em;
42 - padding: 0.01em 0.25em;
43 -}
44 -.suggestions-result-current {
45 - background-color: #4C59A6;
46 - background-color: Highlight;
47 - color: white;
48 - color: HighlightText;
49 -}
50 -.suggestions-special .special-label {
51 - font-size: 0.8em;
52 - color: gray;
53 -}
54 -.suggestions-special .special-query {
55 - color: black;
56 - font-style: italic;
57 -}
58 -.suggestions-special .special-hover {
59 - background-color: silver;
60 -}
61 -.suggestions-result-current .special-label,
62 -.suggestions-result-current .special-query {
63 - color: white;
64 - color: HighlightText;
65 -}
66 -.autoellipsis-matched,
67 -.highlight {
68 - font-weight: bold;
69 -}
\ No newline at end of file
Index: trunk/extensions/UploadWizard/resources/jquery/jquery.suggestions.js
@@ -1,521 +0,0 @@
2 -/**
3 - * This plugin provides a generic way to add suggestions to a text box.
4 - *
5 - * Usage:
6 - *
7 - * Set options:
8 - * $('#textbox').suggestions( { option1: value1, option2: value2 } );
9 - * $('#textbox').suggestions( option, value );
10 - * Get option:
11 - * value = $('#textbox').suggestions( option );
12 - * Initialize:
13 - * $('#textbox').suggestions();
14 - *
15 - * Options:
16 - *
17 - * fetch(query): Callback that should fetch suggestions and set the suggestions property. Executed in the context of the
18 - * textbox
19 - * Type: Function
20 - * cancel: Callback function to call when any pending asynchronous suggestions fetches should be canceled.
21 - * Executed in the context of the textbox
22 - * Type: Function
23 - * special: Set of callbacks for rendering and selecting
24 - * Type: Object of Functions 'render' and 'select'
25 - * result: Set of callbacks for rendering and selecting
26 - * Type: Object of Functions 'render' and 'select'
27 - * $region: jQuery selection of element to place the suggestions below and match width of
28 - * Type: jQuery Object, Default: $(this)
29 - * suggestions: Suggestions to display
30 - * Type: Array of strings
31 - * maxRows: Maximum number of suggestions to display at one time
32 - * Type: Number, Range: 1 - 100, Default: 7
33 - * delay: Number of ms to wait for the user to stop typing
34 - * Type: Number, Range: 0 - 1200, Default: 120
35 - * submitOnClick: Whether to submit the form containing the textbox when a suggestion is clicked
36 - * Type: Boolean, Default: false
37 - * maxExpandFactor: Maximum suggestions box width relative to the textbox width. If set to e.g. 2, the suggestions box
38 - * will never be grown beyond 2 times the width of the textbox.
39 - * Type: Number, Range: 1 - infinity, Default: 3
40 - * positionFromLeft: Whether to position the suggestion box with the left attribute or the right
41 - * Type: Boolean, Default: true
42 - * highlightInput: Whether to hightlight matched portions of the input or not
43 - * Type: Boolean, Default: false
44 - */
45 -( function( $ ) {
46 -
47 -$.suggestions = {
48 - /**
49 - * Cancel any delayed updateSuggestions() call and inform the user so
50 - * they can cancel their result fetching if they use AJAX or something
51 - */
52 - cancel: function( context ) {
53 - if ( context.data.timerID != null ) {
54 - clearTimeout( context.data.timerID );
55 - }
56 - if ( typeof context.config.cancel == 'function' ) {
57 - context.config.cancel.call( context.data.$textbox );
58 - }
59 - },
60 - /**
61 - * Restore the text the user originally typed in the textbox, before it was overwritten by highlight(). This
62 - * restores the value the currently displayed suggestions are based on, rather than the value just before
63 - * highlight() overwrote it; the former is arguably slightly more sensible.
64 - */
65 - restore: function( context ) {
66 - context.data.$textbox.val( context.data.prevText );
67 - },
68 - /**
69 - * Ask the user-specified callback for new suggestions. Any previous delayed call to this function still pending
70 - * will be canceled. If the value in the textbox hasn't changed since the last time suggestions were fetched, this
71 - * function does nothing.
72 - * @param {Boolean} delayed Whether or not to delay this by the currently configured amount of time
73 - */
74 - update: function( context, delayed ) {
75 - // Only fetch if the value in the textbox changed
76 - function maybeFetch() {
77 - if ( context.data.$textbox.val() !== context.data.prevText ) {
78 - context.data.prevText = context.data.$textbox.val();
79 - if ( typeof context.config.fetch == 'function' ) {
80 - context.config.fetch.call( context.data.$textbox, context.data.$textbox.val() );
81 - }
82 - }
83 - }
84 - // Cancel previous call
85 - if ( context.data.timerID != null ) {
86 - clearTimeout( context.data.timerID );
87 - }
88 - if ( delayed ) {
89 - // Start a new asynchronous call
90 - context.data.timerID = setTimeout( maybeFetch, context.config.delay );
91 - } else {
92 - maybeFetch();
93 - }
94 - $.suggestions.special( context );
95 - },
96 - special: function( context ) {
97 - // Allow custom rendering - but otherwise don't do any rendering
98 - if ( typeof context.config.special.render == 'function' ) {
99 - // Wait for the browser to update the value
100 - setTimeout( function() {
101 - // Render special
102 - $special = context.data.$container.find( '.suggestions-special' );
103 - context.config.special.render.call( $special, context.data.$textbox.val() );
104 - }, 1 );
105 - }
106 - },
107 - /**
108 - * Sets the value of a property, and updates the widget accordingly
109 - * @param {String} property Name of property
110 - * @param {Mixed} value Value to set property with
111 - */
112 - configure: function( context, property, value ) {
113 - // Validate creation using fallback values
114 - switch( property ) {
115 - case 'fetch':
116 - case 'cancel':
117 - case 'special':
118 - case 'result':
119 - case '$region':
120 - context.config[property] = value;
121 - break;
122 - case 'suggestions':
123 - context.config[property] = value;
124 - // Update suggestions
125 - if ( typeof context.data !== 'undefined' ) {
126 - if ( context.data.$textbox.val().length == 0 ) {
127 - // Hide the div when no suggestion exist
128 - context.data.$container.hide();
129 - } else {
130 - // Rebuild the suggestions list
131 - context.data.$container.show();
132 - // Update the size and position of the list
133 - var newCSS = {
134 - 'top': context.config.$region.offset().top + context.config.$region.outerHeight(),
135 - 'bottom': 'auto',
136 - 'width': context.config.$region.outerWidth(),
137 - 'height': 'auto'
138 - };
139 - if ( context.config.positionFromLeft ) {
140 - newCSS['left'] = context.config.$region.offset().left;
141 - newCSS['right'] = 'auto';
142 - } else {
143 - newCSS['left'] = 'auto';
144 - newCSS['right'] = $( 'body' ).width() - ( context.config.$region.offset().left + context.config.$region.outerWidth() );
145 - }
146 - context.data.$container.css( newCSS );
147 - var $results = context.data.$container.children( '.suggestions-results' );
148 - $results.empty();
149 - var expWidth = -1;
150 - var $autoEllipseMe = $( [] );
151 - var matchedText = null;
152 - for ( var i = 0; i < context.config.suggestions.length; i++ ) {
153 - var text = context.config.suggestions[i];
154 - var $result = $( '<div />' )
155 - .addClass( 'suggestions-result' )
156 - .attr( 'rel', i )
157 - .data( 'text', context.config.suggestions[i] )
158 - .mousemove( function( e ) {
159 - context.data.selectedWithMouse = true;
160 - $.suggestions.highlight(
161 - context, $(this).closest( '.suggestions-results div' ), false
162 - );
163 - } )
164 - .appendTo( $results );
165 - // Allow custom rendering
166 - if ( typeof context.config.result.render == 'function' ) {
167 - context.config.result.render.call( $result, context.config.suggestions[i] );
168 - } else {
169 - // Add <span> with text
170 - if( context.config.highlightInput ) {
171 - matchedText = context.data.prevText;
172 - }
173 - $result.append( $( '<span />' )
174 - .css( 'whiteSpace', 'nowrap' )
175 - .text( text )
176 - );
177 -
178 - // Widen results box if needed
179 - // New width is only calculated here, applied later
180 - var $span = $result.children( 'span' );
181 - if ( $span.outerWidth() > $result.width() && $span.outerWidth() > expWidth ) {
182 - // factor in any padding, margin, or border space on the parent
183 - expWidth = $span.outerWidth() + ( context.data.$container.width() - $span.parent().width());
184 - }
185 - $autoEllipseMe = $autoEllipseMe.add( $result );
186 - }
187 - }
188 - // Apply new width for results box, if any
189 - if ( expWidth > context.data.$container.width() ) {
190 - var maxWidth = context.config.maxExpandFactor*context.data.$textbox.width();
191 - context.data.$container.width( Math.min( expWidth, maxWidth ) );
192 - }
193 - // autoEllipse the results. Has to be done after changing the width
194 - $autoEllipseMe.autoEllipsis( { hasSpan: true, tooltip: true, matchText: matchedText } );
195 - }
196 - }
197 - break;
198 - case 'maxRows':
199 - context.config[property] = Math.max( 1, Math.min( 100, value ) );
200 - break;
201 - case 'delay':
202 - context.config[property] = Math.max( 0, Math.min( 1200, value ) );
203 - break;
204 - case 'maxExpandFactor':
205 - context.config[property] = Math.max( 1, value );
206 - break;
207 - case 'submitOnClick':
208 - case 'positionFromLeft':
209 - case 'highlightInput':
210 - context.config[property] = value ? true : false;
211 - break;
212 - }
213 - },
214 - /**
215 - * Highlight a result in the results table
216 - * @param result <tr> to highlight: jQuery object, or 'prev' or 'next'
217 - * @param updateTextbox If true, put the suggestion in the textbox
218 - */
219 - highlight: function( context, result, updateTextbox ) {
220 - var selected = context.data.$container.find( '.suggestions-result-current' );
221 - if ( !result.get || selected.get( 0 ) != result.get( 0 ) ) {
222 - if ( result == 'prev' ) {
223 - if( selected.is( '.suggestions-special' ) ) {
224 - result = context.data.$container.find( '.suggestions-result:last' )
225 - } else {
226 - result = selected.prev();
227 - if ( selected.length == 0 ) {
228 - // we are at the begginning, so lets jump to the last item
229 - if ( context.data.$container.find( '.suggestions-special' ).html() != "" ) {
230 - result = context.data.$container.find( '.suggestions-special' );
231 - } else {
232 - result = context.data.$container.find( '.suggestions-results div:last' );
233 - }
234 - }
235 - }
236 - } else if ( result == 'next' ) {
237 - if ( selected.length == 0 ) {
238 - // No item selected, go to the first one
239 - result = context.data.$container.find( '.suggestions-results div:first' );
240 - if ( result.length == 0 && context.data.$container.find( '.suggestions-special' ).html() != "" ) {
241 - // No suggestion exists, go to the special one directly
242 - result = context.data.$container.find( '.suggestions-special' );
243 - }
244 - } else {
245 - result = selected.next();
246 - if ( selected.is( '.suggestions-special' ) ) {
247 - result = $( [] );
248 - } else if (
249 - result.length == 0 &&
250 - context.data.$container.find( '.suggestions-special' ).html() != ""
251 - ) {
252 - // We were at the last item, jump to the specials!
253 - result = context.data.$container.find( '.suggestions-special' );
254 - }
255 - }
256 - }
257 - selected.removeClass( 'suggestions-result-current' );
258 - result.addClass( 'suggestions-result-current' );
259 - }
260 - if ( updateTextbox ) {
261 - if ( result.length == 0 || result.is( '.suggestions-special' ) ) {
262 - $.suggestions.restore( context );
263 - } else {
264 - context.data.$textbox.val( result.data( 'text' ) );
265 - // .val() doesn't call any event handlers, so
266 - // let the world know what happened
267 - context.data.$textbox.change();
268 - }
269 - context.data.$textbox.trigger( 'change' );
270 - }
271 - },
272 - /**
273 - * Respond to keypress event
274 - * @param {Integer} key Code of key pressed
275 - */
276 - keypress: function( e, context, key ) {
277 - var wasVisible = context.data.$container.is( ':visible' );
278 - var preventDefault = false;
279 - switch ( key ) {
280 - // Arrow down
281 - case 40:
282 - if ( wasVisible ) {
283 - $.suggestions.highlight( context, 'next', true );
284 - context.data.selectedWithMouse = false;
285 - } else {
286 - $.suggestions.update( context, false );
287 - }
288 - preventDefault = true;
289 - break;
290 - // Arrow up
291 - case 38:
292 - if ( wasVisible ) {
293 - $.suggestions.highlight( context, 'prev', true );
294 - context.data.selectedWithMouse = false;
295 - }
296 - preventDefault = wasVisible;
297 - break;
298 - // Escape
299 - case 27:
300 - context.data.$container.hide();
301 - $.suggestions.restore( context );
302 - $.suggestions.cancel( context );
303 - context.data.$textbox.trigger( 'change' );
304 - preventDefault = wasVisible;
305 - break;
306 - // Enter
307 - case 13:
308 - context.data.$container.hide();
309 - preventDefault = wasVisible;
310 - selected = context.data.$container.find( '.suggestions-result-current' );
311 - if ( selected.size() == 0 || context.data.selectedWithMouse ) {
312 - // if nothing is selected OR if something was selected with the mouse,
313 - // cancel any current requests and submit the form
314 - $.suggestions.cancel( context );
315 - context.config.$region.closest( 'form' ).submit();
316 - } else if ( selected.is( '.suggestions-special' ) ) {
317 - if ( typeof context.config.special.select == 'function' ) {
318 - context.config.special.select.call( selected, context.data.$textbox );
319 - }
320 - } else {
321 - if ( typeof context.config.result.select == 'function' ) {
322 - $.suggestions.highlight( context, selected, true );
323 - context.config.result.select.call( selected, context.data.$textbox );
324 - } else {
325 - $.suggestions.highlight( context, selected, true );
326 - }
327 - }
328 - break;
329 - default:
330 - $.suggestions.update( context, true );
331 - break;
332 - }
333 - if ( preventDefault ) {
334 - e.preventDefault();
335 - e.stopImmediatePropagation();
336 - }
337 - }
338 -};
339 -$.fn.suggestions = function() {
340 -
341 - // Multi-context fields
342 - var returnValue = null;
343 - var args = arguments;
344 -
345 - $(this).each( function() {
346 -
347 - /* Construction / Loading */
348 -
349 - var context = $(this).data( 'suggestions-context' );
350 - if ( typeof context == 'undefined' || context == null ) {
351 - context = {
352 - config: {
353 - 'fetch' : function() {},
354 - 'cancel': function() {},
355 - 'special': {},
356 - 'result': {},
357 - '$region': $(this),
358 - 'suggestions': [],
359 - 'maxRows': 7,
360 - 'delay': 120,
361 - 'submitOnClick': false,
362 - 'maxExpandFactor': 3,
363 - 'positionFromLeft': true,
364 - 'highlightInput': false
365 - }
366 - };
367 - }
368 -
369 - /* API */
370 -
371 - // Handle various calling styles
372 - if ( args.length > 0 ) {
373 - if ( typeof args[0] == 'object' ) {
374 - // Apply set of properties
375 - for ( var key in args[0] ) {
376 - $.suggestions.configure( context, key, args[0][key] );
377 - }
378 - } else if ( typeof args[0] == 'string' ) {
379 - if ( args.length > 1 ) {
380 - // Set property values
381 - $.suggestions.configure( context, args[0], args[1] );
382 - } else if ( returnValue == null ) {
383 - // Get property values, but don't give access to internal data - returns only the first
384 - returnValue = ( args[0] in context.config ? undefined : context.config[args[0]] );
385 - }
386 - }
387 - }
388 -
389 - /* Initialization */
390 -
391 - if ( typeof context.data == 'undefined' ) {
392 - context.data = {
393 - // ID of running timer
394 - 'timerID': null,
395 - // Text in textbox when suggestions were last fetched
396 - 'prevText': null,
397 - // Number of results visible without scrolling
398 - 'visibleResults': 0,
399 - // Suggestion the last mousedown event occured on
400 - 'mouseDownOn': $( [] ),
401 - '$textbox': $(this),
402 - 'selectedWithMouse': false
403 - };
404 - // Setup the css for positioning the results box
405 - var newCSS = {
406 - 'top': Math.round( context.data.$textbox.offset().top + context.data.$textbox.outerHeight() ),
407 - 'width': context.data.$textbox.outerWidth(),
408 - 'display': 'none'
409 - };
410 - if ( context.config.positionFromLeft ) {
411 - newCSS['left'] = context.config.$region.offset().left;
412 - newCSS['right'] = 'auto';
413 - } else {
414 - newCSS['left'] = 'auto';
415 - newCSS['right'] = $( 'body' ).width() - ( context.config.$region.offset().left + context.config.$region.outerWidth() );
416 - }
417 -
418 - context.data.$container = $( '<div />' )
419 - .css( newCSS )
420 - .addClass( 'suggestions' )
421 - .append(
422 - $( '<div />' ).addClass( 'suggestions-results' )
423 - // Can't use click() because the container div is hidden when the textbox loses focus. Instead,
424 - // listen for a mousedown followed by a mouseup on the same div
425 - .mousedown( function( e ) {
426 - context.data.mouseDownOn = $( e.target ).closest( '.suggestions-results div' );
427 - } )
428 - .mouseup( function( e ) {
429 - var $result = $( e.target ).closest( '.suggestions-results div' );
430 - var $other = context.data.mouseDownOn;
431 - context.data.mouseDownOn = $( [] );
432 - if ( $result.get( 0 ) != $other.get( 0 ) ) {
433 - return;
434 - }
435 - $.suggestions.highlight( context, $result, true );
436 - context.data.$container.hide();
437 - if ( typeof context.config.result.select == 'function' ) {
438 - context.config.result.select.call( $result, context.data.$textbox );
439 - }
440 - context.data.$textbox.focus();
441 - } )
442 - )
443 - .append(
444 - $( '<div />' ).addClass( 'suggestions-special' )
445 - // Can't use click() because the container div is hidden when the textbox loses focus. Instead,
446 - // listen for a mousedown followed by a mouseup on the same div
447 - .mousedown( function( e ) {
448 - context.data.mouseDownOn = $( e.target ).closest( '.suggestions-special' );
449 - } )
450 - .mouseup( function( e ) {
451 - var $special = $( e.target ).closest( '.suggestions-special' );
452 - var $other = context.data.mouseDownOn;
453 - context.data.mouseDownOn = $( [] );
454 - if ( $special.get( 0 ) != $other.get( 0 ) ) {
455 - return;
456 - }
457 - context.data.$container.hide();
458 - if ( typeof context.config.special.select == 'function' ) {
459 - context.config.special.select.call( $special, context.data.$textbox );
460 - }
461 - context.data.$textbox.focus();
462 - } )
463 - .mousemove( function( e ) {
464 - context.data.selectedWithMouse = true;
465 - $.suggestions.highlight(
466 - context, $( e.target ).closest( '.suggestions-special' ), false
467 - );
468 - } )
469 - )
470 - .appendTo( $( 'body' ) );
471 - $(this)
472 - // Stop browser autocomplete from interfering
473 - .attr( 'autocomplete', 'off')
474 - .keydown( function( e ) {
475 - // Store key pressed to handle later
476 - context.data.keypressed = ( e.keyCode == undefined ) ? e.which : e.keyCode;
477 - context.data.keypressedCount = 0;
478 -
479 - switch ( context.data.keypressed ) {
480 - // This preventDefault logic is duplicated from
481 - // $.suggestions.keypress(), which sucks
482 - case 40:
483 - e.preventDefault();
484 - e.stopImmediatePropagation();
485 - break;
486 - case 38:
487 - case 27:
488 - case 13:
489 - if ( context.data.$container.is( ':visible' ) ) {
490 - e.preventDefault();
491 - e.stopImmediatePropagation();
492 - }
493 - }
494 - } )
495 - .keypress( function( e ) {
496 - context.data.keypressedCount++;
497 - $.suggestions.keypress( e, context, context.data.keypressed );
498 - } )
499 - .keyup( function( e ) {
500 - // Some browsers won't throw keypress() for arrow keys. If we got a keydown and a keyup without a
501 - // keypress in between, solve it
502 - if ( context.data.keypressedCount == 0 ) {
503 - $.suggestions.keypress( e, context, context.data.keypressed );
504 - }
505 - } )
506 - .blur( function() {
507 - // When losing focus because of a mousedown
508 - // on a suggestion, don't hide the suggestions
509 - if ( context.data.mouseDownOn.length > 0 ) {
510 - return;
511 - }
512 - context.data.$container.hide();
513 - $.suggestions.cancel( context );
514 - } );
515 - }
516 - // Store the context for next time
517 - $(this).data( 'suggestions-context', context );
518 - } );
519 - return returnValue !== null ? returnValue : $(this);
520 -};
521 -
522 -} )( jQuery );

Follow-up revisions

RevisionCommit summaryAuthorDate
r88621jquery.autoEllipsis is good from core...reedy23:34, 22 May 2011

Past revisions this follows-up on

RevisionCommit summaryAuthorDate
r87847* (bug 26306) Cleaned up stray jquery, jquery.ui, and jquery.json copies in C...brion21:53, 10 May 2011
r88611Kill the jquery.effects and jquery.ui/i18n folders which are dupe of core...reedy22:35, 22 May 2011
r88612Kill themes folder which is a duplicate of what's in phase3 also...reedy22:43, 22 May 2011
r88613Kill jquery.ui as it's duplicate of what's in phase3, and not directly used...reedy22:46, 22 May 2011
r88616More duplicate/unreferenced files...reedy22:52, 22 May 2011

Status & tagging log