r85608 MediaWiki - Code Review archive

Repository:MediaWiki
Revision:r85607‎ | r85608 | r85609 >
Date:06:10, 7 April 2011
Author:dale
Status:deferred
Tags:
Comment:
merge in uploadWizard updates ( r85606 )
Modified paths:
  • /branches/uploadwizard-firefogg/UploadWizard.config.php (modified) (history)
  • /branches/uploadwizard-firefogg/resources/jquery/jquery.mwCoolCats.css (modified) (history)
  • /branches/uploadwizard-firefogg/resources/jquery/jquery.mwCoolCats.js (modified) (history)
  • /branches/uploadwizard-firefogg/resources/jquery/jquery.tipsy.css (modified) (history)
  • /branches/uploadwizard-firefogg/resources/jquery/jquery.tipsy.js (modified) (history)
  • /branches/uploadwizard-firefogg/resources/mediawiki.language.parser.js (modified) (history)
  • /branches/uploadwizard-firefogg/resources/mw.Api.edit.js (modified) (history)
  • /branches/uploadwizard-firefogg/resources/mw.Api.js (modified) (history)
  • /branches/uploadwizard-firefogg/resources/mw.ApiUploadHandler.js (modified) (history)
  • /branches/uploadwizard-firefogg/resources/mw.DestinationChecker.js (modified) (history)
  • /branches/uploadwizard-firefogg/resources/mw.Log.js (modified) (history)
  • /branches/uploadwizard-firefogg/resources/mw.Title.js (modified) (history)
  • /branches/uploadwizard-firefogg/resources/mw.UploadWizard.js (modified) (history)
  • /branches/uploadwizard-firefogg/resources/mw.UploadWizardDeed.js (modified) (history)
  • /branches/uploadwizard-firefogg/resources/mw.UploadWizardDetails.js (modified) (history)
  • /branches/uploadwizard-firefogg/resources/mw.UploadWizardLicenseInput.js (modified) (history)
  • /branches/uploadwizard-firefogg/resources/mw.UploadWizardUploadInterface.js (modified) (history)
  • /branches/uploadwizard-firefogg/resources/mw.UploadWizardUtil.js (modified) (history)
  • /branches/uploadwizard-firefogg/resources/mw.UtilitiesTime.js (modified) (history)
  • /branches/uploadwizard-firefogg/resources/mw.js (deleted) (history)
  • /branches/uploadwizard-firefogg/resources/uploadWizard.css (modified) (history)

Diff [purge]

Index: branches/uploadwizard-firefogg/UploadWizard.config.php
@@ -4,44 +4,180 @@
55 * Do not modify this file, instead use localsettings.php and set:
66 * $wgUploadWizardConfig[ 'name'] = 'value';
77 */
8 -global $wgFileExtensions, $wgServer, $wgScriptPath, $wgAPIModules,
9 -$wgTimedMediaHandlerFileExtensions, $wgAutoloadClasses;
 8+global $wgFileExtensions, $wgServer, $wgScriptPath, $wgAPIModules;
109 return array(
1110 // Upload wizard has an internal debug flag
1211 'debug' => false,
1312
14 - // If the uploaded file should be auto categorized
15 - 'autoCategory' => true,
16 -
1713 // File extensions acceptable in this wiki
1814 'fileExtensions' => $wgFileExtensions,
1915
20 - // Check if we want to enable firefogg ( for transcoding )
21 - 'enableFirefogg' => true,
 16+ // The default api url is for the current wiki ( can override at run time )
 17+ 'apiUrl' => $wgServer . $wgScriptPath . '/api.php',
 18+
 19+ // If the uploaded file should be auto categorized
 20+ 'autoCategory' => false,
2221
23 - // Check if we have the firefogg upload api module enabled:
24 - 'enableFirefoggChunkUpload' => isset( $wgAPIModules['firefoggupload'] )? true : false,
 22+ // 'licenses' is a list of licenses you could possibly use elsewhere, for instance in
 23+ // licensesOwnWork or licensesThirdParty.
 24+ // It just describes what licenses go with what wikitext, and how to display them in
 25+ // a menu of license choices. There probably isn't any reason to delete any entry here.
 26+ // Under normal circumstances, the license name is the name of the wikitext template to insert.
 27+ // For those that aren't, there is a "templates" property.
 28+ 'licenses' => array(
 29+ 'cc-by-sa-3.0' => array(
 30+ 'msg' => 'mwe-upwiz-license-cc-by-sa-3.0',
 31+ 'icons' => array( 'cc-by', 'cc-sa' )
 32+ ),
 33+ 'cc-by-sa-3.0-gfdl' => array(
 34+ 'msg' => 'mwe-upwiz-license-cc-by-sa-3.0-gfdl',
 35+ 'templates' => array( 'gfdl', 'cc-by-sa-3.0' ),
 36+ 'icons' => array( 'cc-by', 'cc-sa' )
 37+ ),
 38+ 'cc-by-3.0-gfdl' => array(
 39+ 'msg' => 'mwe-upwiz-license-cc-by-3.0-gfdl',
 40+ 'templates' => array( 'gfdl', 'cc-by-3.0' ),
 41+ 'icons' => array( 'cc-by' )
 42+ ),
 43+ 'cc-by-3.0' => array(
 44+ 'msg' => 'mwe-upwiz-license-cc-by-3.0',
 45+ 'icons' => array( 'cc-by' )
 46+ ),
 47+ 'cc-zero' => array(
 48+ 'msg' => 'mwe-upwiz-license-cc-zero',
 49+ 'icons' => array( 'cc-zero' )
 50+ ),
 51+ 'own-pd' => array(
 52+ 'msg' => 'mwe-upwiz-license-own-pd',
 53+ 'icons' => array( 'cc-zero' ),
 54+ 'templates' => array( 'cc-zero' )
 55+ ),
 56+ 'cc-by-sa-2.5' => array(
 57+ 'msg' => 'mwe-upwiz-license-cc-by-sa-2.5',
 58+ 'icons' => array( 'cc-by', 'cc-sa' )
 59+ ),
 60+ 'cc-by-2.5' => array(
 61+ 'msg' => 'mwe-upwiz-license-cc-by-2.5',
 62+ 'icons' => array( 'cc-by' )
 63+ ),
 64+ 'cc-by-sa-2.0' => array(
 65+ 'msg' => 'mwe-upwiz-license-cc-by-sa-2.0',
 66+ 'icons' => array( 'cc-by', 'cc-sa' )
 67+ ),
 68+ 'cc-by-2.0' => array(
 69+ 'msg' => 'mwe-upwiz-license-cc-by-2.0',
 70+ 'icons' => array( 'cc-by' )
 71+ ),
 72+ 'fal' => array(
 73+ 'msg' => 'mwe-upwiz-license-fal'
 74+ ),
 75+ 'pd-old-100' => array(
 76+ 'msg' => 'mwe-upwiz-license-pd-old-100'
 77+ ),
 78+ 'pd-old' => array(
 79+ 'msg' => 'mwe-upwiz-license-pd-old'
 80+ ),
 81+ 'pd-art' => array(
 82+ 'msg' => 'mwe-upwiz-license-pd-art'
 83+ ),
 84+ 'pd-us' => array(
 85+ 'msg' => 'mwe-upwiz-license-pd-us'
 86+ ),
 87+ 'pd-usgov' => array(
 88+ 'msg' => 'mwe-upwiz-license-pd-usgov'
 89+ ),
 90+ 'pd-usgov-nasa' => array(
 91+ 'msg' => 'mwe-upwiz-license-pd-usgov-nasa'
 92+ ),
 93+ 'pd-usgov-military-navy' => array(
 94+ 'msg' => 'mwe-upwiz-license-pd-usgov-military-navy'
 95+ ),
 96+ 'pd-ineligible' => array(
 97+ 'msg' => 'mwe-upwiz-license-pd-ineligible'
 98+ ),
 99+ 'pd-textlogo' => array(
 100+ 'msg' => 'mwe-upwiz-license-pd-textlogo',
 101+ 'templates' => array( 'trademarked', 'pd-textlogo' )
 102+ ),
 103+ 'copyrighted-free-use' => array(
 104+ 'msg' => 'mwe-upwiz-license-copyrighted-free-use'
 105+ ),
 106+ 'attribution' => array(
 107+ 'msg' => 'mwe-upwiz-license-attribution'
 108+ ),
 109+ 'gfdl' => array(
 110+ 'msg' => 'mwe-upwiz-license-gfdl'
 111+ )
 112+ ),
25113
26 - // Firefogg encode settings ( if timed media handler extension is installed use HD webm, else mid-rage ogg )
27 - 'firefoggEncodeSettings' => ( class_exists( 'WebVideoTranscode' ) )?
28 - WebVideoTranscode::$derivativeSettings[ WebVideoTranscode::ENC_WEBM_HQ_VBR ] :
29 - array(
30 - 'maxSize' => '480',
31 - 'videoBitrate' => '512',
32 - 'audioBitrate' => '96',
33 - 'noUpscaling' => 'true',
34 - 'twopass' => 'true',
35 - 'keyframeInterval' => '128',
36 - 'bufDelay' => '256',
37 - 'videoCodec' => 'theora',
38 - ),
 114+ // radio button selection of some licenses
 115+ 'licensesOwnWork' => array(
 116+ 'type' => 'or',
 117+ 'filterTemplate' => 'self',
 118+ 'licenses' => array(
 119+ 'cc-by-sa-3.0',
 120+ 'cc-by-3.0',
 121+ 'own-pd'
 122+ ),
 123+ 'defaults' => array( 'cc-by-sa-3.0' )
 124+ ),
39125
40 - // The default api url is for the current wiki ( can override at run time )
41 - 'apiUrl' => $wgServer . $wgScriptPath . '/api.php',
42 -
43 - // The progress update interval for uploads
44 - 'uploadProgressInterval' => 250,
45 -
 126+ // checkbox selection of all licenses
 127+ 'licensesThirdParty' => array(
 128+ 'type' => 'and',
 129+ 'licenseGroups' => array(
 130+ array(
 131+ // This should be a list of all CC licenses we can reasonably expect to find around the web
 132+ 'head' => 'mwe-upwiz-license-cc-head',
 133+ 'subhead' => 'mwe-upwiz-license-cc-subhead',
 134+ 'licenses' => array(
 135+ 'cc-by-sa-3.0',
 136+ 'cc-by-sa-2.5',
 137+ 'cc-by-3.0',
 138+ 'cc-by-2.5',
 139+ 'cc-zero'
 140+ )
 141+ ),
 142+ array(
 143+ // n.b. as of April 2011, Flickr still uses CC 2.0 licenses.
 144+ // The White House also has an account there, hence the Public Domain US Government license
 145+ 'head' => 'mwe-upwiz-license-flickr-head',
 146+ 'subhead' => 'mwe-upwiz-license-flickr-subhead',
 147+ 'prependTemplates' => array( 'flickrreview' ),
 148+ 'licenses' => array(
 149+ 'cc-by-sa-2.0',
 150+ 'cc-by-2.0',
 151+ 'pd-usgov',
 152+ )
 153+ ),
 154+ array(
 155+ 'head' => 'mwe-upwiz-license-public-domain-head',
 156+ 'licenses' => array(
 157+ 'pd-old-100',
 158+ 'pd-old',
 159+ 'pd-art',
 160+ 'pd-us',
 161+ )
 162+ ),
 163+ array(
 164+ // omitted navy because it is believed only MultiChil uses it heavily. Could add it back
 165+ 'head' => 'mwe-upwiz-license-usgov-head',
 166+ 'licenses' => array(
 167+ 'pd-usgov',
 168+ 'pd-usgov-nasa'
 169+ )
 170+ ),
 171+ array(
 172+ 'head' => 'mwe-upwiz-license-misc',
 173+ 'licenses' => array(
 174+ 'fal'
 175+ )
 176+ )
 177+ ),
 178+ 'defaults' => array(),
 179+ ),
 180+
 181+
46182 // Default thumbnail width
47183 'thumbnailWidth' => 120,
48184
@@ -53,7 +189,13 @@
54190
55191 // Small thumbnail max height
56192 'smallThumbnailMaxHeight' => 100,
 193+
 194+ // Large thumbnail width
 195+ 'largeThumbnailWidth' => 500,
57196
 197+ // Large thumbnail max height
 198+ 'largeThumbnailMaxHeight' => 500,
 199+
58200 // Icon thumbnail width:
59201 'iconThumbnailWidth' => 32,
60202
@@ -100,17 +242,6 @@
101243 // so, this workaround will cause tagalog descriptions to be saved with this template instead.
102244 'languageTemplateFixups' => array( 'tl' => 'tgl' ),
103245
104 - // names of all license templates, in order. Case sensitive!
105 - // n.b. in the future, the licenses for a wiki will probably be defined in PHP or even LocalSettings.
106 - 'licenses' => array(
107 - array( 'template' => 'Cc-by-sa-3.0','messageKey' => 'mwe-upwiz-license-cc-by-sa-3.0', 'default' => true ),
108 - array( 'template' => 'Cc-by-3.0', 'messageKey' => 'mwe-upwiz-license-cc-by-3.0', 'default' => false ),
109 - array( 'template' => 'Cc-zero', 'messageKey' => 'mwe-upwiz-license-cc-zero', 'default' => false ),
110 - // n.b. the PD-US is only for testing purposes, obviously we need some geographical discrimination here...
111 - array( 'template' => 'PD-US', 'messageKey' => 'mwe-upwiz-license-pd-us', 'default' => false ),
112 - array( 'template' => 'GFDL', 'messageKey' => 'mwe-upwiz-license-gfdl', 'default' => false )
113 - )
114 -
115246 // XXX this is horribly confusing -- some file restrictions are client side, others are server side
116247 // the filename prefix blacklist is at least server side -- all this should be replaced with PHP regex config
117248 // or actually, in an ideal world, we'd have some way to reliably detect gibberish, rather than trying to
@@ -123,4 +254,17 @@
124255 // /^(test|image|img|bild|example?[\s_-]*)$/, // test stuff
125256 // /^(\d{10}[\s_-][0-9a-f]{10}[\s_-][a-z])$/ // flickr
126257 // ]
 258+
 259+ // Check if we want to enable firefogg when installed on the client ( for transcoding video )
 260+ 'enableFirefogg' => true,
 261+
 262+ // Check if we have the firefogg upload api module enabled:
 263+ 'enableFirefoggChunkUpload' => isset( $wgAPIModules['firefoggupload'] )? true : false,
 264+
 265+ // Set skipTutorial to true to always skip tutorial step
 266+ 'skipTutorial' => false,
 267+
 268+ // Wiki page for leaving Upload Wizard feedback, for example 'Commons:Upload wizard feedback'
 269+ 'feedbackPage' => '',
 270+
127271 );
Index: branches/uploadwizard-firefogg/resources/mw.js
@@ -1,7 +0,0 @@
2 -// dependencies: []
3 -
4 -if ( typeof window.mediaWiki === 'undefined' ) {
5 - window.mediaWiki = {};
6 - window.mw = window.mediaWiki;
7 - mw = window.mediaWiki;
8 -}
Index: branches/uploadwizard-firefogg/resources/jquery/jquery.mwCoolCats.css
@@ -45,7 +45,17 @@
4646 display: inline-block;
4747 }
4848
 49+/* Fix for IE6 */
 50+.cat-widget .ui-button {
 51+ position: static;
 52+}
4953
 54+/* Make category add button smaller */
 55+.cat-widget .ui-button-text {
 56+ padding: 0;
 57+}
 58+
 59+
5060 /* Utilities */
5161 .pkg:after, #content-inner:after {
5262 content: " ";
Index: branches/uploadwizard-firefogg/resources/jquery/jquery.tipsy.js
@@ -19,9 +19,9 @@
2020 if (title && this.enabled) {
2121 var $tip = this.tip();
2222
23 - $tip.find('.tipsy-inner')[this.options.html ? 'html' : 'text'](title);
2423 $tip[0].className = 'tipsy'; // reset classname in case of dynamic gravity
2524 $tip.remove().css({top: 0, left: 0, visibility: 'hidden', display: 'block'}).appendTo(document.body);
 25+ $tip.find('.tipsy-inner')[this.options.html ? 'html' : 'text'](title);
2626
2727 var pos = $.extend({}, this.$element.offset(), {
2828 width: this.$element[0].offsetWidth,
@@ -82,6 +82,7 @@
8383 if ( this.displayed ) {
8484 this.hide();
8585 } else {
 86+ $('.mwe-upwiz-hint').each( function(i) { $(this).tipsy('hide'); } );
8687 this.show();
8788 }
8889 },
@@ -102,7 +103,9 @@
103104 } else if (typeof o.title == 'function') {
104105 title = o.title.call($e[0]);
105106 }
106 - title = ('' + title).replace(/(^\s*|\s*$)/, "");
 107+ if ( typeof title === 'string' ) {
 108+ title = ('' + title).replace(/(^\s*|\s*$)/, "");
 109+ }
107110 return title || o.fallback;
108111 },
109112
Index: branches/uploadwizard-firefogg/resources/jquery/jquery.mwCoolCats.js
@@ -35,6 +35,7 @@
3636
3737 _this.wrap('<div class="cat-widget"></div>');
3838 $container = _this.parent(); // set to the cat-widget class we just wrapped
 39+ $container.prepend('<ul class="cat-list pkg"></ul>');
3940 $container.append( $j( '<button type="button" name="catbutton">'+settings.buttontext+'</button>' )
4041 .button()
4142 .click( function(e) {
@@ -45,8 +46,6 @@
4647 })
4748 );
4849
49 - $container.prepend('<ul class="cat-list pkg"></ul>');
50 -
5150 //XXX ensure this isn't blocking other stuff needed.
5251 _this.parents('form').submit( function() {
5352 _processInput();
@@ -80,6 +79,8 @@
8180 }
8281
8382 function _insertCat( cat, isHidden ) {
 83+ // strip out bad characters
 84+ cat = cat.replace( /[\x00-\x1f\x3c\x3e\x5b\x5d\x7b\x7c\x7d\x7f]+/g, '' );
8485 if ( mw.isEmpty( cat ) || _containsCat( cat ) ) {
8586 return;
8687 }
@@ -120,13 +121,15 @@
121122
122123 function _fetchSuggestions( query ) {
123124 var _this = this;
 125+ // ignore bad characters, they will be stripped out
 126+ var catName = $j( this ).val().replace( /[\x00-\x1f\x3c\x3e\x5b\x5d\x7b\x7c\x7d\x7f]+/g, '' );
124127 var request = $j.ajax( {
125128 url: wgScriptPath + '/api.php',
126129 data: {
127130 'action': 'query',
128131 'list': 'allpages',
129132 'apnamespace': wgNamespaceIds['category'],
130 - 'apprefix': $j( this ).val(),
 133+ 'apprefix': catName,
131134 'format': 'json'
132135 },
133136 dataType: 'json',
Index: branches/uploadwizard-firefogg/resources/jquery/jquery.tipsy.css
@@ -1,5 +1,5 @@
22 .tipsy { padding: 5px; font-size: small; position: absolute; z-index: 100000; }
3 - .tipsy-inner { padding: 5px 8px 4px 8px; background-color:#96d8d9; color: black; max-width: 200px; text-align: center; }
 3+ .tipsy-inner { padding: 5px 8px 4px 8px; background-color:#e8f5ff; color: black; max-width: 200px; text-align: center; }
44 .tipsy-inner { border-radius: 3px; -moz-border-radius:3px; -webkit-border-radius:3px; }
55 .tipsy-arrow { position: absolute; background: url('jquery.tipsy.help.gif') no-repeat top left; width: 9px; height: 5px; }
66 .tipsy-n .tipsy-arrow { top: 0; left: 50%; margin-left: -4px; }
Index: branches/uploadwizard-firefogg/resources/mw.UploadWizardDeed.js
@@ -29,13 +29,7 @@
3030 * @return wikitext of all applicable license templates.
3131 */
3232 getLicenseWikiText: function() {
33 - var _this = this;
34 - var wikiText = '';
35 - $j.each ( _this.licenseInput.getTemplates(), function( i, template ) {
36 - wikiText += "{{" + template + "}}\n";
37 - } );
38 -
39 - return wikiText;
 33+ return this.licenseInput.getWikiText();
4034 }
4135
4236 };
@@ -62,8 +56,10 @@
6357 .addClass( 'mwe-upwiz-sign' );
6458
6559 var licenseInputDiv = $j( '<div class="mwe-upwiz-deed-license"></div>' );
66 - _this.licenseInput = new mw.UploadWizardLicenseInput( licenseInputDiv );
67 - _this.licenseInput.setDefaultValues();
 60+ _this.licenseInput = new mw.UploadWizardLicenseInput( licenseInputDiv,
 61+ undefined,
 62+ mw.UploadWizard.config.licensesOwnWork,
 63+ _this.uploadCount );
6864
6965 return $j.extend( _this, {
7066
@@ -92,56 +88,41 @@
9389 },
9490
9591
96 - getLicenseWikiText: function() {
97 - var wikiText = '{{self';
98 - $j.each( _this.licenseInput.getTemplates(), function( i, template ) {
99 - wikiText += '|' + template;
100 - } );
101 - wikiText += '}}';
102 - return wikiText;
103 - },
104 -
10592 setFormFields: function( $selector ) {
10693 _this.$selector = $selector;
10794
108 - _this.$form = $j( '<form/>' );
 95+ _this.$form = $j( '<form />' );
10996
 97+ var $authorInput2 = $j( '<input />' ).attr( { name: "author2", type: "text" } ).addClass( 'mwe-upwiz-sign' );
11098 var $standardDiv = $j( '<div />' ).append(
111 - $j( '<label for="author2" generated="true" class="mwe-validator-error" style="display:block;"/>' ),
112 - $j( '<p>' )
113 - .html( gM( 'mwe-upwiz-source-ownwork-assert',
114 - uploadCount,
115 - '<span class="mwe-standard-author-input"></span>' )
116 - ),
117 - $j( '<p class="mwe-small-print" />' ).append( gM( 'mwe-upwiz-source-ownwork-assert-note' ) )
 99+ $j( '<label for="author2" generated="true" class="mwe-validator-error" style="display:block;" />' ),
 100+ $j( '<p></p>' ).msg( 'mwe-upwiz-source-ownwork-assert',
 101+ uploadCount,
 102+ $authorInput2 ),
 103+ $j( '<p class="mwe-small-print"></p>' ).msg( 'mwe-upwiz-source-ownwork-assert-note' )
118104 );
119 - $standardDiv.find( '.mwe-standard-author-input' ).append( $j( '<input name="author2" type="text" class="mwe-upwiz-sign" />' ) );
120105
121 - var $customDiv = $j('<div/>').append(
122 - $j( '<label for="author" generated="true" class="mwe-validator-error" style="display:block;"/>' ),
123 - $j( '<p>' )
124 - .html( gM( 'mwe-upwiz-source-ownwork-assert-custom',
125 - uploadCount,
126 - '<span class="mwe-custom-author-input"></span>' ) ),
 106+ var $customDiv = $j('<div />').append(
 107+ $j( '<label for="author" generated="true" class="mwe-validator-error" style="display:block;" />' ),
 108+ $j( '<p></p>' ).msg( 'mwe-upwiz-source-ownwork-assert-custom',
 109+ uploadCount,
 110+ _this.authorInput ),
127111 licenseInputDiv
128112 );
129 - // have to add the author input this way -- gM() will flatten it to a string and we'll lose it as a dom object
130 - $customDiv.find( '.mwe-custom-author-input' ).append( _this.authorInput );
131113
132 -
133 - var $crossfader = $j( '<div>' ).append( $standardDiv, $customDiv );
134 - var $toggler = $j( '<p class="mwe-more-options" style="text-align: right" />' )
 114+ var $crossfader = $j( '<div />' ).append( $standardDiv, $customDiv );
 115+ var $toggler = $j( '<p class="mwe-more-options" style="text-align: right"></p>' )
135116 .append( $j( '<a />' )
136 - .append( gM( 'mwe-upwiz-license-show-all' ) )
 117+ .msg( 'mwe-upwiz-license-show-all' )
137118 .click( function() {
138119 _this.formValidator.resetForm();
139120 if ( $crossfader.data( 'crossfadeDisplay' ) === $customDiv ) {
140121 _this.licenseInput.setDefaultValues();
141122 $crossfader.morphCrossfade( $standardDiv );
142 - $j( this ).html( gM( 'mwe-upwiz-license-show-all' ) );
 123+ $j( this ).msg( 'mwe-upwiz-license-show-all' );
143124 } else {
144125 $crossfader.morphCrossfade( $customDiv );
145 - $j( this ).html( gM( 'mwe-upwiz-license-show-recommended' ) );
 126+ $j( this ).msg( 'mwe-upwiz-license-show-recommended' );
146127 }
147128 } ) );
148129
@@ -172,8 +153,10 @@
173154
174155 // done after added to the DOM, so there are true heights
175156 $crossfader.morphCrossfader();
 157+
 158+ // choose default licenses
 159+ _this.licenseInput.setDefaultValues();
176160
177 -
178161 // and finally, make it validatable
179162 _this.formValidator = _this.$form.validate( {
180163 rules: {
@@ -224,7 +207,10 @@
225208 .growTextArea()
226209 .attr( 'title', gM( 'mwe-upwiz-tooltip-author' ) );
227210 licenseInputDiv = $j( '<div class="mwe-upwiz-deed-license"></div>' );
228 - _this.licenseInput = new mw.UploadWizardLicenseInput( licenseInputDiv );
 211+ _this.licenseInput = new mw.UploadWizardLicenseInput( licenseInputDiv,
 212+ undefined,
 213+ mw.UploadWizard.config.licensesThirdParty,
 214+ _this.uploadCount );
229215
230216
231217 return $j.extend( _this, mw.UploadWizardDeed.prototype, {
@@ -232,26 +218,26 @@
233219
234220 setFormFields: function( $selector ) {
235221 var _this = this;
236 - _this.$form = $j( '<form/>' );
 222+ _this.$form = $j( '<form />' );
237223
238 - var $formFields = $j( '<div class="mwe-upwiz-deed-form-internal"/>' );
 224+ var $formFields = $j( '<div class="mwe-upwiz-deed-form-internal" />' );
239225
240 - if ( uploadCount > 1 ) {
241 - $formFields.append( $j( '<div />' ).append( gM( 'mwe-upwiz-source-thirdparty-custom-multiple-intro' ) ) );
 226+ if ( _this.uploadCount > 1 ) {
 227+ $formFields.append( $j( '<div />' ).msg( 'mwe-upwiz-source-thirdparty-custom-multiple-intro' ) );
242228 }
243229
244230 $formFields.append (
245231 $j( '<div class="mwe-upwiz-source-thirdparty-custom-multiple-intro" />' ),
246 - $j( '<label for="source" generated="true" class="mwe-validator-error" style="display:block;"/>' ),
 232+ $j( '<label for="source" generated="true" class="mwe-validator-error" style="display:block;" />' ),
247233 $j( '<div class="mwe-upwiz-thirdparty-fields" />' )
248 - .append( $j( '<label for="source"/>' ).text( gM( 'mwe-upwiz-source' ) ).addHint( 'source' ),
 234+ .append( $j( '<label for="source" />' ).text( gM( 'mwe-upwiz-source' ) ).addHint( 'source' ),
249235 _this.sourceInput ),
250 - $j( '<label for="author" generated="true" class="mwe-validator-error" style="display:block;"/>' ),
 236+ $j( '<label for="author" generated="true" class="mwe-validator-error" style="display:block;" />' ),
251237 $j( '<div class="mwe-upwiz-thirdparty-fields" />' )
252 - .append( $j( '<label for="author"/>' ).text( gM( 'mwe-upwiz-author' ) ).addHint( 'author' ),
 238+ .append( $j( '<label for="author" />' ).text( gM( 'mwe-upwiz-author' ) ).addHint( 'author' ),
253239 _this.authorInput ),
254240 $j( '<div class="mwe-upwiz-thirdparty-license" />' )
255 - .append( gM( 'mwe-upwiz-source-thirdparty-license', uploadCount ) ),
 241+ .msg( 'mwe-upwiz-source-thirdparty-cases', _this.uploadCount ),
256242 licenseInputDiv
257243 );
258244
@@ -325,16 +311,16 @@
326312 var id = _this.name + '-' + deed.name;
327313 var $deedInterface = $j(
328314 '<div class="mwe-upwiz-deed mwe-upwiz-deed-' + deed.name + '">'
329 - + '<div class="mwe-upwiz-deed-option-title">'
330 - + '<span class="mwe-upwiz-deed-header">'
331 - + '<input id="' + id +'" name="' + _this.name + '" type="radio" value="' + deed.name + ' /">'
332 - + '<label for="' + id + '" class="mwe-upwiz-deed-name">'
333 - + gM( 'mwe-upwiz-source-' + deed.name, _this.uploadCount )
334 - + '</label>'
335 - + '</span>'
336 - + '</div>'
337 - + '<div class="mwe-upwiz-deed-form">'
338 - + '</div>'
 315+ + '<div class="mwe-upwiz-deed-option-title">'
 316+ + '<span class="mwe-upwiz-deed-header">'
 317+ + '<input id="' + id +'" name="' + _this.name + '" type="radio" value="' + deed.name + ' /">'
 318+ + '<label for="' + id + '" class="mwe-upwiz-deed-name">'
 319+ + gM( 'mwe-upwiz-source-' + deed.name, _this.uploadCount )
 320+ + '</label>'
 321+ + '</span>'
 322+ + '</div>'
 323+ + '<div class="mwe-upwiz-deed-form"></div>'
 324+ +'</div>'
339325 );
340326
341327 var $deedSelector = _this.$selector.append( $deedInterface );
Index: branches/uploadwizard-firefogg/resources/mw.UploadWizardLicenseInput.js
@@ -1,39 +1,124 @@
22 /**
3 - * Create a group of checkboxes for licenses. N.b. the licenses are named after the templates they invoke.
4 - * @param div
5 - * @param values (optional) array of license key names to activate by default
 3+ * Create a group of radio buttons for licenses. N.b. the licenses are named after the templates they invoke.
 4+ * @param {String|jQuery} selector to place license input
 5+ * @param {Array} license key name(s) to activate by default
 6+ * @param {Array} configuration of licenseInput. Must have following properties
 7+ * 'type' = ("and"|"or") -- whether inclusive or exclusive license allowed
 8+ * 'defaults' => array of template string names (can be empty array),
 9+ * 'licenses' => array of template string names (matching keys in mw.UploadWizard.config.licenses)
 10+ * optional: 'licenseGroups' => groups of licenses, with more explanation
 11+ * @param {Numbe} count of the things we are licensing (it matters to some texts)
612 */
713
814 ( function( $j ) {
9 -mw.UploadWizardLicenseInput = function( selector, values ) {
 15+mw.UploadWizardLicenseInput = function( selector, values, config, count ) {
1016 var _this = this;
 17+ _this.count = count;
1118
12 - var widgetCount = mw.UploadWizardLicenseInput.prototype.count++;
 19+ if ( ! ( mw.isDefined(config.type)
 20+ && mw.isDefined( config.defaults )
 21+ && ( mw.isDefined( config.licenses ) || mw.isDefined( config.licenseGroups ) ) ) ) {
 22+ throw new Error( 'improper initialization' );
 23+ }
 24+
 25+ _this.$selector = $j( selector );
 26+ _this.$selector.append( $j( '<div class="mwe-error"></div>' ) );
 27+
 28+ _this.type = config.type === 'or' ? 'radio' : 'checkbox';
 29+
 30+ _this.defaults = config.defaults;
 31+
 32+ mw.UploadWizardLicenseInput.prototype.count++;
 33+ _this.name = 'license' + mw.UploadWizardLicenseInput.prototype.count;
 34+
1335
 36+ /**
 37+ * Define the licenses this input will show:
 38+ */
 39+ _this.licenses = [];
1440 _this.inputs = [];
 41+ /**
 42+ * append defined license inputs to element; also records licenses and inputs in _this
 43+ * Abstracts out simple lists of licenses, more complex groups with layout
 44+ * @param {jQuery} selector to add inputs to
 45+ * @param {Array} license configuration, which must have a 'licenses' property, which is an array of license names
 46+ * it may also have: 'prependTemplates' or 'filterTemplate', which alter the final wikitext value
 47+ * 'prependTemplates' will prepend Templates. If prependTemplates were [ 'pre', 'pended' ], then...
 48+ * [ 'fooLicense' ] -> "{{pre}}{{pended}}{{fooLicense}}"
 49+ * 'filterTemplates' will filter Templates, as in "own work". If 'filterTemplate' was 'filter', then...
 50+ * [ 'fooLicense', 'barLicense' ] -> {{filter|fooLicense|barLicense}}
 51+ *
 52+ */
 53+ function appendLicenses( $el, config ) {
 54+ if ( !mw.isDefined( config['licenses'] && typeof config['licenses'] === 'object' ) ) {
 55+ throw new Error( "improper license config" );
 56+ }
 57+ $j.each( config['licenses'], function( i, name ) {
 58+ if ( mw.isDefined( mw.UploadWizard.config.licenses[name] ) ) {
 59+ var license = { name: name, props: mw.UploadWizard.config.licenses[name] };
 60+ _this.licenses.push( license );
 61+ var templates = mw.isDefined( license.props['templates'] ) ? license.props.templates : [ license.name ];
 62+ var origTemplateString = templates.join( '|' );
 63+ if ( mw.isDefined( config['prependTemplates'] ) ) {
 64+ $j.each( config['prependTemplates'], function( i, template ) {
 65+ templates.unshift( template );
 66+ } );
 67+ }
 68+ if ( mw.isDefined( config['filterTemplate'] ) ) {
 69+ templates.unshift( config['filterTemplate'] );
 70+ templates = [ templates.join( '|' ) ];
 71+ }
 72+ // using inputs length to ensure that you can have two options which deliver same result,
 73+ // but the label association still works
 74+ var id = _this.name + '_' + templates.join('_') + '_' + _this.inputs.length;
1575
16 - // TODO incompatibility check of this license versus others
 76+ // the value is literal wikitext; turn template names (or template names + args) into wikitext templates
 77+ var value = ( $j.map( templates, function( t ) { return '{{' + t + '}}'; } ) ).join( '' );
 78+ // IE6 is idiotic about radio buttons; you have to create them as HTML or clicks aren't recorded
 79+ var $input = $j( '<input id="' + id + '" name="' + _this.name + '" type="' + _this.type + '" value="' + value + '" />' );
 80+ $input.click( function() { _this.$selector.trigger( 'changeLicenses' ); } );
 81+ // this is added so that setValues() can find one (or more) checkboxes to check - represent values without wikitext
 82+ $input.data( 'templateString', origTemplateString );
 83+ _this.inputs.push( $input );
 84+
 85+ var messageKey = mw.isDefined( license.props['msg'] ) ? license.props.msg : '[missing msg for ' + license.name + ']';
 86+ var $icons = $j( '<span></span>' );
 87+ if ( mw.isDefined( license.props['icons'] ) ) {
 88+ $j.each( license.props.icons, function( i, icon ) {
 89+ $icons.append( $j( '<span></span>' ).addClass( 'mwe-upwiz-license-icon mwe-upwiz-' + icon + '-icon' ) );
 90+ } );
 91+ }
 92+ $el.append(
 93+ $input,
 94+ $j( '<label />' ).attr( { 'for': id } ).msg( messageKey, _this.count ).append( $icons ),
 95+ $j( '<br/>' )
 96+ // XXX help?
 97+ );
 98+ }
 99+ } );
 100+ }
17101
18 - _this.$selector = $j( selector );
19 - _this.$selector.append( $j( '<div class="mwe-error"></div>' ) );
 102+ if ( mw.isDefined( config['licenseGroups'] ) ) {
 103+ $j.each( config['licenseGroups'], function( i, group ) {
 104+ if ( !mw.isDefined( group['licenses'] ) ) {
 105+ throw new Error( 'improper config' );
 106+ }
 107+ var $group = $j( '<div></div>' ).addClass( 'mwe-upwiz-deed-license-group' );
 108+ if ( mw.isDefined( group['head'] ) ) {
 109+ $group.append( $j( '<p></p>' ).addClass( 'mwe-upwiz-deed-license-group-head' ).msg( group.head, _this.count ) );
 110+ }
 111+ if ( mw.isDefined( group['subhead'] ) ) {
 112+ $group.append( $j( '<p></p>' ).addClass( 'mwe-upwiz-deed-license-group-subhead' ).msg( group.subhead, _this.count ) );
 113+ }
 114+ var $licensesDiv = $j( '<div></div>' ).addClass( 'mwe-upwiz-deed-license' );
 115+ appendLicenses( $licensesDiv, group );
 116+ $group.append( $licensesDiv );
 117+ _this.$selector.append( $group );
 118+ } );
20119
21 - $j.each( mw.UploadWizard.config[ 'licenses' ], function( i, licenseConfig ) {
22 - var template = licenseConfig.template;
23 - var messageKey = licenseConfig.messageKey;
24 -
25 - var name = 'license_' + template;
26 - var id = 'licenseInput' + widgetCount + '_' + name;
27 - var $input = $j( '<input />' )
28 - .attr( { id: id, name: name, type: 'checkbox', value: template } )
29 - // we use the selector because events can't be unbound unless they're in the DOM.
30 - .click( function() { _this.$selector.trigger( 'changeLicenses' ); } );
31 - _this.inputs.push( $input );
32 - _this.$selector.append(
33 - $input,
34 - $j( '<label />' ).attr( { 'for': id } ).html( gM( messageKey ) ),
35 - $j( '<br/>' )
36 - );
37 - } );
 120+ } else {
 121+ appendLicenses( _this.$selector, config );
 122+ }
38123
39124 if ( values ) {
40125 _this.setValues( values );
@@ -46,14 +131,16 @@
47132 count: 0,
48133
49134 /**
50 - * Sets the value(s) of a license input.
51 - * @param object of license-key to boolean values, e.g. { cc_by_sa_30: true, gfdl: true }
 135+ * Sets the value(s) of a license input. This is a little bit klugey because it relies on an inverted dict, and in some
 136+ * cases we are now letting license inputs create multiple templates.
 137+ * @param object of license-key to boolean values, e.g. { 'cc_by_sa_30': true, 'gfdl': true, 'flickrreview|cc_by_sa_30': false }
52138 */
53 - setValues: function( licenseValues ) {
 139+ setValues: function( values ) {
54140 var _this = this;
55141 $j.each( _this.inputs, function( i, $input ) {
56 - var template = $input.val();
57 - $input.attr( 'checked', ~~!!licenseValues[template] );
 142+ var templateString = $input.data( 'templateString' );
 143+ // !! to ensure boolean. ~~ to cast to 0 or 1. Similar to php's (int) (bool) val
 144+ $input.attr( 'checked', ~~!!values[templateString] );
58145 } );
59146 // we use the selector because events can't be unbound unless they're in the DOM.
60147 _this.$selector.trigger( 'changeLicenses' );
@@ -65,23 +152,33 @@
66153 setDefaultValues: function() {
67154 var _this = this;
68155 var values = {};
69 - $j.each( mw.UploadWizard.config[ 'licenses' ], function( i, licenseConfig ) {
70 - values[ licenseConfig.template ] = licenseConfig['default'];
 156+ $j.each( _this.defaults, function( i, lic ) {
 157+ values[lic] = true;
71158 } );
72159 _this.setValues( values );
73160 },
74161
75162 /**
76 - * Gets the templates associated with checked inputs
77 - * @return array of template names
 163+ * Gets the wikitext associated with all checked inputs
 164+ * @return string of wikitext (empty string if no inputs set)
78165 */
79 - getTemplates: function() {
80 - return $j( this.inputs )
81 - .filter( function() { return this.is( ':checked' ); } )
82 - .map( function() { return this.val(); } );
 166+ getWikiText: function() {
 167+ // need to use makeArray because a jQuery-returned set of things won't have .join
 168+ return $j.makeArray(
 169+ this.getCheckedInputs().map( function() { return this.val(); } )
 170+ ).join( "" );
83171 },
84172
85173 /**
 174+ * Gets which inputs are checked
 175+ * @return {jQuery Array} of inputs
 176+ */
 177+ getCheckedInputs: function() {
 178+ return $j( this.inputs ).filter( function() { return this.is( ':checked' ); } );
 179+ },
 180+
 181+
 182+ /**
86183 * Check if a valid value is set, also look for incompatible choices.
87184 * Side effect: if no valid value, add notes to the interface. Add listeners to interface, to revalidate and remove notes.
88185 * @return boolean; true if a value set, false otherwise
@@ -95,8 +192,6 @@
96193 errorHtml = gM( 'mwe-upwiz-deeds-need-license' );
97194 }
98195
99 - // XXX something goes here for licenses incompatible with each other
100 -
101196 var $errorEl = this.$selector.find( '.mwe-error' );
102197 if (isValid) {
103198 $errorEl.fadeOut();
@@ -118,7 +213,7 @@
119214 * @return boolean
120215 */
121216 isSet: function() {
122 - return this.getTemplates().length > 0;
 217+ return this.getCheckedInputs().length > 0;
123218 }
124219
125220 };
Index: branches/uploadwizard-firefogg/resources/mw.UploadWizardUploadInterface.js
@@ -69,7 +69,6 @@
7070 .append( _this.$fileInputCtrl )
7171 )
7272 .append( _this.filenameCtrl )
73 - .append( _this.thumbnailParam )
7473 .get( 0 );
7574
7675
@@ -132,35 +131,31 @@
133132 * @param HTMLImageElement
134133 */
135134 setPreview: function( image ) {
136 - // encoding for url here?
137 - $j( this.div ).find( '.mwe-upwiz-file-preview' ).append(
138 - $j('<img />')
139 - .css({'width':'100%', 'height': '100%'})
140 - .attr('src', image.src )
141 - )
 135+ var $preview = $j( this.div ).find( '.mwe-upwiz-file-preview' );
 136+ if ( image === null ) {
 137+ $preview.addClass( 'mwe-upwiz-file-preview-broken' );
 138+ } else {
 139+ // encoding for url here?
 140+ $preview.css( 'background-image', 'url(' + image.src + ')' );
 141+ }
142142 },
143143
144144 /**
145145 * Set the status line for this upload with an internationalized message string.
146146 * @param String msgKey: key for the message
147147 * @param Array args: array of values, in case any need to be fed to the image.
 148+ * @param Boolean error: if true, show an error
148149 */
149150 setStatus: function( msgKey, args ) {
150151 if ( !mw.isDefined( args ) ) {
151152 args = [];
152153 }
153 - this.setStatusStr( gM( msgKey, args ) );
 154+ // get the status line for our upload
 155+ var $s = $j( this.div ).find( '.mwe-upwiz-file-status' );
 156+ $s.msg( msgKey, args ).show();
154157 },
155158
156159 /**
157 - * Set the status line for this upload
158 - * @param String str: the string to use
159 - */
160 - setStatusStr: function( str ) {
161 - $j( this.div ).find( '.mwe-upwiz-file-status' ).html( str ).show();
162 - },
163 -
164 - /**
165160 * Clear the status line for this upload (hide it, in case there are paddings and such which offset other things.)
166161 */
167162 clearStatus: function() {
@@ -197,11 +192,14 @@
198193 // is this an error that we expect to have a message for?
199194 var msgKey = 'mwe-upwiz-api-error-unknown-code';
200195 var args = [ code ];
 196+
 197+ if ( code === 'http' && info.textStatus === 'timeout' ) {
 198+ code = 'timeout';
 199+ }
 200+
201201 if ( $j.inArray( code, mw.Api.errors ) !== -1 ) {
202202 msgKey = 'mwe-upwiz-api-error-' + code;
203 - // args may change base on particular error messages.
204 - // for instance, we are throwing away the extra info right now. Might be nice to surface that in a debug mode
205 - args = [];
 203+ args = $j.makeArray( info );
206204 }
207205 this.setStatus( msgKey, args );
208206 },
@@ -214,11 +212,31 @@
215213 var _this = this;
216214 _this.clearErrors();
217215 _this.upload.extractLocalFileInfo( _this.$fileInputCtrl.val() );
218 - if ( _this.isGoodExtension( _this.upload.title.getExtension() ) ) {
 216+ var extension = _this.upload.title.getExtension();
 217+ var hasExtension = ! mw.isEmpty( extension );
 218+ var isGoodExtension = false;
 219+ if ( hasExtension ) {
 220+ isGoodExtension = $j.inArray( extension.toLowerCase(), mw.UploadWizard.config[ 'fileExtensions' ] ) !== -1;
 221+ }
 222+ if ( hasExtension && isGoodExtension ) {
219223 _this.updateFilename();
220224 } else {
221 - //_this.error( 'bad-filename-extension', ext );
222 - alert("bad extension");
 225+ var errorMessage = hasExtension ? 'mwe-upwiz-upload-error-bad-filename-extension' : 'mwe-upwiz-upload-error-bad-filename-no-extension';
 226+ $( '<div>' )
 227+ .append(
 228+ $j( '<p>' ).msg( errorMessage, extension ),
 229+ $j( '<p>' ).msg( 'mwe-upwiz-allowed-filename-extensions' ),
 230+ $j( '<blockquote>' ).append( $j( '<tt>' ).append(
 231+ mw.UploadWizard.config[ 'fileExtensions' ].join( " " )
 232+ ) )
 233+ )
 234+ .dialog({
 235+ width: 500,
 236+ zIndex: 200000,
 237+ autoOpen: true,
 238+ title: gM( 'mwe-upwiz-help-popup' ) + ': ' + gM( 'mwe-upwiz-help-allowed-filename-extensions' ),
 239+ modal: true
 240+ });
223241 }
224242 this.clearStatus();
225243 },
@@ -273,7 +291,22 @@
274292 // visible filename
275293 $j( _this.form ).find( '.mwe-upwiz-visible-file-filename-text' ).html( path );
276294
277 - _this.upload.title = new mw.Title( mw.UploadWizardUtil.getBasename( path ), 'file' );
 295+ var filename = mw.UploadWizardUtil.getBasename( path );
 296+ try {
 297+ _this.upload.title = new mw.Title( filename, 'file' );
 298+ } catch ( e ) {
 299+ $( '<div>' )
 300+ .msg( 'mwe-upwiz-unparseable-filename', filename )
 301+ .dialog({
 302+ width: 500,
 303+ zIndex: 200000,
 304+ autoOpen: true,
 305+ modal: true
 306+ });
 307+ _this.$fileInputCtrl.val();
 308+ return;
 309+ }
 310+
278311 $j( _this.filenameCtrl ).val( _this.upload.title.getMain() );
279312
280313 if ( ! _this.isFilled ) {
@@ -335,19 +368,6 @@
336369 // apply a error style to entire did
337370 $j( _this.div ).addClass( 'mwe-upwiz-upload-error' );
338371 $j( _this.errorDiv ).show();
339 - },
340 -
341 - /**
342 - * This is used when checking for "bad" extensions in a filename.
343 - * @param ext
344 - * @return boolean if extension was acceptable
345 - */
346 - isGoodExtension: function( ext ) {
347 - // ugly but we don't have a base "uploadHandler" class
348 - if( this.upload.getUploadHandler().isGoodExtension ){
349 - return this.upload.getUploadHandler().isGoodExtension( ext );
350 - }
351 - return $j.inArray( ext.toLowerCase(), mw.UploadWizard.config[ 'fileExtensions' ] ) !== -1;
352372 }
353373
354 -};
\ No newline at end of file
 374+};
Index: branches/uploadwizard-firefogg/resources/mw.UploadWizardUtil.js
@@ -16,26 +16,23 @@
1717 $j( toggleDiv ).append( $toggleLink );
1818
1919
20 - var toggle = function( open ) {
21 - if ( typeof open === 'undefined' ) {
22 - open = ! ( $j( this ).data( 'open' ) ) ;
23 - }
24 - $j( this ).data( 'open', open );
25 - if ( open ) {
26 - moreDiv.show(); // maskSafeShow();
27 - /* when open, show control to close */
28 - $toggleLink.html( gM( 'mwe-upwiz-fewer-options' ) );
29 - $toggleLink.addClass( "mwe-upwiz-toggler-open" );
30 - } else {
 20+ var toggle = function() {
 21+ var isOpen = $toggleLink.hasClass( "mwe-upwiz-toggler-open" );
 22+ if ( isOpen ) {
 23+ // hide the extra options
3124 moreDiv.hide(); // maskSafeHide();
3225 /* when closed, show control to open */
33 - $toggleLink.html( gM( 'mwe-upwiz-more-options' ) );
34 - $toggleLink.removeClass( "mwe-upwiz-toggler-open" );
 26+ $toggleLink.msg( 'mwe-upwiz-more-options' ).removeClass( "mwe-upwiz-toggler-open" );
 27+ } else {
 28+ // show the extra options
 29+ moreDiv.show(); // maskSafeShow();
 30+ /* when open, show control to close */
 31+ $toggleLink.msg( 'mwe-upwiz-fewer-options' ).addClass( "mwe-upwiz-toggler-open" );
3532 }
3633 };
 34+
 35+ moreDiv.hide();
3736
38 - toggle(false);
39 -
4037 $toggleLink.click( function( e ) { e.stopPropagation(); toggle(); } );
4138
4239 $j( moreDiv ).addClass( 'mwe-upwiz-toggled' );
Index: branches/uploadwizard-firefogg/resources/mw.DestinationChecker.js
@@ -104,10 +104,11 @@
105105 */
106106 checkUnique: function() {
107107 var _this = this;
108 -
109108 var found = false;
110 - // XXX if input is empty don't bother? but preprocess gives us File:.png...
111109 var title = _this.getTitle();
 110+
 111+ // if input is empty don't bother.
 112+ if ( title == '' ) return;
112113
113114 if ( _this.cachedResult[name] !== undefined ) {
114115 _this.processResult( _this.cachedResult[name] );
@@ -126,8 +127,8 @@
127128 'iiurlwidth': 150
128129 };
129130
130 - // Do the destination check
131 - _this.api.get( params, function( data ) {
 131+
 132+ var ok = function( data ) {
132133 // Remove spinner
133134 _this.spinner( false );
134135
@@ -138,7 +139,7 @@
139140
140141 if ( !data || !data.query || !data.query.pages ) {
141142 // Ignore a null result
142 - mw.log("mw.DestinationChecker::checkUnique> No data in checkUnique result");
 143+ mw.log("mw.DestinationChecker::checkUnique> No data in checkUnique result", 'debug');
143144 return;
144145 }
145146
@@ -146,7 +147,6 @@
147148
148149 if ( data.query.pages[-1] ) {
149150 // No conflict found; this file name is unique
150 - mw.log("mw.DestinationChecker::checkUnique> No pages in checkUnique result");
151151 result = { isUnique: true };
152152
153153 } else {
@@ -157,8 +157,6 @@
158158 }
159159
160160 // Conflict found, this filename is NOT unique
161 - mw.log( "mw.DestinationChecker::checkUnique> conflict! " );
162 -
163161 var ntitle;
164162 if ( data.query.normalized ) {
165163 ntitle = data.query.normalized[0].to;
@@ -184,7 +182,16 @@
185183 _this.processResult( result );
186184 }
187185
188 - } );
 186+ };
 187+
 188+ var err = function( code, result ) {
 189+ _this.spinner( false );
 190+ mw.log("mw.DestinationChecker::checkUnique> error in checkUnique result: " + code, 'debug');
 191+ return;
 192+ };
 193+
 194+ // Do the destination check
 195+ _this.api.get( params, { ok: ok, err: err } );
189196 }
190197
191198 };
Index: branches/uploadwizard-firefogg/resources/mw.Api.js
@@ -47,8 +47,7 @@
4848
4949 // caller can supply handlers for http transport error or api errors
5050 err: function( code, result ) {
51 - var errorMsg = "mw.Api error: " + code;
52 - mw.log( _method + errorMsg );
 51+ mw.log( "mw.Api error: " + code, 'debug' );
5352 },
5453
5554 timeout: 30000, /* 30 seconds */
@@ -129,6 +128,7 @@
130129 ajaxOptions.err( 'http', { xhr: xhr, textStatus: textStatus, exception: exception } );
131130 };
132131
 132+
133133 /* success just means 200 OK; also check for output and API errors */
134134 ajaxOptions.success = function( result ) {
135135 if ( mw.isEmpty( result ) ) {
@@ -153,6 +153,20 @@
154154 * available.
155155 */
156156 mw.Api.errors = [
 157+ /* occurs when POST aborted - jQuery 1.4 can't distinguish abort or lost connection from 200 OK + empty result */
 158+ 'ok-but-empty',
 159+
 160+ // timeout
 161+ 'timeout',
 162+
 163+ /* really a warning, but we treat it like an error */
 164+ 'duplicate',
 165+
 166+ /* upload succeeded, but no image info.
 167+ this is probably impossible, but might as well check for it */
 168+ 'noimageinfo',
 169+
 170+ /* remote errors, defined in API */
157171 'uploaddisabled',
158172 'nomodule',
159173 'mustbeposted',
Index: branches/uploadwizard-firefogg/resources/mw.Title.js
@@ -42,13 +42,13 @@
4343 var ext = null;
4444
4545 /**
46 - * strip every illegal char we can think of
 46+ * strip some illegal chars: control chars, colon, less than, greater than, brackets, braces, pipe, whitespace
4747 * yes, I know this leaves other insanity intact, like unicode bidi chars, but let's start someplace
4848 * @return {String}
4949 */
5050 function clean( s ) {
5151 if ( mw.isDefined( s ) ) {
52 - return s.replace( /[\x00-\x1f\s]+/g, '_' );
 52+ return s.replace( /[\x00-\x1f\x23\x3a\x3c\x3e\x5b\x5d\x7b\x7c\x7d\x7f\s]+/g, '_' );
5353 }
5454 }
5555
Index: branches/uploadwizard-firefogg/resources/mw.UtilitiesTime.js
@@ -15,8 +15,7 @@
1616 */
1717 mw.seconds2npt = function( sec, show_ms ) {
1818 if ( isNaN( sec ) ) {
19 - mw.log("mw.seconds2npt> Warning: trying to get npt time on NaN:" + sec);
20 - return '0:00:00';
 19+ sec = 0;
2120 }
2221
2322 var tm = mw.seconds2Measurements( sec );
@@ -66,8 +65,7 @@
6766 */
6867 mw.npt2seconds = function ( npt_str ) {
6968 if ( !npt_str ) {
70 - // mw.log('npt2seconds:not valid ntp:'+ntp);
71 - return false;
 69+ return undefined;
7270 }
7371 // Strip {npt:}01:02:20 or 32{s} from time if present
7472 npt_str = npt_str.replace( /npt:|s/g, '' );
Index: branches/uploadwizard-firefogg/resources/mw.Api.edit.js
@@ -1,7 +1,5 @@
22 // library to assist with edits
33
4 -// dependencies: [ mw.Api, jQuery ]
5 -
64 ( function( mw, $ ) {
75
86 // cached token so we don't have to keep fetching new ones for every single post
@@ -19,36 +17,27 @@
2018 */
2119 postWithEditToken: function( params, ok, err ) {
2220 var api = this;
23 - var _method = 'mw.api.edit::postWithEditToken> ';
24 - mw.log( 'post with edit token' );
2521 if ( cachedToken === null ) {
26 - mw.log( _method + 'no cached token' );
2722 // We don't have a valid cached token, so get a fresh one and try posting.
2823 // We do not trap any 'badtoken' or 'notoken' errors, because we don't want
2924 // an infinite loop. If this fresh token is bad, something else is very wrong.
3025 var useTokenToPost = function( token ) {
31 - mw.log( _method + 'posting with token = ' + token );
3226 params.token = token;
3327 this.post( params, ok, err );
3428 };
35 - mw.log( _method + 'getting edit token' );
3629 api.getEditToken( useTokenToPost, err );
3730 } else {
3831 // We do have a token, but it might be expired. So if it is 'bad' then
3932 // start over with a new token.
4033 params.token = cachedToken;
41 - mw.log( _method + 'we do have a token = ' + params.token );
4234 var getTokenIfBad = function( code, result ) {
43 - mw.log( _method + "error with posting with token!" );
4435 if ( code === 'badtoken' ) {
45 - mw.log( _method + "bad token; try again" );
4636 cachedToken = null; // force a new token
47 - api.postWidthEditToken( params, ok, err );
 37+ api.postWithEditToken( params, ok, err );
4838 } else {
4939 err( code, result );
5040 }
5141 };
52 - mw.log ( _method + "posting with the token that was cached " );
5342 api.post( params, ok, getTokenIfBad );
5443 }
5544 },
Index: branches/uploadwizard-firefogg/resources/mediawiki.language.parser.js
@@ -1,5 +1,8 @@
22 /**
3 - * n.b. if this is ever moved to be mediawiki.language.parser, then mediawiki.language.procPLURAL will be obsolete
 3+ * Experimental advanced wikitext parser-emitter.
 4+ * See: http://www.mediawiki.org/wiki/Extension:UploadWizard/MessageParser for docs
 5+ *
 6+ * @author neilk@wikimedia.org
47 */
58
69 ( function( mw, $j ) {
@@ -69,7 +72,7 @@
7073 * @return {jQuery} this
7174 */
7275 return function( key /* , replacements */ ) {
73 - var $target = this;
 76+ var $target = this.empty();
7477 $j.each( parser.parse( key, getVariadicArgs( arguments, 1 ) ).contents(), function( i, node ) {
7578 $target.append( node );
7679 } );
@@ -132,41 +135,9 @@
133136 /*
134137 * Parses the input wikiText into an abstract syntax tree, essentially an s-expression.
135138 *
136 - * Why you would want to do this: ASTs make some complex stuff easy.
137 - * - this allows the parser to be stateless -- all parsing state is in the AST, which is cached, or is just there temporarily as
138 - * replacement parameters are swapped in
139 - * - it's MUCH simpler to do complex replacements, such as swapping in jQuery objects into wikitext like "[$1 my link]". No string hackery required
140 - * - decouples target format from parsing -- you can output a string of HTML, or jQuery-compatible array of nodes, or whatever you like.
141 - *
142 - * However, these are also all arguments for doing this on the server. Stay tuned... we can reduce the code on the client by half
143 - * by porting everything below this function to PHP.
144 - *
145 - * Examples:
146 - * "A simple input string" => "A simple input string"
147 - * "Simple {{TEMPLATE}} message" => [ 'CONCAT', 'Simple ', [ 'TEMPLATE' ], ' message' ];
148 - * "Undelete {{PLURAL:$1|one edit|$1 edits}}", =>
149 - * [ 'CONCAT',
150 - * 'Undelete ',
151 - * [ 'PLURAL',
152 - * [ 'REPLACE', 0 ], // zero-based index. $1 is argument 0
153 - * 'one edit',
154 - * [ 'CONCAT',
155 - * [ 'REPLACE', 0 ]
156 - * ' edits'
157 - * ]
158 - * ]
159 - * ]
160 - *
161 - * The following code is a highly hand-hacked and optimized
162 - * parser based on a generated PEG parser using the grammar in the file mediawiki.parser.peg
163 - *
164 - * CAVEAT: This does not parse all wikitext, and it makes a lot of assumptions that may not reflect
165 - * how the actual parser works. It works for pretty much all cases where we will want to pass translation
166 - * strings to the frontend, however.
 139+ * CAVEAT: This does not parse all wikitext. It could be more efficient, but it's pretty good already.
 140+ * n.b. We want to move this functionality to the server. Nothing here is required to be on the client.
167141 *
168 - * More caveats: there are a lot of things here which could be more efficient, but it's already pretty
169 - * efficient already and we may not use this client side for very long until we move it server side.
170 - *
171142 * @param {String} message string wikitext
172143 * @throws Error
173144 * @return {Mixed} abstract syntax tree
Index: branches/uploadwizard-firefogg/resources/uploadWizard.css
@@ -1,5 +1,8 @@
 2+/* for feedback request message */
 3+#contentSub {
 4+ margin-left: 0;
 5+}
26
3 -
47 form.mwe-upwiz-form {
58 display: inline;
69 }
@@ -170,6 +173,12 @@
171174 background: url('images/32px-Blank-document.svg.png') no-repeat center top;
172175 }
173176
 177+.mwe-upwiz-file-preview-broken {
 178+ height: 40px !important;
 179+ width: 40px !important;
 180+ background: url('images/32px-Blank-document-broken.svg.png') no-repeat center top !important;
 181+}
 182+
174183 .mwe-upwiz-add-files-n {
175184 float: left;
176185 }
@@ -275,10 +284,34 @@
276285 background: url('images/24px-spinner-0645ad.gif') no-repeat center center;
277286 }
278287
 288+.mwe-upwiz-file.hover .mwe-upwiz-status-progress {
 289+ background: url('images/24px-spinner-0645ad-e0f0ff.gif') no-repeat center center;
 290+}
 291+
279292 .mwe-upwiz-status-stashed, .mwe-upwiz-status-uploaded {
280293 background: url('images/32px-Dialog-apply-009900.svg.png') no-repeat center center;
281294 }
282295
 296+
 297+.mwe-upwiz-license-icon {
 298+ height: 18px;
 299+ margin-left: 6px;
 300+ padding: 9px;
 301+ width: 18px;
 302+}
 303+
 304+.mwe-upwiz-cc-zero-icon {
 305+ background: url('images/18px-Cc-zero.svg.png') no-repeat center center;
 306+}
 307+
 308+.mwe-upwiz-cc-sa-icon {
 309+ background: url('images/18px-Share_Alike.svg.png') no-repeat center center;
 310+}
 311+
 312+.mwe-upwiz-cc-by-icon {
 313+ background: url('images/18px-Cc-by_new_white.svg.png') no-repeat center center;
 314+}
 315+
283316 .mwe-upwiz-status-error {
284317 background: url('images/32px-Nuvola_apps_important_orange.svg.png') no-repeat center center;
285318 font-weight: bold;
@@ -466,15 +499,17 @@
467500
468501 .mwe-grow-textarea, .mwe-long-textarea {
469502 overflow: hidden;
470 - font-family: sans-serif; /* XXX is this right? */
 503+ font-family: sans-serif;
471504 font-size: small;
472505 }
473506
474 -fieldset .mwe-long-textarea {
475 - width: 17em;
 507+
 508+.mwe-title {
 509+ font-family: sans-serif;
 510+ font-size: small;
 511+ width: 100%;
476512 }
477513
478 -
479514 .mwe-upwiz-details-fieldname-input {
480515 margin-bottom: 1em;
481516 }
@@ -489,7 +524,6 @@
490525 }
491526
492527 fieldset.mwe-fieldset {
493 - width: 450px;
494528 border: 1px solid #cccccc;
495529 padding-left: 12px;
496530 padding-right: 12px;
@@ -628,6 +662,11 @@
629663 background: url('images/question-hover.png') no-repeat;
630664 }
631665
 666+/* Hack for Explorer 6 and 7 - line up buttons with other content */
 667+.mwe-upwiz-buttons .ui-button, .cat-widget .ui-button {
 668+ *vertical-align: middle;
 669+}
 670+
632671 #upload-wizard .ui-corner-tl { -moz-border-radius-topleft: 4px; -webkit-border-top-left-radius: 4px; }
633672 #upload-wizard .ui-corner-tr { -moz-border-radius-topright: 4px; -webkit-border-top-right-radius: 4px; }
634673 #upload-wizard .ui-corner-bl { -moz-border-radius-bottomleft: 4px; -webkit-border-bottom-left-radius: 4px; }
@@ -637,3 +676,19 @@
638677 #upload-wizard .ui-corner-right { -moz-border-radius-topright: 4px; -webkit-border-top-right-radius: 4px; -moz-border-radius-bottomright: 4px; -webkit-border-bottom-right-radius: 4px; }
639678 #upload-wizard .ui-corner-left { -moz-border-radius-topleft: 4px; -webkit-border-top-left-radius: 4px; -moz-border-radius-bottomleft: 4px; -webkit-border-bottom-left-radius: 4px; }
640679 #upload-wizard .ui-corner-all { -moz-border-radius: 4px; -webkit-border-radius: 4px; }
 680+
 681+.mwe-upwiz-deed-license-group {
 682+ margin-top: 1em;
 683+ margin-bottom: 1.5em;
 684+}
 685+
 686+.mwe-upwiz-deed-license-group-head {
 687+ font-weight: bold;
 688+ margin-bottom: 0em;
 689+}
 690+
 691+.mwe-upwiz-deed-license-group-subhead {
 692+ color: #797979;
 693+ margin-top: 0em;
 694+ margin-bottom: 0em;
 695+}
Index: branches/uploadwizard-firefogg/resources/mw.UploadWizard.js
@@ -22,7 +22,7 @@
2323 this.transportWeight = 1; // default
2424 this.detailsWeight = 1; // default
2525
26 - // details
 26+ // details
2727 this.ui = new mw.UploadWizardUploadInterface( this, filesDiv );
2828
2929 // handler -- usually ApiUploadHandler
@@ -62,14 +62,13 @@
6363 // we signal to the wizard to update itself, which has to delete the final vestige of
6464 // this upload (the ui.div). We have to do this silly dance because we
6565 // trigger through the div. Triggering through objects doesn't always work.
66 - // TODO fix -- this now works in jquery 1.4.2
 66+ // TODO v.1.1 fix, don't need to use the div any more -- this now works in jquery 1.4.2
6767 $j( this.ui.div ).trigger( 'removeUploadEvent' );
6868 },
6969
7070
7171 /**
7272 * Wear our current progress, for observing processes to see
73 - * XXX this is kind of a misnomer; this event is not firing except for the very first time.
7473 * @param fraction
7574 */
7675 setTransportProgress: function ( fraction ) {
@@ -80,6 +79,16 @@
8180 },
8281
8382 /**
 83+ * Queue some warnings for possible later consumption
 84+ */
 85+ addWarning: function( code, info ) {
 86+ if ( !mw.isDefined( this.warnings ) ) {
 87+ this.warnings = [];
 88+ }
 89+ this.warnings.push( [ code, info ] );
 90+ },
 91+
 92+ /**
8493 * Stop the upload -- we have failed for some reason
8594 */
8695 setError: function( code, info ) {
@@ -95,60 +104,143 @@
96105 setTransported: function( result ) {
97106 var _this = this;
98107 if ( _this.state == 'aborted' ) {
99 - return ;
100 - }
101 - if ( result.upload && result.upload.imageinfo ) {
102 - mw.log( 'UploadWizard::setTransported> process api imageinfo' );
103 - // success
104 - _this.state = 'transported';
105 - _this.transportProgress = 1;
106 - _this.ui.setStatus( 'mwe-upwiz-getting-metadata' );
107 - _this.extractUploadInfo( result );
108 -
109 - // use blocking preload for thumbnail, no loading spinner.
110 - _this.getThumbnail(
111 - function( image ) {
112 - _this.ui.setPreview( image );
113 - _this.deedPreview.setup();
114 - _this.details.populate();
115 - _this.state = 'stashed';
116 - _this.ui.showStashed();
117 - },
118 - mw.UploadWizard.config[ 'iconThumbnailWidth' ],
119 - mw.UploadWizard.config[ 'iconThumbnailMaxHeight' ]
120 - );
121 -
122 - } else if ( result.upload && result.upload.sessionkey ) {
123 - // there was a warning - type error which prevented it from adding the result to the db
124 - if ( result.upload.warnings.duplicate ) {
125 - var duplicates = result.upload.warnings.duplicate;
126 - _this.details.errorDuplicate( result.upload.sessionkey, duplicates );
 108+ return;
 109+ }
 110+
 111+ // default error state
 112+ var code = 'unknown';
 113+ var info = 'unknown';
 114+
 115+ if ( result.upload && result.upload.warnings ) {
 116+ if ( result.upload.warnings.exists ) {
 117+ // the filename we uploaded is in use already. Not a problem since we stashed it under a temporary name anyway
 118+ // potentially we could indicate to the upload that it should set the Title field to error state now, but we'll let them deal with that later.
 119+ // however, we don't get imageinfo, so let's try to get it and pretend that we did
 120+ var existsFileName = result.upload.warnings.exists;
 121+ try {
 122+ code = 'exists';
 123+ info = _this.filenameToUrl( existsFileName ).toString();
 124+ } catch ( e ) {
 125+ code = 'unknown';
 126+ info = 'Warned about existing filename, but filename is unparseable: "' + existsFileName + "'";
 127+ }
 128+ _this.addWarning( code, info );
 129+ _this.extractUploadInfo( result.upload );
 130+ var success = function( imageinfo ) {
 131+ if ( imageinfo === null ) {
 132+ _this.setError( 'noimageinfo' );
 133+ } else {
 134+ result.upload.stashimageinfo = imageinfo;
 135+ _this.setSuccess( result );
 136+ }
 137+ };
 138+ _this.getStashImageInfo( success, [ 'timestamp', 'url', 'size', 'dimensions', 'sha1', 'mime', 'metadata', 'bitdepth' ] );
 139+ } else if ( result.upload.warnings.duplicate ) {
 140+ if ( typeof result.upload.warnings.duplicate == 'object' ) {
 141+ var duplicates = result.upload.warnings.duplicate;
 142+ var $ul = $j( '<ul></ul>' );
 143+ $j.each( duplicates, function( i, filename ) {
 144+ var $a = $j( '<a/>' ).append( filename );
 145+ var href;
 146+ try {
 147+ href = _this.filenameToUrl( filename );
 148+ $a.attr( { 'href': href, 'target': '_blank' } );
 149+ } catch ( e ) {
 150+ $a.click( function() { alert('could not parse filename=' + filename ); } );
 151+ $a.attr( 'href', '#' );
 152+ }
 153+ $ul.append( $j( '<li></li>' ).append( $a ) );
 154+ } );
 155+ var dialogFn = function() {
 156+ $j( '<div></div>' )
 157+ .html( $ul )
 158+ .dialog( {
 159+ width: 500,
 160+ zIndex: 200000,
 161+ autoOpen: true,
 162+ title: gM( 'mwe-upwiz-api-error-duplicate-popup-title', duplicates.length ),
 163+ modal: true
 164+ } );
 165+ };
 166+ code = 'duplicate';
 167+ info = [ duplicates.length, dialogFn ];
 168+ }
 169+ _this.setError( code, info );
127170 }
128 -
129 - // and other errors that result in a stash
 171+ } else if ( result.upload && result.upload.result === 'Success' ) {
 172+ if ( result.upload.imageinfo ) {
 173+ _this.setSuccess( result );
 174+ } else {
 175+ _this.setError( 'noimageinfo' );
 176+ }
130177 } else {
131 - // XXX handle errors better -- get code and pass to showError
132 - var code = 'unknown';
133 - var info = 'unknown';
134178 if ( result.error ) {
135 - code = result.error.code;
136 - info = result.error.info;
137 - }
 179+ if ( result.error.code ) {
 180+ code = result.error.code;
 181+ }
 182+ if ( result.error.info ) {
 183+ info = result.error.info;
 184+ }
 185+ }
138186 _this.setError( code, info );
139187 }
140 -
 188+
 189+
141190 },
142191
143192
144193 /**
 194+ * Called from any upload success condition
 195+ * @param {Mixed} result -- result of AJAX call
 196+ */
 197+ setSuccess: function( result ) {
 198+ var _this = this; // was a triumph
 199+ _this.state = 'transported';
 200+ _this.transportProgress = 1;
 201+
 202+ // I'm making a note here
 203+ _this.ui.setStatus( 'mwe-upwiz-getting-metadata' );
 204+ if ( result.upload ) {
 205+ _this.extractUploadInfo( result.upload );
 206+ // create the small thumbnail used on the 'upload' step
 207+ _this.getThumbnail(
 208+ function( image ) {
 209+ // n.b. if server returns a URL, which is a 404, we do NOT get broken image
 210+ _this.ui.setPreview( image ); // make the thumbnail the preview image
 211+ },
 212+ mw.UploadWizard.config[ 'iconThumbnailWidth' ],
 213+ mw.UploadWizard.config[ 'iconThumbnailMaxHeight' ]
 214+ );
 215+ // create the large thumbnail that the other thumbnails link to
 216+ _this.getThumbnail(
 217+ function( image ) {},
 218+ mw.UploadWizard.config[ 'largeThumbnailWidth' ],
 219+ mw.UploadWizard.config[ 'largeThumbnailMaxHeight' ]
 220+ );
 221+ _this.deedPreview.setup();
 222+ _this.details.populate();
 223+ _this.state = 'stashed';
 224+ _this.ui.showStashed();
 225+ } else {
 226+ _this.setError( 'noimageinfo' );
 227+ }
 228+
 229+ },
 230+
 231+ /**
145232 * Called when the file is entered into the file input
146233 * Get as much data as possible -- maybe exif, even thumbnail maybe
147234 */
148 - extractLocalFileInfo: function( localFilename ) {
 235+ extractLocalFileInfo: function( filename ) {
149236 if ( false ) { // FileAPI, one day
150237 this.transportWeight = getFileSize();
151238 }
152 - this.title = new mw.Title( mw.UploadWizardUtil.getBasename( localFilename ), 'file' );
 239+ // XXX sanitize filename
 240+ try {
 241+ this.title = new mw.Title( mw.UploadWizardUtil.getBasename( filename ).replace( /:/g, '_' ), 'file' );
 242+ } catch ( e ) {
 243+ this.setError( 'mwe-upwiz-unparseable-filename', filename );
 244+ }
153245 },
154246
155247 /**
@@ -156,9 +248,17 @@
157249 *
158250 * @param result The JSON object from a successful API upload result.
159251 */
160 - extractUploadInfo: function( result ) {
161 - this.sessionKey = result.upload.sessionkey;
162 - this.extractImageInfo( result.upload.imageinfo );
 252+ extractUploadInfo: function( resultUpload ) {
 253+ if ( resultUpload.sessionkey ) {
 254+ this.sessionKey = resultUpload.sessionkey;
 255+ }
 256+ if ( resultUpload.imageinfo ) {
 257+ this.extractImageInfo( resultUpload.imageinfo );
 258+ } else if ( resultUpload.stashimageinfo ) {
 259+ this.extractImageInfo( resultUpload.stashimageinfo );
 260+ }
 261+
 262+
163263 },
164264
165265 /**
@@ -184,9 +284,10 @@
185285 }
186286 }
187287
188 - // TODO this needs to be rethought.
189 - // we should already have an extension, but if we don't... ??
190288 if ( _this.title.getExtension() === null ) {
 289+ 1;
 290+ // TODO v1.1 what if we don't have an extension? Should be impossible as it is currently impossible to upload without extension, but you
 291+ // never know... theoretically there is no restriction on extensions if we are uploading to the stash, but the check is performed anyway.
191292 /*
192293 var extension = mw.UploadWizardUtil.getExtension( _this.imageinfo.url );
193294 if ( !extension ) {
@@ -199,7 +300,58 @@
200301 */
201302 }
202303 },
 304+
203305 /**
 306+ * Get information about stashed images
 307+ * See API documentation for prop=stashimageinfo for what 'props' can contain
 308+ * @param {Function} callback -- called with null if failure, with imageinfo data structure if success
 309+ * @param {Array} properties to extract
 310+ * @param {Number} optional, width of thumbnail. Will force 'url' to be added to props
 311+ * @param {Number} optional, height of thumbnail. Will force 'url' to be added to props
 312+ */
 313+ getStashImageInfo: function( callback, props, width, height ) {
 314+ var _this = this;
 315+
 316+ if (!mw.isDefined( props ) ) {
 317+ props = [];
 318+ }
 319+
 320+ var params = {
 321+ 'prop': 'stashimageinfo',
 322+ 'siisessionkey': _this.sessionKey,
 323+ 'siiprop': props.join( '|' )
 324+ };
 325+
 326+ if ( mw.isDefined( width ) || mw.isDefined( height ) ) {
 327+ if ( ! $j.inArray( 'url', props ) ) {
 328+ props.push( 'url' );
 329+ }
 330+ if ( mw.isDefined( width ) ) {
 331+ params['siiurlwidth'] = width;
 332+ }
 333+ if ( mw.isDefined( height ) ) {
 334+ params['siiurlheight'] = height;
 335+ }
 336+ }
 337+
 338+ var ok = function( data ) {
 339+ if ( !data || !data.query || !data.query.stashimageinfo ) {
 340+ mw.log("mw.UploadWizardUpload::getStashImageInfo> No data? ");
 341+ callback( null );
 342+ return;
 343+ }
 344+ callback( data.query.stashimageinfo );
 345+ };
 346+
 347+ var err = function( code, result ) {
 348+ mw.log( 'mw.UploadWizardUpload::getStashImageInfo> error: ' + code, 'debug' );
 349+ callback( null );
 350+ };
 351+
 352+ this.api.get( params, { ok: ok, err: err } );
 353+ },
 354+
 355+ /**
204356 * Set the upload handler per browser capabilities
205357 */
206358 getUploadHandler: function(){
@@ -218,12 +370,12 @@
219371 }
220372 return this.uploadHandler;
221373 },
222 -
 374+
223375 /**
224376 * Fetch a thumbnail for a stashed upload of the desired width.
225377 * It is assumed you don't call this until it's been transported.
226378 *
227 - * @param callback - callback to execute once thumbnail has been obtained -- must accept Image object
 379+ * @param callback - callback to execute once thumbnail has been obtained -- must accept Image object for success, null for error
228380 * @param width - desired width of thumbnail (height will scale to match)
229381 * @param height - (optional) maximum height of thumbnail
230382 */
@@ -236,38 +388,29 @@
237389 if ( mw.isDefined( _this.thumbnails[key] ) ) {
238390 callback( _this.thumbnails[key] );
239391 } else {
240 - var params = {
241 - 'prop': 'stashimageinfo',
242 - 'siisessionkey': _this.sessionKey,
243 - 'siiurlwidth': width,
244 - 'siiurlheight': height,
245 - 'siiprop': 'url'
246 - };
247 -
248 - this.api.get( params, function( data ) {
249 - if ( !data || !data.query || !data.query.stashimageinfo ) {
250 - mw.log("mw.UploadWizardUpload::getThumbnail> No data? ");
251 - // XXX do something about the thumbnail spinner, maybe call the callback with a broken image.
252 - return;
253 - }
254 - var thumbnails = data.query.stashimageinfo;
255 - for ( var i = 0; i < thumbnails.length; i++ ) {
256 - var thumb = thumbnails[i];
257 - if ( ! ( thumb.thumburl && thumb.thumbwidth && thumb.thumbheight ) ) {
258 - mw.log( "mw.UploadWizardUpload::getThumbnail> thumbnail missing information" );
259 - // XXX error
260 - return ;
 392+ var apiCallback = function( thumbnails ) {
 393+ if ( thumbnails === null ) {
 394+ callback( null );
 395+ } else {
 396+ for ( var i = 0; i < thumbnails.length; i++ ) {
 397+ var thumb = thumbnails[i];
 398+ if ( thumb.thumberror || ( ! ( thumb.thumburl && thumb.thumbwidth && thumb.thumbheight ) ) ) {
 399+ mw.log( "mw.UploadWizardUpload::getThumbnail> thumbnail error or missing information" );
 400+ callback( null );
 401+ return;
 402+ }
 403+ var image = document.createElement( 'img' );
 404+ $j( image ).load( function() {
 405+ callback( image );
 406+ } );
 407+ image.width = thumb.thumbwidth;
 408+ image.height = thumb.thumbheight;
 409+ image.src = thumb.thumburl;
 410+ _this.thumbnails[key] = image;
261411 }
262 - var image = document.createElement( 'img' );
263 - $j( image ).load( function() {
264 - callback( image );
265 - } );
266 - image.width = thumb.thumbwidth;
267 - image.height = thumb.thumbheight;
268 - image.src = thumb.thumburl;
269 - _this.thumbnails[key] = image;
270412 }
271 - } );
 413+ };
 414+ _this.getStashImageInfo( apiCallback, [ 'url' ], width, height );
272415 }
273416 },
274417
@@ -279,32 +422,58 @@
280423 * @param height (optional)
281424 */
282425 setThumbnail: function( selector, width, height ) {
283 -
284426 var _this = this;
285427 if ( typeof width === 'undefined' || width === null || width <= 0 ) {
286 - width = mw.UploadWizard.config[ 'thumbnailWidth' ];
 428+ width = mw.UploadWizard.config[ 'thumbnailWidth' ];
287429 }
288430 width = parseInt( width, 10 );
289431 height = null;
290432 if ( !mw.isEmpty( height ) ) {
291433 height = parseInt( height, 10 );
292434 }
293 -
 435+
 436+ var href = '#';
 437+ $j.each( [ 'descriptionurl', 'url' ], function( i, propName ) {
 438+ var prop = _this.imageinfo[ propName ];
 439+ if ( prop ) {
 440+ href = prop;
 441+ return false;
 442+ }
 443+ } );
 444+
294445 var callback = function( image ) {
295 - $j( selector ).html(
296 - $j( '<a/>' )
297 - .attr( { 'href': _this.imageinfo.url,
298 - 'target' : '_new' } )
299 - .append(
300 - $j( '<img/>' )
301 - .attr( { 'width': image.width,
302 - 'height': image.height,
303 - 'src': image.src } )
304 - )
305 - );
 446+ if ( image === null ) {
 447+ $j( selector ).addClass( 'mwe-upwiz-file-preview-broken' );
 448+ _this.ui.setStatus( 'mwe-upwiz-thumbnail-failed' );
 449+ } else {
 450+ $j( selector ).html(
 451+ $j( '<a/>' )
 452+ .attr( { 'href': href,
 453+ 'target' : '_new' } )
 454+ .append(
 455+ $j( '<img/>' )
 456+ .attr( { 'width': image.width,
 457+ 'height': image.height,
 458+ 'src': image.src } )
 459+ )
 460+ );
 461+ }
306462 };
307463
308464 _this.getThumbnail( callback, width, height );
 465+ },
 466+
 467+ /**
 468+ * Given a filename like "Foo.jpg", get the URL to that filename, assuming the browser is on the same wiki.
 469+ * Candidate for a utility function...
 470+ * @param {String} filename
 471+ */
 472+ filenameToUrl: function( filename ) {
 473+ var fileUrl = new mw.Uri( document.URL );
 474+ fileUrl.path = wgScript;
 475+ var fileTitle = new mw.Title( filename, 'file' );
 476+ fileUrl.query = { title: fileTitle, action: 'view' };
 477+ return fileUrl;
309478 }
310479
311480 };
@@ -344,7 +513,7 @@
345514 * Depending on whether we split uploading / detailing, it may actually always be as simple as loading a URL
346515 */
347516 reset: function() {
348 - window.location.reload();
 517+ window.location = wgArticlePath.replace( '$1', 'Special:UploadWizard?skiptutorial=true' );
349518 },
350519
351520
@@ -352,9 +521,22 @@
353522 * create the basic interface to make an upload in this div
354523 * @param div The div in the DOM to put all of this into.
355524 */
356 - createInterface: function( selector ) {
 525+createInterface: function( selector ) {
357526 var _this = this;
 527+
 528+ // remove first spinner
 529+ $j( '#mwe-first-spinner' ).remove();
358530
 531+ // feedback request
 532+ if ( UploadWizardConfig['feedbackPage'] != '' ) {
 533+ $j( '#contentSub' ).html('<i>Please <a id="mwe-upwiz-feedback" href="#">let us know</a> what you think of Upload Wizard!</i>');
 534+ $j( '#mwe-upwiz-feedback')
 535+ .click( function() {
 536+ _this.launchFeedback();
 537+ return false;
 538+ } );
 539+ }
 540+
359541 // construct the arrow steps from the UL in the HTML
360542 $j( '#mwe-upwiz-steps' )
361543 .addClass( 'ui-helper-clearfix ui-state-default ui-widget ui-helper-reset ui-helper-clearfix' )
@@ -390,8 +572,14 @@
391573 .click( function() {
392574 // check if there is an upload at all (should never happen)
393575 if ( _this.uploads.length === 0 ) {
394 - // XXX use standard error message
395 - alert( gM( 'mwe-upwiz-file-need-file' ) );
 576+ $j( '<div></div>' )
 577+ .html( gM( 'mwe-upwiz-file-need-file' ) )
 578+ .dialog({
 579+ width: 500,
 580+ zIndex: 200000,
 581+ autoOpen: true,
 582+ modal: true
 583+ });
396584 return;
397585 }
398586
@@ -413,7 +601,7 @@
414602
415603 $j( '#mwe-upwiz-stepdiv-deeds .mwe-upwiz-button-next')
416604 .click( function() {
417 - $('.mwe-upwiz-hint').each( function(i) { $(this).tipsy('hide') } ); // close tipsy help balloons
 605+ $j( '.mwe-upwiz-hint' ).each( function(i) { $j( this ).tipsy( 'hide' ); } ); // close tipsy help balloons
418606 // validate has the side effect of notifying the user of problems, or removing existing notifications.
419607 // if returns false, you can assume there are notifications in the interface.
420608 if ( _this.deedChooser.valid() ) {
@@ -446,7 +634,7 @@
447635
448636 $j( '#mwe-upwiz-stepdiv-details .mwe-upwiz-button-next' )
449637 .click( function() {
450 - $('.mwe-upwiz-hint').each( function(i) { $(this).tipsy('hide') } ); // close tipsy help balloons
 638+ $j( '.mwe-upwiz-hint' ).each( function(i) { $j( this ).tipsy( 'hide' ); } ); // close tipsy help balloons
451639 if ( _this.detailsValid() ) {
452640 _this.detailsSubmit( function() {
453641 _this.prefillThanksPage();
@@ -460,7 +648,7 @@
461649 // WIZARD
462650
463651 // check to see if the the skip tutorial cookie is set
464 - if ( document.cookie.indexOf('skiptutorial=1') != -1 ) {
 652+ if ( document.cookie.indexOf('skiptutorial=1') != -1 || UploadWizardConfig['skipTutorial'] ) {
465653 // "select" the second step - highlight, make it visible, hide all others
466654 _this.moveToStep( 'file' );
467655 } else {
@@ -471,6 +659,7 @@
472660 },
473661
474662
 663+
475664 // do some last minute prep before advancing to the DEEDS page
476665 prepareAndMoveToDeeds: function() {
477666 var _this = this;
@@ -496,14 +685,14 @@
497686 deeds,
498687 _this.uploads.length );
499688
500 - $j( '<div>' )
 689+ $j( '<div></div>' )
501690 .insertBefore( _this.deedChooser.$selector.find( '.mwe-upwiz-deed-ownwork' ) )
502 - .html( gM( 'mwe-upwiz-deeds-macro-prompt', _this.uploads.length ) );
 691+ .msg( 'mwe-upwiz-deeds-macro-prompt', _this.uploads.length );
503692
504693 if ( _this.uploads.length > 1 ) {
505 - $j( '<div style="margin-top: 1em">' )
 694+ $j( '<div style="margin-top: 1em"></div>' )
506695 .insertBefore( _this.deedChooser.$selector.find( '.mwe-upwiz-deed-custom' ) )
507 - .html( gM( 'mwe-upwiz-deeds-custom-prompt' ) );
 696+ .msg( 'mwe-upwiz-deeds-custom-prompt' );
508697 }
509698
510699 _this.moveToStep( 'deeds' );
@@ -582,13 +771,9 @@
583772 $j( upload.ui.div ).bind( 'filenameAccepted', function(e) { _this.updateFileCounts(); e.stopPropagation(); } );
584773 $j( upload.ui.div ).bind( 'removeUploadEvent', function(e) { _this.removeUpload( upload ); e.stopPropagation(); } );
585774 $j( upload.ui.div ).bind( 'filled', function(e) {
586 - mw.log( "mw.UploadWizardUpload::newUpload> filled! received!" );
587775 _this.newUpload();
588 - mw.log( "mw.UploadWizardUpload::newUpload> filled! new upload!" );
589776 _this.setUploadFilled(upload);
590 - mw.log( "mw.UploadWizardUpload::newUpload> filled! set upload filled!" );
591777 e.stopPropagation();
592 - mw.log( "mw.UploadWizardUpload::newUpload> filled! stop propagation!" );
593778 } );
594779 // XXX bind to some error state
595780
@@ -615,9 +800,7 @@
616801
617802 upload.deedPreview = new mw.UploadWizardDeedPreview( upload );
618803
619 - // XXX do we really need to do this now? some things will even change after step 2.
620 - // legacy.
621 - // set up details
 804+ // TODO v1.1 consider if we really have to set up details now
622805 upload.details = new mw.UploadWizardDetails( upload, $j( '#mwe-upwiz-macro-files' ) );
623806 },
624807
@@ -854,7 +1037,7 @@
8551038 $j( '#mwe-upwiz-upload-ctrl-container' ).show();
8561039
8571040 // changes the "click here to add files" to "add another file"
858 - $j( '#mwe-upwiz-add-file span' ).html( gM( 'mwe-upwiz-add-file-n' ) );
 1041+ $j( '#mwe-upwiz-add-file span' ).msg( 'mwe-upwiz-add-file-n' );
8591042 $j( '#mwe-upwiz-add-file-container' ).removeClass('mwe-upwiz-add-files-0');
8601043 $j( '#mwe-upwiz-add-file-container' ).addClass('mwe-upwiz-add-files-n');
8611044
@@ -881,7 +1064,7 @@
8821065 $j( '#mwe-upwiz-stepdiv-file .mwe-upwiz-buttons' ).hide();
8831066
8841067 // change "add another file" into "click here to add a file"
885 - $j( '#mwe-upwiz-add-file' ).html( gM( 'mwe-upwiz-add-file-0' ) );
 1068+ $j( '#mwe-upwiz-add-file span' ).msg( 'mwe-upwiz-add-file-0' );
8861069 $j( '#mwe-upwiz-add-file-container' ).addClass('mwe-upwiz-add-files-0');
8871070 $j( '#mwe-upwiz-add-file-container' ).removeClass('mwe-upwiz-add-files-n');
8881071 }
@@ -922,8 +1105,6 @@
9231106 // some details blocks cannot be submitted (for instance, identical file hash)
9241107 _this.removeBlockedDetails();
9251108
926 - // XXX validate all
927 -
9281109 // remove ability to edit details
9291110 $j.each( _this.uploads, function( i, upload ) {
9301111 upload.details.div.mask();
@@ -969,23 +1150,27 @@
9701151 var _this = this;
9711152
9721153 $j( '#mwe-upwiz-thanks' )
973 - .append( $j( '<h3 style="text-align: center;">' ).append( gM( 'mwe-upwiz-thanks-intro' ) ),
 1154+ .append( $j( '<h3 style="text-align: center;"></h3>' ).msg( 'mwe-upwiz-thanks-intro' ),
9741155 $j( '<p style="margin-bottom: 2em; text-align: center;">' )
975 - .append( gM( 'mwe-upwiz-thanks-explain', _this.uploads.length ) ) );
 1156+ .msg( 'mwe-upwiz-thanks-explain', _this.uploads.length ) );
9761157
9771158 $j.each( _this.uploads, function(i, upload) {
978 - var thanksDiv = $j( '<div class="mwe-upwiz-thanks ui-helper-clearfix" />' );
979 - _this.thanksDiv = thanksDiv;
 1159+ var id = 'thanksDiv' + i;
 1160+ var $thanksDiv = $j( '<div></div>' ).attr( 'id', id ).addClass( "mwe-upwiz-thanks ui-helper-clearfix" );
 1161+ _this.thanksDiv = $thanksDiv;
9801162
981 - var thumbnailDiv = $j( '<div class="mwe-upwiz-thumbnail mwe-upwiz-thumbnail-side" id="thanks-thumbnail"></div>' );
982 - upload.setThumbnail( thumbnailDiv );
 1163+ var $thumbnailDiv = $j( '<div class="mwe-upwiz-thumbnail mwe-upwiz-thumbnail-side"></div>' );
 1164+ $thanksDiv.append( $thumbnailDiv );
 1165+ upload.setThumbnail( $thumbnailDiv );
 1166+ //upload.setThumbnail( '#' + id + ' .mwe-upwiz-thumbnail' );
9831167
984 - thanksDiv.append( thumbnailDiv );
 1168+ // Switch the thumbnail link so that it points to the image description page
 1169+ $thumbnailDiv.find( 'a' ).attr( 'href', upload.imageinfo.descriptionurl );
9851170
9861171 var thumbTitle = String(upload.title);
9871172 var thumbWikiText = "[[" + thumbTitle.replace('_', ' ') + "|thumb|" + gM( 'mwe-upwiz-thanks-caption' ) + "]]";
9881173
989 - thanksDiv.append(
 1174+ $thanksDiv.append(
9901175 $j( '<div class="mwe-upwiz-data"></div>' )
9911176 .append(
9921177 $j('<p/>').append(
@@ -1009,13 +1194,87 @@
10101195 )
10111196 );
10121197
1013 - $j( '#mwe-upwiz-thanks' ).append( thanksDiv );
1014 - // Switch the thumbnail link so that it points to the image description page
1015 - $j( '#thanks-thumbnail a' ).attr( 'href', upload.imageinfo.descriptionurl );
 1198+ $j( '#mwe-upwiz-thanks' ).append( $thanksDiv );
10161199 } );
10171200 },
10181201
10191202 /**
 1203+ * Build interface for collecting user feedback on Upload Wizard
 1204+ */
 1205+ launchFeedback: function() {
 1206+ _this = this;
 1207+
 1208+ var displayError = function( message ) {
 1209+ $j( '#mwe-upwiz-feedback-form div' ).hide(); // remove everything else from the dialog box
 1210+ $j( '#mwe-upwiz-feedback-form' ).append ( $j( '<div style="color:#990000;margin-top:0.4em;"></div>' ).msg( message ) );
 1211+ }
 1212+
 1213+ // Set up buttons for dialog box. We have to do it the hard way since the json keys are localized
 1214+ var cancelButton = gM( 'mwe-upwiz-feedback-cancel' );
 1215+ var submitButton = gM( 'mwe-upwiz-feedback-submit' );
 1216+ var buttonSettings = {};
 1217+ buttonSettings[cancelButton] = function() { $j( this ).dialog( 'close' ); };
 1218+ buttonSettings[submitButton] = function() {
 1219+ $feedbackForm.dialog({buttons:{}});
 1220+ $j( '#mwe-upwiz-feedback-form div' ).hide(); // remove everything else from the dialog box
 1221+ $j( '#mwe-upwiz-feedback-form' ).append ( $j( '<div style="text-align:center;margin:3em 0;"></div>' ).append( gM( 'mwe-upwiz-feedback-adding' ), $j( '<br/>' ), $j( '<img src="http://upload.wikimedia.org/wikipedia/commons/4/42/Loading.gif" />' ) ) );
 1222+ var subject = $j( '#mwe-upwiz-feedback-subject' ).val();
 1223+ var message = $j( '#mwe-upwiz-feedback-message' ).val();
 1224+ if ( message.indexOf( '~~~' ) == -1 ) {
 1225+ message = message+' ~~~~';
 1226+ }
 1227+ var useTokenToPostFeedback = function( token ) {
 1228+ $j.ajax({
 1229+ url: wgScriptPath + '/api.php',
 1230+ data: $.param({
 1231+ action: 'edit',
 1232+ title: mw.UploadWizard.config['feedbackPage'],
 1233+ section: 'new',
 1234+ summary: subject,
 1235+ text: message,
 1236+ format: 'json',
 1237+ token: token
 1238+ }),
 1239+ dataType: 'json',
 1240+ type: 'POST',
 1241+ success: function( data ) {
 1242+ if ( typeof data.edit != 'undefined' ) {
 1243+ if ( data.edit.result == 'Success' ) {
 1244+ $feedbackForm.dialog( 'close' ); // edit complete, close dialog box
 1245+ } else {
 1246+ displayError( 'mwe-upwiz-feedback-error1' ); // unknown API result
 1247+ }
 1248+ } else {
 1249+ displayError( 'mwe-upwiz-feedback-error2' ); // edit failed
 1250+ }
 1251+ },
 1252+ error: function( xhr ) {
 1253+ displayError( 'mwe-upwiz-feedback-error3' ); // ajax request failed
 1254+ }
 1255+ }); // close Ajax request
 1256+ }; // close useTokenToPost function
 1257+ _this.api.getEditToken( useTokenToPostFeedback );
 1258+ }; // close submit button function
 1259+
 1260+ // Construct the feedback form
 1261+ var feedbackLink = '<a href="'+wgArticlePath.replace( '$1', mw.UploadWizard.config['feedbackPage'].replace( /\s/g, '_' ) )+'" target="_blank">'+mw.UploadWizard.config['feedbackPage']+'</a>';
 1262+ $feedbackForm = $j( '<div id="mwe-upwiz-feedback-form" style="position:relative;"></div>' )
 1263+ .append( $j( '<div style="margin-top:0.4em;"></div>' ).append( $j( '<small></small>' ).msg( 'mwe-upwiz-feedback-note', feedbackLink ) ) )
 1264+ .append( $j( '<div style="margin-top:1em;"></div>' ).append( gM( 'mwe-upwiz-feedback-subject' ), $j( '<br/>' ), $j( '<input type="text" id="mwe-upwiz-feedback-subject" name="subject" maxlength="60" style="width:99%;"/>' ) ) )
 1265+ .append( $j( '<div style="margin-top:0.4em;"></div>' ).append( gM( 'mwe-upwiz-feedback-message' ), $j( '<br/>' ), $j( '<textarea name="message" id="mwe-upwiz-feedback-message" style="width:99%;" rows="4" cols="60"></textarea>' ) ) )
 1266+ .dialog({
 1267+ width: 500,
 1268+ autoOpen: false,
 1269+ title: gM( 'mwe-upwiz-feedback-title' ),
 1270+ modal: true,
 1271+ buttons: buttonSettings
 1272+ }); // close dialog, end $feedbackForm definition
 1273+
 1274+ $feedbackForm.dialog( 'open' );
 1275+
 1276+ }, // close launchFeedback function
 1277+
 1278+ /**
10201279 * Set a cookie which lets the user skip the tutorial step in the future
10211280 */
10221281 setSkipTutorialCookie: function() {
@@ -1038,6 +1297,7 @@
10391298 stop: function() {
10401299
10411300 }
 1301+
10421302 };
10431303
10441304
@@ -1118,13 +1378,30 @@
11191379 };
11201380
11211381 /**
1122 - * Adds a tipsy pop-up help button to the field.
1123 - * @param name The short name of the field, for example, 'title'. This should correspond with a
1124 - * message key of the form 'mwe-upwiz-tooltip-<name>'.
 1382+ * Adds a tipsy pop-up help button to the field. Can be called in two ways -- with simple string id, which identifies
 1383+ * the string as 'mwe-upwiz-tooltip-' plus that id, and creates the hint with a similar id
 1384+ * or with function and id -- function will be called to generate the hint every time
 1385+ * TODO v1.1 split into two plugins?
 1386+ * @param key {string} -- will base the tooltip on a message found with this key
 1387+ * @param fn {function} optional -- call this function every time tip is created to generate message. If present HTML element gets an id of the exact key specified
11251388 */
1126 - $j.fn.addHint = function( name ) {
1127 - return this.append( $j( '<span class="mwe-upwiz-hint" id="mwe-upwiz-'+name+'-hint" onclick=\"$(this).tipsy(\'toggle\');return false;\">' )
1128 - .attr( 'title', gM( 'mwe-upwiz-tooltip-'+name ) ).tipsy( {html: true, opacity: 0.9, gravity: 'sw', trigger: 'manual'} ) );
 1389+ $j.fn.addHint = function( key, fn ) {
 1390+ var attrs, contentSource, html = false;
 1391+ if ( typeof fn === 'function' ) {
 1392+ attrs = { id: key };
 1393+ contentSource = fn;
 1394+ html = true;
 1395+ } else {
 1396+ attrs = { 'title': gM( 'mwe-upwiz-tooltip-' + key ) };
 1397+ contentSource = 'title';
 1398+ }
 1399+ return this.append(
 1400+ $j( '<span/>' )
 1401+ .addClass( 'mwe-upwiz-hint' )
 1402+ .attr( attrs )
 1403+ .click( function() { $j( this ).tipsy( 'toggle' ); return false; } )
 1404+ .tipsy( { title: contentSource, html: html, opacity: 1.0, gravity: 'sw', trigger: 'manual'} )
 1405+ );
11291406 };
11301407
11311408 /**
@@ -1183,7 +1460,7 @@
11841461 $j( el ).find( "select" ).addClass( "masked-hidden" );
11851462 }
11861463
1187 - var mask = $j( '<div class="mwe-upwiz-mask"/>' )
 1464+ var mask = $j( '<div class="mwe-upwiz-mask"></div>' )
11881465 .css( {
11891466 'backgroundColor' : 'white',
11901467 'width' : el.offsetWidth + 'px',
@@ -1191,7 +1468,7 @@
11921469 'z-index' : 90
11931470 } );
11941471
1195 - var status = $j( '<div class="mwe-upwiz-status"/>' )
 1472+ var status = $j( '<div class="mwe-upwiz-status"></div>' )
11961473 .css( {
11971474 'width' : el.offsetWidth + 'px',
11981475 'height' : el.offsetHeight + 'px',
Index: branches/uploadwizard-firefogg/resources/mw.UploadWizardDetails.js
@@ -45,7 +45,7 @@
4646 // http://commons.wikimedia.org/wiki/MediaWiki:Filename-prefix-blacklist
4747 // XXX make sure they can't use ctrl characters or returns or any other bad stuff.
4848 _this.titleId = "title" + _this.upload.index;
49 - _this.titleInput = $j( '<textarea type="text" id="' + _this.titleId + '" name="' + _this.titleId + '" rows="1" class="mwe-title mwe-long-textarea"></textarea>' )
 49+ _this.titleInput = $j( '<input type="text" id="' + _this.titleId + '" name="' + _this.titleId + '" class="mwe-title"/>' )
5050 .keyup( function() {
5151 _this.upload.title.setNameText( _this.titleInput.value );
5252 // TODO update a display of filename
@@ -54,22 +54,49 @@
5555 api: _this.upload.api,
5656 spinner: function(bool) { _this.toggleDestinationBusy(bool); },
5757 preprocess: function( name ) {
58 - // turn the contents of the input into a MediaWiki title ("File:foo_bar.jpg") to look up
59 - return _this.upload.title.setNameText( name ).toString();
 58+ if ( name != '' ) {
 59+ // turn the contents of the input into a MediaWiki title ("File:foo_bar.jpg") to look up
 60+ return _this.upload.title.setNameText( name ).toString();
 61+ } else {
 62+ return name;
 63+ }
6064 },
6165 processResult: function( result ) { _this.processDestinationCheck( result ); }
6266 } );
6367
64 - _this.titleErrorDiv = $j('<div class="mwe-upwiz-details-input-error"><label class="mwe-error" for="' + _this.titleId + '" generated="true"/></div>');
 68+ _this.titleErrorDiv = $j('<div class="mwe-upwiz-details-input-error"><label class="mwe-validator-error" for="' + _this.titleId + '" generated="true"/></div>');
6569
 70+ var titleHintId = 'mwe-upwiz-title-hint-' + _this.upload.index;
 71+ var $titleDialog = $('<div>')
 72+ .html( gM( 'mwe-upwiz-dialog-title' ) )
 73+ .dialog({
 74+ width: 500,
 75+ zIndex: 200000,
 76+ autoOpen: false,
 77+ title: gM( 'mwe-upwiz-help-popup' ) + ': ' + gM( 'mwe-upwiz-help-popup-title' ),
 78+ modal: true
 79+ })
 80+ .bind( "dialogclose", function( event, ui ) {
 81+ $j( '#' + titleHintId ).tipsy( "hide" );
 82+ });
 83+
 84+ // tipsy hides tips by removing them from the DOM. This causes all bindings to be lost.
 85+ // so we send a function to recreate everything, every time!
 86+ // (is it really necessary for tipsy to remove elements?)
 87+ var titleHinter = function() {
 88+ return $j( '<span>' ).msg( 'mwe-upwiz-tooltip-title', function() {
 89+ $titleDialog.dialog( 'open' );
 90+ // TODO scroll to the dialog, or otherwise ensure it's in the middle of the page no matter what
 91+ } );
 92+ };
 93+
6694 var titleContainerDiv = $j('<div class="mwe-upwiz-details-fieldname-input ui-helper-clearfix"></div>')
6795 .append(
6896 _this.titleErrorDiv,
6997 $j( '<div class="mwe-upwiz-details-fieldname"></div>' )
 98+ .msg( 'mwe-upwiz-title' )
7099 .requiredFieldLabel()
71 - .append( gM( 'mwe-upwiz-title' ) )
72 - .addHint( 'title' ),
73 -
 100+ .addHint( titleHintId, titleHinter ),
74101 $j( '<div class="mwe-upwiz-details-input"></div>' ).append( _this.titleInput )
75102 );
76103
@@ -86,7 +113,13 @@
87114 + '<div class="mwe-upwiz-details-fieldname"></div>'
88115 + '<div class="mwe-upwiz-details-input"></div>'
89116 + '</div>' );
90 - $categoriesDiv.find( '.mwe-upwiz-details-fieldname' ).append( gM( 'mwe-upwiz-categories' ) ).addHint( 'categories' );
 117+ var commonsCategoriesLink = $j( '<a>' ).attr( { 'target': '_blank', 'href': 'http://commons.wikimedia.org/wiki/Commons:Categories' } );
 118+ var categoriesHint = $j( '<span>' ).msg( 'mwe-upwiz-tooltip-categories', commonsCategoriesLink );
 119+ var categoriesHinter = function() { return categoriesHint; };
 120+ $categoriesDiv
 121+ .find( '.mwe-upwiz-details-fieldname' )
 122+ .append( gM( 'mwe-upwiz-categories' ) )
 123+ .addHint( 'mwe-upwiz-categories-hint', categoriesHinter );
91124 var categoriesId = 'categories' + _this.upload.index;
92125 $categoriesDiv.find( '.mwe-upwiz-details-input' )
93126 .append( $j( '<input/>' ).attr( { id: categoriesId,
@@ -132,19 +165,7 @@
133166 otherInformationDiv
134167 );
135168
136 - $titleDialog = $('<div>')
137 - .html( gM( 'mwe-upwiz-dialog-title' ) )
138 - .dialog({
139 - width: 500,
140 - zIndex: 200000,
141 - autoOpen: false,
142 - title: 'Help: Title',
143 - modal: true
144 - })
145 - .bind( "dialogclose", function(event, ui) {
146 - $("#mwe-upwiz-title-hint").tipsy("hide");
147 - });
148 -
 169+
149170 /* Build the form for the file upload */
150171 _this.$form = $j( '<form></form>' );
151172 _this.$form.append(
@@ -204,6 +225,15 @@
205226 _this.addDescription( true, mw.config.get( 'wgUserLanguage' ) );
206227 $j( containerDiv ).append( _this.div );
207228
 229+ // make the title field required
 230+ _this.$form.find( '.mwe-title' )
 231+ .rules( "add", {
 232+ required: true,
 233+ messages: {
 234+ required: gM( 'mwe-upwiz-error-blank' )
 235+ }
 236+ } );
 237+
208238 // make this a category picker
209239 var hiddenCats = [];
210240 if ( mw.isDefined( mw.UploadWizard.config.autoCategory ) ) {
@@ -224,36 +254,33 @@
225255 * check entire form for validity
226256 */
227257 // return boolean if we are ready to go.
228 - // side effect: add error text to the page for fields in an incorrect state.
 258+ // side effect: add error text to the page for fields in an incorrect state.
229259 // we must call EVERY valid() function due to side effects; do not short-circuit.
230 - valid: function() {
 260+ valid: function() {
231261 var _this = this;
232 - // at least one description -- never mind, we are disallowing removal of first description
233 - // all the descriptions -- check min & max length
 262+ // at least one description -- never mind, we are disallowing removal of first description
 263+ // all the descriptions -- check min & max length
 264+ // categories are assumed valid
 265+ // pop open the 'more-options' if the date is bad
 266+ // location?
234267
235 - // the title
 268+ // make sure title is valid
236269 var titleInputValid = $j( _this.titleInput ).data( 'valid' );
237270 if ( typeof titleInputValid == 'undefined' ) {
238271 alert( "please wait, still checking the title for uniqueness..." );
239272 return false;
240273 }
241 -
 274+
 275+ // make sure licenses are valid (needed for multi-file deed selection)
 276+ var deedValid = _this.upload.deedChooser.valid();
 277+
242278 // all other fields validated with validator js
243279 var formValid = _this.$form.valid();
244 - return titleInputValid && formValid;
245280
246 - // categories are assumed valid
247 -
248 - // the license, if any
 281+ return titleInputValid && deedValid && formValid;
 282+ },
249283
250 - // pop open the 'more-options' if the date is bad
251 - // the date
252284
253 - // location?
254 - },
255 -
256 -
257 -
258285 /**
259286 * toggles whether we use the 'macro' deed or our own
260287 */
@@ -291,7 +318,6 @@
292319 */
293320 processDestinationCheck: function( result ) {
294321 var _this = this;
295 -
296322 if ( result.isUnique ) {
297323 $j( _this.titleInput ).data( 'valid', true );
298324 _this.$form.find( 'label[for=' + _this.titleId + ']' ).hide().empty();
@@ -302,7 +328,14 @@
303329 $j( _this.titleInput ).data( 'valid', false );
304330
305331 // result is NOT unique
306 - var title = new mw.Title( result.title ).setNamespace( 'file' ).getNameText();
 332+ var title;
 333+ try {
 334+ title = new mw.Title( result.title ).setNamespace( 'file' ).getNameText();
 335+ } catch ( e ) {
 336+ // unparseable result from unique test?
 337+ title = '[unparseable name]';
 338+ }
 339+
307340 /* var img = result.img;
308341 var href = result.href; */
309342
@@ -463,7 +496,6 @@
464497 */
465498 populate: function() {
466499 var _this = this;
467 - mw.log( "mw.UploadWizardUpload::populate> populating details from upload" );
468500 _this.upload.setThumbnail( _this.thumbnailDiv, mw.UploadWizard.config['thumbnailWidth'], mw.UploadWizard.config['thumbnailMaxHeight'] );
469501 _this.prefillDate();
470502 _this.prefillSource();
@@ -487,11 +519,12 @@
488520 var _this = this;
489521 var yyyyMmDdRegex = /^(\d\d\d\d)[:\/-](\d\d)[:\/-](\d\d)\D.*/;
490522 var dateObj;
491 - var metadata = _this.upload.imageinfo.metadata;
492 - $j.each([metadata.datetimeoriginal, metadata.datetimedigitized, metadata.datetime, metadata['date']],
493 - function( i, imageinfoDate ) {
494 - if ( ! mw.isEmpty( imageinfoDate ) ) {
495 - var matches = $j.trim( imageinfoDate ).match( yyyyMmDdRegex );
 523+ if ( _this.upload.imageinfo.metadata ) {
 524+ var metadata = _this.upload.imageinfo.metadata;
 525+ $j.each( [ 'datetimeoriginal', 'datetimedigitized', 'datetime', 'date' ], function( i, propName ) {
 526+ var dateInfo = metadata[propName];
 527+ if ( ! mw.isEmpty( dateInfo ) ) {
 528+ var matches = $j.trim( dateInfo ).match( yyyyMmDdRegex );
496529 if ( ! mw.isEmpty( matches ) ) {
497530 dateObj = new Date( parseInt( matches[1], 10 ),
498531 parseInt( matches[2], 10 ) - 1,
@@ -499,13 +532,13 @@
500533 return false; // break from $j.each
501534 }
502535 }
503 - }
504 - );
 536+ } );
 537+ }
505538
506539 // if we don't have EXIF or other metadata, let's use "now"
507540 // XXX if we have FileAPI, it might be clever to look at file attrs, saved
508541 // in the upload object for use here later, perhaps
509 - if (typeof dateObj === 'undefined') {
 542+ if ( !mw.isDefined( dateObj ) ) {
510543 dateObj = new Date();
511544 }
512545 dateStr = dateObj.getUTCFullYear() + '-' + pad( dateObj.getUTCMonth() ) + '-' + pad( dateObj.getUTCDate() );
@@ -551,13 +584,7 @@
552585 * "122/1" -- 122 m (altitude)
553586 */
554587 prefillLocation: function() {
555 - var _this = this;
556 - var metadata = _this.upload.imageinfo.metadata;
557 - if (metadata === undefined) {
558 - return;
559 - }
560 -
561 -
 588+ /* unimplemented -- awaiting bawolff's GSoC 2010 project to be committedd... */ return;
562589 },
563590
564591 /**
@@ -594,7 +621,7 @@
595622 */
596623 prefillAuthor: function() {
597624 var _this = this;
598 - if (_this.upload.imageinfo.metadata.author !== undefined) {
 625+ if ( _this.upload.imageinfo.metadata && _this.upload.imageinfo.metadata.author ) {
599626 $j( _this.authorInput ).val( _this.upload.imageinfo.metadata.author );
600627 }
601628
@@ -606,20 +633,22 @@
607634 */
608635 prefillLicense: function() {
609636 var _this = this;
610 - var copyright = _this.upload.imageinfo.metadata.copyright;
611 - if (copyright !== undefined) {
612 - if (copyright.match(/\bcc-by-sa\b/i)) {
613 - alert("unimplemented cc-by-sa in prefillLicense");
614 - // XXX set license to be that CC-BY-SA
615 - } else if (copyright.match(/\bcc-by\b/i)) {
616 - alert("unimplemented cc-by in prefillLicense");
617 - // XXX set license to be that
618 - } else if (copyright.match(/\bcc-zero\b/i)) {
619 - alert("unimplemented cc-zero in prefillLicense");
620 - // XXX set license to be that
621 - // XXX any other licenses we could guess from copyright statement
622 - } else {
623 - $j( _this.licenseInput ).val( copyright );
 637+ if ( _this.upload.imageinfo.metadata ) {
 638+ var copyright = _this.upload.imageinfo.metadata.copyright;
 639+ if (copyright !== undefined) {
 640+ if (copyright.match(/\bcc-by-sa\b/i)) {
 641+ alert("unimplemented cc-by-sa in prefillLicense");
 642+ // XXX set license to be that CC-BY-SA
 643+ } else if (copyright.match(/\bcc-by\b/i)) {
 644+ alert("unimplemented cc-by in prefillLicense");
 645+ // XXX set license to be that
 646+ } else if (copyright.match(/\bcc-zero\b/i)) {
 647+ alert("unimplemented cc-zero in prefillLicense");
 648+ // XXX set license to be that
 649+ // XXX any other licenses we could guess from copyright statement
 650+ } else {
 651+ $j( _this.licenseInput ).val( copyright );
 652+ }
624653 }
625654 }
626655 // if we still haven't set a copyright use the user's preferences?
@@ -628,7 +657,6 @@
629658
630659 /**
631660 * Convert entire details for this file into wikiText, which will then be posted to the file
632 - * XXX there is a WikiText sanitizer in use on UploadForm -- use that here, or port it
633661 * @return wikitext representing all details
634662 */
635663 getWikiText: function() {
@@ -699,6 +727,10 @@
700728 // group categories together, maybe?
701729 wikiText += deed.getLicenseWikiText() + _this.div.find( '.categoryInput' ).get(0).getWikiText() + "\n\n";
702730
 731+ // sanitize wikitext if TextCleaner is defined (MediaWiki:TextCleaner.js)
 732+ if ( typeof TextCleaner != 'undefined' && typeof TextCleaner.sanitizeWikiText == 'function' ) {
 733+ wikiText = TextCleaner.sanitizeWikiText( wikiText, true );
 734+ }
703735
704736 return wikiText;
705737 },
@@ -713,7 +745,6 @@
714746
715747 // XXX check state of details for okayness ( license selected, at least one desc, sane filename )
716748 var wikiText = _this.getWikiText();
717 - mw.log( "mw.UploadWizardUpload::submit> submiting wikiText:\n" + wikiText );
718749
719750 var params = {
720751 action: 'upload',
@@ -728,10 +759,7 @@
729760 _this.completeDetailsSubmission();
730761 };
731762
732 - mw.log( "mw.UploadWizardUpload::submit> uploading: \n" + params );
733763 var callback = function( result ) {
734 - mw.log( "mw.UploadWizardUpload::submit> result:\n" + result );
735 - mw.log( "mw.UploadWizardUpload::submit> successful upload" );
736764 finalCallback( result );
737765 };
738766
@@ -740,41 +768,6 @@
741769 _this.upload.api.postWithEditToken( params, callback );
742770 },
743771
744 -
745 - /**
746 - * Get new image info, for instance, after we renamed... or? published? an image
747 - * XXX deprecated?
748 - * XXX move to mw.API
749 - *
750 - * @param upload an UploadWizardUpload object
751 - * @param title title to look up remotely
752 - * @param endCallback execute upon completion
753 - */
754 - getImageInfo: function( upload, callback ) {
755 - var params = {
756 - 'titles': upload.title.toString(),
757 - 'prop': 'imageinfo',
758 - 'iiprop': 'timestamp|url|user|size|sha1|mime|metadata'
759 - };
760 - // XXX timeout callback?
761 - this.api.get( params, function( data ) {
762 - if ( data && data.query && data.query.pages ) {
763 - if ( ! data.query.pages[-1] ) {
764 - for ( var page_id in data.query.pages ) {
765 - var page = data.query.pages[ page_id ];
766 - if ( ! page.imageinfo ) {
767 - alert("unimplemented error check, missing imageinfo");
768 - // XXX not found? error
769 - } else {
770 - upload.extractImageInfo( page.imageinfo[0] );
771 - }
772 - }
773 - }
774 - }
775 - callback();
776 - } );
777 - },
778 -
779772 completeDetailsSubmission: function() {
780773 var _this = this;
781774 _this.upload.state = 'complete';
Index: branches/uploadwizard-firefogg/resources/mw.ApiUploadHandler.js
@@ -51,14 +51,7 @@
5252
5353 // we use JSON in HTML because according to mdale, some browsers cannot handle just JSON
5454 _this.addFormInputIfMissing( 'format', 'jsonfm' );
55 -
56 - // XXX only for testing, so it stops complaining about dupes
57 - /*
58 - if ( mw.UploadWizard.DEBUG ) {
59 - _this.addFormInputIfMissing( 'ignorewarnings', '1' );
60 - }
61 - */
62 - },
 55+ },
6356 /**
6457 * Modify our form to have a fresh edit token.
6558 * If successful, return true to a callback.
Index: branches/uploadwizard-firefogg/resources/mw.Log.js
@@ -1,44 +1,81 @@
2 -// dependencies: [ mw ]
 2+/**
 3+ * Logging library
 4+ *
 5+ * Synopsis:
 6+ *
 7+ * mw.log( "falls in the forest" ); // not logged, we start off at 'silent' log level
 8+ *
 9+ * // set the minimum level for a log message to be shown. Can be 'silent', 'fatal', 'warn', 'info', 'debug'
 10+ * mw.log.level = 'debug'; // show just about everything
 11+ * mw.log( 'some random crap' ); // will be shown
 12+ *
 13+ * mw.log.level = 'warn'; // show less stuff
 14+ *
 15+ * mw.log( 'foo bar' ); // won't be shown, the default level for a log is 'info'
 16+ *
 17+ * mw.log( 'this is a warning', 'warn' ); // will be shown
 18+ *
 19+ * var warnCute = mw.log.getLogger( 'cuteness', 'warn' );
 20+ * warnCute( 'overload!' ); // will log 'cuteness> overload!'
 21+ *
 22+ * var logDebug = mw.log.getLogger( undefined, 'debug' );
 23+ * mw.log.level = 'debug';
 24+ * logDebug( "random spammy log for developers..." ); // will log "random spammy log for developers...";
 25+ */
326
427 ( function( mw, $j ) {
528
 29+ /* how all the logging levels sort */
 30+ var priority = {
 31+ silent: 0,
 32+ fatal: 10,
 33+ warn: 20,
 34+ info: 30,
 35+ debug: 100
 36+ };
 37+
638 /**
7 - * Log a string msg to the console
8 - *
9 - * @param {String} string String to output to console
10 - */
 39+ * Log output to the console.
 40+ *
 41+ * In the case that the browser does not have a console available, one is created by appending a
 42+ * <div> element to the bottom of the body and then appending a <div> element to that for each
 43+ * message.
 44+ *
 45+ * @author Michael Dale <mdale@wikimedia.org>
 46+ * @author Trevor Parscal <tparscal@wikimedia.org>
 47+ * @author Neil Kandalgaonkar <neilk@wikimedia.org>
 48+ * @param {string} string Message to output to console
 49+ * @param {string} optional, logging priority (see priority)
 50+ */
1151 mw.log = function( s, level ) {
12 -
13 - if ( typeof level === 'undefined' ) {
14 - level = 30;
 52+
 53+ if ( typeof level === 'undefined' || ! priority.hasOwnProperty( level ) ) {
 54+ level = 'info';
1555 }
1656
17 - if ( level > mw.log.level ) {
 57+ // don't show log message if lower priority than mw.log.level
 58+ if ( priority[ mw.log.level ] < priority[ level ] ) {
1859 return;
19 - }
20 -
21 - // Add any prepend debug ss if necessary
22 - if ( mw.log.preAppendLog ) {
23 - s = mw.log.preAppendLog + s;
2460 }
2561
2662 if ( typeof window.console !== 'undefined' && typeof window.console.log === 'function' ) {
2763 window.console.log( s );
2864 } else {
29 -/*
 65+ // Set timestamp
 66+ var d = new Date();
 67+ var time = ( pad( d.getHours(), 2 ) + ':' + pad( d.getMinutes(), 2 ) + pad( d.getSeconds(), 2 ) + pad( d.getMilliseconds(), 3 ) );
3068 // Show a log box for console-less browsers
3169 var $log = $( '#mw-log-console' );
3270 if ( !$log.length ) {
3371 $log = $( '<div id="mw-log-console"></div>' )
3472 .css( {
35 - 'position': 'absolute',
 73+ 'position': 'fixed',
3674 'overflow': 'auto',
3775 'z-index': 500,
3876 'bottom': '0px',
3977 'left': '0px',
4078 'right': '0px',
41 - 'height': '100px',
42 - 'width': '100%',
 79+ 'height': '150px',
4380 'background-color': 'white',
4481 'border-top': 'solid 2px #ADADAD'
4582 } )
@@ -53,27 +90,42 @@
5491 'padding': '0.125em 0.25em'
5592 } )
5693 .text( s )
 94+ .append( '<span style="float:right">[' + time + ']</span>' )
5795 );
58 -*/
5996 }
6097 };
6198
62 - mw.log.level = mw.log.NONE = 0;
63 - mw.log.FATAL = 10;
64 - mw.log.WARN = 20;
65 - mw.log.INFO = 30;
66 - mw.log.ALL = 100;
67 -
68 - mw.log.fatal = function( s ) {
69 - mw.log( s, mw.log.FATAL );
 99+ // Logging is silent by default -- if you're debugging an app, figure out a way to turn it on
 100+ mw.log.level = 0;
 101+
 102+ /**
 103+ * Convenience function for logging cases where you want to repeatedly log with a prefix for each message,
 104+ * and/or at a particular logging level
 105+ *
 106+ * @param {string} prefix
 107+ * @param {string} level name
 108+ * @return {function} logging function which prepends that prefix, and logs at that level
 109+ */
 110+ mw.log.getLogger = function( prefixArg, level ) {
 111+ var prefix = typeof prefixArg === 'undefined' ? '' : prefixArg + '> ';
 112+ return function( s ) {
 113+ mw.log( prefix + s, level );
 114+ };
70115 };
71 - mw.log.warn = function( s ) {
72 - mw.log( s, mw.log.WARN );
73 - };
74 - mw.log.info = function( s ) {
75 - mw.log( s, mw.log.INFO );
76 - };
77 - mw.log.level = mw.log.ALL;
78116
 117+ /**
 118+ * Helper function for logging date/time -- given number, return string zero-padded to requested length
 119+ * @param d {number} the number
 120+ * @param n {number} length
 121+ * @return {string}
 122+ */
 123+ function pad( d, n ) {
 124+ var s = d.toString();
 125+ while ( s.length < n ) {
 126+ s = '0' + s;
 127+ }
 128+ return s;
 129+ }
 130+
79131 } )( window.mediaWiki, jQuery );
80132

Past revisions this follows-up on

RevisionCommit summaryAuthorDate
r85606beginning of fix for Bug 28316kaldari02:00, 7 April 2011

Status & tagging log