2015-12-11 14:57:49 +00:00
|
|
|
/*!
|
|
|
|
* VisualEditor MediaWiki Initialization Target class.
|
|
|
|
*
|
2023-12-01 16:06:11 +00:00
|
|
|
* @copyright See AUTHORS.txt
|
2015-12-11 14:57:49 +00:00
|
|
|
* @license The MIT License (MIT); see LICENSE.txt
|
|
|
|
*/
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Initialization MediaWiki target.
|
|
|
|
*
|
|
|
|
* @class
|
|
|
|
* @extends ve.init.Target
|
|
|
|
*
|
|
|
|
* @constructor
|
2021-09-11 08:05:31 +00:00
|
|
|
* @param {Object} config
|
2024-05-27 04:59:02 +00:00
|
|
|
* @param {string[]} [config.surfaceClasses=[]] Surface classes to apply
|
2015-12-11 14:57:49 +00:00
|
|
|
*/
|
|
|
|
ve.init.mw.Target = function VeInitMwTarget( config ) {
|
2021-05-17 21:39:22 +00:00
|
|
|
this.surfaceClasses = config.surfaceClasses || [];
|
|
|
|
|
2015-12-11 14:57:49 +00:00
|
|
|
// Parent constructor
|
|
|
|
ve.init.mw.Target.super.call( this, config );
|
|
|
|
|
2016-11-22 02:02:12 +00:00
|
|
|
this.active = false;
|
2017-05-20 21:24:10 +00:00
|
|
|
this.pageName = mw.config.get( 'wgRelevantPageName' );
|
2019-04-17 16:28:48 +00:00
|
|
|
this.recovered = false;
|
|
|
|
this.fromEditedState = false;
|
|
|
|
this.originalHtml = null;
|
2016-11-22 02:02:12 +00:00
|
|
|
|
2015-12-11 14:57:49 +00:00
|
|
|
// Initialization
|
|
|
|
this.$element.addClass( 've-init-mw-target' );
|
|
|
|
};
|
|
|
|
|
|
|
|
/* Inheritance */
|
|
|
|
|
|
|
|
OO.inheritClass( ve.init.mw.Target, ve.init.Target );
|
|
|
|
|
|
|
|
/* Static Properties */
|
|
|
|
|
2016-04-21 11:28:00 +00:00
|
|
|
/**
|
|
|
|
* Symbolic name for this target class.
|
|
|
|
*
|
|
|
|
* @static
|
|
|
|
* @property {string}
|
|
|
|
* @inheritable
|
|
|
|
*/
|
|
|
|
ve.init.mw.Target.static.name = null;
|
|
|
|
|
2015-12-11 14:57:49 +00:00
|
|
|
ve.init.mw.Target.static.toolbarGroups = [
|
|
|
|
{
|
2018-05-12 15:18:07 +00:00
|
|
|
name: 'history',
|
2023-06-15 23:02:57 +00:00
|
|
|
include: [ { group: 'history' } ]
|
2018-05-12 15:18:07 +00:00
|
|
|
},
|
|
|
|
{
|
|
|
|
name: 'format',
|
2015-12-11 14:57:49 +00:00
|
|
|
type: 'menu',
|
|
|
|
title: OO.ui.deferMsg( 'visualeditor-toolbar-format-tooltip' ),
|
|
|
|
include: [ { group: 'format' } ],
|
|
|
|
promote: [ 'paragraph' ],
|
|
|
|
demote: [ 'preformatted', 'blockquote', 'heading1' ]
|
|
|
|
},
|
|
|
|
{
|
2018-05-12 15:18:07 +00:00
|
|
|
name: 'style',
|
2015-12-11 14:57:49 +00:00
|
|
|
type: 'list',
|
|
|
|
icon: 'textStyle',
|
|
|
|
title: OO.ui.deferMsg( 'visualeditor-toolbar-style-tooltip' ),
|
2020-03-13 21:03:50 +00:00
|
|
|
label: OO.ui.deferMsg( 'visualeditor-toolbar-style-tooltip' ),
|
|
|
|
invisibleLabel: true,
|
2023-07-27 13:47:20 +00:00
|
|
|
include: [ { group: 'textStyle' } ],
|
2015-12-11 14:57:49 +00:00
|
|
|
forceExpand: [ 'bold', 'italic', 'clear' ],
|
2023-07-27 13:47:20 +00:00
|
|
|
promote: [ 'bold', 'italic', 'superscript', 'subscript' ],
|
|
|
|
demote: [ 'clear' ]
|
2015-12-11 14:57:49 +00:00
|
|
|
},
|
|
|
|
{
|
2018-05-12 15:18:07 +00:00
|
|
|
name: 'link',
|
|
|
|
include: [ 'link' ]
|
|
|
|
},
|
|
|
|
// Placeholder for reference tools (e.g. Cite and/or Citoid)
|
|
|
|
{
|
|
|
|
name: 'reference'
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: 'structure',
|
2015-12-11 14:57:49 +00:00
|
|
|
type: 'list',
|
|
|
|
icon: 'listBullet',
|
2016-03-29 11:05:06 +00:00
|
|
|
title: OO.ui.deferMsg( 'visualeditor-toolbar-structure' ),
|
2020-03-13 21:03:50 +00:00
|
|
|
label: OO.ui.deferMsg( 'visualeditor-toolbar-structure' ),
|
|
|
|
invisibleLabel: true,
|
2015-12-11 14:57:49 +00:00
|
|
|
include: [ { group: 'structure' } ],
|
|
|
|
demote: [ 'outdent', 'indent' ]
|
|
|
|
},
|
|
|
|
{
|
2018-05-12 15:18:07 +00:00
|
|
|
name: 'insert',
|
2015-12-11 14:57:49 +00:00
|
|
|
label: OO.ui.deferMsg( 'visualeditor-toolbar-insert' ),
|
2016-03-29 11:05:06 +00:00
|
|
|
title: OO.ui.deferMsg( 'visualeditor-toolbar-insert' ),
|
2022-05-20 16:25:41 +00:00
|
|
|
narrowConfig: {
|
|
|
|
invisibleLabel: true,
|
|
|
|
icon: 'add'
|
|
|
|
},
|
2015-12-11 14:57:49 +00:00
|
|
|
include: '*',
|
|
|
|
forceExpand: [ 'media', 'transclusion', 'insertTable' ],
|
|
|
|
promote: [ 'media', 'transclusion', 'insertTable' ]
|
|
|
|
},
|
2018-05-12 15:18:07 +00:00
|
|
|
{
|
|
|
|
name: 'specialCharacter',
|
|
|
|
include: [ 'specialCharacter' ]
|
|
|
|
}
|
2015-12-11 14:57:49 +00:00
|
|
|
];
|
|
|
|
|
2019-04-09 17:46:19 +00:00
|
|
|
ve.init.mw.Target.static.importRules = ve.copy( ve.init.mw.Target.static.importRules );
|
|
|
|
|
2019-04-23 18:48:49 +00:00
|
|
|
ve.init.mw.Target.static.importRules.external.removeOriginalDomElements = true;
|
|
|
|
|
2019-04-09 17:46:19 +00:00
|
|
|
ve.init.mw.Target.static.importRules.external.blacklist = ve.extendObject( {
|
|
|
|
// Annotations
|
|
|
|
'textStyle/underline': true,
|
|
|
|
'meta/language': true,
|
|
|
|
'textStyle/datetime': true,
|
|
|
|
'link/mwExternal': !mw.config.get( 'wgVisualEditorConfig' ).allowExternalLinkPaste,
|
|
|
|
// Node
|
|
|
|
article: true,
|
|
|
|
section: true
|
|
|
|
}, ve.init.mw.Target.static.importRules.external.blacklist );
|
|
|
|
|
2019-09-04 16:14:38 +00:00
|
|
|
ve.init.mw.Target.static.importRules.external.htmlBlacklist.remove = ve.extendObject( {
|
2019-09-10 11:32:10 +00:00
|
|
|
// TODO: Create a plugin system for extending the blacklist, so this code
|
|
|
|
// can be moved to the Cite extension.
|
2019-04-09 17:46:19 +00:00
|
|
|
// Remove reference numbers copied from MW read mode (T150418)
|
2019-09-10 11:32:10 +00:00
|
|
|
'sup.reference:not( [typeof] )': true,
|
|
|
|
// ...sometimes we need a looser match if the HTML has been mangled
|
|
|
|
// in a third-party editor e.g. LibreOffice (T232461)
|
|
|
|
'a[ href *= "#cite_note" ]': true
|
2019-09-04 16:14:38 +00:00
|
|
|
}, ve.init.mw.Target.static.importRules.external.htmlBlacklist.remove );
|
2019-02-27 19:54:55 +00:00
|
|
|
|
2023-06-15 15:18:23 +00:00
|
|
|
// This is required to prevent an invalid insertion (as mwHeading can only be at the root) (T339155)
|
|
|
|
// TODO: This should be handled by the DM based on ve.dm.MWHeadingNode.static.suggestedParentNodeTypes,
|
|
|
|
// rather than just throwing an exception.
|
|
|
|
// This would also not prevent pasting from a VE standalone editor as that is considered
|
|
|
|
// an internal paste.
|
|
|
|
ve.init.mw.Target.static.importRules.external.htmlBlacklist.unwrap = ve.extendObject( {
|
|
|
|
'li h1, li h2, li h3, li h4, li h5, li h6': true,
|
|
|
|
'blockquote h1, blockquote h2, blockquote h3, blockquote h4, blockquote h5, blockquote h6': true
|
|
|
|
}, ve.init.mw.Target.static.importRules.external.htmlBlacklist.unwrap );
|
|
|
|
|
2015-12-11 14:57:49 +00:00
|
|
|
/**
|
2023-03-18 02:34:32 +00:00
|
|
|
* Type of integration. Used for event tracking.
|
2015-12-11 14:57:49 +00:00
|
|
|
*
|
|
|
|
* @static
|
|
|
|
* @property {string}
|
|
|
|
* @inheritable
|
|
|
|
*/
|
|
|
|
ve.init.mw.Target.static.integrationType = null;
|
|
|
|
|
|
|
|
/**
|
2023-03-18 02:34:32 +00:00
|
|
|
* Type of platform. Used for event tracking.
|
2015-12-11 14:57:49 +00:00
|
|
|
*
|
|
|
|
* @static
|
|
|
|
* @property {string}
|
|
|
|
* @inheritable
|
|
|
|
*/
|
|
|
|
ve.init.mw.Target.static.platformType = null;
|
|
|
|
|
|
|
|
/* Static Methods */
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Fix the base URL from Parsoid if necessary.
|
|
|
|
*
|
|
|
|
* Absolutizes the base URL if it's relative, and sets a base URL based on wgArticlePath
|
|
|
|
* if there was no base URL at all.
|
|
|
|
*
|
|
|
|
* @param {HTMLDocument} doc Parsoid document
|
|
|
|
*/
|
|
|
|
ve.init.mw.Target.static.fixBase = function ( doc ) {
|
|
|
|
ve.fixBase( doc, document, ve.resolveUrl(
|
|
|
|
// Don't replace $1 with the page name, because that'll break if
|
|
|
|
// the page name contains a slash
|
|
|
|
mw.config.get( 'wgArticlePath' ).replace( '$1', '' ),
|
|
|
|
document
|
|
|
|
) );
|
|
|
|
};
|
|
|
|
|
2017-06-08 13:35:31 +00:00
|
|
|
/**
|
2018-09-06 12:11:16 +00:00
|
|
|
* @inheritdoc
|
2017-06-08 13:35:31 +00:00
|
|
|
*/
|
2018-09-06 12:11:16 +00:00
|
|
|
ve.init.mw.Target.static.createModelFromDom = function ( doc, mode, options ) {
|
2024-05-21 14:22:56 +00:00
|
|
|
const conf = mw.config.get( 'wgVisualEditor' );
|
2017-06-08 13:35:31 +00:00
|
|
|
|
2018-09-06 12:11:16 +00:00
|
|
|
options = ve.extendObject( {
|
|
|
|
lang: conf.pageLanguageCode,
|
|
|
|
dir: conf.pageLanguageDir
|
|
|
|
}, options );
|
|
|
|
|
|
|
|
// Parent method
|
|
|
|
return ve.init.mw.Target.super.static.createModelFromDom.call( this, doc, mode, options );
|
2017-06-08 13:35:31 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
// Deprecated alias
|
|
|
|
ve.init.mw.Target.prototype.createModelFromDom = function () {
|
|
|
|
return this.constructor.static.createModelFromDom.apply( this.constructor.static, arguments );
|
|
|
|
};
|
|
|
|
|
2015-12-11 14:57:49 +00:00
|
|
|
/**
|
2016-11-30 18:41:50 +00:00
|
|
|
* @inheritdoc
|
2019-04-16 15:17:29 +00:00
|
|
|
* @param {string} documentString
|
|
|
|
* @param {string} mode
|
2020-03-31 20:04:30 +00:00
|
|
|
* @param {string|null} [section] Section. Use null to unwrap all sections.
|
2022-02-14 15:18:57 +00:00
|
|
|
* @param {boolean} [onlySection=false] Only return the requested section, otherwise returns the
|
2019-02-13 13:21:26 +00:00
|
|
|
* whole document with just the requested section still wrapped (visual mode only).
|
2019-11-01 15:45:27 +00:00
|
|
|
* @return {HTMLDocument|string} HTML document, or document string (source mode)
|
2015-12-11 14:57:49 +00:00
|
|
|
*/
|
2019-02-13 13:21:26 +00:00
|
|
|
ve.init.mw.Target.static.parseDocument = function ( documentString, mode, section, onlySection ) {
|
2024-05-21 14:22:56 +00:00
|
|
|
let doc;
|
2016-11-30 12:03:56 +00:00
|
|
|
if ( mode === 'source' ) {
|
2016-11-30 18:41:50 +00:00
|
|
|
// Parent method
|
2017-06-21 00:51:51 +00:00
|
|
|
doc = ve.init.mw.Target.super.static.parseDocument.call( this, documentString, mode );
|
2016-11-14 16:07:13 +00:00
|
|
|
} else {
|
2023-04-12 15:32:59 +00:00
|
|
|
doc = ve.createDocumentFromHtml( documentString );
|
2018-03-12 12:24:18 +00:00
|
|
|
if ( section !== undefined ) {
|
2019-02-13 13:21:26 +00:00
|
|
|
if ( onlySection ) {
|
2024-05-21 14:22:56 +00:00
|
|
|
const sectionNode = doc.body.querySelector( '[data-mw-section-id="' + section + '"]' );
|
2019-02-13 13:21:26 +00:00
|
|
|
doc.body.innerHTML = '';
|
|
|
|
if ( sectionNode ) {
|
|
|
|
doc.body.appendChild( sectionNode );
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// Strip Parsoid sections
|
2020-03-20 17:08:24 +00:00
|
|
|
mw.libs.ve.unwrapParsoidSections( doc.body, section );
|
2018-03-12 12:24:18 +00:00
|
|
|
}
|
|
|
|
}
|
2017-12-06 20:24:49 +00:00
|
|
|
// Strip legacy IDs, for example in section headings
|
2020-03-20 17:08:24 +00:00
|
|
|
mw.libs.ve.stripParsoidFallbackIds( doc.body );
|
2022-01-24 21:11:29 +00:00
|
|
|
// Re-duplicate deduplicated TemplateStyles, for correct rendering when editing a section or
|
|
|
|
// when templates are removed during the edit
|
|
|
|
mw.libs.ve.reduplicateStyles( doc.body );
|
2018-09-06 12:11:16 +00:00
|
|
|
// Fix relative or missing base URL if needed
|
|
|
|
this.fixBase( doc );
|
2022-05-10 16:29:36 +00:00
|
|
|
// Test: Remove tags injected by plugins during parse (T298147)
|
2024-04-30 16:44:25 +00:00
|
|
|
Array.prototype.forEach.call( doc.querySelectorAll( 'script' ), ( element ) => {
|
2022-05-10 16:29:36 +00:00
|
|
|
function truncate( text, l ) {
|
|
|
|
return text.length > l ? text.slice( 0, l ) + '…' : text;
|
|
|
|
}
|
2024-05-21 14:22:56 +00:00
|
|
|
const errorMessage = 'DOM content matching deny list found during parse:\n' + truncate( element.outerHTML, 100 ) +
|
2022-05-11 23:20:54 +00:00
|
|
|
'\nContext:\n' + truncate( element.parentNode.outerHTML, 200 );
|
2022-05-10 16:29:36 +00:00
|
|
|
mw.log.error( errorMessage );
|
2024-05-21 14:22:56 +00:00
|
|
|
const err = new Error( errorMessage );
|
2022-05-10 16:29:36 +00:00
|
|
|
err.name = 'VeDomDenyListWarning';
|
|
|
|
mw.errorLogger.logError( err, 'error.visualeditor' );
|
|
|
|
element.parentNode.removeChild( element );
|
|
|
|
} );
|
2016-11-14 16:07:13 +00:00
|
|
|
}
|
2016-12-19 21:08:58 +00:00
|
|
|
|
2015-12-11 14:57:49 +00:00
|
|
|
return doc;
|
|
|
|
};
|
|
|
|
|
2017-06-08 13:43:09 +00:00
|
|
|
/* Methods */
|
|
|
|
|
2015-12-11 14:57:49 +00:00
|
|
|
/**
|
|
|
|
* Handle both DOM and modules being loaded and ready.
|
|
|
|
*
|
2018-09-06 12:11:16 +00:00
|
|
|
* @param {HTMLDocument|string} doc HTML document or source text
|
2015-12-11 14:57:49 +00:00
|
|
|
*/
|
|
|
|
ve.init.mw.Target.prototype.documentReady = function ( doc ) {
|
2018-08-25 12:59:24 +00:00
|
|
|
this.setupSurface( doc );
|
2015-12-11 14:57:49 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
2017-03-22 20:35:42 +00:00
|
|
|
* Once surface is ready, initialize the UI
|
2015-12-11 14:57:49 +00:00
|
|
|
*
|
2024-04-29 18:14:26 +00:00
|
|
|
* @fires ve.init.Target#surfaceReady
|
2015-12-11 14:57:49 +00:00
|
|
|
*/
|
|
|
|
ve.init.mw.Target.prototype.surfaceReady = function () {
|
|
|
|
this.emit( 'surfaceReady' );
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
2019-11-01 15:45:27 +00:00
|
|
|
* @deprecated Moved to mw.libs.ve.targetSaver.getHtml
|
|
|
|
* @param {HTMLDocument} newDoc
|
|
|
|
* @param {HTMLDocument} [oldDoc]
|
|
|
|
* @return {string}
|
2015-12-11 14:57:49 +00:00
|
|
|
*/
|
|
|
|
ve.init.mw.Target.prototype.getHtml = function ( newDoc, oldDoc ) {
|
2019-11-01 15:45:27 +00:00
|
|
|
OO.ui.warnDeprecation( 've.init.mw.Target#getHtml is deprecated. Use mw.libs.ve.targetSaver.getHtml.' );
|
|
|
|
return mw.libs.ve.targetSaver.getHtml( newDoc, oldDoc );
|
2015-12-11 14:57:49 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Track an event
|
|
|
|
*
|
|
|
|
* @param {string} name Event name
|
|
|
|
*/
|
|
|
|
ve.init.mw.Target.prototype.track = function () {};
|
|
|
|
|
2021-05-17 21:39:22 +00:00
|
|
|
/**
|
|
|
|
* Get a list of CSS classes to be added to surfaces, and target widget surfaces
|
|
|
|
*
|
|
|
|
* @return {string[]} CSS classes
|
|
|
|
*/
|
|
|
|
ve.init.mw.Target.prototype.getSurfaceClasses = function () {
|
|
|
|
return this.surfaceClasses;
|
|
|
|
};
|
|
|
|
|
2016-03-02 15:22:57 +00:00
|
|
|
/**
|
|
|
|
* @inheritdoc
|
|
|
|
*/
|
2016-11-20 22:53:34 +00:00
|
|
|
ve.init.mw.Target.prototype.createTargetWidget = function ( config ) {
|
2019-10-09 13:55:40 +00:00
|
|
|
return new ve.ui.MWTargetWidget( ve.extendObject( {
|
2016-11-14 16:07:13 +00:00
|
|
|
// Reset to visual mode for target widgets
|
2019-10-09 13:55:40 +00:00
|
|
|
modes: [ 'visual' ],
|
2020-03-11 14:20:03 +00:00
|
|
|
defaultMode: 'visual',
|
2024-04-30 16:44:25 +00:00
|
|
|
toolbarGroups: this.toolbarGroups.filter( ( group ) => group.align !== 'after' ),
|
2021-05-17 21:39:22 +00:00
|
|
|
surfaceClasses: this.getSurfaceClasses()
|
2019-10-09 13:55:40 +00:00
|
|
|
}, config ) );
|
2016-03-02 15:22:57 +00:00
|
|
|
};
|
|
|
|
|
2015-12-11 14:57:49 +00:00
|
|
|
/**
|
|
|
|
* @inheritdoc
|
|
|
|
*/
|
2016-11-14 16:07:13 +00:00
|
|
|
ve.init.mw.Target.prototype.createSurface = function ( dmDoc, config ) {
|
2016-11-30 12:03:56 +00:00
|
|
|
if ( config && config.mode === 'source' ) {
|
2024-05-21 14:22:56 +00:00
|
|
|
const importRules = ve.copy( this.constructor.static.importRules );
|
2016-11-14 16:07:13 +00:00
|
|
|
importRules.all = importRules.all || {};
|
|
|
|
// Preserve empty linebreaks on paste in source editor
|
|
|
|
importRules.all.keepEmptyContentBranches = true;
|
2016-11-30 12:03:56 +00:00
|
|
|
config = this.getSurfaceConfig( ve.extendObject( {}, config, {
|
2016-11-14 16:07:13 +00:00
|
|
|
importRules: importRules
|
2016-11-30 12:03:56 +00:00
|
|
|
} ) );
|
2023-01-28 14:05:05 +00:00
|
|
|
return new ve.ui.MWWikitextSurface( this, dmDoc, config );
|
2016-11-14 16:07:13 +00:00
|
|
|
}
|
2015-12-11 14:57:49 +00:00
|
|
|
|
2023-01-28 14:05:05 +00:00
|
|
|
return new ve.ui.MWSurface( this, dmDoc, this.getSurfaceConfig( config ) );
|
2017-10-10 20:14:25 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @inheritdoc
|
|
|
|
*/
|
|
|
|
ve.init.mw.Target.prototype.getSurfaceConfig = function ( config ) {
|
|
|
|
// If we're not asking for a specific mode's config, use the default mode.
|
|
|
|
config = ve.extendObject( { mode: this.defaultMode }, config );
|
2021-05-17 21:39:22 +00:00
|
|
|
// eslint-disable-next-line mediawiki/class-doc
|
2017-10-10 20:14:25 +00:00
|
|
|
return ve.init.mw.Target.super.prototype.getSurfaceConfig.call( this, ve.extendObject( {
|
|
|
|
// Provide the wikitext versions of the registries, if we're using source mode
|
|
|
|
commandRegistry: config.mode === 'source' ? ve.ui.wikitextCommandRegistry : ve.ui.commandRegistry,
|
|
|
|
sequenceRegistry: config.mode === 'source' ? ve.ui.wikitextSequenceRegistry : ve.ui.sequenceRegistry,
|
2021-05-17 21:39:22 +00:00
|
|
|
dataTransferHandlerFactory: config.mode === 'source' ? ve.ui.wikitextDataTransferHandlerFactory : ve.ui.dataTransferHandlerFactory,
|
|
|
|
classes: this.getSurfaceClasses()
|
2017-10-10 20:14:25 +00:00
|
|
|
}, config ) );
|
2015-12-11 14:57:49 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Switch to editing mode.
|
|
|
|
*
|
2018-09-06 12:11:16 +00:00
|
|
|
* @param {HTMLDocument|string} doc HTML document or source text
|
2015-12-11 14:57:49 +00:00
|
|
|
*/
|
2018-08-25 12:59:24 +00:00
|
|
|
ve.init.mw.Target.prototype.setupSurface = function ( doc ) {
|
2024-04-30 16:44:25 +00:00
|
|
|
setTimeout( () => {
|
2015-12-11 14:57:49 +00:00
|
|
|
// Build model
|
2024-05-01 12:32:49 +00:00
|
|
|
this.track( 'trace.convertModelFromDom.enter' );
|
2024-05-21 14:22:56 +00:00
|
|
|
const dmDoc = this.constructor.static.createModelFromDom( doc, this.getDefaultMode() );
|
2024-05-01 12:32:49 +00:00
|
|
|
this.track( 'trace.convertModelFromDom.exit' );
|
2015-12-11 14:57:49 +00:00
|
|
|
|
|
|
|
// Build DM tree now (otherwise it gets lazily built when building the CE tree)
|
2024-05-01 12:32:49 +00:00
|
|
|
this.track( 'trace.buildModelTree.enter' );
|
2015-12-11 14:57:49 +00:00
|
|
|
dmDoc.buildNodeTree();
|
2024-05-01 12:32:49 +00:00
|
|
|
this.track( 'trace.buildModelTree.exit' );
|
2015-12-11 14:57:49 +00:00
|
|
|
|
2024-04-30 16:44:25 +00:00
|
|
|
setTimeout( () => {
|
2024-05-01 12:32:49 +00:00
|
|
|
this.addSurface( dmDoc );
|
2015-12-11 14:57:49 +00:00
|
|
|
} );
|
|
|
|
} );
|
|
|
|
};
|
2016-05-26 10:56:08 +00:00
|
|
|
|
2018-08-25 12:59:24 +00:00
|
|
|
/**
|
|
|
|
* @inheritdoc
|
|
|
|
*/
|
|
|
|
ve.init.mw.Target.prototype.addSurface = function () {
|
|
|
|
// Clear dummy surfaces
|
|
|
|
// TODO: Move to DesktopArticleTarget
|
|
|
|
this.clearSurfaces();
|
|
|
|
|
|
|
|
// Create ui.Surface (also creates ce.Surface and dm.Surface and builds CE tree)
|
|
|
|
this.track( 'trace.createSurface.enter' );
|
|
|
|
// Parent method
|
2024-05-21 14:22:56 +00:00
|
|
|
const surface = ve.init.mw.Target.super.prototype.addSurface.apply( this, arguments );
|
2018-08-25 12:59:24 +00:00
|
|
|
// Add classes specific to surfaces attached directly to the target,
|
|
|
|
// as opposed to TargetWidget surfaces
|
2021-06-04 15:26:01 +00:00
|
|
|
if ( !surface.inTargetWidget ) {
|
|
|
|
surface.$element.addClass( 've-init-mw-target-surface' );
|
|
|
|
}
|
2018-08-25 12:59:24 +00:00
|
|
|
this.track( 'trace.createSurface.exit' );
|
|
|
|
|
|
|
|
this.setSurface( surface );
|
|
|
|
|
2024-04-30 16:44:25 +00:00
|
|
|
setTimeout( () => {
|
2018-08-25 12:59:24 +00:00
|
|
|
// Initialize surface
|
2024-05-01 12:32:49 +00:00
|
|
|
this.track( 'trace.initializeSurface.enter' );
|
2018-08-25 12:59:24 +00:00
|
|
|
|
2024-05-01 12:32:49 +00:00
|
|
|
this.active = true;
|
2018-08-25 12:59:24 +00:00
|
|
|
// Now that the surface is attached to the document and ready,
|
|
|
|
// let it initialize itself
|
|
|
|
surface.initialize();
|
|
|
|
|
2024-05-01 12:32:49 +00:00
|
|
|
this.track( 'trace.initializeSurface.exit' );
|
|
|
|
this.surfaceReady();
|
2018-08-25 12:59:24 +00:00
|
|
|
} );
|
|
|
|
|
|
|
|
return surface;
|
|
|
|
};
|
|
|
|
|
2016-05-26 10:56:08 +00:00
|
|
|
/**
|
|
|
|
* @inheritdoc
|
|
|
|
*/
|
|
|
|
ve.init.mw.Target.prototype.setSurface = function ( surface ) {
|
|
|
|
if ( !surface.$element.parent().length ) {
|
|
|
|
this.$element.append( surface.$element );
|
|
|
|
}
|
|
|
|
|
|
|
|
// Parent method
|
|
|
|
ve.init.mw.Target.super.prototype.setSurface.apply( this, arguments );
|
|
|
|
};
|
2017-05-20 21:24:10 +00:00
|
|
|
|
2019-04-17 16:28:48 +00:00
|
|
|
/**
|
2022-02-22 23:09:02 +00:00
|
|
|
* Intialise autosave, recovering changes if applicable
|
|
|
|
*
|
2022-06-12 12:51:52 +00:00
|
|
|
* @param {Object} [config] Configuration options
|
2024-05-27 04:59:02 +00:00
|
|
|
* @param {boolean} [config.suppressNotification=false] Don't notify the user if changes are recovered
|
|
|
|
* @param {string} [config.docId] Document ID for storage grouping
|
|
|
|
* @param {ve.init.SafeStorage} [config.storage] Storage interface
|
|
|
|
* @param {number} [config.storageExpiry] Storage expiry time in seconds (optional)
|
2019-04-17 16:28:48 +00:00
|
|
|
*/
|
2022-06-12 12:51:52 +00:00
|
|
|
ve.init.mw.Target.prototype.initAutosave = function ( config ) {
|
|
|
|
// Old function signature
|
|
|
|
// TODO: Remove after fixed downstream
|
|
|
|
if ( typeof config === 'boolean' ) {
|
|
|
|
config = { suppressNotification: config };
|
|
|
|
} else {
|
|
|
|
config = config || {};
|
|
|
|
}
|
|
|
|
|
2024-05-21 14:22:56 +00:00
|
|
|
const surfaceModel = this.getSurface().getModel();
|
2022-06-12 12:51:52 +00:00
|
|
|
|
|
|
|
if ( config.docId ) {
|
|
|
|
surfaceModel.setAutosaveDocId( config.docId );
|
|
|
|
}
|
|
|
|
|
|
|
|
if ( config.storage ) {
|
2022-09-01 22:31:14 +00:00
|
|
|
surfaceModel.setStorage( config.storage, config.storageExpiry );
|
2022-06-12 12:51:52 +00:00
|
|
|
}
|
|
|
|
|
2019-04-17 16:28:48 +00:00
|
|
|
if ( this.recovered ) {
|
|
|
|
// Restore auto-saved transactions if document state was recovered
|
|
|
|
try {
|
|
|
|
surfaceModel.restoreChanges();
|
2022-06-12 12:51:52 +00:00
|
|
|
if ( !config.suppressNotification ) {
|
2022-02-22 23:09:02 +00:00
|
|
|
ve.init.platform.notify(
|
|
|
|
ve.msg( 'visualeditor-autosave-recovered-text' ),
|
|
|
|
ve.msg( 'visualeditor-autosave-recovered-title' )
|
|
|
|
);
|
|
|
|
}
|
2019-04-17 16:28:48 +00:00
|
|
|
} catch ( e ) {
|
|
|
|
mw.log.warn( e );
|
|
|
|
ve.init.platform.notify(
|
|
|
|
ve.msg( 'visualeditor-autosave-not-recovered-text' ),
|
|
|
|
ve.msg( 'visualeditor-autosave-not-recovered-title' ),
|
|
|
|
{ type: 'error' }
|
|
|
|
);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// ...otherwise store this document state for later recovery
|
|
|
|
if ( this.fromEditedState ) {
|
|
|
|
// Store immediately if the document was previously edited
|
|
|
|
// (e.g. in a different mode)
|
|
|
|
this.storeDocState( this.originalHtml );
|
|
|
|
} else {
|
|
|
|
// Only store after the first change if this is an unmodified document
|
2024-04-30 16:44:25 +00:00
|
|
|
surfaceModel.once( 'undoStackChange', () => {
|
2019-04-17 16:28:48 +00:00
|
|
|
// Check the surface hasn't been destroyed
|
2024-05-01 12:32:49 +00:00
|
|
|
if ( this.getSurface() ) {
|
|
|
|
this.storeDocState( this.originalHtml );
|
2019-04-17 16:28:48 +00:00
|
|
|
}
|
|
|
|
} );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// Start auto-saving transactions
|
|
|
|
surfaceModel.startStoringChanges();
|
|
|
|
// TODO: Listen to autosaveFailed event to notify user
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Store a snapshot of the current document state.
|
|
|
|
*
|
|
|
|
* @param {string} [html] Document HTML, will generate from current state if not provided
|
|
|
|
*/
|
|
|
|
ve.init.mw.Target.prototype.storeDocState = function ( html ) {
|
2024-05-21 14:22:56 +00:00
|
|
|
const mode = this.getSurface().getMode();
|
2019-04-17 16:28:48 +00:00
|
|
|
this.getSurface().getModel().storeDocState( { mode: mode }, html );
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Clear any stored document state
|
|
|
|
*/
|
|
|
|
ve.init.mw.Target.prototype.clearDocState = function () {
|
|
|
|
if ( this.getSurface() ) {
|
|
|
|
this.getSurface().getModel().removeDocStateAndChanges();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @inheritdoc
|
|
|
|
*/
|
|
|
|
ve.init.mw.Target.prototype.teardown = function () {
|
|
|
|
// If target is closed cleanly (after save or deliberate close) then remove autosave state
|
|
|
|
this.clearDocState();
|
|
|
|
|
|
|
|
// Parent method
|
|
|
|
return ve.init.mw.Target.super.prototype.teardown.call( this );
|
|
|
|
};
|
|
|
|
|
2017-05-20 21:24:10 +00:00
|
|
|
/**
|
2023-08-14 20:29:30 +00:00
|
|
|
* Refresh our knowledge about the logged-in user.
|
2017-05-20 21:24:10 +00:00
|
|
|
*
|
2023-08-14 20:29:30 +00:00
|
|
|
* This should be called in response to a user assertion error, to look up
|
|
|
|
* the new user name, and update the current user variables in mw.config.
|
2017-05-20 21:24:10 +00:00
|
|
|
*
|
2018-05-04 13:30:10 +00:00
|
|
|
* @param {ve.dm.Document} [doc] Document to associate with the API request
|
2020-02-15 01:22:39 +00:00
|
|
|
* @return {jQuery.Promise} Promise resolved with new username, or null if anonymous
|
2017-05-20 21:24:10 +00:00
|
|
|
*/
|
2020-02-15 01:22:39 +00:00
|
|
|
ve.init.mw.Target.prototype.refreshUser = function ( doc ) {
|
2020-02-15 02:50:16 +00:00
|
|
|
return this.getContentApi( doc ).get( {
|
2017-05-20 21:24:10 +00:00
|
|
|
action: 'query',
|
2020-02-15 01:22:39 +00:00
|
|
|
meta: 'userinfo'
|
2024-04-30 16:44:25 +00:00
|
|
|
} ).then( ( data ) => {
|
2024-05-21 14:22:56 +00:00
|
|
|
const userInfo = data.query && data.query.userinfo;
|
2020-02-15 02:50:16 +00:00
|
|
|
|
|
|
|
if ( userInfo.anon !== undefined ) {
|
|
|
|
// New session is an anonymous user
|
|
|
|
mw.config.set( {
|
|
|
|
// wgUserId is unset for anonymous users, not set to null
|
|
|
|
wgUserId: undefined,
|
|
|
|
// wgUserName is explicitly set to null for anonymous users,
|
|
|
|
// functions like mw.user.isAnon rely on this.
|
|
|
|
wgUserName: null
|
|
|
|
} );
|
2023-08-14 20:29:30 +00:00
|
|
|
|
|
|
|
// Call this only after clearing wgUserId, otherwise it does nothing
|
|
|
|
return mw.user.acquireTempUserName();
|
2020-02-15 02:50:16 +00:00
|
|
|
} else {
|
2023-08-14 20:29:30 +00:00
|
|
|
// New session is a logged in user (or a temporary user)
|
2020-02-15 02:50:16 +00:00
|
|
|
mw.config.set( {
|
|
|
|
wgUserId: userInfo.id,
|
|
|
|
wgUserName: userInfo.name
|
|
|
|
} );
|
|
|
|
|
2023-08-14 20:29:30 +00:00
|
|
|
return mw.user.getName();
|
|
|
|
}
|
2020-02-15 02:50:16 +00:00
|
|
|
} );
|
2017-05-20 21:24:10 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get a wikitext fragment from a document
|
|
|
|
*
|
2021-06-04 11:58:18 +00:00
|
|
|
* @param {ve.dm.Document} doc
|
2020-06-19 12:02:51 +00:00
|
|
|
* @param {boolean} [useRevision=true] Whether to use the revision ID + ETag
|
2017-05-20 21:24:10 +00:00
|
|
|
* @return {jQuery.Promise} Abortable promise which resolves with a wikitext string
|
|
|
|
*/
|
2020-06-19 12:02:51 +00:00
|
|
|
ve.init.mw.Target.prototype.getWikitextFragment = function ( doc, useRevision ) {
|
2020-04-29 16:32:09 +00:00
|
|
|
// Shortcut for empty document
|
|
|
|
if ( !doc.data.hasContent() ) {
|
2020-06-19 12:02:51 +00:00
|
|
|
return ve.createDeferred().resolve( '' );
|
2017-05-20 21:24:10 +00:00
|
|
|
}
|
|
|
|
|
2024-05-21 14:22:56 +00:00
|
|
|
const params = {
|
2020-04-29 16:32:09 +00:00
|
|
|
action: 'visualeditoredit',
|
|
|
|
paction: 'serialize',
|
2022-03-16 15:42:43 +00:00
|
|
|
html: mw.libs.ve.targetSaver.getHtml(
|
|
|
|
ve.dm.converter.getDomFromModel( doc )
|
|
|
|
),
|
2020-06-19 12:02:51 +00:00
|
|
|
page: this.getPageName()
|
|
|
|
};
|
|
|
|
|
|
|
|
if ( useRevision === undefined || useRevision ) {
|
|
|
|
params.oldid = this.revid;
|
|
|
|
params.etag = this.etag;
|
|
|
|
}
|
2017-05-20 21:24:10 +00:00
|
|
|
|
2024-05-21 14:22:56 +00:00
|
|
|
const xhr = this.getContentApi( doc ).postWithToken( 'csrf',
|
2017-05-20 21:24:10 +00:00
|
|
|
params,
|
|
|
|
{ contentType: 'multipart/form-data' }
|
|
|
|
);
|
|
|
|
|
2024-04-30 16:44:25 +00:00
|
|
|
return xhr.then( ( response ) => {
|
2017-05-20 21:24:10 +00:00
|
|
|
if ( response.visualeditoredit ) {
|
|
|
|
return response.visualeditoredit.content;
|
|
|
|
}
|
2020-06-19 12:02:51 +00:00
|
|
|
return ve.createDeferred().reject();
|
2020-02-15 02:50:16 +00:00
|
|
|
} ).promise( { abort: xhr.abort } );
|
2017-05-20 21:24:10 +00:00
|
|
|
};
|
2018-04-06 11:30:57 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Parse a fragment of wikitext into HTML
|
|
|
|
*
|
2021-06-04 11:58:18 +00:00
|
|
|
* @param {string} wikitext
|
2018-04-06 11:30:57 +00:00
|
|
|
* @param {boolean} pst Perform pre-save transform
|
2018-05-04 13:30:10 +00:00
|
|
|
* @param {ve.dm.Document} [doc] Parse for a specific document, defaults to current surface's
|
2018-04-06 11:30:57 +00:00
|
|
|
* @return {jQuery.Promise} Abortable promise
|
|
|
|
*/
|
2018-05-04 13:30:10 +00:00
|
|
|
ve.init.mw.Target.prototype.parseWikitextFragment = function ( wikitext, pst, doc ) {
|
2024-05-21 14:22:56 +00:00
|
|
|
let abortable, aborted;
|
|
|
|
const abortedPromise = ve.createDeferred().reject( 'http',
|
2023-07-18 02:14:09 +00:00
|
|
|
{ textStatus: 'abort', exception: 'abort' } ).promise();
|
|
|
|
|
|
|
|
function abort() {
|
|
|
|
aborted = true;
|
|
|
|
if ( abortable && abortable.abort ) {
|
|
|
|
abortable.abort();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Acquire a temporary user username before previewing or diffing, so that signatures and
|
|
|
|
// user-related magic words display the temp user instead of IP user in the preview. (T331397)
|
2024-05-21 14:22:56 +00:00
|
|
|
let tempUserNamePromise;
|
2023-07-18 02:14:09 +00:00
|
|
|
if ( pst ) {
|
|
|
|
tempUserNamePromise = mw.user.acquireTempUserName();
|
|
|
|
} else {
|
|
|
|
tempUserNamePromise = ve.createDeferred().resolve( null );
|
|
|
|
}
|
|
|
|
|
|
|
|
return tempUserNamePromise
|
2024-04-30 16:44:25 +00:00
|
|
|
.then( () => {
|
2023-07-18 02:14:09 +00:00
|
|
|
if ( aborted ) {
|
|
|
|
return abortedPromise;
|
|
|
|
}
|
2024-05-01 12:32:49 +00:00
|
|
|
return ( abortable = this.getContentApi( doc ).post( {
|
2023-07-18 02:14:09 +00:00
|
|
|
action: 'visualeditor',
|
|
|
|
paction: 'parsefragment',
|
2024-05-01 12:32:49 +00:00
|
|
|
page: this.getPageName( doc ),
|
2023-07-18 02:14:09 +00:00
|
|
|
wikitext: wikitext,
|
|
|
|
pst: pst
|
|
|
|
} ) );
|
|
|
|
} )
|
|
|
|
.promise( { abort: abort } );
|
2018-04-06 11:30:57 +00:00
|
|
|
};
|
2018-05-04 13:30:10 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the page name associated with a specific document
|
|
|
|
*
|
|
|
|
* @param {ve.dm.Document} [doc] Document, defaults to current surface's
|
|
|
|
* @return {string} Page name
|
|
|
|
*/
|
|
|
|
ve.init.mw.Target.prototype.getPageName = function () {
|
|
|
|
return this.pageName;
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get an API object associated with the wiki where the document
|
|
|
|
* content is hosted.
|
|
|
|
*
|
|
|
|
* This would be overridden if editing content on another wiki.
|
|
|
|
*
|
|
|
|
* @param {ve.dm.Document} [doc] API for a specific document, should default to document of current surface.
|
|
|
|
* @param {Object} [options] API options
|
2021-07-07 10:09:22 +00:00
|
|
|
* @param {Object} [options.parameters] Default query parameters for all API requests. Defaults
|
|
|
|
* include action=query, format=json, and formatversion=2 if not specified otherwise.
|
2021-06-04 11:58:18 +00:00
|
|
|
* @return {mw.Api}
|
2018-05-04 13:30:10 +00:00
|
|
|
*/
|
|
|
|
ve.init.mw.Target.prototype.getContentApi = function ( doc, options ) {
|
2019-04-30 18:30:50 +00:00
|
|
|
options = options || {};
|
|
|
|
options.parameters = ve.extendObject( { formatversion: 2 }, options.parameters );
|
2018-05-04 13:30:10 +00:00
|
|
|
return new mw.Api( options );
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get an API object associated with the local wiki.
|
|
|
|
*
|
|
|
|
* For example you would always use getLocalApi for actions
|
|
|
|
* associated with the current user.
|
|
|
|
*
|
|
|
|
* @param {Object} [options] API options
|
2021-06-04 11:58:18 +00:00
|
|
|
* @return {mw.Api}
|
2018-05-04 13:30:10 +00:00
|
|
|
*/
|
|
|
|
ve.init.mw.Target.prototype.getLocalApi = function ( options ) {
|
2019-04-30 18:30:50 +00:00
|
|
|
options = options || {};
|
|
|
|
options.parameters = ve.extendObject( { formatversion: 2 }, options.parameters );
|
2018-05-04 13:30:10 +00:00
|
|
|
return new mw.Api( options );
|
|
|
|
};
|