|
|
| (같은 사용자의 중간 판 11개는 보이지 않습니다) |
| 1번째 줄: |
1번째 줄: |
| /*
| |
| * JavaScript for WikiEditor
| |
| */
| |
|
| |
|
| ( function () {
| |
| var editingSessionId;
| |
|
| |
| // This sets $.wikiEditor and $.fn.wikiEditor
| |
| require( './jquery.wikiEditor.js' );
| |
|
| |
| function logEditEvent( data ) {
| |
| if ( mw.config.get( 'wgMFMode' ) !== null ) {
| |
| // Visiting a ?action=edit URL can, depending on user settings, result
| |
| // in the MobileFrontend overlay appearing on top of WikiEditor. In
| |
| // these cases, don't log anything.
| |
| return;
| |
| }
| |
| mw.track( 'editAttemptStep', $.extend( {
| |
| // eslint-disable-next-line camelcase
| |
| editor_interface: 'wikitext',
| |
| platform: 'desktop', // FIXME T249944
| |
| integration: 'page'
| |
| }, data ) );
| |
| }
| |
|
| |
| function logEditFeature( feature, action ) {
| |
| if ( mw.config.get( 'wgMFMode' ) !== null ) {
| |
| // Visiting a ?action=edit URL can, depending on user settings, result
| |
| // in the MobileFrontend overlay appearing on top of WikiEditor. In
| |
| // these cases, don't log anything.
| |
| return;
| |
| }
| |
| mw.track( 'visualEditorFeatureUse', {
| |
| feature: feature,
| |
| action: action,
| |
| // eslint-disable-next-line camelcase
| |
| editor_interface: 'wikitext',
| |
| platform: 'desktop', // FIXME T249944
| |
| integration: 'page'
| |
| } );
| |
| }
| |
|
| |
| function logAbort( switchingToVE, unmodified ) {
| |
| if ( switchingToVE ) {
| |
| logEditFeature( 'editor-switch', 'visual-desktop' );
| |
| }
| |
|
| |
| var abortType;
| |
| if ( switchingToVE && unmodified ) {
| |
| abortType = 'switchnochange';
| |
| } else if ( switchingToVE ) {
| |
| abortType = 'switchwithout';
| |
| } else if ( unmodified ) {
| |
| abortType = 'nochange';
| |
| } else {
| |
| abortType = 'abandon';
| |
| }
| |
|
| |
| logEditEvent( {
| |
| action: 'abort',
| |
| type: abortType
| |
| } );
| |
| }
| |
|
| |
| $( function () {
| |
| var $textarea = $( '#wpTextbox1' ),
| |
| $editingSessionIdInput = $( '#editingStatsId' ),
| |
| origText = $textarea.val();
| |
|
| |
| // T263505, T249038
| |
| $( '#wikieditorUsed' ).val( 'yes' );
| |
|
| |
| if ( $editingSessionIdInput.length ) {
| |
| editingSessionId = $editingSessionIdInput.val();
| |
| if ( window.performance && window.performance.timing ) {
| |
| // We want to track from the time the user started to try to
| |
| // launch the editor which navigationStart approximates. All
| |
| // of our supported browsers *should* allow this. Rather than
| |
| // fall back to the timestamp when the page loaded for those
| |
| // that don't, we just ignore them, so as to not skew the
| |
| // results towards better-performance in those cases.
| |
| var readyTime = Date.now();
| |
| logEditEvent( {
| |
| action: 'ready',
| |
| timing: readyTime - window.performance.timing.navigationStart
| |
| } );
| |
| $textarea.on( 'wikiEditor-toolbar-doneInitialSections', function () {
| |
| logEditEvent( {
| |
| action: 'loaded',
| |
| timing: Date.now() - window.performance.timing.navigationStart
| |
| } );
| |
| } ).one( 'input', function () {
| |
| logEditEvent( {
| |
| action: 'firstChange',
| |
| timing: Date.now() - readyTime
| |
| } );
| |
| } );
| |
| }
| |
| var $form = $textarea.closest( 'form' );
| |
| if ( mw.user.options.get( 'uselivepreview' ) ) {
| |
| $form.find( '#wpPreview' ).on( 'click', function () {
| |
| logEditFeature( 'preview', 'preview-live' );
| |
| } );
| |
| }
| |
|
| |
| var submitting;
| |
| $form.on( 'submit', function () {
| |
| submitting = true;
| |
| } );
| |
| var onUnloadFallback = window.onunload;
| |
|
| |
| window.onunload = function () {
| |
| var unmodified = mw.config.get( 'wgAction' ) !== 'submit' && origText === $textarea.val(),
| |
| caVeEdit = $( '#ca-ve-edit' )[ 0 ],
| |
| switchingToVE = caVeEdit && (
| |
| document.activeElement === caVeEdit ||
| |
| $.contains( caVeEdit, document.activeElement )
| |
| );
| |
|
| |
| var fallbackResult;
| |
| if ( onUnloadFallback ) {
| |
| fallbackResult = onUnloadFallback();
| |
| }
| |
|
| |
| if ( !submitting ) {
| |
| logAbort( switchingToVE, unmodified );
| |
| }
| |
|
| |
| // If/when the user uses the back button to go back to the edit form
| |
| // and the browser serves this from bfcache, regenerate the session ID
| |
| // so we don't use the same ID twice. Ideally we'd do this by listening to the pageshow
| |
| // event and checking e.originalEvent.persisted, but that doesn't work in Chrome:
| |
| // https://code.google.com/p/chromium/issues/detail?id=344507
| |
| // So instead we modify the DOM here, after sending the abort event.
| |
| editingSessionId = mw.user.generateRandomSessionId();
| |
| $editingSessionIdInput.val( editingSessionId );
| |
|
| |
| return fallbackResult;
| |
| };
| |
| $textarea.on( 'wikiEditor-switching-visualeditor', function () {
| |
| var unmodified = mw.config.get( 'wgAction' ) !== 'submit' && origText === $textarea.val();
| |
| // A non-navigation switch to VE has occurred. As such, avoid eventually
| |
| // double-logging an abort when VE is done.
| |
| window.onunload = onUnloadFallback;
| |
|
| |
| logAbort( true, unmodified );
| |
| } );
| |
|
| |
| // Add logging for Realtime Preview.
| |
| mw.hook( 'ext.WikiEditor.realtimepreview.enable' ).add( function () {
| |
| logEditFeature( 'preview', 'preview-realtime-on' );
| |
| } );
| |
| mw.hook( 'ext.WikiEditor.realtimepreview.inuse' ).add( function () {
| |
| logEditFeature( 'preview', 'preview-realtime-inuse' );
| |
| } );
| |
| mw.hook( 'ext.WikiEditor.realtimepreview.disable' ).add( function () {
| |
| logEditFeature( 'preview', 'preview-realtime-off' );
| |
| } );
| |
| mw.hook( 'ext.WikiEditor.realtimepreview.loaded' ).add( function () {
| |
| logEditFeature( 'preview', 'preview-realtime-loaded' );
| |
| } );
| |
| mw.hook( 'ext.WikiEditor.realtimepreview.stop' ).add( function () {
| |
| logEditFeature( 'preview', 'preview-realtime-error-stopped' );
| |
| } );
| |
| mw.hook( 'ext.WikiEditor.realtimepreview.reloadError' ).add( function () {
| |
| logEditFeature( 'preview', 'preview-realtime-reload-error' );
| |
| } );
| |
| mw.hook( 'ext.WikiEditor.realtimepreview.reloadHover' ).add( function () {
| |
| logEditFeature( 'preview', 'preview-realtime-reload-hover' );
| |
| } );
| |
| mw.hook( 'ext.WikiEditor.realtimepreview.reloadManual' ).add( function () {
| |
| logEditFeature( 'preview', 'preview-realtime-reload-manual' );
| |
| } );
| |
| }
| |
|
| |
| // The old toolbar is still in place and needs to be removed so there aren't two toolbars
| |
| $( '#toolbar' ).remove();
| |
| // Add toolbar module
| |
| // TODO: Implement .wikiEditor( 'remove' )
| |
| mw.addWikiEditor( $textarea );
| |
| } );
| |
|
| |
| mw.addWikiEditor = function ( $textarea ) {
| |
| if ( $textarea.css( 'display' ) === 'none' ) {
| |
| return;
| |
| }
| |
|
| |
| $textarea.wikiEditor(
| |
| 'addModule', require( './jquery.wikiEditor.toolbar.config.js' )
| |
| );
| |
|
| |
| var dialogsConfig = require( './jquery.wikiEditor.dialogs.config.js' );
| |
| // Replace icons
| |
| dialogsConfig.replaceIcons( $textarea );
| |
| // Add dialogs module
| |
| $textarea.wikiEditor( 'addModule', dialogsConfig.getDefaultConfig() );
| |
|
| |
| };
| |
|
| |
| }() );
| |
|
| |
|
| |
| /**
| |
| * This plugin provides a way to build a wiki-text editing user interface around a textarea.
| |
| *
| |
| * @example To initialize without any modules,
| |
| * overqualified `div#edittoolbar` to avoid MediaWiki's heading to id automatism:
| |
| * $( 'div#edittoolbar' ).wikiEditor();
| |
| *
| |
| * @example To initialize with one or more modules, or to add modules after it's already been initialized:
| |
| * $( 'textarea#wpTextbox1' ).wikiEditor( 'addModule', 'toolbar', { ... config ... } );
| |
| *
| |
| */
| |
| ( function () {
| |
|
| |
| var hasOwn = Object.prototype.hasOwnProperty,
| |
|
| |
| /**
| |
| * Array of language codes.
| |
| */
| |
| fallbackChain = ( function () {
| |
| // eslint-disable-next-line no-jquery/no-class-state
| |
| var isRTL = $( document.body ).hasClass( 'rtl' ),
| |
| chain = mw.language.getFallbackLanguageChain();
| |
|
| |
| // Do not fallback to 'en'
| |
| if ( chain.length >= 2 && !/^en-/.test( chain[ chain.length - 2 ] ) ) {
| |
| chain.pop();
| |
| }
| |
| if ( isRTL ) {
| |
| chain.push( 'default-rtl' );
| |
| }
| |
| chain.push( 'default' );
| |
| return chain;
| |
| }() );
| |
|
| |
| /**
| |
| * Helper function to mark the automatic message functionality in
| |
| * the autoMsg and autoSafeMsg functions as deprecated.
| |
| *
| |
| * @param {string} property
| |
| * @param {string} key
| |
| */
| |
| function deprecateAutoMsg( property, key ) {
| |
| var searchParam = mw.config.get( 'wgSearchType' ) === 'CirrusSearch' ?
| |
| 'insource:/' + property + 'Msg: \'' + key + '\'/' :
| |
| property + 'Msg: ' + key;
| |
| var searchUri = mw.config.get( 'wgServer' ) +
| |
| mw.util.getUrl(
| |
| 'Special:Search',
| |
| { search: searchParam, ns2: 1, ns8: 1 }
| |
| );
| |
| if ( searchUri.slice( 0, 2 ) === '//' ) {
| |
| searchUri = location.protocol + searchUri;
| |
| }
| |
|
| |
| var messageMethod;
| |
| if ( property === 'html' || property === 'text' || property === 'title' ) {
| |
| messageMethod = 'mw.message( ' + JSON.stringify( key ) + ' ).parse()';
| |
| } else {
| |
| messageMethod = 'mw.msg( ' + JSON.stringify( key ) + ' )';
| |
| }
| |
| var deprecationMsg = mw.log.makeDeprecated(
| |
| 'wikiEditor_autoMsg',
| |
| 'WikiEditor: Use `' + property + ': ' + messageMethod + '` instead of `' + property + 'Msg: ' + JSON.stringify( key ) + '`.\nSearch: ' + searchUri
| |
| );
| |
| deprecationMsg();
| |
| }
| |
|
| |
| /**
| |
| * Global static object for wikiEditor that provides generally useful functionality to all modules and contexts.
| |
| */
| |
| $.wikiEditor = {
| |
| /**
| |
| * For each module that is loaded, static code shared by all instances is loaded into this object organized by
| |
| * module name. The existence of a module in this object only indicates the module is available. To check if a
| |
| * module is in use by a specific context check the context.modules object.
| |
| */
| |
| modules: {
| |
| toolbar: require( './jquery.wikiEditor.toolbar.js' ),
| |
| dialogs: require( './jquery.wikiEditor.dialogs.js' )
| |
| },
| |
|
| |
| /**
| |
| * A context can be extended, such as adding iframe support, on a per-wikiEditor instance basis.
| |
| */
| |
| extensions: {},
| |
|
| |
| /**
| |
| * In some cases like with the iframe's HTML file, it's convenient to have a lookup table of all instances of the
| |
| * WikiEditor. Each context contains an instance field which contains a key that corresponds to a reference to the
| |
| * textarea which the WikiEditor was build around. This way, by passing a simple integer you can provide a way back
| |
| * to a specific context.
| |
| */
| |
| instances: [],
| |
|
| |
| /**
| |
| * Path to images - this is a bit messy, and it would need to change if this code (and images) gets moved into the
| |
| * core - or anywhere for that matter...
| |
| */
| |
| imgPath: mw.config.get( 'wgExtensionAssetsPath' ) + '/WikiEditor/modules/images/',
| |
|
| |
| /**
| |
| * Checks if a module has a specific requirement
| |
| *
| |
| * @param {Object} module Module object
| |
| * @param {string} requirement String identifying requirement
| |
| * @return {boolean}
| |
| */
| |
| isRequired: function ( module, requirement ) {
| |
| if ( typeof module.req !== 'undefined' ) {
| |
| for ( var req in module.req ) {
| |
| if ( module.req[ req ] === requirement ) {
| |
| return true;
| |
| }
| |
| }
| |
| }
| |
| return false;
| |
| },
| |
|
| |
| /**
| |
| * Provides a way to extract messages from objects. Wraps a mw.message( ... ).text() call.
| |
| *
| |
| * FIXME: This is a security nightmare. Only use is for the help toolbar panel. Inline the
| |
| * special need instead?
| |
| * FIXME: Also, this is ludicrously complex. Just use mw.message().text() directly.
| |
| *
| |
| * @deprecated Since v0.5.4. Use mw.message() directly instead of <key>Msg
| |
| *
| |
| * @param {Object} object Object to extract messages from
| |
| * @param {string} property String of name of property which contains the message. This should be the base name of the
| |
| * property, which means that in the case of the object { this: 'that', fooMsg: 'bar' }, passing property as 'this'
| |
| * would return the raw text 'that', while passing property as 'foo' would return the internationalized message
| |
| * with the key 'bar'.
| |
| * @return {string}
| |
| */
| |
| autoMsg: function ( object, property ) {
| |
| // Accept array of possible properties, of which the first one found will be used
| |
| if ( typeof property === 'object' ) {
| |
| for ( var i in property ) {
| |
| if ( property[ i ] in object || property[ i ] + 'Msg' in object ) {
| |
| property = property[ i ];
| |
| break;
| |
| }
| |
| }
| |
| }
| |
| if ( property in object ) {
| |
| return object[ property ];
| |
| } else if ( property + 'Msg' in object ) {
| |
| var p = object[ property + 'Msg' ];
| |
| if ( Array.isArray( p ) && p.length >= 2 ) {
| |
| deprecateAutoMsg( property, p[ 0 ] );
| |
| return mw.message.apply( mw.message, p ).text();
| |
| } else {
| |
| deprecateAutoMsg( property, p );
| |
| // eslint-disable-next-line mediawiki/msg-doc
| |
| return mw.message( p ).text();
| |
| }
| |
| } else {
| |
| return '';
| |
| }
| |
| },
| |
|
| |
| /**
| |
| * Provides a way to extract messages from objects. Wraps a mw.message( ... ).escaped() call.
| |
| *
| |
| * FIXME: This is ludicrously complex. Just use mw.message().escaped() directly.
| |
| *
| |
| * @deprecated Since v0.5.4. Use mw.message() directly instead of <key>Msg
| |
| *
| |
| * @param {Object} object Object to extract messages from
| |
| * @param {string} property String of name of property which contains the message. This should be the base name of the
| |
| * property, which means that in the case of the object { this: 'that', fooMsg: 'bar' }, passing property as 'this'
| |
| * would return the raw text 'that', while passing property as 'foo' would return the internationalized message
| |
| * with the key 'bar'. This is then escaped.
| |
| * @return {string}
| |
| */
| |
| autoSafeMsg: function ( object, property ) {
| |
| // Accept array of possible properties, of which the first one found will be used
| |
| if ( typeof property === 'object' ) {
| |
| for ( var i in property ) {
| |
| if ( property[ i ] in object || property[ i ] + 'Msg' in object ) {
| |
| property = property[ i ];
| |
| break;
| |
| }
| |
| }
| |
| }
| |
| if ( property in object ) {
| |
| return object[ property ];
| |
| } else if ( property + 'Msg' in object ) {
| |
| var p = object[ property + 'Msg' ];
| |
| if ( Array.isArray( p ) && p.length >= 2 ) {
| |
| deprecateAutoMsg( property, p[ 0 ] );
| |
| return mw.message.apply( mw.message, p ).escaped();
| |
| } else {
| |
| deprecateAutoMsg( property, p );
| |
| // eslint-disable-next-line mediawiki/msg-doc
| |
| return mw.message( p ).escaped();
| |
| }
| |
| } else {
| |
| return '';
| |
| }
| |
| },
| |
|
| |
| /**
| |
| * Provides a way to extract a property of an object in a certain language, falling back on the property keyed as
| |
| * 'default' or 'default-rtl'. If such key doesn't exist, the object itself is considered the actual value, which
| |
| * should ideally be the case so that you may use a string or object of any number of strings keyed by language
| |
| * with a default.
| |
| *
| |
| * @param {Object} object Object to extract property from
| |
| * @return {Object}
| |
| */
| |
| autoLang: function ( object ) {
| |
| for ( var i = 0; i < fallbackChain.length; i++ ) {
| |
| var key = fallbackChain[ i ];
| |
| if ( hasOwn.call( object, key ) ) {
| |
| return object[ key ];
| |
| }
| |
| }
| |
| return object;
| |
| },
| |
|
| |
| /**
| |
| * Provides a way to extract the path of an icon in a certain language, automatically appending a version number for
| |
| * caching purposes and prepending an image path when icon paths are relative.
| |
| *
| |
| * @param {Object} icon Icon object from e.g. toolbar config
| |
| * @param {string} path Default icon path, defaults to $.wikiEditor.imgPath
| |
| * @return {Object}
| |
| */
| |
| autoIcon: function ( icon, path ) {
| |
| path = path || $.wikiEditor.imgPath;
| |
|
| |
| for ( var i = 0; i < fallbackChain.length; i++ ) {
| |
| var key = fallbackChain[ i ];
| |
| if ( icon && hasOwn.call( icon, key ) ) {
| |
| var src = icon[ key ];
| |
|
| |
| // Return a data URL immediately
| |
| if ( src.slice( 0, 5 ) === 'data:' ) {
| |
| return src;
| |
| }
| |
|
| |
| // Prepend path if src is not absolute
| |
| if ( src.slice( 0, 7 ) !== 'http://' && src.slice( 0, 8 ) !== 'https://' && src[ 0 ] !== '/' ) {
| |
| src = path + src;
| |
| }
| |
| return src;
| |
| }
| |
| }
| |
| return icon;
| |
| }
| |
| };
| |
|
| |
| /**
| |
| * jQuery plugin that provides a way to initialize a wikiEditor instance on a textarea.
| |
| *
| |
| * @return {jQuery}
| |
| */
| |
| $.fn.wikiEditor = function () {
| |
| /* Initialization */
| |
|
| |
| // The wikiEditor context is stored in the element's data, so when this function gets called again we can pick up right
| |
| // where we left off
| |
| var context = $( this ).data( 'wikiEditor-context' );
| |
| // On first call, we need to set things up, but on all following calls we can skip right to the API handling
| |
| if ( !context ) {
| |
|
| |
| // Star filling the context with useful data - any jQuery selections, as usual should be named with a preceding $
| |
| context = {
| |
| // Reference to the textarea element which the wikiEditor is being built around
| |
| $textarea: $( this ),
| |
| // Reference to the focused element before the dialog opens, so it can be restored once it closes
| |
| $focusedElem: null,
| |
| // Container for any number of mutually exclusive views that are accessible by tabs
| |
| views: {},
| |
| // Container for any number of module-specific data - only including data for modules in use on this context
| |
| modules: {},
| |
| // General place to shove bits of data into
| |
| data: {},
| |
| // Unique numeric ID of this instance used both for looking up and differentiating instances of wikiEditor
| |
| instance: $.wikiEditor.instances.push( $( this ) ) - 1,
| |
| // Saved selection state for old IE (<=10)
| |
| savedSelection: null,
| |
| // List of extensions active on this context
| |
| extensions: []
| |
| };
| |
|
| |
| /**
| |
| * Externally Accessible API
| |
| *
| |
| * These are available using calls to $( selection ).wikiEditor( call, data ) where selection is a jQuery selection
| |
| * of the textarea that the wikiEditor instance was built around.
| |
| */
| |
|
| |
| context.api = {
| |
| /*!
| |
| * Activates a module on a specific context with optional configuration data.
| |
| *
| |
| * @param data Either a string of the name of a module to add without any additional configuration parameters,
| |
| * or an object with members keyed with module names and valued with configuration objects.
| |
| */
| |
| addModule: function ( ctx, data ) {
| |
| var modules = {};
| |
| if ( typeof data === 'string' ) {
| |
| modules[ data ] = {};
| |
| } else if ( typeof data === 'object' ) {
| |
| modules = data;
| |
| }
| |
| for ( var module in modules ) {
| |
| // Check for the existence of an available module with a matching name and a create function
| |
| if ( typeof module === 'string' && typeof $.wikiEditor.modules[ module ] !== 'undefined' ) {
| |
| // Extend the context's core API with this module's own API calls
| |
| if ( 'api' in $.wikiEditor.modules[ module ] ) {
| |
| for ( var call in $.wikiEditor.modules[ module ].api ) {
| |
| // Modules may not overwrite existing API functions - first come, first serve
| |
| if ( !( call in ctx.api ) ) {
| |
| ctx.api[ call ] = $.wikiEditor.modules[ module ].api[ call ];
| |
| }
| |
| }
| |
| }
| |
| // Activate the module on this context
| |
| if ( 'fn' in $.wikiEditor.modules[ module ] &&
| |
| 'create' in $.wikiEditor.modules[ module ].fn &&
| |
| typeof ctx.modules[ module ] === 'undefined'
| |
| ) {
| |
| // Add a place for the module to put it's own stuff
| |
| ctx.modules[ module ] = {};
| |
| // Tell the module to create itself on the context
| |
| $.wikiEditor.modules[ module ].fn.create( ctx, modules[ module ] );
| |
| }
| |
| }
| |
| }
| |
| }
| |
| };
| |
|
| |
| /**
| |
| * Event Handlers
| |
| *
| |
| * These act as filters returning false if the event should be ignored or returning true if it should be passed
| |
| * on to all modules. This is also where we can attach some extra information to the events.
| |
| */
| |
|
| |
| context.evt = {
| |
| /* Empty until extensions add some; see jquery.wikiEditor.iframe.js for examples. */
| |
| };
| |
|
| |
| /* Internal Functions */
| |
|
| |
| context.fn = {
| |
| /**
| |
| * Executes core event filters as well as event handlers provided by modules.
| |
| *
| |
| * @param {string} name
| |
| * @param {Object} event
| |
| * @return {boolean}
| |
| */
| |
| trigger: function ( name, event ) {
| |
| // Event is an optional argument, but from here on out, at least the type field should be dependable
| |
| event = event || { type: 'custom' };
| |
| // Ensure there's a place for extra information to live
| |
| event.data = event.data || {};
| |
|
| |
| // Allow filtering to occur
| |
| if ( name in context.evt ) {
| |
| if ( !context.evt[ name ]( event ) ) {
| |
| return false;
| |
| }
| |
| }
| |
| var returnFromModules = true;
| |
| // Pass the event around to all modules activated on this context
| |
|
| |
| for ( var module in context.modules ) {
| |
| if (
| |
| module in $.wikiEditor.modules &&
| |
| 'evt' in $.wikiEditor.modules[ module ] &&
| |
| name in $.wikiEditor.modules[ module ].evt
| |
| ) {
| |
| var ret = $.wikiEditor.modules[ module ].evt[ name ]( context, event );
| |
| if ( ret !== null ) {
| |
| // if 1 returns false, the end result is false
| |
| returnFromModules = returnFromModules && ret;
| |
| }
| |
| }
| |
| }
| |
| return returnFromModules;
| |
| },
| |
|
| |
| /**
| |
| * Adds a button to the UI
| |
| *
| |
| * @param {Object} options
| |
| * @return {jQuery}
| |
| */
| |
| addButton: function ( options ) {
| |
| // Ensure that buttons and tabs are visible
| |
| context.$controls.show();
| |
| context.$buttons.show();
| |
| return $( '<button>' )
| |
| .text( $.wikiEditor.autoMsg( options, 'caption' ) )
| |
| .on( 'click', options.action )
| |
| .appendTo( context.$buttons );
| |
| },
| |
|
| |
| /**
| |
| * Adds a view to the UI, which is accessed using a set of tabs. Views are mutually exclusive and by default a
| |
| * wikitext view will be present. Only when more than one view exists will the tabs will be visible.
| |
| *
| |
| * @param {Object} options
| |
| * @return {jQuery}
| |
| */
| |
| addView: function ( options ) {
| |
| // Adds a tab
| |
| function addTab( opts ) {
| |
| // Ensure that buttons and tabs are visible
| |
| context.$controls.show();
| |
| context.$tabs.show();
| |
| // Return the newly appended tab
| |
| return $( '<div>' )
| |
| .attr( 'rel', 'wikiEditor-ui-view-' + opts.name )
| |
| .addClass( context.view === opts.name ? 'current' : null )
| |
| .append( $( '<a>' )
| |
| .attr( 'tabindex', 0 )
| |
| .on( 'mousedown', function () {
| |
| // No dragging!
| |
| return false;
| |
| } )
| |
| .on( 'click keydown', function ( event ) {
| |
| if (
| |
| event.type === 'click' ||
| |
| event.type === 'keydown' && event.key === 'Enter'
| |
| ) {
| |
| context.$ui.find( '.wikiEditor-ui-view' ).hide();
| |
| context.$ui.find( '.' + $( this ).parent().attr( 'rel' ) ).show();
| |
| context.$tabs.find( 'div' ).removeClass( 'current' );
| |
| $( this ).parent().addClass( 'current' );
| |
| $( this ).trigger( 'blur' );
| |
| if ( 'init' in opts && typeof opts.init === 'function' ) {
| |
| opts.init( context );
| |
| }
| |
| event.preventDefault();
| |
| return false;
| |
| }
| |
| } )
| |
| .text( $.wikiEditor.autoMsg( opts, 'title' ) )
| |
| )
| |
| .appendTo( context.$tabs );
| |
| }
| |
| // Automatically add the previously not-needed wikitext tab
| |
| if ( !context.$tabs.children().length ) {
| |
| addTab( { name: 'wikitext', title: mw.message( 'wikieditor-wikitext-tab' ).parse() } );
| |
| }
| |
| // Add the tab for the view we were actually asked to add
| |
| addTab( options );
| |
| // Return newly appended view
| |
| // eslint-disable-next-line mediawiki/class-doc
| |
| return $( '<div>' )
| |
| .addClass( 'wikiEditor-ui-view wikiEditor-ui-view-' + options.name )
| |
| .hide()
| |
| .appendTo( context.$ui );
| |
| },
| |
|
| |
| /**
| |
| * Save text selection
| |
| */
| |
| saveSelection: function () {
| |
| context.$focusedElem = $( ':focus' );
| |
| context.$textarea.trigger( 'focus' );
| |
| context.savedSelection = context.$textarea.textSelection( 'getCaretPosition', { startAndEnd: true } );
| |
| },
| |
|
| |
| /**
| |
| * Restore text selection
| |
| */
| |
| restoreSelection: function () {
| |
| if ( context.savedSelection ) {
| |
| context.$textarea.trigger( 'focus' );
| |
| context.$textarea.textSelection( 'setSelection', { start: context.savedSelection[ 0 ], end: context.savedSelection[ 1 ] } );
| |
| context.savedSelection = null;
| |
| }
| |
| if ( context.$focusedElem ) {
| |
| context.$focusedElem.trigger( 'focus' );
| |
| }
| |
| }
| |
| };
| |
|
| |
| /**
| |
| * Base UI Construction
| |
| *
| |
| * The UI is built from several containers, the outer-most being a div classed as "wikiEditor-ui". These containers
| |
| * provide a certain amount of "free" layout, but in some situations procedural layout is needed, which is performed
| |
| * as a response to the "resize" event.
| |
| */
| |
|
| |
| /* Preserving cursor and focus state, which will get lost due to wrapAll */
| |
| var hasFocus = context.$textarea.is( ':focus' );
| |
| var cursorPos = context.$textarea.textSelection( 'getCaretPosition', { startAndEnd: true } );
| |
| // Encapsulate the textarea with some containers for layout
| |
| context.$textarea
| |
| .wrapAll( $( '<div>' ).addClass( 'wikiEditor-ui' ) )
| |
| .wrapAll( $( '<div>' ).addClass( 'wikiEditor-ui-view wikiEditor-ui-view-wikitext' ) )
| |
| .wrapAll( $( '<div>' ).addClass( 'wikiEditor-ui-left' ) )
| |
| .wrapAll( $( '<div>' ).addClass( 'wikiEditor-ui-bottom' ) )
| |
| .wrapAll( $( '<div>' ).addClass( 'wikiEditor-ui-text' ) );
| |
| // Restore scroll position after this wrapAll (tracked by mediawiki.action.edit)
| |
| context.$textarea.prop( 'scrollTop', $( '#wpScrolltop' ).val() );
| |
| // Restore focus and cursor if needed
| |
| if ( hasFocus ) {
| |
| context.$textarea.trigger( 'focus' );
| |
| context.$textarea.textSelection( 'setSelection', { start: cursorPos[ 0 ], end: cursorPos[ 1 ] } );
| |
| }
| |
|
| |
| // Get references to some of the newly created containers
| |
| context.$ui = context.$textarea.parent().parent().parent().parent().parent();
| |
| context.$wikitext = context.$textarea.parent().parent().parent().parent();
| |
| // Add in tab and button containers
| |
| context.$wikitext.before(
| |
| $( '<div>' ).addClass( 'wikiEditor-ui-controls' ).append(
| |
| $( '<div>' ).addClass( 'wikiEditor-ui-tabs' ).hide(),
| |
| $( '<div>' ).addClass( 'wikiEditor-ui-buttons' )
| |
| ),
| |
| $( '<div>' ).addClass( 'wikiEditor-ui-clear' )
| |
| );
| |
| // Get references to some of the newly created containers
| |
| context.$controls = context.$ui.find( '.wikiEditor-ui-buttons' ).hide();
| |
| context.$buttons = context.$ui.find( '.wikiEditor-ui-buttons' );
| |
| context.$tabs = context.$ui.find( '.wikiEditor-ui-tabs' );
| |
| // Clear all floating after the UI
| |
| context.$ui.after( $( '<div>' ).addClass( 'wikiEditor-ui-clear' ) );
| |
| // Attach a right container
| |
| context.$wikitext.append(
| |
| $( '<div>' ).addClass( 'wikiEditor-ui-right' ),
| |
| $( '<div>' ).addClass( 'wikiEditor-ui-clear' )
| |
| );
| |
| // Attach a top container to the left pane
| |
| context.$wikitext.find( '.wikiEditor-ui-left' ).prepend( $( '<div>' ).addClass( 'wikiEditor-ui-top' ) );
| |
| // Setup the initial view
| |
| context.view = 'wikitext';
| |
| // Trigger the "resize" event anytime the window is resized
| |
| $( window ).on( 'resize', function ( event ) {
| |
| context.fn.trigger( 'resize', event );
| |
| } );
| |
| }
| |
|
| |
| /* API Execution */
| |
|
| |
| // Since javascript gives arguments as an object, we need to convert them so they can be used more easily
| |
| var args = $.makeArray( arguments );
| |
|
| |
| // Dynamically setup core extensions for modules that are required
| |
| if ( args[ 0 ] === 'addModule' && typeof args[ 1 ] !== 'undefined' ) {
| |
| var modulesArg = args[ 1 ];
| |
| if ( typeof modulesArg !== 'object' ) {
| |
| modulesArg = {};
| |
| modulesArg[ args[ 1 ] ] = '';
| |
| }
| |
| for ( var m in modulesArg ) {
| |
| if ( m in $.wikiEditor.modules ) {
| |
| // Activate all required core extensions on context
| |
| for ( var extension in $.wikiEditor.extensions ) {
| |
| if (
| |
| $.wikiEditor.isRequired( $.wikiEditor.modules[ m ], extension ) &&
| |
| context.extensions.indexOf( extension ) === -1
| |
| ) {
| |
| context.extensions[ context.extensions.length ] = extension;
| |
| $.wikiEditor.extensions[ extension ]( context );
| |
| }
| |
| }
| |
| break;
| |
| }
| |
| }
| |
| }
| |
|
| |
| // There would need to be some arguments if the API is being called
| |
| if ( args.length > 0 ) {
| |
| // Handle API calls
| |
| var callArg = args.shift();
| |
| if ( callArg in context.api ) {
| |
| context.api[ callArg ]( context, args[ 0 ] || {} );
| |
| }
| |
| }
| |
|
| |
| // Store the context for next time, and support chaining
| |
| return $( this ).data( 'wikiEditor-context', context );
| |
|
| |
| };
| |
|
| |
| }() );
| |