mediawiki-extensions-Visual.../modules/ve-mw/preinit/ve.init.mw.DesktopArticleTarget.init.js
Ed Sanders 8cb070f4d7 Remove VE user-agent based browser compatibility checks
Bug: T367735
Change-Id: I07fb1bbbd2f907672400cb9bc74fd40dea973da9
2024-06-17 13:03:04 +01:00

1649 lines
56 KiB
JavaScript

/*!
* VisualEditor MediaWiki DesktopArticleTarget init.
*
* This file must remain as widely compatible as the base compatibility
* for MediaWiki itself (see mediawiki/core:/resources/startup.js).
* Avoid use of: SVG, HTML5 DOM, ContentEditable etc.
*
* @copyright See AUTHORS.txt
* @license The MIT License (MIT); see LICENSE.txt
*/
/* eslint-disable no-jquery/no-global-selector */
// TODO: ve.now and ve.track should be moved to mw.libs.ve
/* global ve */
/**
* Platform preparation for the MediaWiki view page. This loads (when user needs it) the
* actual MediaWiki integration and VisualEditor library.
*/
( function () {
const configData = require( './data.json' ),
veactionToMode = {
edit: 'visual',
editsource: 'source'
},
availableModes = [];
let init = null,
conf = null,
tabMessages = null,
pageExists = null,
viewUrl = null,
veEditUrl = null,
tabPreference = null;
let veEditSourceUrl, targetPromise, url,
initialWikitext, oldId,
isLoading, tempWikitextEditor, tempWikitextEditorData,
$toolbarPlaceholder, $toolbarPlaceholderBar,
contentTop, wasFloating,
active = false,
targetLoaded = false,
plugins = [],
welcomeDialogDisabled = false,
educationPopupsDisabled = false,
// Defined after document-ready below
$targetContainer = null;
if ( mw.config.get( 'wgMFMode' ) ) {
mw.log.warn( 'Attempted to load desktop target on mobile.' );
return;
}
/**
* Show the loading progress bar
*/
function showLoading() {
if ( isLoading ) {
return;
}
isLoading = true;
$( 'html' ).addClass( 've-activated ve-loading' );
if ( !init.$loading ) {
init.progressBar = new mw.libs.ve.ProgressBarWidget();
init.$loading = $( '<div>' )
.addClass( 've-init-mw-desktopArticleTarget-loading-overlay' )
.append( init.progressBar.$element );
}
$( document ).on( 'keydown', onDocumentKeyDown );
$toolbarPlaceholderBar.append( init.$loading );
}
/**
* Increment loading progress by one step
*
* See mw.libs.ve.ProgressBarWidget for steps.
*/
function incrementLoadingProgress() {
init.progressBar.incrementLoadingProgress();
}
/**
* Clear and hide the loading progress bar
*/
function clearLoading() {
init.progressBar.clearLoading();
isLoading = false;
$( document ).off( 'keydown', onDocumentKeyDown );
$( 'html' ).removeClass( 've-loading' );
if ( init.$loading ) {
init.$loading.detach();
}
if ( tempWikitextEditor ) {
teardownTempWikitextEditor();
}
hideToolbarPlaceholder();
}
/**
* Handle window scroll events
*
* @param {Event} e
*/
function onWindowScroll() {
const scrollTop = $( document.documentElement ).scrollTop();
const floating = scrollTop > contentTop;
if ( floating !== wasFloating ) {
const width = $targetContainer.outerWidth();
$toolbarPlaceholder.toggleClass( 've-init-mw-desktopArticleTarget-toolbarPlaceholder-floating', floating );
$toolbarPlaceholderBar.css( 'width', width );
wasFloating = floating;
}
}
const onWindowScrollListener = mw.util.throttle( onWindowScroll, 250 );
/**
* Show a placeholder for the VE toolbar
*/
function showToolbarPlaceholder() {
if ( !$toolbarPlaceholder ) {
// Create an equal-height placeholder for the toolbar to avoid vertical jump
// when the real toolbar is ready.
$toolbarPlaceholder = $( '<div>' ).addClass( 've-init-mw-desktopArticleTarget-toolbarPlaceholder' );
$toolbarPlaceholderBar = $( '<div>' ).addClass( 've-init-mw-desktopArticleTarget-toolbarPlaceholder-bar' );
$toolbarPlaceholder.append( $toolbarPlaceholderBar );
}
// Toggle -floating class before append (if required) to avoid content moving later
contentTop = $targetContainer.offset().top;
wasFloating = null;
onWindowScroll();
const scrollTopBefore = $( document.documentElement ).scrollTop();
$targetContainer.prepend( $toolbarPlaceholder );
window.addEventListener( 'scroll', onWindowScrollListener, { passive: true } );
if ( wasFloating ) {
// Browser might not support scroll anchoring:
// https://developer.mozilla.org/en-US/docs/Web/CSS/overflow-anchor/Guide_to_scroll_anchoring
// ...so compute the new scroll offset ourselves.
window.scrollTo( 0, scrollTopBefore + $toolbarPlaceholder.outerHeight() );
}
// Add class for transition after first render
setTimeout( () => {
$toolbarPlaceholder.addClass( 've-init-mw-desktopArticleTarget-toolbarPlaceholder-open' );
} );
}
/**
* Hide the placeholder for the VE toolbar
*/
function hideToolbarPlaceholder() {
if ( $toolbarPlaceholder ) {
window.removeEventListener( 'scroll', onWindowScrollListener );
$toolbarPlaceholder.detach();
$toolbarPlaceholder.removeClass( 've-init-mw-desktopArticleTarget-toolbarPlaceholder-open' );
}
}
/**
* Create a temporary `<textarea>` wikitext editor while source mode loads
*
* @param {Object} data Initialisation data for VE
*/
function setupTempWikitextEditor( data ) {
let wikitext = data.content;
// Add trailing linebreak to non-empty wikitext documents for consistency
// with old editor and usability. Will be stripped on save. T156609
if ( wikitext ) {
wikitext += '\n';
}
tempWikitextEditor = new mw.libs.ve.MWTempWikitextEditorWidget( { value: wikitext } );
tempWikitextEditorData = data;
// Bring forward some transformations that show the editor is now ready
// Grey out the page title if it is below the editing toolbar (depending on skin), to show it is uneditable.
$( '.ve-init-mw-desktopArticleTarget-targetContainer #firstHeading' ).addClass( 've-init-mw-desktopArticleTarget-uneditableContent' );
$( '#mw-content-text' )
.before( tempWikitextEditor.$element )
.addClass( 'oo-ui-element-hidden' );
$( 'html' ).addClass( 've-tempSourceEditing' ).removeClass( 've-loading' );
// Resize the textarea to fit content. We could do this more often (e.g. on change)
// but hopefully this temporary textarea won't be visible for too long.
tempWikitextEditor.adjustSize().moveCursorToStart();
ve.track( 'editAttemptStep', { action: 'ready', mode: 'source', platform: 'desktop' } );
mw.libs.ve.tempWikitextEditor = tempWikitextEditor;
mw.hook( 've.wikitextInteractive' ).fire();
}
/**
* Synchronise state of temporary wikitexteditor back to the VE initialisation data object
*/
function syncTempWikitextEditor() {
let wikitext = tempWikitextEditor.getValue();
// Strip trailing linebreak. Will get re-added in ArticleTarget#parseDocument.
if ( wikitext.slice( -1 ) === '\n' ) {
wikitext = wikitext.slice( 0, -1 );
}
if ( wikitext !== tempWikitextEditorData.content ) {
// Write changes back to response data object,
// which will be used to construct the surface.
tempWikitextEditorData.content = wikitext;
// TODO: Consider writing changes using a
// transaction so they can be undone.
// For now, just mark surface as pre-modified
tempWikitextEditorData.fromEditedState = true;
}
// Store the last-seen selection and pass to the target
tempWikitextEditorData.initialSourceRange = tempWikitextEditor.getRange();
tempWikitextEditor.$element.prop( 'readonly', true );
}
/**
* Teardown the temporary wikitext editor
*/
function teardownTempWikitextEditor() {
// Destroy widget and placeholder
tempWikitextEditor.$element.remove();
mw.libs.ve.tempWikitextEditor = tempWikitextEditor = null;
tempWikitextEditorData = null;
$( '#mw-content-text' ).removeClass( 'oo-ui-element-hidden' );
$( 'html' ).removeClass( 've-tempSourceEditing' );
}
/**
* Abort loading the editor
*/
function abortLoading() {
$( 'html' ).removeClass( 've-activated' );
active = false;
updateTabs( false );
// Push read tab URL to history
if ( $( '#ca-view a' ).length ) {
history.pushState( { tag: 'visualeditor' }, '', $( '#ca-view a' ).attr( 'href' ) );
}
clearLoading();
}
/**
* Handle keydown events on the document
*
* @param {jQuery.Event} e Keydown event
*/
function onDocumentKeyDown( e ) {
if ( e.which === 27 /* OO.ui.Keys.ESCAPE */ ) {
abortLoading();
e.preventDefault();
}
}
/**
* Parse a section value from a query string object
*
* @example
* parseSection( new URL( location.href ).searchParams.get( 'section' ) )
*
* @param {string|undefined} section Section value from query object
* @return {string|null} Section if valid, null otherwise
*/
function parseSection( section ) {
// Section must be a number, 'new' or 'T-' prefixed
if ( section && /^(new|\d+|T-\d+)$/.test( section ) ) {
return section;
}
return null;
}
/**
* Use deferreds to avoid loading and instantiating Target multiple times.
*
* @private
* @param {string} mode Target mode: 'visual' or 'source'
* @param {string} section Section to edit
* @return {jQuery.Promise}
*/
function getTarget( mode, section ) {
if ( !targetPromise ) {
// The TargetLoader module is loaded in the bottom queue, so it should have been
// requested already but it might not have finished loading yet
targetPromise = mw.loader.using( 'ext.visualEditor.targetLoader' )
.then( () => {
mw.libs.ve.targetLoader.addPlugin(
// Run VisualEditorPreloadModules, but if they fail, we still want to continue
// loading, so convert failure to success
() => mw.loader.using( conf.preloadModules ).catch(
() => $.Deferred().resolve()
)
);
// Add modules specific to desktop (modules shared between desktop
// and mobile are already added by TargetLoader)
[
'ext.visualEditor.desktopArticleTarget',
// Add requested plugins
...plugins
].forEach( mw.libs.ve.targetLoader.addPlugin );
plugins = [];
return mw.libs.ve.targetLoader.loadModules( mode );
} )
.then( () => {
if ( !active ) {
// Loading was aborted
// TODO: Make loaders abortable instead of waiting
targetPromise = null;
return $.Deferred().reject().promise();
}
const target = ve.init.mw.targetFactory.create(
conf.contentModels[ mw.config.get( 'wgPageContentModel' ) ], {
modes: availableModes,
defaultMode: mode
}
);
target.on( 'deactivate', () => {
active = false;
updateTabs( false );
} );
target.on( 'reactivate', () => {
url = new URL( location.href );
activateTarget(
getEditModeFromUrl( url ),
parseSection( url.searchParams.get( 'section' ) )
);
} );
target.setContainer( $targetContainer );
targetLoaded = true;
return target;
}, ( e ) => {
mw.log.warn( 'VisualEditor failed to load: ' + e );
} );
}
targetPromise.then( ( target ) => {
target.section = section;
} );
return targetPromise;
}
/**
* @private
* @param {Object} initData
* @param {URL} [linkUrl]
*/
function trackActivateStart( initData, linkUrl ) {
if ( !linkUrl ) {
linkUrl = url;
}
if ( linkUrl.searchParams.get( 'wvprov' ) === 'sticky-header' ) {
initData.mechanism += '-sticky-header';
}
ve.track( 'trace.activate.enter', { mode: initData.mode } );
initData.action = 'init';
initData.integration = 'page';
ve.track( 'editAttemptStep', initData );
mw.libs.ve.activationStart = ve.now();
}
/**
* Get the skin-specific message for an edit tab
*
* @param {string} tabMsg Base tab message key
* @return {string} Message text
*/
function getTabMessage( tabMsg ) {
let tabMsgKey = tabMessages[ tabMsg ];
const skinMsgKeys = {
edit: 'edit',
create: 'create',
editlocaldescription: 'edit-local',
createlocaldescription: 'create-local'
};
const key = skinMsgKeys[ tabMsg ];
if ( !tabMsgKey && key ) {
// Some skins don't use the default skin message keys.
// The following messages can be used here:
// * vector-view-edit
// * vector-view-create
// * vector-view-edit-local
// * vector-view-create-local
// * messages for other skins
tabMsgKey = mw.config.get( 'skin' ) + '-view-' + key;
if ( !mw.message( tabMsgKey ).exists() ) {
// The following messages can be used here:
// * skin-view-edit
// * skin-view-create
// * skin-view-edit-local
// * skin-view-create-local
tabMsgKey = 'skin-view-' + key;
}
}
// eslint-disable-next-line mediawiki/msg-doc
const msg = mw.message( tabMsgKey );
if ( !msg.isParseable() ) {
mw.log.warn( 'VisualEditor: MediaWiki:' + tabMsgKey + ' contains unsupported syntax. ' +
'https://www.mediawiki.org/wiki/Manual:Messages_API#Feature_support_in_JavaScript' );
return undefined;
}
return msg.text();
}
/**
* Set the user's new preferred editor
*
* @param {string} editor Preferred editor, 'visualeditor' or 'wikitext'
* @return {jQuery.Promise} Promise which resolves when the preference has been set
*/
function setEditorPreference( editor ) {
// If visual mode isn't available, don't set the editor preference as the
// user has expressed no choice by opening this editor. (T246259)
// Strictly speaking the same thing should happen if visual mode is
// available but source mode isn't, but that is never the case.
if ( !init.isVisualAvailable ) {
return $.Deferred().resolve().promise();
}
if ( editor !== 'visualeditor' && editor !== 'wikitext' ) {
throw new Error( 'setEditorPreference called with invalid option: ', editor );
}
let key = pageExists ? 'edit' : 'create',
sectionKey = 'editsection';
if (
mw.config.get( 'wgVisualEditorConfig' ).singleEditTab &&
tabPreference === 'remember-last'
) {
if ( $( '#ca-view-foreign' ).length ) {
key += 'localdescription';
}
if ( editor === 'wikitext' ) {
key += 'source';
sectionKey += 'source';
}
$( '#ca-edit a' ).text( getTabMessage( key ) );
$( '.mw-editsection a' ).text( getTabMessage( sectionKey ) );
}
mw.cookie.set( 'VEE', editor, { path: '/', expires: 30 * 86400, prefix: '' } );
// Save user preference if logged in
if (
mw.user.isNamed() &&
mw.user.options.get( 'visualeditor-editor' ) !== editor
) {
// Same as ve.init.target.getLocalApi()
return new mw.Api().saveOption( 'visualeditor-editor', editor ).then( () => {
mw.user.options.set( 'visualeditor-editor', editor );
} );
}
return $.Deferred().resolve().promise();
}
/**
* Update state of editing tabs
*
* @param {boolean} editing Whether the editor is loaded
* @param {string} [mode='visual'] Edit mode ('visual' or 'source')
* @param {boolean} [isNewSection] Adding a new section
*/
function updateTabs( editing, mode, isNewSection ) {
let $tab;
if ( editing ) {
if ( isNewSection ) {
$tab = $( '#ca-addsection' );
} else if ( $( '#ca-ve-edit' ).length ) {
if ( !mode || mode === 'visual' ) {
$tab = $( '#ca-ve-edit' );
} else {
$tab = $( '#ca-edit' );
}
} else {
// Single edit tab
$tab = $( '#ca-edit' );
}
} else {
$tab = $( '#ca-view' );
}
// Deselect current mode (e.g. "view" or "history") in skins that have
// separate tab sections for content actions and namespaces, like Vector.
$( '#p-views' ).find( 'li.selected' ).removeClass( 'selected' );
// In skins like MonoBook that don't have the separate tab sections,
// deselect the known tabs for editing modes (when switching or exiting editor).
$( '#ca-edit, #ca-ve-edit, #ca-addsection' ).not( $tab ).removeClass( 'selected' );
$tab.addClass( 'selected' );
}
/**
* Scroll to a specific heading before VE loads
*
* Similar to ve.init.mw.ArticleTarget.prototype.scrollToHeading
*
* @param {string} section Parsed section (string)
*/
function scrollToSection( section ) {
if ( section === '0' || section === 'new' ) {
return;
}
let $heading;
$( '#mw-content-text .mw-editsection a:not( .mw-editsection-visualeditor )' ).each( ( i, el ) => {
const linkUrl = new URL( el.href );
if ( section === parseSection( linkUrl.searchParams.get( 'section' ) ) ) {
$heading = $( el ).closest( '.mw-heading, h1, h2, h3, h4, h5, h6' );
return false;
}
} );
// When loading on action=edit URLs, there is no page content
if ( !$heading || !$heading.length ) {
return;
}
let offset = 0;
const enableVisualSectionEditing = mw.config.get( 'wgVisualEditorConfig' ).enableVisualSectionEditing;
if ( enableVisualSectionEditing === true || enableVisualSectionEditing === 'desktop' ) {
// Heading will jump to the top of the page in visual section editing.
// This measurement already includes the height of $toolbarPlaceholder.
offset = $( '#mw-content-text' ).offset().top;
} else {
// Align with top of heading margin. Doesn't apply in visual section editing as the margin collapses.
offset = parseInt( $heading.css( 'margin-top' ) ) + $toolbarPlaceholder.outerHeight();
}
// Support for CSS `scroll-behavior: smooth;` and JS `window.scroll( { behavior: 'smooth' } )`
// is correlated:
// * https://caniuse.com/css-scroll-behavior
// * https://caniuse.com/mdn-api_window_scroll_options_behavior_parameter
const supportsSmoothScroll = 'scrollBehavior' in document.documentElement.style;
const newScrollTop = $heading.offset().top - offset;
if ( supportsSmoothScroll ) {
window.scroll( {
top: newScrollTop,
behavior: 'smooth'
} );
} else {
// Ideally we would use OO.ui.Element.static.getRootScrollableElement here
// as it has slightly better browser support (Chrome < 60)
const scrollContainer = document.documentElement;
$( scrollContainer ).animate( {
scrollTop: newScrollTop
} );
}
}
/**
* Load and activate the target.
*
* If you need to call methods on the target before activate is called, call getTarget()
* yourself, chain your work onto that promise, and pass that chained promise in as targetPromise.
* E.g. `activateTarget( getTarget().then( function( target ) { target.doAThing(); } ) );`
*
* @private
* @param {string} mode Target mode: 'visual' or 'source'
* @param {string} [section] Section to edit.
* If visual section editing is not enabled, we will jump to the start of this section, and still
* the heading to prefix the edit summary.
* @param {jQuery.Promise} [tPromise] Promise that will be resolved with a ve.init.mw.DesktopArticleTarget
* @param {boolean} [modified=false] The page has been modified before loading (e.g. in source mode)
*/
function activateTarget( mode, section, tPromise, modified ) {
let dataPromise;
updateTabs( true, mode, section === 'new' );
// Only call requestPageData early if the target object isn't there yet.
// If the target object is there, this is a second or subsequent load, and the
// internal state of the target object can influence the load request.
if ( !targetLoaded ) {
// The TargetLoader module is loaded in the bottom queue, so it should have been
// requested already but it might not have finished loading yet
dataPromise = mw.loader.using( 'ext.visualEditor.targetLoader' )
.then( () => mw.libs.ve.targetLoader.requestPageData( mode, mw.config.get( 'wgRelevantPageName' ), {
sessionStore: true,
section: section,
oldId: oldId,
// Should be ve.init.mw.DesktopArticleTarget.static.trackingName, but the
// class hasn't loaded yet.
// This is used for stats tracking, so do not change!
targetName: 'mwTarget',
modified: modified,
editintro: url.searchParams.get( 'editintro' ),
preload: url.searchParams.get( 'preload' ),
preloadparams: mw.util.getArrayParam( 'preloadparams', url.searchParams ),
// If switching to visual with modifications, check if we have wikitext to convert
wikitext: mode === 'visual' && modified ? $( '#wpTextbox1' ).textSelection( 'getContents' ) : undefined
} ) );
dataPromise
.then( ( response ) => {
if (
// Check target promise hasn't already failed (isLoading=false)
isLoading &&
// TODO: Support tempWikitextEditor when section=new (T185633)
mode === 'source' && section !== 'new' &&
// Can't use temp editor when recovering an autosave
!( response.visualeditor && response.visualeditor.recovered )
) {
setupTempWikitextEditor( response.visualeditor );
}
} )
.then( incrementLoadingProgress );
}
// Do this before section scrolling
showToolbarPlaceholder();
mw.hook( 've.activationStart' ).fire();
let visibleSection = null;
let visibleSectionOffset = null;
if ( section === null ) {
let firstVisibleEditSection = null;
$( '#firstHeading, #mw-content-text .mw-editsection' ).each( ( i, el ) => {
const top = el.getBoundingClientRect().top;
if ( top > 0 ) {
firstVisibleEditSection = el;
// break
return false;
}
} );
if ( firstVisibleEditSection && firstVisibleEditSection.id !== 'firstHeading' ) {
const firstVisibleSectionLink = firstVisibleEditSection.querySelector( 'a' );
const linkUrl = new URL( firstVisibleSectionLink.href );
visibleSection = parseSection( linkUrl.searchParams.get( 'section' ) );
const firstVisibleHeading = $( firstVisibleEditSection ).closest( '.mw-heading, h1, h2, h3, h4, h5, h6' )[ 0 ];
visibleSectionOffset = firstVisibleHeading.getBoundingClientRect().top;
}
} else if ( mode === 'visual' ) {
scrollToSection( section );
}
showLoading( mode );
incrementLoadingProgress();
active = true;
tPromise = tPromise || getTarget( mode, section );
tPromise
.then( ( target ) => {
target.visibleSection = visibleSection;
target.visibleSectionOffset = visibleSectionOffset;
incrementLoadingProgress();
// If target was already loaded, ensure the mode is correct
target.setDefaultMode( mode );
// syncTempWikitextEditor modified the result object in the dataPromise
if ( tempWikitextEditor ) {
syncTempWikitextEditor();
}
const deactivating = target.deactivatingDeferred || $.Deferred().resolve();
return deactivating.then( () => {
target.currentUrl = new URL( location.href );
const activatePromise = target.activate( dataPromise );
// toolbarSetupDeferred resolves slightly before activatePromise, use done
// to run in the same paint cycle as the VE toolbar being drawn
target.toolbarSetupDeferred.done( () => {
hideToolbarPlaceholder();
} );
return activatePromise;
} );
} )
.then( () => {
if ( mode === 'visual' ) {
// `action: 'ready'` has already been fired for source mode in setupTempWikitextEditor
ve.track( 'editAttemptStep', { action: 'ready', mode: mode } );
} else if ( !tempWikitextEditor ) {
// We're in source mode, but skipped the
// tempWikitextEditor, so make sure we do relevant
// tracking / hooks:
ve.track( 'editAttemptStep', { action: 'ready', mode: mode } );
mw.hook( 've.wikitextInteractive' ).fire();
}
ve.track( 'editAttemptStep', { action: 'loaded', mode: mode } );
} )
.always( clearLoading );
}
/**
* @private
* @param {string} mode Target mode: 'visual' or 'source'
* @param {string} [section]
* @param {boolean} [modified=false] The page has been modified before loading (e.g. in source mode)
* @param {URL} [linkUrl] URL to navigate to, potentially with extra parameters
*/
function activatePageTarget( mode, section, modified, linkUrl ) {
trackActivateStart( { type: 'page', mechanism: mw.config.get( 'wgArticleId' ) ? 'click' : 'new', mode: mode }, linkUrl );
if ( !active ) {
// Replace the current state with one that is tagged as ours, to prevent the
// back button from breaking when used to exit VE. FIXME: there should be a better
// way to do this. See also similar code in the DesktopArticleTarget constructor.
history.replaceState( { tag: 'visualeditor' }, '', url );
// Set action=edit or veaction=edit/editsource
// Use linkUrl to preserve parameters like 'editintro' (T56029)
history.pushState( { tag: 'visualeditor' }, '', linkUrl || ( mode === 'source' ? veEditSourceUrl : veEditUrl ) );
// Update URL instance
url = linkUrl || veEditUrl;
activateTarget( mode, section, undefined, modified );
}
}
/**
* Get the last mode a user used
*
* @return {string|null} 'visualeditor', 'wikitext' or null
*/
function getLastEditor() {
// This logic matches VisualEditorHooks::getLastEditor
let editor = mw.cookie.get( 'VEE', '' );
// Set editor to user's preference or site's default (ignore the cookie) if …
if (
// … user is logged in,
mw.user.isNamed() ||
// … no cookie is set, or
!editor ||
// value is invalid.
!( editor === 'visualeditor' || editor === 'wikitext' )
) {
editor = mw.user.options.get( 'visualeditor-editor' );
}
return editor;
}
/**
* Get the preferred editor for this edit page
*
* For the preferred *available* editor, use getAvailableEditPageEditor.
*
* @return {string|null} 'visualeditor', 'wikitext' or null
*/
function getEditPageEditor() {
// This logic matches VisualEditorHooks::getEditPageEditor
// !!+ casts '0' to false
const isRedLink = !!+url.searchParams.get( 'redlink' );
// On dual-edit-tab wikis, the edit page must mean the user wants wikitext,
// unless following a redlink
if ( !mw.config.get( 'wgVisualEditorConfig' ).singleEditTab && !isRedLink ) {
return 'wikitext';
}
switch ( tabPreference ) {
case 'prefer-ve':
return 'visualeditor';
case 'prefer-wt':
return 'wikitext';
case 'multi-tab':
// 'multi-tab'
// TODO: See VisualEditor.hooks.php
return isRedLink ?
getLastEditor() :
'wikitext';
case 'remember-last':
default:
return getLastEditor();
}
}
/**
* Get the preferred editor which is also available on this edit page
*
* @return {string} 'visual' or 'source'
*/
function getAvailableEditPageEditor() {
switch ( getEditPageEditor() ) {
case 'visualeditor':
if ( init.isVisualAvailable ) {
return 'visual';
}
if ( init.isWikitextAvailable ) {
return 'source';
}
return null;
case 'wikitext':
default:
return init.isWikitextAvailable ? 'source' : null;
}
}
/**
* Check if a boolean preference is set in user options, mw.storage or a cookie
*
* @param {string} prefName Preference name
* @param {string} storageKey mw.storage key
* @param {string} cookieName Cookie name
* @return {boolean} Preference is set
*/
function checkPreferenceOrStorage( prefName, storageKey, cookieName ) {
storageKey = storageKey || prefName;
cookieName = cookieName || storageKey;
return !!( mw.user.options.get( prefName ) ||
(
!mw.user.isNamed() && (
mw.storage.get( storageKey ) ||
mw.cookie.get( cookieName, '' )
)
)
);
}
/**
* Set a boolean preference to true in user options, mw.storage or a cookie
*
* @param {string} prefName Preference name
* @param {string} storageKey mw.storage key
* @param {string} cookieName Cookie name
*/
function setPreferenceOrStorage( prefName, storageKey, cookieName ) {
storageKey = storageKey || prefName;
cookieName = cookieName || storageKey;
if ( !mw.user.isNamed() ) {
// Try local storage first; if that fails, set a cookie
if ( !mw.storage.set( storageKey, 1 ) ) {
mw.cookie.set( cookieName, 1, { path: '/', expires: 30 * 86400, prefix: '' } );
}
} else {
new mw.Api().saveOption( prefName, '1' );
mw.user.options.set( prefName, '1' );
}
}
conf = mw.config.get( 'wgVisualEditorConfig' );
tabMessages = conf.tabMessages;
viewUrl = new URL( mw.util.getUrl( mw.config.get( 'wgRelevantPageName' ) ), location.href );
url = new URL( location.href );
// T156998: Don't trust 'oldid' query parameter, it'll be wrong if 'diff' or 'direction'
// is set to 'next' or 'prev'.
oldId = mw.config.get( 'wgRevisionId' ) || $( 'input[name=parentRevId]' ).val();
if ( oldId === mw.config.get( 'wgCurRevisionId' ) || mw.config.get( 'wgEditLatestRevision' ) ) {
// The page may have been edited by someone else after we loaded it, setting this to "undefined"
// indicates that we should load the actual latest revision.
oldId = undefined;
}
pageExists = !!mw.config.get( 'wgRelevantArticleId' );
const isViewPage = mw.config.get( 'wgIsArticle' ) && !url.searchParams.has( 'diff' );
const wgAction = mw.config.get( 'wgAction' );
const isEditPage = wgAction === 'edit' || wgAction === 'submit';
const pageCanLoadEditor = isViewPage || isEditPage;
const pageIsProbablyEditable = mw.config.get( 'wgIsProbablyEditable' ) ||
mw.config.get( 'wgRelevantPageIsProbablyEditable' );
// Cast "0" (T89513)
const enable = !!+mw.user.options.get( 'visualeditor-enable' );
const tempdisable = !!+mw.user.options.get( 'visualeditor-betatempdisable' );
const autodisable = !!+mw.user.options.get( 'visualeditor-autodisable' );
tabPreference = mw.user.options.get( 'visualeditor-tabs' );
/**
* The only edit tab shown to the user is for visual mode
*
* @return {boolean}
*/
function isOnlyTabVE() {
return conf.singleEditTab && getAvailableEditPageEditor() === 'visual';
}
/**
* The only edit tab shown to the user is for source mode
*
* @return {boolean}
*/
function isOnlyTabWikitext() {
return conf.singleEditTab && getAvailableEditPageEditor() === 'source';
}
init = {
/**
* Add a plugin module or function.
*
* Plugins are run after VisualEditor is loaded, but before it is initialized. This allows
* plugins to add classes and register them with the factories and registries.
*
* The parameter to this function can be a ResourceLoader module name or a function.
*
* If it's a module name, it will be loaded together with the VisualEditor core modules when
* VE is loaded. No special care is taken to ensure that the module runs after the VE
* classes are loaded, so if this is desired, the module should depend on
* ext.visualEditor.core .
*
* If it's a function, it will be invoked once the VisualEditor core modules and any
* plugin modules registered through this function have been loaded, but before the editor
* is intialized. The function can optionally return a jQuery.Promise . VisualEditor will
* only be initialized once all promises returned by plugin functions have been resolved.
*
* // Register ResourceLoader module
* mw.libs.ve.addPlugin( 'ext.gadget.foobar' );
*
* // Register a callback
* mw.libs.ve.addPlugin( ( target ) => {
* ve.dm.Foobar = .....
* } );
*
* // Register a callback that loads another script
* mw.libs.ve.addPlugin( () => $.getScript( 'http://example.com/foobar.js' ) );
*
* @param {string|Function} plugin Module name or callback that optionally returns a promise
*/
addPlugin: function ( plugin ) {
plugins.push( plugin );
},
/**
* Adjust edit page links in the current document
*
* This will run multiple times in a page lifecycle, notably when the
* page first loads and after post-save content replacement occurs. It
* needs to avoid doing anything which will cause problems if it's run
* twice or more.
*/
setupEditLinks: function () {
// NWE
if ( init.isWikitextAvailable && !isOnlyTabVE() ) {
$(
// Edit section links, except VE ones when both editors visible
'.mw-editsection a:not( .mw-editsection-visualeditor ),' +
// Edit tab
'#ca-edit a,' +
// Add section is currently a wikitext-only feature
'#ca-addsection a'
).each( ( i, el ) => {
if ( !el.href ) {
// Not a real link, probably added by a gadget or another extension (T328094)
return;
}
const linkUrl = new URL( el.href );
if ( linkUrl.searchParams.has( 'action' ) ) {
linkUrl.searchParams.delete( 'action' );
linkUrl.searchParams.set( 'veaction', 'editsource' );
$( el ).attr( 'href', linkUrl.toString() );
}
} );
}
// Set up the tabs appropriately if the user has VE on
if ( init.isAvailable ) {
// … on two-edit-tab wikis, or single-edit-tab wikis, where the user wants both …
if (
!init.isSingleEditTab && init.isVisualAvailable &&
// T253941: This option does not actually disable the editor, only leaves the tabs/links unchanged
!( conf.disableForAnons && mw.user.isAnon() )
) {
// … set the skin up with both tabs and both section edit links.
init.setupMultiTabSkin();
} else if (
pageCanLoadEditor && (
( init.isVisualAvailable && isOnlyTabVE() ) ||
( init.isWikitextAvailable && isOnlyTabWikitext() )
)
) {
// … on single-edit-tab wikis, where VE or NWE is the user's preferred editor
// Handle section edit link clicks
$( '.mw-editsection a' ).off( '.ve-target' ).on( 'click.ve-target', ( e ) => {
// isOnlyTabVE is computed on click as it may have changed since load
init.onEditSectionLinkClick( isOnlyTabVE() ? 'visual' : 'source', e );
} );
// Allow instant switching to edit mode, without refresh
$( '#ca-edit' ).off( '.ve-target' ).on( 'click.ve-target', ( e ) => {
init.onEditTabClick( isOnlyTabVE() ? 'visual' : 'source', e );
} );
}
}
},
/**
* Setup multiple edit tabs and section links (edit + edit source)
*/
setupMultiTabSkin: function () {
init.setupMultiTabs();
init.setupMultiSectionLinks();
},
/**
* Setup multiple edit tabs (edit + edit source)
*/
setupMultiTabs: function () {
// Minerva puts the '#ca-...' ids on <a> nodes, other skins put them on <li>
const $caEdit = $( '#ca-edit' );
const $caVeEdit = $( '#ca-ve-edit' );
if ( pageCanLoadEditor ) {
// Allow instant switching to edit mode, without refresh
$caVeEdit.off( '.ve-target' ).on( 'click.ve-target', init.onEditTabClick.bind( init, 'visual' ) );
}
if ( pageCanLoadEditor ) {
// Always bind "Edit source" tab, because we want to handle switching with changes
$caEdit.off( '.ve-target' ).on( 'click.ve-target', init.onEditTabClick.bind( init, 'source' ) );
}
if ( pageCanLoadEditor && init.isWikitextAvailable ) {
// Only bind "Add topic" tab if NWE is available, because VE doesn't support section
// so we never have to switch from it when editing a section
$( '#ca-addsection' ).off( '.ve-target' ).on( 'click.ve-target', init.onEditTabClick.bind( init, 'source' ) );
}
if ( init.isVisualAvailable ) {
if ( conf.tabPosition === 'before' ) {
$caEdit.addClass( 'collapsible' );
} else {
$caVeEdit.addClass( 'collapsible' );
}
}
},
/**
* Setup multiple section links (edit + edit source)
*/
setupMultiSectionLinks: function () {
const $editsections = $( '#mw-content-text .mw-editsection' ),
bodyDir = $( document.body ).css( 'direction' );
// Match direction of the user interface
// TODO: Why is this needed? It seems to work fine without.
if ( $editsections.css( 'direction' ) !== bodyDir ) {
// Avoid creating inline style attributes if the inherited value is already correct
$editsections.css( 'direction', bodyDir );
}
if ( pageCanLoadEditor ) {
// Only init without refresh if we're on a view page. Though section edit links
// are rarely shown on non-view pages, they appear in one other case, namely
// when on a diff against the latest version of a page. In that case we mustn't
// init without refresh as that'd initialise for the wrong rev id (T52925)
// and would preserve the wrong DOM with a diff on top.
$editsections.find( '.mw-editsection-visualeditor' )
.off( '.ve-target' ).on( 'click.ve-target', init.onEditSectionLinkClick.bind( init, 'visual' ) );
if ( init.isWikitextAvailable ) {
// TOOD: Make this less fragile
$editsections.find( 'a:not( .mw-editsection-visualeditor )' )
.off( '.ve-target' ).on( 'click.ve-target', init.onEditSectionLinkClick.bind( init, 'source' ) );
}
}
},
/**
* Check whether a jQuery event represents a plain left click, without
* any modifiers or a programmatically triggered click.
*
* This is a duplicate of a function in ve.utils, because this file runs
* before any of VE core or OOui has been loaded.
*
* @param {jQuery.Event} e
* @return {boolean} Whether it was an unmodified left click
*/
isUnmodifiedLeftClick: function ( e ) {
return e && ( (
e.which && e.which === 1 && !( e.shiftKey || e.altKey || e.ctrlKey || e.metaKey )
) || e.isTrigger );
},
/**
* Handle click events on an edit tab
*
* @param {string} mode Edit mode, 'visual' or 'source'
* @param {Event} e Event
*/
onEditTabClick: function ( mode, e ) {
if ( !init.isUnmodifiedLeftClick( e ) ) {
return;
}
if ( !active && mode === 'source' && !init.isWikitextAvailable ) {
// We're not active so we don't need to manage a switch, and
// we don't have source mode available so we don't need to
// activate VE. Just follow the link.
return;
}
e.preventDefault();
if ( isLoading ) {
return;
}
const section = $( e.target ).closest( '#ca-addsection' ).length ? 'new' : null;
if ( active ) {
targetPromise.done( ( target ) => {
if ( target.getDefaultMode() === 'source' ) {
if ( mode === 'visual' ) {
target.switchToVisualEditor();
} else if ( mode === 'source' ) {
// Requested section may have changed --
// switchToWikitextSection will do nothing if the
// section is unchanged.
target.switchToWikitextSection( section );
}
} else if ( target.getDefaultMode() === 'visual' ) {
if ( mode === 'source' ) {
if ( section ) {
// switching from visual via the "add section" tab
target.switchToWikitextSection( section );
} else {
target.editSource();
}
}
// Visual-to-visual doesn't need to do anything,
// because we don't have any section concerns. Just
// no-op it.
}
} );
} else {
const link = $( e.target ).closest( 'a' )[ 0 ];
const linkUrl = link && link.href ? new URL( link.href ) : null;
if ( section !== null ) {
init.activateVe( mode, linkUrl, section );
} else {
// Do not pass `section` to handle switching from section editing in WikiEditor if needed
init.activateVe( mode, linkUrl );
}
}
},
/**
* Activate VE
*
* @param {string} mode Target mode: 'visual' or 'source'
* @param {URL} [linkUrl] URL to navigate to, potentially with extra parameters
* @param {string} [section]
*/
activateVe: function ( mode, linkUrl, section ) {
const wikitext = $( '#wpTextbox1' ).textSelection( 'getContents' ),
modified = mw.config.get( 'wgAction' ) === 'submit' ||
(
mw.config.get( 'wgAction' ) === 'edit' &&
wikitext !== initialWikitext
);
if ( section === undefined ) {
const sectionVal = $( 'input[name=wpSection]' ).val();
section = sectionVal !== '' && sectionVal !== undefined ? sectionVal : null;
}
// Close any open jQuery.UI dialogs (e.g. WikiEditor's find and replace)
if ( $.fn.dialog ) {
$( '.ui-dialog-content' ).dialog( 'close' );
}
// Release the edit warning on #wpTextbox1 which was setup in mediawiki.action.edit.editWarning.js
$( window ).off( 'beforeunload.editwarning' );
activatePageTarget( mode, section, modified, linkUrl );
},
/**
* Handle section edit links being clicked
*
* @param {string} mode Edit mode
* @param {jQuery.Event} e Click event
* @param {string} [section] Override edit section, taken from link URL if not specified
*/
onEditSectionLinkClick: function ( mode, e, section ) {
const link = $( e.target ).closest( 'a' )[ 0 ];
if ( !link || !link.href ) {
// Not a real link, probably added by a gadget or another extension (T328094)
return;
}
const linkUrl = new URL( link.href );
const title = mw.Title.newFromText( linkUrl.searchParams.get( 'title' ) || '' );
if (
// Modified click (e.g. ctrl+click)
!init.isUnmodifiedLeftClick( e ) ||
// Not an edit action
!( linkUrl.searchParams.has( 'action' ) || linkUrl.searchParams.has( 'veaction' ) ) ||
// Edit target is on another host (e.g. commons file)
linkUrl.host !== location.host ||
// Title param doesn't match current page
title && title.getPrefixedText() !== new mw.Title( mw.config.get( 'wgRelevantPageName' ) ).getPrefixedText()
) {
return;
}
e.preventDefault();
if ( isLoading ) {
return;
}
trackActivateStart( { type: 'section', mechanism: section === 'new' ? 'new' : 'click', mode: mode }, linkUrl );
if ( !active ) {
// Replace the current state with one that is tagged as ours, to prevent the
// back button from breaking when used to exit VE. FIXME: there should be a better
// way to do this. See also similar code in the DesktopArticleTarget constructor.
history.replaceState( { tag: 'visualeditor' }, '', url );
// Use linkUrl to preserve the 'section' parameter and others like 'editintro' (T56029)
history.pushState( { tag: 'visualeditor' }, '', linkUrl );
// Update URL instance
url = linkUrl;
// Use section from URL
if ( section === undefined ) {
section = parseSection( linkUrl.searchParams.get( 'section' ) );
}
const tPromise = getTarget( mode, section );
activateTarget( mode, section, tPromise );
}
},
/**
* Check whether the welcome dialog should be shown.
*
* The welcome dialog can be disabled in configuration; or by calling disableWelcomeDialog();
* or using a query string parameter; or if we've recorded that we've already shown it before
* in a user preference, local storage or a cookie.
*
* @return {boolean}
*/
shouldShowWelcomeDialog: function () {
return !(
// Disabled in config?
!mw.config.get( 'wgVisualEditorConfig' ).showBetaWelcome ||
// Disabled for the current request?
this.isWelcomeDialogSuppressed() ||
// Joining a collab session
url.searchParams.has( 'collabSession' ) ||
// Hidden using preferences, local storage or cookie?
checkPreferenceOrStorage( 'visualeditor-hidebetawelcome', 've-beta-welcome-dialog' )
);
},
/**
* Check whether the welcome dialog is temporarily disabled.
*
* @return {boolean}
*/
isWelcomeDialogSuppressed: function () {
return !!(
// Disabled by calling disableWelcomeDialog()?
welcomeDialogDisabled ||
// Hidden using URL parameter?
new URL( location.href ).searchParams.has( 'vehidebetadialog' ) ||
// Check for deprecated hidewelcomedialog parameter (T249954)
new URL( location.href ).searchParams.has( 'hidewelcomedialog' )
);
},
/**
* Record that we've already shown the welcome dialog to this user, so that it won't be shown
* to them again.
*
* Uses a preference for logged-in users; uses local storage or a cookie for anonymous users.
*/
stopShowingWelcomeDialog: function () {
setPreferenceOrStorage( 'visualeditor-hidebetawelcome', 've-beta-welcome-dialog' );
},
/**
* Prevent the welcome dialog from being shown on this page view only.
*
* Causes shouldShowWelcomeDialog() to return false, but doesn't save anything to preferences
* or local storage, so future page views are not affected.
*/
disableWelcomeDialog: function () {
welcomeDialogDisabled = true;
},
/**
* Check whether the user education popups (ve.ui.MWEducationPopupWidget) should be shown.
*
* The education popups can be disabled by calling disableWelcomeDialog(), or if we've
* recorded that we've already shown it before in a user preference, local storage or a cookie.
*
* @return {boolean}
*/
shouldShowEducationPopups: function () {
return !(
// Disabled by calling disableEducationPopups()?
educationPopupsDisabled ||
// Hidden using preferences, local storage, or cookie?
checkPreferenceOrStorage( 'visualeditor-hideusered', 've-hideusered' )
);
},
/**
* Record that we've already shown the education popups to this user, so that it won't be
* shown to them again.
*
* Uses a preference for logged-in users; uses local storage or a cookie for anonymous users.
*/
stopShowingEducationPopups: function () {
setPreferenceOrStorage( 'visualeditor-hideusered', 've-hideusered' );
},
/**
* Prevent the education popups from being shown on this page view only.
*
* Causes shouldShowEducationPopups() to return false, but doesn't save anything to
* preferences or local storage, so future page views are not affected.
*/
disableEducationPopups: function () {
educationPopupsDisabled = true;
}
};
init.isSingleEditTab = conf.singleEditTab && tabPreference !== 'multi-tab';
// On a view page, extend the current URL so extra parameters are carried over
// On a non-view page, use viewUrl
veEditUrl = new URL( pageCanLoadEditor ? url : viewUrl );
if ( oldId ) {
veEditUrl.searchParams.set( 'oldid', oldId );
}
veEditUrl.searchParams.delete( 'veaction' );
veEditUrl.searchParams.delete( 'action' );
if ( init.isSingleEditTab ) {
veEditUrl.searchParams.set( 'action', 'edit' );
veEditSourceUrl = veEditUrl;
} else {
veEditSourceUrl = new URL( veEditUrl );
veEditUrl.searchParams.set( 'veaction', 'edit' );
veEditSourceUrl.searchParams.set( 'veaction', 'editsource' );
}
// Whether VisualEditor should be available for the current user, page, wiki, mediawiki skin,
// browser etc.
init.isAvailable = VisualEditorSupportCheck();
// Extensions can disable VE in certain circumstances using the VisualEditorBeforeEditor hook (T174180)
const enabledForUser = (
// User has 'visualeditor-enable' preference enabled (for alpha opt-in)
// User has 'visualeditor-betatempdisable' preference disabled
// User has 'visualeditor-autodisable' preference disabled
( conf.isBeta ? enable : !tempdisable ) && !autodisable
);
// Duplicated in VisualEditor.hooks.php#isVisualAvailable()
init.isVisualAvailable = (
init.isAvailable &&
// If forced by the URL parameter, skip the namespace check (T221892) and preference check
( url.searchParams.get( 'veaction' ) === 'edit' || (
// Only in enabled namespaces
conf.namespaces.indexOf( new mw.Title( mw.config.get( 'wgRelevantPageName' ) ).getNamespaceId() ) !== -1 &&
// Enabled per user preferences
enabledForUser
) ) &&
// Only for pages with a supported content model
Object.prototype.hasOwnProperty.call( conf.contentModels, mw.config.get( 'wgPageContentModel' ) )
);
// Duplicated in VisualEditor.hooks.php#isWikitextAvailable()
init.isWikitextAvailable = (
init.isAvailable &&
// Enabled on site
conf.enableWikitext &&
// User preference
mw.user.options.get( 'visualeditor-newwikitext' ) &&
// Only on wikitext pages
mw.config.get( 'wgPageContentModel' ) === 'wikitext'
);
if ( init.isVisualAvailable ) {
availableModes.push( 'visual' );
}
if ( init.isWikitextAvailable ) {
availableModes.push( 'source' );
}
// FIXME: We should do this more elegantly
init.setEditorPreference = setEditorPreference;
init.updateTabs = updateTabs;
// Note: Though VisualEditor itself only needed this exposure for a very small reason
// (namely to access the old init.unsupportedList from the unit tests...) this has become one
// of the nicest ways to easily detect whether the VisualEditor initialisation code is present.
//
// The VE global was once available always, but now that platform integration initialisation
// is properly separated, it doesn't exist until the platform loads VisualEditor core.
//
// Most of mw.libs.ve is considered subject to change and private. An exception is that
// mw.libs.ve.isVisualAvailable is public, and indicates whether the VE editor itself can be loaded
// on this page. See above for why it may be false.
mw.libs.ve = $.extend( mw.libs.ve || {}, init );
if ( init.isVisualAvailable ) {
$( 'html' ).addClass( 've-available' );
} else {
$( 'html' ).addClass( 've-not-available' );
// Don't return here because we do want the skin setup to consistently happen
// for e.g. "Edit" > "Edit source" even when VE is not available.
}
/**
* Check if a URL doesn't contain any params which would prevent VE from loading, e.g. 'undo'
*
* @param {URL} editUrl
* @return {boolean} URL contains no unsupported params
*/
function isSupportedEditPage( editUrl ) {
return configData.unsupportedEditParams.every( ( param ) => !editUrl.searchParams.has( param ) );
}
/**
* Get the edit mode for the given URL
*
* @param {URL} editUrl Edit URL
* @return {string|null} 'visual' or 'source', null if the editor is not being loaded
*/
function getEditModeFromUrl( editUrl ) {
if ( mw.config.get( 'wgDiscussionToolsStartNewTopicTool' ) ) {
// Avoid conflicts with DiscussionTools
return null;
}
if ( isViewPage && init.isAvailable ) {
// On view pages if veaction is correctly set
const mode = veactionToMode[ editUrl.searchParams.get( 'veaction' ) ] ||
// Always load VE visual mode if collabSession is set
( editUrl.searchParams.has( 'collabSession' ) ? 'visual' : null );
if ( mode && availableModes.indexOf( mode ) !== -1 ) {
return mode;
}
}
// Edit pages
if ( isEditPage && isSupportedEditPage( editUrl ) ) {
// User has disabled VE, or we are in view source only mode, or we have landed here with posted data
if ( !enabledForUser || $( '#ca-viewsource' ).length || mw.config.get( 'wgAction' ) === 'submit' ) {
return null;
}
return getAvailableEditPageEditor();
}
return null;
}
$( () => {
$targetContainer = $(
document.querySelector( '[data-mw-ve-target-container]' ) ||
document.getElementById( 'content' )
);
if ( pageCanLoadEditor ) {
$targetContainer.addClass( 've-init-mw-desktopArticleTarget-targetContainer' );
}
let showWikitextWelcome = true;
const numEditButtons = $( '#ca-edit, #ca-ve-edit' ).length,
section = parseSection( url.searchParams.get( 'section' ) );
const requiredSkinElements =
$targetContainer.length &&
$( '#mw-content-text' ).length &&
// A link to open the editor is technically not necessary if it's going to open itself
( isEditPage || numEditButtons );
if ( url.searchParams.get( 'action' ) === 'edit' && $( '#wpTextbox1' ).length ) {
initialWikitext = $( '#wpTextbox1' ).textSelection( 'getContents' );
}
if ( ( init.isVisualAvailable || init.isWikitextAvailable ) &&
pageCanLoadEditor &&
pageIsProbablyEditable &&
!requiredSkinElements
) {
mw.log.warn(
'Your skin is incompatible with VisualEditor. ' +
'See https://www.mediawiki.org/wiki/Extension:VisualEditor/Skin_requirements for the requirements.'
);
// If the edit buttons are not there it's likely a browser extension or gadget for anonymous user
// has removed them. We're not interested in errors from this scenario so don't log.
// If they exist log the error so we can address the problem.
if ( numEditButtons > 0 ) {
const err = new Error( 'Incompatible with VisualEditor' );
err.name = 'VeIncompatibleSkinWarning';
mw.errorLogger.logError( err, 'error.visualeditor' );
}
} else if ( init.isAvailable ) {
const mode = getEditModeFromUrl( url );
if ( mode ) {
showWikitextWelcome = false;
trackActivateStart( {
type: section === null ? 'page' : 'section',
mechanism: ( section === 'new' || !mw.config.get( 'wgArticleId' ) ) ? 'url-new' : 'url',
mode: mode
} );
activateTarget( mode, section );
} else if (
init.isVisualAvailable &&
pageCanLoadEditor &&
init.isSingleEditTab
) {
// In single edit tab mode we never have an edit tab
// with accesskey 'v' so create one
$( document.body ).append(
$( '<a>' )
.attr( { accesskey: mw.msg( 'accesskey-ca-ve-edit' ), href: veEditUrl } )
// Accesskey fires a click event
.on( 'click.ve-target', init.onEditTabClick.bind( init, 'visual' ) )
.addClass( 'oo-ui-element-hidden' )
);
}
// Add the switch button to WikiEditor on edit pages
if (
init.isVisualAvailable &&
isEditPage &&
$( '#wpTextbox1' ).length
) {
mw.loader.load( 'ext.visualEditor.switching' );
mw.hook( 'wikiEditor.toolbarReady' ).add( ( $textarea ) => {
mw.loader.using( 'ext.visualEditor.switching' ).done( () => {
const showPopup = url.searchParams.has( 'veswitched' ) && !mw.user.options.get( 'visualeditor-hidesourceswitchpopup' ),
toolFactory = new OO.ui.ToolFactory(),
toolGroupFactory = new OO.ui.ToolGroupFactory();
toolFactory.register( mw.libs.ve.MWEditModeVisualTool );
toolFactory.register( mw.libs.ve.MWEditModeSourceTool );
const switchToolbar = new OO.ui.Toolbar( toolFactory, toolGroupFactory, {
classes: [ 've-init-mw-editSwitch' ]
} );
switchToolbar.on( 'switchEditor', ( m ) => {
if ( m === 'visual' ) {
$( '#wpTextbox1' ).trigger( 'wikiEditor-switching-visualeditor' );
init.activateVe( 'visual' );
}
} );
switchToolbar.setup( [ {
name: 'editMode',
type: 'list',
icon: 'edit',
title: mw.msg( 'visualeditor-mweditmode-tooltip' ),
label: mw.msg( 'visualeditor-mweditmode-tooltip' ),
invisibleLabel: true,
include: [ 'editModeVisual', 'editModeSource' ]
} ] );
const popup = new mw.libs.ve.SwitchPopupWidget( 'source' );
switchToolbar.tools.editModeVisual.toolGroup.$element.append( popup.$element );
switchToolbar.emit( 'updateState' );
$textarea.wikiEditor( 'addToToolbar', {
section: 'secondary',
group: 'default',
tools: {
veEditSwitch: {
type: 'element',
element: switchToolbar.$element
}
}
} );
popup.toggle( showPopup );
// Duplicate of this code in ve.init.mw.DesktopArticleTarget.js
// eslint-disable-next-line no-jquery/no-class-state
if ( $( '#ca-edit' ).hasClass( 'visualeditor-showtabdialog' ) ) {
$( '#ca-edit' ).removeClass( 'visualeditor-showtabdialog' );
// Set up a temporary window manager
const windowManager = new OO.ui.WindowManager();
$( OO.ui.getTeleportTarget() ).append( windowManager.$element );
const editingTabDialog = new mw.libs.ve.EditingTabDialog();
windowManager.addWindows( [ editingTabDialog ] );
windowManager.openWindow( editingTabDialog )
.closed.then( ( data ) => {
// Detach the temporary window manager
windowManager.destroy();
if ( data && data.action === 'prefer-ve' ) {
location.href = veEditUrl;
} else if ( data && data.action === 'multi-tab' ) {
location.reload();
}
} );
}
} );
} );
// Remember that the user wanted wikitext, at least this time
mw.libs.ve.setEditorPreference( 'wikitext' );
// If the user has loaded WikiEditor, clear any auto-save state they
// may have from a previous VE session
// We don't have access to the VE session storage methods, but invalidating
// the docstate is sufficient to prevent the data from being used.
mw.storage.session.remove( 've-docstate' );
}
init.setupEditLinks();
}
if (
pageCanLoadEditor &&
showWikitextWelcome &&
// At least one editor is available (T201928)
( init.isVisualAvailable || init.isWikitextAvailable || $( '#wpTextbox1' ).length ) &&
isEditPage &&
init.shouldShowWelcomeDialog() &&
// Not on protected pages
pageIsProbablyEditable
) {
mw.loader.using( 'ext.visualEditor.welcome' ).done( () => {
// Check shouldShowWelcomeDialog() again: any code that might have called
// stopShowingWelcomeDialog() wouldn't have had an opportunity to do that
// yet by the first time we checked
if ( !init.shouldShowWelcomeDialog() ) {
return;
}
const windowManager = new OO.ui.WindowManager();
const welcomeDialog = new mw.libs.ve.WelcomeDialog();
$( OO.ui.getTeleportTarget() ).append( windowManager.$element );
windowManager.addWindows( [ welcomeDialog ] );
windowManager.openWindow(
welcomeDialog,
{
switchable: init.isVisualAvailable,
editor: 'source'
}
)
.closed.then( ( data ) => {
windowManager.destroy();
if ( data && data.action === 'switch-ve' ) {
init.activateVe( 'visual' );
}
} );
init.stopShowingWelcomeDialog();
} );
}
if ( url.searchParams.has( 'venotify' ) ) {
url.searchParams.delete( 'venotify' );
// Get rid of the ?venotify= from the URL
history.replaceState( null, '', url );
}
} );
}() );