[ Index ]

PHP Cross Reference of MediaWiki-1.24.0

title

Body

[close]

/extensions/WikiEditor/modules/ -> jquery.wikiEditor.js (source)

   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 ) );


Generated: Fri Nov 28 14:03:12 2014 Cross-referenced by PHPXref 0.7.1