[ Index ] |
PHP Cross Reference of MediaWiki-1.24.0 |
[Summary view] [Print] [Text view]
1 /** 2 * This plugin provides a way to build a wiki-text editing user interface around a textarea. 3 * 4 * @example To intialize without any modules: 5 * $( 'div#edittoolbar' ).wikiEditor(); 6 * 7 * @example To initialize with one or more modules, or to add modules after it's already been initialized: 8 * $( 'textarea#wpTextbox1' ).wikiEditor( 'addModule', 'toolbar', { ... config ... } ); 9 * 10 */ 11 /*jshint onevar:false, boss:true */ 12 ( function ( $, mw ) { 13 14 /** 15 * Global static object for wikiEditor that provides generally useful functionality to all modules and contexts. 16 */ 17 $.wikiEditor = { 18 /** 19 * For each module that is loaded, static code shared by all instances is loaded into this object organized by 20 * module name. The existance of a module in this object only indicates the module is available. To check if a 21 * module is in use by a specific context check the context.modules object. 22 */ 23 modules: {}, 24 25 /** 26 * A context can be extended, such as adding iframe support, on a per-wikiEditor instance basis. 27 */ 28 extensions: {}, 29 30 /** 31 * In some cases like with the iframe's HTML file, it's convienent to have a lookup table of all instances of the 32 * WikiEditor. Each context contains an instance field which contains a key that corrosponds to a reference to the 33 * textarea which the WikiEditor was build around. This way, by passing a simple integer you can provide a way back 34 * to a specific context. 35 */ 36 instances: [], 37 38 /** 39 * For each browser name, an array of conditions that must be met are supplied in [operaton, value]-form where 40 * operation is a string containing a JavaScript compatible binary operator and value is either a number to be 41 * compared with $.browser.versionNumber or a string to be compared with $.browser.version. If a browser is not 42 * specifically mentioned, we just assume things will work. 43 */ 44 browsers: { 45 // Left-to-right languages 46 ltr: { 47 // The toolbar layout is broken in IE6 48 msie: [['>=', 7]], 49 // Layout issues in FF < 2 50 firefox: [['>=', 2]], 51 // Text selection bugs galore 52 opera: [['>=', 9.6]], 53 // jQuery minimums 54 safari: [['>=', 3]], 55 chrome: [['>=', 3]], 56 netscape: [['>=', 9]], 57 blackberry: false, 58 ipod: [['>=', 6]], 59 iphone: [['>=', 6]] 60 }, 61 // Right-to-left languages 62 rtl: { 63 // The toolbar layout is broken in IE 7 in RTL mode, and IE6 in any mode 64 msie: [['>=', 8]], 65 // Layout issues in FF < 2 66 firefox: [['>=', 2]], 67 // Text selection bugs galore 68 opera: [['>=', 9.6]], 69 // jQuery minimums 70 safari: [['>=', 3]], 71 chrome: [['>=', 3]], 72 netscape: [['>=', 9]], 73 blackberry: false, 74 ipod: [['>=', 6]], 75 iphone: [['>=', 6]] 76 } 77 }, 78 79 /** 80 * Path to images - this is a bit messy, and it would need to change if this code (and images) gets moved into the 81 * core - or anywhere for that matter... 82 */ 83 imgPath : mw.config.get( 'wgExtensionAssetsPath' ) + '/WikiEditor/modules/images/', 84 85 /** 86 * Checks the current browser against the browsers object to determine if the browser has been black-listed or not. 87 * Because these rules are often very complex, the object contains configurable operators and can check against 88 * either the browser version number or string. This process also involves checking if the current browser is amung 89 * those which we have configured as compatible or not. If the browser was not configured as comptible we just go on 90 * assuming things will work - the argument here is to prevent the need to update the code when a new browser comes 91 * to market. The assumption here is that any new browser will be built on an existing engine or be otherwise so 92 * similar to another existing browser that things actually do work as expected. The merrits of this argument, which 93 * is essentially to blacklist rather than whitelist are debateable, but at this point we've decided it's the more 94 * "open-web" way to go. 95 * @param module Module object, defaults to $.wikiEditor 96 */ 97 isSupported: function ( module ) { 98 // Fallback to the wikiEditor browser map if no special map is provided in the module 99 var mod = module && 'browsers' in module ? module : $.wikiEditor; 100 // Check for and make use of cached value and early opportunities to bail 101 if ( typeof mod.supported !== 'undefined' ) { 102 // Cache hit 103 return mod.supported; 104 } 105 // Run a browser support test and then cache and return the result 106 return mod.supported = $.client.test( mod.browsers ); 107 }, 108 109 /** 110 * Checks if a module has a specific requirement 111 * @param module Module object 112 * @param requirement String identifying requirement 113 */ 114 isRequired: function ( module, requirement ) { 115 if ( typeof module.req !== 'undefined' ) { 116 for ( var req in module.req ) { 117 if ( module.req[req] === requirement ) { 118 return true; 119 } 120 } 121 } 122 return false; 123 }, 124 125 /** 126 * Provides a way to extract messages from objects. Wraps the mediaWiki.msg() function, which 127 * may eventually become a wrapper for some kind of core MW functionality. 128 * 129 * @param object Object to extract messages from 130 * @param property String of name of property which contains the message. This should be the base name of the 131 * property, which means that in the case of the object { this: 'that', fooMsg: 'bar' }, passing property as 'this' 132 * would return the raw text 'that', while passing property as 'foo' would return the internationalized message 133 * with the key 'bar'. 134 */ 135 autoMsg: function ( object, property ) { 136 var i, p; 137 // Accept array of possible properties, of which the first one found will be used 138 if ( typeof property === 'object' ) { 139 for ( i in property ) { 140 if ( property[i] in object || property[i] + 'Msg' in object ) { 141 property = property[i]; 142 break; 143 } 144 } 145 } 146 if ( property in object ) { 147 return object[property]; 148 } else if ( property + 'Msg' in object ) { 149 p = object[property + 'Msg']; 150 if ( $.isArray( p ) && p.length >= 2 ) { 151 return mw.message.apply( mw.message, p ).plain(); 152 } else { 153 return mw.message( p ).plain(); 154 } 155 } else { 156 return ''; 157 } 158 }, 159 160 /** 161 * Provides a way to extract a property of an object in a certain language, falling back on the property keyed as 162 * 'default' or 'default-rtl'. If such key doesn't exist, the object itself is considered the actual value, which 163 * should ideally be the case so that you may use a string or object of any number of strings keyed by language 164 * with a default. 165 * 166 * @param object Object to extract property from 167 * @param lang Language code, defaults to wgUserLanguage 168 */ 169 autoLang: function ( object, lang ) { 170 var defaultKey = $( 'body' ).hasClass( 'rtl' ) ? 'default-rtl' : 'default'; 171 return object[lang || mw.config.get( 'wgUserLanguage' )] || object[defaultKey] || object['default'] || object; 172 }, 173 174 /** 175 * Provides a way to extract the path of an icon in a certain language, automatically appending a version number for 176 * caching purposes and prepending an image path when icon paths are relative. 177 * 178 * @param icon Icon object from e.g. toolbar config 179 * @param path Default icon path, defaults to $.wikiEditor.imgPath 180 * @param lang Language code, defaults to wgUserLanguage 181 */ 182 autoIcon: function ( icon, path, lang ) { 183 var src = $.wikiEditor.autoLang( icon, lang ); 184 path = path || $.wikiEditor.imgPath; 185 // Prepend path if src is not absolute 186 if ( src.substr( 0, 7 ) !== 'http://' && src.substr( 0, 8 ) !== 'https://' && src[0] !== '/' ) { 187 src = path + src; 188 } 189 return src + '?' + mw.loader.getVersion( 'jquery.wikiEditor' ); 190 }, 191 192 /** 193 * Get the sprite offset for a language if available, icon for a language if available, or the default offset or icon, 194 * in that order of preference. 195 * @param icon Icon object, see autoIcon() 196 * @param offset Offset object 197 * @param path Icon path, see autoIcon() 198 * @param lang Language code, defaults to wgUserLanguage 199 */ 200 autoIconOrOffset: function ( icon, offset, path, lang ) { 201 lang = lang || mw.config.get( 'wgUserLanguage' ); 202 if ( typeof offset === 'object' && lang in offset ) { 203 return offset[lang]; 204 } else if ( typeof icon === 'object' && lang in icon ) { 205 return $.wikiEditor.autoIcon( icon, undefined, lang ); 206 } else { 207 return $.wikiEditor.autoLang( offset, lang ); 208 } 209 } 210 }; 211 212 /** 213 * jQuery plugin that provides a way to initialize a wikiEditor instance on a textarea. 214 */ 215 $.fn.wikiEditor = function () { 216 217 // Skip any further work when running in browsers that are unsupported 218 if ( !$.wikiEditor.isSupported() ) { 219 return $( this ); 220 } 221 222 /* Initialization */ 223 224 // The wikiEditor context is stored in the element's data, so when this function gets called again we can pick up right 225 // where we left off 226 var context = $( this ).data( 'wikiEditor-context' ); 227 // On first call, we need to set things up, but on all following calls we can skip right to the API handling 228 if ( !context || typeof context === 'undefined' ) { 229 230 // Star filling the context with useful data - any jQuery selections, as usual should be named with a preceding $ 231 context = { 232 // Reference to the textarea element which the wikiEditor is being built around 233 '$textarea': $( this ), 234 // Container for any number of mutually exclusive views that are accessible by tabs 235 'views': {}, 236 // Container for any number of module-specific data - only including data for modules in use on this context 237 'modules': {}, 238 // General place to shouve bits of data into 239 'data': {}, 240 // Unique numeric ID of this instance used both for looking up and differentiating instances of wikiEditor 241 'instance': $.wikiEditor.instances.push( $( this ) ) - 1, 242 // Saved selection state for old IE (<=10) 243 'savedSelection': null, 244 // List of extensions active on this context 245 'extensions': [] 246 }; 247 248 /** 249 * Externally Accessible API 250 * 251 * These are available using calls to $( selection ).wikiEditor( call, data ) where selection is a jQuery selection 252 * of the textarea that the wikiEditor instance was built around. 253 */ 254 255 context.api = { 256 /** 257 * Activates a module on a specific context with optional configuration data. 258 * 259 * @param data Either a string of the name of a module to add without any additional configuration parameters, 260 * or an object with members keyed with module names and valued with configuration objects. 261 */ 262 'addModule': function ( context, data ) { 263 var module, call, 264 modules = {}; 265 if ( typeof data === 'string' ) { 266 modules[data] = {}; 267 } else if ( typeof data === 'object' ) { 268 modules = data; 269 } 270 for ( module in modules ) { 271 // Check for the existance of an available / supported module with a matching name and a create function 272 if ( typeof module === 'string' && typeof $.wikiEditor.modules[module] !== 'undefined' && 273 $.wikiEditor.isSupported( $.wikiEditor.modules[module] ) ) 274 { 275 // Extend the context's core API with this module's own API calls 276 if ( 'api' in $.wikiEditor.modules[module] ) { 277 for ( call in $.wikiEditor.modules[module].api ) { 278 // Modules may not overwrite existing API functions - first come, first serve 279 if ( !( call in context.api ) ) { 280 context.api[call] = $.wikiEditor.modules[module].api[call]; 281 } 282 } 283 } 284 // Activate the module on this context 285 if ( 'fn' in $.wikiEditor.modules[module] && 'create' in $.wikiEditor.modules[module].fn ) { 286 // Add a place for the module to put it's own stuff 287 context.modules[module] = {}; 288 // Tell the module to create itself on the context 289 $.wikiEditor.modules[module].fn.create( context, modules[module] ); 290 } 291 } 292 } 293 } 294 }; 295 296 /** 297 * Event Handlers 298 * 299 * These act as filters returning false if the event should be ignored or returning true if it should be passed 300 * on to all modules. This is also where we can attach some extra information to the events. 301 */ 302 303 context.evt = { 304 /* Empty until extensions add some; see jquery.wikiEditor.iframe.js for examples. */ 305 }; 306 307 /* Internal Functions */ 308 309 context.fn = { 310 /** 311 * Executes core event filters as well as event handlers provided by modules. 312 */ 313 trigger: function ( name, event ) { 314 // Event is an optional argument, but from here on out, at least the type field should be dependable 315 if ( typeof event === 'undefined' ) { 316 event = { 'type': 'custom' }; 317 } 318 // Ensure there's a place for extra information to live 319 if ( typeof event.data === 'undefined' ) { 320 event.data = {}; 321 } 322 323 // Allow filtering to occur 324 if ( name in context.evt ) { 325 if ( !context.evt[name]( event ) ) { 326 return false; 327 } 328 } 329 var returnFromModules = null; //they return null by default 330 // Pass the event around to all modules activated on this context 331 332 for ( var module in context.modules ) { 333 if ( 334 module in $.wikiEditor.modules && 335 'evt' in $.wikiEditor.modules[module] && 336 name in $.wikiEditor.modules[module].evt 337 ) { 338 var ret = $.wikiEditor.modules[module].evt[name]( context, event ); 339 if ( ret !== null ) { 340 //if 1 returns false, the end result is false 341 if ( returnFromModules === null ) { 342 returnFromModules = ret; 343 } else { 344 returnFromModules = returnFromModules && ret; 345 } 346 } 347 } 348 } 349 if ( returnFromModules !== null ) { 350 return returnFromModules; 351 } else { 352 return true; 353 } 354 }, 355 356 /** 357 * Adds a button to the UI 358 */ 359 addButton: function ( options ) { 360 // Ensure that buttons and tabs are visible 361 context.$controls.show(); 362 context.$buttons.show(); 363 return $( '<button>' ) 364 .text( $.wikiEditor.autoMsg( options, 'caption' ) ) 365 .click( options.action ) 366 .appendTo( context.$buttons ); 367 }, 368 369 /** 370 * Adds a view to the UI, which is accessed using a set of tabs. Views are mutually exclusive and by default a 371 * wikitext view will be present. Only when more than one view exists will the tabs will be visible. 372 */ 373 addView: function ( options ) { 374 // Adds a tab 375 function addTab( options ) { 376 // Ensure that buttons and tabs are visible 377 context.$controls.show(); 378 context.$tabs.show(); 379 // Return the newly appended tab 380 return $( '<div>' ) 381 .attr( 'rel', 'wikiEditor-ui-view-' + options.name ) 382 .addClass( context.view === options.name ? 'current' : null ) 383 .append( $( '<a>' ) 384 .attr( 'href', '#' ) 385 .mousedown( function () { 386 // No dragging! 387 return false; 388 } ) 389 .click( function ( event ) { 390 context.$ui.find( '.wikiEditor-ui-view' ).hide(); 391 context.$ui.find( '.' + $( this ).parent().attr( 'rel' ) ).show(); 392 context.$tabs.find( 'div' ).removeClass( 'current' ); 393 $( this ).parent().addClass( 'current' ); 394 $( this ).blur(); 395 if ( 'init' in options && typeof options.init === 'function' ) { 396 options.init( context ); 397 } 398 event.preventDefault(); 399 return false; 400 } ) 401 .text( $.wikiEditor.autoMsg( options, 'title' ) ) 402 ) 403 .appendTo( context.$tabs ); 404 } 405 // Automatically add the previously not-needed wikitext tab 406 if ( !context.$tabs.children().length ) { 407 addTab( { 'name': 'wikitext', 'titleMsg': 'wikieditor-wikitext-tab' } ); 408 } 409 // Add the tab for the view we were actually asked to add 410 addTab( options ); 411 // Return newly appended view 412 return $( '<div>' ) 413 .addClass( 'wikiEditor-ui-view wikiEditor-ui-view-' + options.name ) 414 .hide() 415 .appendTo( context.$ui ); 416 }, 417 418 /** 419 * Save scrollTop and cursor position for IE 420 */ 421 saveCursorAndScrollTop: function () { 422 if ( $.client.profile().name === 'msie' ) { 423 var IHateIE = { 424 'scrollTop' : context.$textarea.scrollTop(), 425 'pos': context.$textarea.textSelection( 'getCaretPosition', { startAndEnd: true } ) 426 }; 427 context.$textarea.data( 'IHateIE', IHateIE ); 428 } 429 }, 430 431 /** 432 * Restore scrollTo and cursor position for IE 433 */ 434 restoreCursorAndScrollTop: function () { 435 if ( $.client.profile().name === 'msie' ) { 436 var IHateIE = context.$textarea.data( 'IHateIE' ); 437 if ( IHateIE ) { 438 context.$textarea.scrollTop( IHateIE.scrollTop ); 439 context.$textarea.textSelection( 'setSelection', { start: IHateIE.pos[0], end: IHateIE.pos[1] } ); 440 context.$textarea.data( 'IHateIE', null ); 441 } 442 } 443 }, 444 445 /** 446 * Save text selection for old IE (<=10) 447 */ 448 saveSelection: function () { 449 if ( $.client.profile().name === 'msie' && document.selection && document.selection.createRange ) { 450 context.$textarea.focus(); 451 context.savedSelection = document.selection.createRange(); 452 } 453 }, 454 455 /** 456 * Restore text selection for old IE (<=10) 457 */ 458 restoreSelection: function () { 459 if ( $.client.profile().name === 'msie' && context.savedSelection !== null ) { 460 context.$textarea.focus(); 461 context.savedSelection.select(); 462 context.savedSelection = null; 463 } 464 } 465 }; 466 467 /** 468 * Workaround for a scrolling bug in IE8 (bug 61908) 469 */ 470 if ( $.client.profile().name === 'msie' ) { 471 context.$textarea.css( 'height', context.$textarea.height() ); 472 } 473 474 /** 475 * Base UI Construction 476 * 477 * The UI is built from several containers, the outer-most being a div classed as "wikiEditor-ui". These containers 478 * provide a certain amount of "free" layout, but in some situations procedural layout is needed, which is performed 479 * as a response to the "resize" event. 480 */ 481 482 // Assemble a temporary div to place over the wikiEditor while it's being constructed 483 /* Disabling our loading div for now 484 var $loader = $( '<div>' ) 485 .addClass( 'wikiEditor-ui-loading' ) 486 .append( $( '<span>' + mediaWiki.msg( 'wikieditor-loading' ) + '</span>' ) 487 .css( 'marginTop', context.$textarea.height() / 2 ) ); 488 */ 489 /* Preserving cursor and focus state, which will get lost due to wrapAll */ 490 var hasFocus = context.$textarea.is( ':focus' ), 491 cursorPos = context.$textarea.textSelection( 'getCaretPosition', { startAndEnd: true } ); 492 // Encapsulate the textarea with some containers for layout 493 context.$textarea 494 /* Disabling our loading div for now 495 .after( $loader ) 496 .add( $loader ) 497 */ 498 .wrapAll( $( '<div>' ).addClass( 'wikiEditor-ui' ) ) 499 .wrapAll( $( '<div>' ).addClass( 'wikiEditor-ui-view wikiEditor-ui-view-wikitext' ) ) 500 .wrapAll( $( '<div>' ).addClass( 'wikiEditor-ui-left' ) ) 501 .wrapAll( $( '<div>' ).addClass( 'wikiEditor-ui-bottom' ) ) 502 .wrapAll( $( '<div>' ).addClass( 'wikiEditor-ui-text' ) ); 503 // Restore scroll position after this wrapAll (tracked by mediawiki.action.edit) 504 context.$textarea.prop( 'scrollTop', $( '#wpScrolltop' ).val() ); 505 // Restore focus and cursor if needed 506 if ( hasFocus ) { 507 context.$textarea.focus(); 508 context.$textarea.textSelection( 'setSelection', { start: cursorPos[0], end: cursorPos[1] } ); 509 } 510 511 // Get references to some of the newly created containers 512 context.$ui = context.$textarea.parent().parent().parent().parent().parent(); 513 context.$wikitext = context.$textarea.parent().parent().parent().parent(); 514 // Add in tab and button containers 515 context.$wikitext 516 .before( 517 $( '<div>' ).addClass( 'wikiEditor-ui-controls' ) 518 .append( $( '<div>' ).addClass( 'wikiEditor-ui-tabs' ).hide() ) 519 .append( $( '<div>' ).addClass( 'wikiEditor-ui-buttons' ) ) 520 ) 521 .before( $( '<div>' ).addClass( 'wikiEditor-ui-clear' ) ); 522 // Get references to some of the newly created containers 523 context.$controls = context.$ui.find( '.wikiEditor-ui-buttons' ).hide(); 524 context.$buttons = context.$ui.find( '.wikiEditor-ui-buttons' ); 525 context.$tabs = context.$ui.find( '.wikiEditor-ui-tabs' ); 526 // Clear all floating after the UI 527 context.$ui.after( $( '<div>' ).addClass( 'wikiEditor-ui-clear' ) ); 528 // Attach a right container 529 context.$wikitext.append( $( '<div>' ).addClass( 'wikiEditor-ui-right' ) ); 530 context.$wikitext.append( $( '<div>' ).addClass( 'wikiEditor-ui-clear' ) ); 531 // Attach a top container to the left pane 532 context.$wikitext.find( '.wikiEditor-ui-left' ).prepend( $( '<div>' ).addClass( 'wikiEditor-ui-top' ) ); 533 // Setup the intial view 534 context.view = 'wikitext'; 535 // Trigger the "resize" event anytime the window is resized 536 $( window ).resize( function ( event ) { 537 context.fn.trigger( 'resize', event ); 538 } ); 539 } 540 541 /* API Execution */ 542 543 // Since javascript gives arguments as an object, we need to convert them so they can be used more easily 544 var args = $.makeArray( arguments ); 545 546 // Dynamically setup core extensions for modules that are required 547 if ( args[0] === 'addModule' && typeof args[1] !== 'undefined' ) { 548 var modules = args[1]; 549 if ( typeof modules !== 'object' ) { 550 modules = {}; 551 modules[args[1]] = ''; 552 } 553 for ( var module in modules ) { 554 // Only allow modules which are supported (and thus actually being turned on) affect the decision to extend 555 if ( module in $.wikiEditor.modules && $.wikiEditor.isSupported( $.wikiEditor.modules[module] ) ) { 556 // Activate all required core extensions on context 557 for ( var e in $.wikiEditor.extensions ) { 558 if ( 559 $.wikiEditor.isRequired( $.wikiEditor.modules[module], e ) && 560 $.inArray( e, context.extensions ) === -1 561 ) { 562 context.extensions[context.extensions.length] = e; 563 $.wikiEditor.extensions[e]( context ); 564 } 565 } 566 break; 567 } 568 } 569 } 570 571 // There would need to be some arguments if the API is being called 572 if ( args.length > 0 ) { 573 // Handle API calls 574 var call = args.shift(); 575 if ( call in context.api ) { 576 context.api[call]( context, typeof args[0] === 'undefined' ? {} : args[0] ); 577 } 578 } 579 580 // Store the context for next time, and support chaining 581 return $( this ).data( 'wikiEditor-context', context ); 582 583 }; 584 585 }( jQuery, mediaWiki ) );
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Fri Nov 28 14:03:12 2014 | Cross-referenced by PHPXref 0.7.1 |