JSDuck: Generated code documentation!
See CODING.md for how to run it.
Mistakes fixed:
* Warning: Unknown type function
-> Function
* Warning: Unknown type DOMElement
-> HTMLElement
* Warning: Unknown type DOM Node
-> HTMLElement
* Warning: Unknown type Integer
-> Mixed
* Warning: Unknown type Command
-> ve.Command
* Warning: Unknown type any
-> number
* Warning: Unknown type ve.Transaction
-> ve.dm.Transaction
* Warning: Unknown type ve.dm.AnnotationSet
-> ve.AnnotationSet
* Warning: Unknown type false
-> boolean
* Warning: Unknown type ve.dm.AlienNode
ve.dm doesn't have a generic AlienNode like ve.ce
-> Unknown type ve.dm.AlienInlineNode|ve.dm.AlienBlockNode
* Warning: Unknown type ve.ve.Surface
-> ve.ce.Surface
* ve.example.lookupNode:
-> Last @param should be @return
* ve.dm.Transaction.prototype.pushReplace:
-> @param {Array] should be @param {Array}
* Warning: ve.BranchNode.js:27: {@link ve.Node#hasChildren} links to non-existing member
-> (removed)
* Warning: ve.LeafNode.js:21: {@link ve.Node#hasChildren} links to non-existing member
-> (removed)
Differences fixed:
* Variadic arguments are like @param {Type...} [name]
instead of @param {Type} [name...]
* Convert all file headers from /** to /*! because JSDuck tries
to parse all /** blocks and fails to parse with all sorts of
errors for "Global property", "Unnamed property", and
"Duplicate property".
Find: \/\*\*([^@]+)(@copyright)
Replace: /*!$1$2
* Indented blocks are considered code examples.
A few methods had documentation with numbered lists that were
indented, which have now been updated to not be intended.
* The free-form text descriptions are parsed with Markdown,
which requires lists to be separated from paragraphs by an
empty line.
And we should use `backticks` instead of {braces} for inline
code in text paragraphs.
* Doc blocks for classes and their constructor have to be
in the correct order (@constructor, @param, @return must be
before @class, @abstract, @extends etc.)
* `@extends Class` must not have Class {wrapped}
* @throws must start with a {Type}
* @example means something else. It is used for an inline demo
iframe, not code block. For that simply indent with spaces.
* @member means something else.
Non-function properties are marked with @property, not @member.
* To create a link to a class or member, in most cases the name
is enough to create a link. E.g. Foo, Foo.bar, Foo.bar#quux,
where a hash stands for "instance member", so Foo.bar#quux,
links to Foo.bar.prototype.quux (the is not supported, as
"prototype" is considered an implementation detail, it only
indexes class name and method name).
If the magic linker doesn't work for some case, the
verbose syntax is {@link #target label}.
* @property can't have sub-properties (nested @param and @return
values are supported, only @static @property can't be nested).
We only have one case of this, which can be worked around by
moving those in a new virtual class. The code is unaltered
(only moved down so that it isn't with the scope of the main
@class block). ve.dm.TransactionProcessor.processors.
New:
* @mixins: Classes mixed into the current class.
* @event: Events that can be emitted by a class. These are also
inherited by subclasses. (+ @param, @return and @preventable).
So ve.Node#event-attach is inherited to ve.dm.BreakNode,
just like @method is.
* @singleton: Plain objects such as ve, ve.dm, ve.ce were missing
documentation causing a tree error. Documented those as a
JSDuck singleton, which they but just weren't documented yet.
NB: Members of @singleton don't need @static (if present,
triggers a compiler warning).
* @chainable: Shorthand for "@return this". We were using
"@return {classname}" which is ambiguous (returns the same
instance or another instance?), @chainable is specifically
for "@return this". Creates proper labels in the generated
HTML pages.
Removed:
* @mixin: (not to be confused with @mixins). Not supported by
JSDuck. Every class is standalone anyway. Where needed marked
them @class + @abstract instead.
Change-Id: I6a7c9e8ee8f995731bc205d666167874eb2ebe23
2013-01-04 08:54:17 +00:00
|
|
|
/*!
|
2015-12-10 16:07:50 +00:00
|
|
|
* VisualEditor MediaWiki Initialization ArticleTarget class.
|
2012-07-19 21:25:16 +00:00
|
|
|
*
|
2023-12-01 16:06:11 +00:00
|
|
|
* @copyright See AUTHORS.txt
|
2012-07-19 00:11:26 +00:00
|
|
|
* @license The MIT License (MIT); see LICENSE.txt
|
|
|
|
*/
|
|
|
|
|
2019-02-20 20:23:43 +00:00
|
|
|
/* eslint-disable no-jquery/no-global-selector */
|
2019-01-08 17:00:09 +00:00
|
|
|
|
2012-06-11 06:54:41 +00:00
|
|
|
/**
|
2015-12-10 16:07:50 +00:00
|
|
|
* Initialization MediaWiki article target.
|
2012-06-11 06:54:41 +00:00
|
|
|
*
|
|
|
|
* @class
|
2015-12-11 14:57:49 +00:00
|
|
|
* @extends ve.init.mw.Target
|
2013-02-20 19:44:44 +00:00
|
|
|
*
|
2012-06-11 06:54:41 +00:00
|
|
|
* @constructor
|
2015-07-30 09:32:40 +00:00
|
|
|
* @param {Object} [config] Configuration options
|
2024-05-27 04:59:02 +00:00
|
|
|
* @param {Object} [config.toolbarConfig]
|
|
|
|
* @param {boolean} [config.register=true]
|
2012-06-11 06:54:41 +00:00
|
|
|
*/
|
2017-09-11 14:59:38 +00:00
|
|
|
ve.init.mw.ArticleTarget = function VeInitMwArticleTarget( config ) {
|
2015-07-30 09:32:40 +00:00
|
|
|
config = config || {};
|
2021-11-15 17:25:51 +00:00
|
|
|
config.toolbarConfig = ve.extendObject( {
|
2015-04-09 09:18:22 +00:00
|
|
|
shadow: true,
|
|
|
|
actions: true,
|
2015-08-10 12:31:46 +00:00
|
|
|
floatable: true
|
2015-07-30 09:32:40 +00:00
|
|
|
}, config.toolbarConfig );
|
|
|
|
|
|
|
|
// Parent constructor
|
2015-12-10 16:07:50 +00:00
|
|
|
ve.init.mw.ArticleTarget.super.call( this, config );
|
2014-12-03 00:04:07 +00:00
|
|
|
|
2020-03-11 14:45:06 +00:00
|
|
|
// Register
|
|
|
|
if ( config.register !== false ) {
|
|
|
|
// ArticleTargets are never destroyed, but we can't trust ve.init.target to
|
|
|
|
// not get overridden by other targets that may get created on the page.
|
|
|
|
ve.init.articleTarget = this;
|
|
|
|
}
|
|
|
|
|
2012-06-11 06:54:41 +00:00
|
|
|
// Properties
|
2015-08-05 21:43:23 +00:00
|
|
|
this.saveDialog = null;
|
|
|
|
this.saveDeferred = null;
|
2018-11-23 17:01:25 +00:00
|
|
|
this.saveFields = {};
|
2015-08-05 21:43:23 +00:00
|
|
|
this.docToSave = null;
|
2017-07-17 16:38:32 +00:00
|
|
|
this.originalDmDocPromise = null;
|
2017-10-05 16:31:15 +00:00
|
|
|
this.originalHtml = null;
|
2015-07-31 11:03:14 +00:00
|
|
|
this.toolbarSaveButton = null;
|
2016-06-30 14:01:24 +00:00
|
|
|
this.pageExists = mw.config.get( 'wgRelevantArticleId', 0 ) !== 0;
|
2024-05-21 14:22:56 +00:00
|
|
|
const enableVisualSectionEditing = mw.config.get( 'wgVisualEditorConfig' ).enableVisualSectionEditing;
|
2019-02-13 13:21:26 +00:00
|
|
|
this.enableVisualSectionEditing = enableVisualSectionEditing === true || enableVisualSectionEditing === this.constructor.static.trackingName;
|
2015-08-10 12:31:46 +00:00
|
|
|
this.toolbarScrollOffset = mw.config.get( 'wgVisualEditorToolbarScrollOffset', 0 );
|
2022-12-17 15:44:56 +00:00
|
|
|
this.currentUrl = new URL( location.href );
|
2016-09-06 19:16:55 +00:00
|
|
|
this.section = null;
|
2021-12-21 18:48:27 +00:00
|
|
|
this.visibleSection = null;
|
|
|
|
this.visibleSectionOffset = null;
|
2016-12-08 19:24:10 +00:00
|
|
|
this.sectionTitle = null;
|
2017-03-09 23:26:08 +00:00
|
|
|
this.editSummaryValue = null;
|
|
|
|
this.initialEditSummary = null;
|
2020-05-26 22:08:49 +00:00
|
|
|
this.initialCheckboxes = {};
|
2014-05-14 21:11:19 +00:00
|
|
|
|
2022-12-17 15:44:56 +00:00
|
|
|
this.viewUrl = new URL( mw.util.getUrl( this.getPageName() ), location.href );
|
2022-05-13 17:55:48 +00:00
|
|
|
this.isViewPage = (
|
|
|
|
mw.config.get( 'wgAction' ) === 'view' &&
|
2022-12-17 15:44:56 +00:00
|
|
|
!this.currentUrl.searchParams.has( 'diff' )
|
2022-05-13 17:55:48 +00:00
|
|
|
);
|
|
|
|
|
2022-01-24 13:06:19 +00:00
|
|
|
this.copyrightWarning = null;
|
2016-11-05 06:16:26 +00:00
|
|
|
this.checkboxFields = null;
|
|
|
|
this.checkboxesByName = null;
|
2017-10-05 16:31:15 +00:00
|
|
|
this.$saveAccessKeyElements = null;
|
2016-11-05 06:16:26 +00:00
|
|
|
|
2022-02-17 22:06:14 +00:00
|
|
|
this.$editableContent = this.getEditableContent();
|
|
|
|
|
2014-05-14 21:11:19 +00:00
|
|
|
// Sometimes we actually don't want to send a useful oldid
|
|
|
|
// if we do, PostEdit will give us a 'page restored' message
|
2020-09-15 21:26:59 +00:00
|
|
|
// Use undefined instead of 0 for new documents (T262838)
|
2023-06-07 12:58:08 +00:00
|
|
|
this.requestedRevId = mw.config.get( 'wgEditLatestRevision' ) ? mw.config.get( 'wgCurRevisionId' ) : mw.config.get( 'wgRevisionId' ) || undefined;
|
2020-09-15 21:26:59 +00:00
|
|
|
this.currentRevisionId = mw.config.get( 'wgCurRevisionId' ) || undefined;
|
2016-03-29 11:49:35 +00:00
|
|
|
this.revid = this.requestedRevId || this.currentRevisionId;
|
2014-05-14 21:11:19 +00:00
|
|
|
|
2018-03-26 14:23:56 +00:00
|
|
|
this.edited = false;
|
2016-03-29 11:49:35 +00:00
|
|
|
this.restoring = !!this.requestedRevId && this.requestedRevId !== this.currentRevisionId;
|
2015-08-04 13:37:13 +00:00
|
|
|
this.pageDeletedWarning = false;
|
2019-01-08 17:00:09 +00:00
|
|
|
this.events = {
|
2024-04-30 16:44:25 +00:00
|
|
|
track: () => {},
|
|
|
|
trackActivationStart: () => {},
|
|
|
|
trackActivationComplete: () => {}
|
2019-01-08 17:00:09 +00:00
|
|
|
};
|
2013-10-11 13:04:11 +00:00
|
|
|
|
2013-11-06 08:22:11 +00:00
|
|
|
this.preparedCacheKeyPromise = null;
|
2013-12-03 02:29:11 +00:00
|
|
|
this.clearState();
|
2015-07-01 11:11:36 +00:00
|
|
|
|
2015-07-30 09:32:40 +00:00
|
|
|
// Initialization
|
2015-12-10 16:07:50 +00:00
|
|
|
this.$element.addClass( 've-init-mw-articleTarget' );
|
2012-06-11 06:54:41 +00:00
|
|
|
};
|
|
|
|
|
2014-04-10 18:47:34 +00:00
|
|
|
/* Inheritance */
|
|
|
|
|
2015-12-11 14:57:49 +00:00
|
|
|
OO.inheritClass( ve.init.mw.ArticleTarget, ve.init.mw.Target );
|
2014-04-10 18:47:34 +00:00
|
|
|
|
2013-12-19 02:06:55 +00:00
|
|
|
/* Events */
|
2013-03-20 07:09:43 +00:00
|
|
|
|
JSDuck: Generated code documentation!
See CODING.md for how to run it.
Mistakes fixed:
* Warning: Unknown type function
-> Function
* Warning: Unknown type DOMElement
-> HTMLElement
* Warning: Unknown type DOM Node
-> HTMLElement
* Warning: Unknown type Integer
-> Mixed
* Warning: Unknown type Command
-> ve.Command
* Warning: Unknown type any
-> number
* Warning: Unknown type ve.Transaction
-> ve.dm.Transaction
* Warning: Unknown type ve.dm.AnnotationSet
-> ve.AnnotationSet
* Warning: Unknown type false
-> boolean
* Warning: Unknown type ve.dm.AlienNode
ve.dm doesn't have a generic AlienNode like ve.ce
-> Unknown type ve.dm.AlienInlineNode|ve.dm.AlienBlockNode
* Warning: Unknown type ve.ve.Surface
-> ve.ce.Surface
* ve.example.lookupNode:
-> Last @param should be @return
* ve.dm.Transaction.prototype.pushReplace:
-> @param {Array] should be @param {Array}
* Warning: ve.BranchNode.js:27: {@link ve.Node#hasChildren} links to non-existing member
-> (removed)
* Warning: ve.LeafNode.js:21: {@link ve.Node#hasChildren} links to non-existing member
-> (removed)
Differences fixed:
* Variadic arguments are like @param {Type...} [name]
instead of @param {Type} [name...]
* Convert all file headers from /** to /*! because JSDuck tries
to parse all /** blocks and fails to parse with all sorts of
errors for "Global property", "Unnamed property", and
"Duplicate property".
Find: \/\*\*([^@]+)(@copyright)
Replace: /*!$1$2
* Indented blocks are considered code examples.
A few methods had documentation with numbered lists that were
indented, which have now been updated to not be intended.
* The free-form text descriptions are parsed with Markdown,
which requires lists to be separated from paragraphs by an
empty line.
And we should use `backticks` instead of {braces} for inline
code in text paragraphs.
* Doc blocks for classes and their constructor have to be
in the correct order (@constructor, @param, @return must be
before @class, @abstract, @extends etc.)
* `@extends Class` must not have Class {wrapped}
* @throws must start with a {Type}
* @example means something else. It is used for an inline demo
iframe, not code block. For that simply indent with spaces.
* @member means something else.
Non-function properties are marked with @property, not @member.
* To create a link to a class or member, in most cases the name
is enough to create a link. E.g. Foo, Foo.bar, Foo.bar#quux,
where a hash stands for "instance member", so Foo.bar#quux,
links to Foo.bar.prototype.quux (the is not supported, as
"prototype" is considered an implementation detail, it only
indexes class name and method name).
If the magic linker doesn't work for some case, the
verbose syntax is {@link #target label}.
* @property can't have sub-properties (nested @param and @return
values are supported, only @static @property can't be nested).
We only have one case of this, which can be worked around by
moving those in a new virtual class. The code is unaltered
(only moved down so that it isn't with the scope of the main
@class block). ve.dm.TransactionProcessor.processors.
New:
* @mixins: Classes mixed into the current class.
* @event: Events that can be emitted by a class. These are also
inherited by subclasses. (+ @param, @return and @preventable).
So ve.Node#event-attach is inherited to ve.dm.BreakNode,
just like @method is.
* @singleton: Plain objects such as ve, ve.dm, ve.ce were missing
documentation causing a tree error. Documented those as a
JSDuck singleton, which they but just weren't documented yet.
NB: Members of @singleton don't need @static (if present,
triggers a compiler warning).
* @chainable: Shorthand for "@return this". We were using
"@return {classname}" which is ambiguous (returns the same
instance or another instance?), @chainable is specifically
for "@return this". Creates proper labels in the generated
HTML pages.
Removed:
* @mixin: (not to be confused with @mixins). Not supported by
JSDuck. Every class is standalone anyway. Where needed marked
them @class + @abstract instead.
Change-Id: I6a7c9e8ee8f995731bc205d666167874eb2ebe23
2013-01-04 08:54:17 +00:00
|
|
|
/**
|
2024-04-29 11:52:40 +00:00
|
|
|
* @event ve.init.mw.ArticleTarget#save
|
2019-11-04 14:54:53 +00:00
|
|
|
* @param {Object} data Save data from the API, see ve.init.mw.ArticleTarget#saveComplete
|
2019-06-29 04:49:39 +00:00
|
|
|
* Fired immediately after a save is successfully completed
|
2013-03-20 07:09:43 +00:00
|
|
|
*/
|
|
|
|
|
2024-04-30 09:14:36 +00:00
|
|
|
/**
|
|
|
|
* @event ve.init.mw.ArticleTarget#savePreview
|
|
|
|
*/
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @event ve.init.mw.ArticleTarget#saveReview
|
|
|
|
*/
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @event ve.init.mw.ArticleTarget#saveInitiated
|
|
|
|
*/
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @event ve.init.mw.ArticleTarget#saveWorkflowBegin
|
|
|
|
*/
|
|
|
|
|
2013-03-20 07:09:43 +00:00
|
|
|
/**
|
2024-04-29 11:52:40 +00:00
|
|
|
* @event ve.init.mw.ArticleTarget#showChanges
|
JSDuck: Generated code documentation!
See CODING.md for how to run it.
Mistakes fixed:
* Warning: Unknown type function
-> Function
* Warning: Unknown type DOMElement
-> HTMLElement
* Warning: Unknown type DOM Node
-> HTMLElement
* Warning: Unknown type Integer
-> Mixed
* Warning: Unknown type Command
-> ve.Command
* Warning: Unknown type any
-> number
* Warning: Unknown type ve.Transaction
-> ve.dm.Transaction
* Warning: Unknown type ve.dm.AnnotationSet
-> ve.AnnotationSet
* Warning: Unknown type false
-> boolean
* Warning: Unknown type ve.dm.AlienNode
ve.dm doesn't have a generic AlienNode like ve.ce
-> Unknown type ve.dm.AlienInlineNode|ve.dm.AlienBlockNode
* Warning: Unknown type ve.ve.Surface
-> ve.ce.Surface
* ve.example.lookupNode:
-> Last @param should be @return
* ve.dm.Transaction.prototype.pushReplace:
-> @param {Array] should be @param {Array}
* Warning: ve.BranchNode.js:27: {@link ve.Node#hasChildren} links to non-existing member
-> (removed)
* Warning: ve.LeafNode.js:21: {@link ve.Node#hasChildren} links to non-existing member
-> (removed)
Differences fixed:
* Variadic arguments are like @param {Type...} [name]
instead of @param {Type} [name...]
* Convert all file headers from /** to /*! because JSDuck tries
to parse all /** blocks and fails to parse with all sorts of
errors for "Global property", "Unnamed property", and
"Duplicate property".
Find: \/\*\*([^@]+)(@copyright)
Replace: /*!$1$2
* Indented blocks are considered code examples.
A few methods had documentation with numbered lists that were
indented, which have now been updated to not be intended.
* The free-form text descriptions are parsed with Markdown,
which requires lists to be separated from paragraphs by an
empty line.
And we should use `backticks` instead of {braces} for inline
code in text paragraphs.
* Doc blocks for classes and their constructor have to be
in the correct order (@constructor, @param, @return must be
before @class, @abstract, @extends etc.)
* `@extends Class` must not have Class {wrapped}
* @throws must start with a {Type}
* @example means something else. It is used for an inline demo
iframe, not code block. For that simply indent with spaces.
* @member means something else.
Non-function properties are marked with @property, not @member.
* To create a link to a class or member, in most cases the name
is enough to create a link. E.g. Foo, Foo.bar, Foo.bar#quux,
where a hash stands for "instance member", so Foo.bar#quux,
links to Foo.bar.prototype.quux (the is not supported, as
"prototype" is considered an implementation detail, it only
indexes class name and method name).
If the magic linker doesn't work for some case, the
verbose syntax is {@link #target label}.
* @property can't have sub-properties (nested @param and @return
values are supported, only @static @property can't be nested).
We only have one case of this, which can be worked around by
moving those in a new virtual class. The code is unaltered
(only moved down so that it isn't with the scope of the main
@class block). ve.dm.TransactionProcessor.processors.
New:
* @mixins: Classes mixed into the current class.
* @event: Events that can be emitted by a class. These are also
inherited by subclasses. (+ @param, @return and @preventable).
So ve.Node#event-attach is inherited to ve.dm.BreakNode,
just like @method is.
* @singleton: Plain objects such as ve, ve.dm, ve.ce were missing
documentation causing a tree error. Documented those as a
JSDuck singleton, which they but just weren't documented yet.
NB: Members of @singleton don't need @static (if present,
triggers a compiler warning).
* @chainable: Shorthand for "@return this". We were using
"@return {classname}" which is ambiguous (returns the same
instance or another instance?), @chainable is specifically
for "@return this". Creates proper labels in the generated
HTML pages.
Removed:
* @mixin: (not to be confused with @mixins). Not supported by
JSDuck. Every class is standalone anyway. Where needed marked
them @class + @abstract instead.
Change-Id: I6a7c9e8ee8f995731bc205d666167874eb2ebe23
2013-01-04 08:54:17 +00:00
|
|
|
*/
|
|
|
|
|
2013-05-14 17:40:00 +00:00
|
|
|
/**
|
2024-04-29 11:52:40 +00:00
|
|
|
* @event ve.init.mw.ArticleTarget#noChanges
|
2013-05-14 17:40:00 +00:00
|
|
|
*/
|
|
|
|
|
2013-11-26 19:29:14 +00:00
|
|
|
/**
|
2024-04-29 11:52:40 +00:00
|
|
|
* @event ve.init.mw.ArticleTarget#saveError
|
2021-01-15 19:06:20 +00:00
|
|
|
* @param {string} code Error code
|
2016-03-10 18:58:41 +00:00
|
|
|
*/
|
|
|
|
|
JSDuck: Generated code documentation!
See CODING.md for how to run it.
Mistakes fixed:
* Warning: Unknown type function
-> Function
* Warning: Unknown type DOMElement
-> HTMLElement
* Warning: Unknown type DOM Node
-> HTMLElement
* Warning: Unknown type Integer
-> Mixed
* Warning: Unknown type Command
-> ve.Command
* Warning: Unknown type any
-> number
* Warning: Unknown type ve.Transaction
-> ve.dm.Transaction
* Warning: Unknown type ve.dm.AnnotationSet
-> ve.AnnotationSet
* Warning: Unknown type false
-> boolean
* Warning: Unknown type ve.dm.AlienNode
ve.dm doesn't have a generic AlienNode like ve.ce
-> Unknown type ve.dm.AlienInlineNode|ve.dm.AlienBlockNode
* Warning: Unknown type ve.ve.Surface
-> ve.ce.Surface
* ve.example.lookupNode:
-> Last @param should be @return
* ve.dm.Transaction.prototype.pushReplace:
-> @param {Array] should be @param {Array}
* Warning: ve.BranchNode.js:27: {@link ve.Node#hasChildren} links to non-existing member
-> (removed)
* Warning: ve.LeafNode.js:21: {@link ve.Node#hasChildren} links to non-existing member
-> (removed)
Differences fixed:
* Variadic arguments are like @param {Type...} [name]
instead of @param {Type} [name...]
* Convert all file headers from /** to /*! because JSDuck tries
to parse all /** blocks and fails to parse with all sorts of
errors for "Global property", "Unnamed property", and
"Duplicate property".
Find: \/\*\*([^@]+)(@copyright)
Replace: /*!$1$2
* Indented blocks are considered code examples.
A few methods had documentation with numbered lists that were
indented, which have now been updated to not be intended.
* The free-form text descriptions are parsed with Markdown,
which requires lists to be separated from paragraphs by an
empty line.
And we should use `backticks` instead of {braces} for inline
code in text paragraphs.
* Doc blocks for classes and their constructor have to be
in the correct order (@constructor, @param, @return must be
before @class, @abstract, @extends etc.)
* `@extends Class` must not have Class {wrapped}
* @throws must start with a {Type}
* @example means something else. It is used for an inline demo
iframe, not code block. For that simply indent with spaces.
* @member means something else.
Non-function properties are marked with @property, not @member.
* To create a link to a class or member, in most cases the name
is enough to create a link. E.g. Foo, Foo.bar, Foo.bar#quux,
where a hash stands for "instance member", so Foo.bar#quux,
links to Foo.bar.prototype.quux (the is not supported, as
"prototype" is considered an implementation detail, it only
indexes class name and method name).
If the magic linker doesn't work for some case, the
verbose syntax is {@link #target label}.
* @property can't have sub-properties (nested @param and @return
values are supported, only @static @property can't be nested).
We only have one case of this, which can be worked around by
moving those in a new virtual class. The code is unaltered
(only moved down so that it isn't with the scope of the main
@class block). ve.dm.TransactionProcessor.processors.
New:
* @mixins: Classes mixed into the current class.
* @event: Events that can be emitted by a class. These are also
inherited by subclasses. (+ @param, @return and @preventable).
So ve.Node#event-attach is inherited to ve.dm.BreakNode,
just like @method is.
* @singleton: Plain objects such as ve, ve.dm, ve.ce were missing
documentation causing a tree error. Documented those as a
JSDuck singleton, which they but just weren't documented yet.
NB: Members of @singleton don't need @static (if present,
triggers a compiler warning).
* @chainable: Shorthand for "@return this". We were using
"@return {classname}" which is ambiguous (returns the same
instance or another instance?), @chainable is specifically
for "@return this". Creates proper labels in the generated
HTML pages.
Removed:
* @mixin: (not to be confused with @mixins). Not supported by
JSDuck. Every class is standalone anyway. Where needed marked
them @class + @abstract instead.
Change-Id: I6a7c9e8ee8f995731bc205d666167874eb2ebe23
2013-01-04 08:54:17 +00:00
|
|
|
/**
|
2024-04-29 11:52:40 +00:00
|
|
|
* @event ve.init.mw.ArticleTarget#loadError
|
JSDuck: Generated code documentation!
See CODING.md for how to run it.
Mistakes fixed:
* Warning: Unknown type function
-> Function
* Warning: Unknown type DOMElement
-> HTMLElement
* Warning: Unknown type DOM Node
-> HTMLElement
* Warning: Unknown type Integer
-> Mixed
* Warning: Unknown type Command
-> ve.Command
* Warning: Unknown type any
-> number
* Warning: Unknown type ve.Transaction
-> ve.dm.Transaction
* Warning: Unknown type ve.dm.AnnotationSet
-> ve.AnnotationSet
* Warning: Unknown type false
-> boolean
* Warning: Unknown type ve.dm.AlienNode
ve.dm doesn't have a generic AlienNode like ve.ce
-> Unknown type ve.dm.AlienInlineNode|ve.dm.AlienBlockNode
* Warning: Unknown type ve.ve.Surface
-> ve.ce.Surface
* ve.example.lookupNode:
-> Last @param should be @return
* ve.dm.Transaction.prototype.pushReplace:
-> @param {Array] should be @param {Array}
* Warning: ve.BranchNode.js:27: {@link ve.Node#hasChildren} links to non-existing member
-> (removed)
* Warning: ve.LeafNode.js:21: {@link ve.Node#hasChildren} links to non-existing member
-> (removed)
Differences fixed:
* Variadic arguments are like @param {Type...} [name]
instead of @param {Type} [name...]
* Convert all file headers from /** to /*! because JSDuck tries
to parse all /** blocks and fails to parse with all sorts of
errors for "Global property", "Unnamed property", and
"Duplicate property".
Find: \/\*\*([^@]+)(@copyright)
Replace: /*!$1$2
* Indented blocks are considered code examples.
A few methods had documentation with numbered lists that were
indented, which have now been updated to not be intended.
* The free-form text descriptions are parsed with Markdown,
which requires lists to be separated from paragraphs by an
empty line.
And we should use `backticks` instead of {braces} for inline
code in text paragraphs.
* Doc blocks for classes and their constructor have to be
in the correct order (@constructor, @param, @return must be
before @class, @abstract, @extends etc.)
* `@extends Class` must not have Class {wrapped}
* @throws must start with a {Type}
* @example means something else. It is used for an inline demo
iframe, not code block. For that simply indent with spaces.
* @member means something else.
Non-function properties are marked with @property, not @member.
* To create a link to a class or member, in most cases the name
is enough to create a link. E.g. Foo, Foo.bar, Foo.bar#quux,
where a hash stands for "instance member", so Foo.bar#quux,
links to Foo.bar.prototype.quux (the is not supported, as
"prototype" is considered an implementation detail, it only
indexes class name and method name).
If the magic linker doesn't work for some case, the
verbose syntax is {@link #target label}.
* @property can't have sub-properties (nested @param and @return
values are supported, only @static @property can't be nested).
We only have one case of this, which can be worked around by
moving those in a new virtual class. The code is unaltered
(only moved down so that it isn't with the scope of the main
@class block). ve.dm.TransactionProcessor.processors.
New:
* @mixins: Classes mixed into the current class.
* @event: Events that can be emitted by a class. These are also
inherited by subclasses. (+ @param, @return and @preventable).
So ve.Node#event-attach is inherited to ve.dm.BreakNode,
just like @method is.
* @singleton: Plain objects such as ve, ve.dm, ve.ce were missing
documentation causing a tree error. Documented those as a
JSDuck singleton, which they but just weren't documented yet.
NB: Members of @singleton don't need @static (if present,
triggers a compiler warning).
* @chainable: Shorthand for "@return this". We were using
"@return {classname}" which is ambiguous (returns the same
instance or another instance?), @chainable is specifically
for "@return this". Creates proper labels in the generated
HTML pages.
Removed:
* @mixin: (not to be confused with @mixins). Not supported by
JSDuck. Every class is standalone anyway. Where needed marked
them @class + @abstract instead.
Change-Id: I6a7c9e8ee8f995731bc205d666167874eb2ebe23
2013-01-04 08:54:17 +00:00
|
|
|
*/
|
|
|
|
|
|
|
|
/**
|
2024-04-29 11:52:40 +00:00
|
|
|
* @event ve.init.mw.ArticleTarget#showChangesError
|
2013-03-20 07:09:43 +00:00
|
|
|
*/
|
|
|
|
|
|
|
|
/**
|
2024-04-29 11:52:40 +00:00
|
|
|
* @event ve.init.mw.ArticleTarget#serializeError
|
JSDuck: Generated code documentation!
See CODING.md for how to run it.
Mistakes fixed:
* Warning: Unknown type function
-> Function
* Warning: Unknown type DOMElement
-> HTMLElement
* Warning: Unknown type DOM Node
-> HTMLElement
* Warning: Unknown type Integer
-> Mixed
* Warning: Unknown type Command
-> ve.Command
* Warning: Unknown type any
-> number
* Warning: Unknown type ve.Transaction
-> ve.dm.Transaction
* Warning: Unknown type ve.dm.AnnotationSet
-> ve.AnnotationSet
* Warning: Unknown type false
-> boolean
* Warning: Unknown type ve.dm.AlienNode
ve.dm doesn't have a generic AlienNode like ve.ce
-> Unknown type ve.dm.AlienInlineNode|ve.dm.AlienBlockNode
* Warning: Unknown type ve.ve.Surface
-> ve.ce.Surface
* ve.example.lookupNode:
-> Last @param should be @return
* ve.dm.Transaction.prototype.pushReplace:
-> @param {Array] should be @param {Array}
* Warning: ve.BranchNode.js:27: {@link ve.Node#hasChildren} links to non-existing member
-> (removed)
* Warning: ve.LeafNode.js:21: {@link ve.Node#hasChildren} links to non-existing member
-> (removed)
Differences fixed:
* Variadic arguments are like @param {Type...} [name]
instead of @param {Type} [name...]
* Convert all file headers from /** to /*! because JSDuck tries
to parse all /** blocks and fails to parse with all sorts of
errors for "Global property", "Unnamed property", and
"Duplicate property".
Find: \/\*\*([^@]+)(@copyright)
Replace: /*!$1$2
* Indented blocks are considered code examples.
A few methods had documentation with numbered lists that were
indented, which have now been updated to not be intended.
* The free-form text descriptions are parsed with Markdown,
which requires lists to be separated from paragraphs by an
empty line.
And we should use `backticks` instead of {braces} for inline
code in text paragraphs.
* Doc blocks for classes and their constructor have to be
in the correct order (@constructor, @param, @return must be
before @class, @abstract, @extends etc.)
* `@extends Class` must not have Class {wrapped}
* @throws must start with a {Type}
* @example means something else. It is used for an inline demo
iframe, not code block. For that simply indent with spaces.
* @member means something else.
Non-function properties are marked with @property, not @member.
* To create a link to a class or member, in most cases the name
is enough to create a link. E.g. Foo, Foo.bar, Foo.bar#quux,
where a hash stands for "instance member", so Foo.bar#quux,
links to Foo.bar.prototype.quux (the is not supported, as
"prototype" is considered an implementation detail, it only
indexes class name and method name).
If the magic linker doesn't work for some case, the
verbose syntax is {@link #target label}.
* @property can't have sub-properties (nested @param and @return
values are supported, only @static @property can't be nested).
We only have one case of this, which can be worked around by
moving those in a new virtual class. The code is unaltered
(only moved down so that it isn't with the scope of the main
@class block). ve.dm.TransactionProcessor.processors.
New:
* @mixins: Classes mixed into the current class.
* @event: Events that can be emitted by a class. These are also
inherited by subclasses. (+ @param, @return and @preventable).
So ve.Node#event-attach is inherited to ve.dm.BreakNode,
just like @method is.
* @singleton: Plain objects such as ve, ve.dm, ve.ce were missing
documentation causing a tree error. Documented those as a
JSDuck singleton, which they but just weren't documented yet.
NB: Members of @singleton don't need @static (if present,
triggers a compiler warning).
* @chainable: Shorthand for "@return this". We were using
"@return {classname}" which is ambiguous (returns the same
instance or another instance?), @chainable is specifically
for "@return this". Creates proper labels in the generated
HTML pages.
Removed:
* @mixin: (not to be confused with @mixins). Not supported by
JSDuck. Every class is standalone anyway. Where needed marked
them @class + @abstract instead.
Change-Id: I6a7c9e8ee8f995731bc205d666167874eb2ebe23
2013-01-04 08:54:17 +00:00
|
|
|
*/
|
|
|
|
|
2013-12-10 01:39:46 +00:00
|
|
|
/**
|
|
|
|
* Fired when serialization is complete
|
2024-04-30 11:32:58 +00:00
|
|
|
*
|
|
|
|
* @event ve.init.mw.ArticleTarget#serializeComplete
|
2013-12-10 01:39:46 +00:00
|
|
|
*/
|
|
|
|
|
2013-12-13 20:24:54 +00:00
|
|
|
/* Static Properties */
|
|
|
|
|
2013-12-10 01:39:46 +00:00
|
|
|
/**
|
2016-04-21 11:28:00 +00:00
|
|
|
* @inheritdoc
|
|
|
|
*/
|
|
|
|
ve.init.mw.ArticleTarget.static.name = 'article';
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Tracking name of target class. Used by ArticleTargetEvents to identify which target we are tracking.
|
2013-12-10 01:39:46 +00:00
|
|
|
*
|
|
|
|
* @static
|
|
|
|
* @property {string}
|
|
|
|
* @inheritable
|
|
|
|
*/
|
2016-04-21 11:28:00 +00:00
|
|
|
ve.init.mw.ArticleTarget.static.trackingName = 'mwTarget';
|
2013-12-10 01:39:46 +00:00
|
|
|
|
2014-10-29 01:19:52 +00:00
|
|
|
/**
|
2015-12-11 14:57:49 +00:00
|
|
|
* @inheritdoc
|
2014-10-29 01:19:52 +00:00
|
|
|
*/
|
2015-12-10 16:07:50 +00:00
|
|
|
ve.init.mw.ArticleTarget.static.integrationType = 'page';
|
2014-10-29 01:19:52 +00:00
|
|
|
|
2015-09-03 01:24:48 +00:00
|
|
|
/**
|
2015-12-11 14:57:49 +00:00
|
|
|
* @inheritdoc
|
2015-09-03 01:24:48 +00:00
|
|
|
*/
|
2015-12-10 16:07:50 +00:00
|
|
|
ve.init.mw.ArticleTarget.static.platformType = 'other';
|
2015-09-03 01:24:48 +00:00
|
|
|
|
2017-09-11 16:00:17 +00:00
|
|
|
/**
|
|
|
|
* @inheritdoc
|
|
|
|
*/
|
2024-05-29 16:03:23 +00:00
|
|
|
ve.init.mw.ArticleTarget.static.documentCommands = [
|
|
|
|
...ve.init.mw.ArticleTarget.super.static.documentCommands,
|
2022-06-15 12:07:36 +00:00
|
|
|
// Make help dialog triggerable from anywhere
|
|
|
|
'commandHelp',
|
2017-09-11 16:00:17 +00:00
|
|
|
// Make save commands triggerable from anywhere
|
|
|
|
'showSave',
|
|
|
|
'showChanges',
|
|
|
|
'showPreview',
|
|
|
|
'showMinoredit',
|
|
|
|
'showWatchthis'
|
2024-05-29 16:03:23 +00:00
|
|
|
];
|
2017-09-11 16:00:17 +00:00
|
|
|
|
2017-11-17 21:15:27 +00:00
|
|
|
/* Static methods */
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @inheritdoc
|
|
|
|
*/
|
2019-02-13 13:21:26 +00:00
|
|
|
ve.init.mw.ArticleTarget.static.parseDocument = function ( documentString, mode, section, onlySection ) {
|
2017-11-21 15:42:04 +00:00
|
|
|
// Add trailing linebreak to non-empty wikitext documents for consistency
|
2017-11-17 21:15:27 +00:00
|
|
|
// with old editor and usability. Will be stripped on save. T156609
|
2017-11-21 15:42:04 +00:00
|
|
|
if ( mode === 'source' && documentString ) {
|
2017-11-17 21:15:27 +00:00
|
|
|
documentString += '\n';
|
|
|
|
}
|
|
|
|
|
|
|
|
// Parent method
|
2019-02-13 13:21:26 +00:00
|
|
|
return ve.init.mw.ArticleTarget.super.static.parseDocument.call( this, documentString, mode, section, onlySection );
|
2017-11-17 21:15:27 +00:00
|
|
|
};
|
|
|
|
|
2022-02-17 22:06:14 +00:00
|
|
|
/**
|
|
|
|
* Get the editable part of the page
|
|
|
|
*
|
|
|
|
* @return {jQuery} Editable DOM selection
|
|
|
|
*/
|
|
|
|
ve.init.mw.ArticleTarget.prototype.getEditableContent = function () {
|
|
|
|
return $( '#mw-content-text' );
|
|
|
|
};
|
|
|
|
|
2018-04-02 16:28:01 +00:00
|
|
|
/**
|
|
|
|
* Build DOM for the redirect page subtitle (#redirectsub).
|
|
|
|
*
|
|
|
|
* @return {jQuery}
|
|
|
|
*/
|
|
|
|
ve.init.mw.ArticleTarget.static.buildRedirectSub = function () {
|
2024-05-21 14:22:56 +00:00
|
|
|
const $subMsg = mw.message( 'redirectpagesub' ).parseDom();
|
2018-04-02 16:28:01 +00:00
|
|
|
// Page subtitle
|
|
|
|
// Compare: Article::view()
|
|
|
|
return $( '<span>' )
|
|
|
|
.attr( 'id', 'redirectsub' )
|
2021-10-25 15:51:29 +00:00
|
|
|
.append( $subMsg );
|
2018-04-02 16:28:01 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Build DOM for the redirect page content header (.redirectMsg).
|
|
|
|
*
|
|
|
|
* @param {string} title Redirect target
|
|
|
|
* @return {jQuery}
|
|
|
|
*/
|
|
|
|
ve.init.mw.ArticleTarget.static.buildRedirectMsg = function ( title ) {
|
2024-05-21 14:22:56 +00:00
|
|
|
const $link = $( '<a>' )
|
2018-04-02 16:28:01 +00:00
|
|
|
.attr( {
|
|
|
|
href: mw.Title.newFromText( title ).getUrl(),
|
|
|
|
title: mw.msg( 'visualeditor-redirect-description', title )
|
|
|
|
} )
|
|
|
|
.text( title );
|
|
|
|
ve.init.platform.linkCache.styleElement( title, $link );
|
|
|
|
|
|
|
|
// Page content header
|
2023-09-15 19:14:19 +00:00
|
|
|
// Compare: LinkRenderer::makeRedirectHeader()
|
2018-04-02 16:28:01 +00:00
|
|
|
return $( '<div>' )
|
|
|
|
.addClass( 'redirectMsg' )
|
|
|
|
// Hack: This is normally inside #mw-content-text, but we may insert it before, so we need this.
|
2020-04-09 13:33:54 +00:00
|
|
|
// The following classes are used here:
|
|
|
|
// * mw-content-ltr
|
|
|
|
// * mw-content-rtl
|
2018-04-02 16:28:01 +00:00
|
|
|
.addClass( 'mw-content-' + mw.config.get( 'wgVisualEditor' ).pageLanguageDir )
|
|
|
|
.append(
|
|
|
|
$( '<p>' ).text( mw.msg( 'redirectto' ) ),
|
|
|
|
$( '<ul>' )
|
|
|
|
.addClass( 'redirectText' )
|
|
|
|
.append( $( '<li>' ).append( $link ) )
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
2015-07-31 15:03:33 +00:00
|
|
|
/* Methods */
|
|
|
|
|
2016-05-26 12:08:26 +00:00
|
|
|
/**
|
2016-10-27 22:29:38 +00:00
|
|
|
* @inheritdoc
|
|
|
|
*/
|
2016-11-30 12:03:56 +00:00
|
|
|
ve.init.mw.ArticleTarget.prototype.setDefaultMode = function () {
|
2024-05-21 14:22:56 +00:00
|
|
|
const oldDefaultMode = this.defaultMode;
|
2016-10-27 22:29:38 +00:00
|
|
|
// Parent method
|
2016-11-30 12:03:56 +00:00
|
|
|
ve.init.mw.ArticleTarget.super.prototype.setDefaultMode.apply( this, arguments );
|
2016-10-27 22:29:38 +00:00
|
|
|
|
2016-11-30 12:03:56 +00:00
|
|
|
if ( this.defaultMode !== oldDefaultMode ) {
|
2022-08-10 16:11:16 +00:00
|
|
|
this.updateTabs();
|
2016-11-05 05:50:32 +00:00
|
|
|
if ( mw.libs.ve.setEditorPreference ) {
|
|
|
|
// only set up by DAT.init
|
2016-11-30 12:03:56 +00:00
|
|
|
mw.libs.ve.setEditorPreference( this.defaultMode === 'visual' ? 'visualeditor' : 'wikitext' );
|
2016-11-05 05:50:32 +00:00
|
|
|
}
|
2016-05-26 12:08:26 +00:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2016-08-25 21:02:40 +00:00
|
|
|
/**
|
2022-08-10 16:11:16 +00:00
|
|
|
* Update state of editing tabs from this target
|
2016-08-25 21:02:40 +00:00
|
|
|
*/
|
2022-08-10 16:00:17 +00:00
|
|
|
ve.init.mw.ArticleTarget.prototype.updateTabs = function () {};
|
2016-08-25 21:02:40 +00:00
|
|
|
|
2012-06-18 20:12:32 +00:00
|
|
|
/**
|
|
|
|
* Handle response to a successful load request.
|
|
|
|
*
|
|
|
|
* This method is called within the context of a target instance. If successful the DOM from the
|
2016-03-01 22:03:07 +00:00
|
|
|
* server will be parsed, stored in {this.doc} and then {this.documentReady} will be called.
|
2012-06-18 20:12:32 +00:00
|
|
|
*
|
2015-01-24 00:22:17 +00:00
|
|
|
* @param {Object} response API response data
|
JSDuck: Generated code documentation!
See CODING.md for how to run it.
Mistakes fixed:
* Warning: Unknown type function
-> Function
* Warning: Unknown type DOMElement
-> HTMLElement
* Warning: Unknown type DOM Node
-> HTMLElement
* Warning: Unknown type Integer
-> Mixed
* Warning: Unknown type Command
-> ve.Command
* Warning: Unknown type any
-> number
* Warning: Unknown type ve.Transaction
-> ve.dm.Transaction
* Warning: Unknown type ve.dm.AnnotationSet
-> ve.AnnotationSet
* Warning: Unknown type false
-> boolean
* Warning: Unknown type ve.dm.AlienNode
ve.dm doesn't have a generic AlienNode like ve.ce
-> Unknown type ve.dm.AlienInlineNode|ve.dm.AlienBlockNode
* Warning: Unknown type ve.ve.Surface
-> ve.ce.Surface
* ve.example.lookupNode:
-> Last @param should be @return
* ve.dm.Transaction.prototype.pushReplace:
-> @param {Array] should be @param {Array}
* Warning: ve.BranchNode.js:27: {@link ve.Node#hasChildren} links to non-existing member
-> (removed)
* Warning: ve.LeafNode.js:21: {@link ve.Node#hasChildren} links to non-existing member
-> (removed)
Differences fixed:
* Variadic arguments are like @param {Type...} [name]
instead of @param {Type} [name...]
* Convert all file headers from /** to /*! because JSDuck tries
to parse all /** blocks and fails to parse with all sorts of
errors for "Global property", "Unnamed property", and
"Duplicate property".
Find: \/\*\*([^@]+)(@copyright)
Replace: /*!$1$2
* Indented blocks are considered code examples.
A few methods had documentation with numbered lists that were
indented, which have now been updated to not be intended.
* The free-form text descriptions are parsed with Markdown,
which requires lists to be separated from paragraphs by an
empty line.
And we should use `backticks` instead of {braces} for inline
code in text paragraphs.
* Doc blocks for classes and their constructor have to be
in the correct order (@constructor, @param, @return must be
before @class, @abstract, @extends etc.)
* `@extends Class` must not have Class {wrapped}
* @throws must start with a {Type}
* @example means something else. It is used for an inline demo
iframe, not code block. For that simply indent with spaces.
* @member means something else.
Non-function properties are marked with @property, not @member.
* To create a link to a class or member, in most cases the name
is enough to create a link. E.g. Foo, Foo.bar, Foo.bar#quux,
where a hash stands for "instance member", so Foo.bar#quux,
links to Foo.bar.prototype.quux (the is not supported, as
"prototype" is considered an implementation detail, it only
indexes class name and method name).
If the magic linker doesn't work for some case, the
verbose syntax is {@link #target label}.
* @property can't have sub-properties (nested @param and @return
values are supported, only @static @property can't be nested).
We only have one case of this, which can be worked around by
moving those in a new virtual class. The code is unaltered
(only moved down so that it isn't with the scope of the main
@class block). ve.dm.TransactionProcessor.processors.
New:
* @mixins: Classes mixed into the current class.
* @event: Events that can be emitted by a class. These are also
inherited by subclasses. (+ @param, @return and @preventable).
So ve.Node#event-attach is inherited to ve.dm.BreakNode,
just like @method is.
* @singleton: Plain objects such as ve, ve.dm, ve.ce were missing
documentation causing a tree error. Documented those as a
JSDuck singleton, which they but just weren't documented yet.
NB: Members of @singleton don't need @static (if present,
triggers a compiler warning).
* @chainable: Shorthand for "@return this". We were using
"@return {classname}" which is ambiguous (returns the same
instance or another instance?), @chainable is specifically
for "@return this". Creates proper labels in the generated
HTML pages.
Removed:
* @mixin: (not to be confused with @mixins). Not supported by
JSDuck. Every class is standalone anyway. Where needed marked
them @class + @abstract instead.
Change-Id: I6a7c9e8ee8f995731bc205d666167874eb2ebe23
2013-01-04 08:54:17 +00:00
|
|
|
* @param {string} status Text status message
|
2012-06-18 20:12:32 +00:00
|
|
|
*/
|
2015-12-10 16:07:50 +00:00
|
|
|
ve.init.mw.ArticleTarget.prototype.loadSuccess = function ( response ) {
|
2024-05-21 14:22:56 +00:00
|
|
|
const data = response ? ( response.visualeditor || response.visualeditoredit ) : null;
|
2012-12-13 00:22:10 +00:00
|
|
|
|
2016-10-26 00:19:05 +00:00
|
|
|
if ( !data || typeof data.content !== 'string' ) {
|
2020-02-12 20:12:48 +00:00
|
|
|
this.loadFail( 've-api', { errors: [ {
|
|
|
|
code: 've-api',
|
2020-02-12 21:11:25 +00:00
|
|
|
html: mw.message( 'api-clientside-error-invalidresponse' ).parse()
|
2020-02-12 20:12:48 +00:00
|
|
|
} ] } );
|
2019-04-10 18:34:26 +00:00
|
|
|
} else if ( response.veMode && response.veMode !== this.getDefaultMode() ) {
|
2020-02-12 20:12:48 +00:00
|
|
|
this.loadFail( 've-mode', { errors: [ {
|
|
|
|
code: 've-mode',
|
2020-02-12 21:11:25 +00:00
|
|
|
html: mw.message( 'visualeditor-loaderror-wrongmode',
|
|
|
|
response.veMode, this.getDefaultMode() ).parse()
|
2020-02-12 20:12:48 +00:00
|
|
|
} ] } );
|
2012-06-18 20:12:32 +00:00
|
|
|
} else {
|
2017-12-07 11:14:00 +00:00
|
|
|
this.track( 'trace.parseResponse.enter' );
|
2013-04-18 01:22:39 +00:00
|
|
|
this.originalHtml = data.content;
|
2015-10-08 22:16:56 +00:00
|
|
|
this.etag = data.etag;
|
2021-05-03 13:34:25 +00:00
|
|
|
// We are reading from `preloaded` which comes from the VE API. If we want
|
|
|
|
// to make the VE API non-blocking in the future we will need to handle
|
|
|
|
// special-cases like this where the content doesn't come from RESTBase.
|
|
|
|
this.fromEditedState = !!data.fromEditedState || !!data.preloaded;
|
2023-06-24 10:21:30 +00:00
|
|
|
this.switched = data.switched;
|
2024-05-21 14:22:56 +00:00
|
|
|
const mode = this.getDefaultMode();
|
|
|
|
const section = ( mode === 'source' || this.enableVisualSectionEditing ) ? this.section : null;
|
2019-02-13 13:21:26 +00:00
|
|
|
this.doc = this.constructor.static.parseDocument( this.originalHtml, mode, section );
|
2020-12-08 17:23:48 +00:00
|
|
|
this.originalDmDocPromise = null;
|
2012-12-13 00:22:10 +00:00
|
|
|
|
2018-11-08 18:23:47 +00:00
|
|
|
// Properties that don't come from the API
|
2018-01-23 20:51:11 +00:00
|
|
|
this.initialSourceRange = data.initialSourceRange;
|
2018-11-08 18:23:47 +00:00
|
|
|
this.recovered = data.recovered;
|
2023-06-15 00:06:42 +00:00
|
|
|
this.isRedirect = false;
|
2014-03-11 00:46:26 +00:00
|
|
|
|
2018-11-29 03:19:08 +00:00
|
|
|
// Parse data this not available in RESTBase
|
|
|
|
if ( !this.parseMetadata( response ) ) {
|
|
|
|
// Invalid metadata, loadFail() or load() has been called
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2017-12-07 11:14:00 +00:00
|
|
|
this.track( 'trace.parseResponse.exit' );
|
2018-02-21 22:58:50 +00:00
|
|
|
|
Load RL modules in one load.php request, rather than in two stages
This introduces TargetLoader, which manages plugins and RL modules
in a slightly more generic fashion so that Targets themselves don't
have to. This allows us to load all RL modules in one load.php
request, rather than first loading ViewPageTarget which then
loads the other modules.
TargetLoader loads in the bottom queue, so it will be loaded
as part of the main load.php request, but in VPT.init.js we
still have to wait for it with using() because it might not
have arrived yet. This also degrades gracefully on cached pages
where TargetLoader isn't in the bottom queue: it'll be loaded
as a separate request instead, which is suboptimal but no
worse that what we were doing before.
Right now TargetLoader is small enough that it could also be in
the top queue, but in the future we want to add things like
the action=visualeditor API request to it, and mw.Api is
relatively big.
Note: this also makes a breaking change to the plugin API:
plugin callbacks no longer receive the target instance
as a parameter, as they're now executed before the target
has been constructed rather than after. In the long term,
if we want to give plugins access to the target instance,
we could give them the target promise somehow. For now,
I've killed this feature because nothing used it and
the change from a direct object reference to a promise
would have been a breaking change anyway.
Also fixed incorrect documentation index for ve.init.mw.ViewPageTarget.init.
Bug: T53569
Change-Id: Ibfa6abbeaf872ae2aadc6ed9d5beba7473ea441a
2015-02-26 01:22:44 +00:00
|
|
|
// Everything worked, the page was loaded, continue initializing the editor
|
2015-12-11 14:57:49 +00:00
|
|
|
this.documentReady( this.doc );
|
2012-06-18 20:12:32 +00:00
|
|
|
}
|
2016-11-05 06:16:26 +00:00
|
|
|
|
2022-09-28 15:44:35 +00:00
|
|
|
if ( !this.isViewPage ) {
|
2017-03-28 17:47:51 +00:00
|
|
|
$( '#firstHeading' ).text(
|
2018-05-04 13:30:10 +00:00
|
|
|
mw.Title.newFromText( this.getPageName() ).getPrefixedText()
|
2016-11-05 06:16:26 +00:00
|
|
|
);
|
|
|
|
}
|
2018-11-08 18:23:47 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Parse document metadata from the API response
|
|
|
|
*
|
|
|
|
* @param {Object} response API response data
|
2018-11-29 03:19:08 +00:00
|
|
|
* @return {boolean} Whether metadata was loaded successfully. If true, you should call
|
|
|
|
* loadSuccess(). If false, either that loadFail() has been called or we're retrying via load().
|
2018-11-08 18:23:47 +00:00
|
|
|
*/
|
|
|
|
ve.init.mw.ArticleTarget.prototype.parseMetadata = function ( response ) {
|
2024-05-21 14:22:56 +00:00
|
|
|
const data = response ? ( response.visualeditor || response.visualeditoredit ) : null;
|
2018-11-08 18:23:47 +00:00
|
|
|
|
|
|
|
if ( !data ) {
|
2020-02-12 20:12:48 +00:00
|
|
|
this.loadFail( 've-api', { errors: [ {
|
|
|
|
code: 've-api',
|
2020-02-12 21:11:25 +00:00
|
|
|
html: mw.message( 'api-clientside-error-invalidresponse' ).parse()
|
2020-02-12 20:12:48 +00:00
|
|
|
} ] } );
|
2018-11-29 03:19:08 +00:00
|
|
|
return false;
|
2018-11-08 18:23:47 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
this.remoteNotices = ve.getObjectValues( data.notices );
|
|
|
|
this.protectedClasses = data.protectedClasses;
|
|
|
|
|
|
|
|
this.baseTimeStamp = data.basetimestamp;
|
|
|
|
this.startTimeStamp = data.starttimestamp;
|
2020-09-15 21:26:59 +00:00
|
|
|
this.revid = data.oldid || undefined;
|
2018-11-29 14:38:53 +00:00
|
|
|
this.preloaded = !!data.preloaded;
|
2018-11-08 18:23:47 +00:00
|
|
|
|
2022-01-24 13:06:19 +00:00
|
|
|
this.copyrightWarning = data.copyrightWarning;
|
|
|
|
|
2018-11-08 18:23:47 +00:00
|
|
|
this.checkboxesDef = data.checkboxesDef;
|
|
|
|
this.checkboxesMessages = data.checkboxesMessages;
|
|
|
|
mw.messages.set( data.checkboxesMessages );
|
|
|
|
|
2019-05-18 15:09:13 +00:00
|
|
|
this.canEdit = data.canEdit;
|
2023-06-15 00:06:42 +00:00
|
|
|
this.wouldautocreate = data.wouldautocreate;
|
2019-05-18 15:09:13 +00:00
|
|
|
|
2021-10-13 12:57:45 +00:00
|
|
|
// When docRevId is `undefined` it indicates that the page doesn't exist
|
2024-05-21 14:22:56 +00:00
|
|
|
let docRevId;
|
|
|
|
const aboutDoc = this.doc.documentElement && this.doc.documentElement.getAttribute( 'about' );
|
2018-11-08 18:23:47 +00:00
|
|
|
if ( aboutDoc ) {
|
2024-05-21 14:22:56 +00:00
|
|
|
const docRevIdMatches = aboutDoc.match( /revision\/([0-9]*)$/ );
|
2018-11-08 18:23:47 +00:00
|
|
|
if ( docRevIdMatches.length >= 2 ) {
|
|
|
|
docRevId = parseInt( docRevIdMatches[ 1 ] );
|
|
|
|
}
|
|
|
|
}
|
2020-09-15 21:26:59 +00:00
|
|
|
// There is no docRevId in source mode (doc is just a string), new visual documents, or when
|
|
|
|
// switching from source mode with changes.
|
2020-07-05 17:30:33 +00:00
|
|
|
if ( this.getDefaultMode() === 'visual' && !( this.switched && this.fromEditedState ) && docRevId !== this.revid ) {
|
2018-11-08 18:23:47 +00:00
|
|
|
if ( this.retriedRevIdConflict ) {
|
|
|
|
// Retried already, just error the second time.
|
2020-02-12 20:12:48 +00:00
|
|
|
this.loadFail( 've-api', { errors: [ {
|
|
|
|
code: 've-api',
|
2020-02-12 21:11:25 +00:00
|
|
|
html: mw.message( 'visualeditor-loaderror-revidconflict',
|
|
|
|
String( docRevId ), String( this.revid ) ).parse()
|
2020-02-12 20:12:48 +00:00
|
|
|
} ] } );
|
2018-11-08 18:23:47 +00:00
|
|
|
} else {
|
|
|
|
this.retriedRevIdConflict = true;
|
|
|
|
// TODO this retries both requests, in RESTbase mode we should only retry
|
|
|
|
// the request that gave us the lower revid
|
2019-11-04 14:54:53 +00:00
|
|
|
this.loading = null;
|
2018-11-08 18:23:47 +00:00
|
|
|
// HACK: Load with explicit revid to hopefully prevent this from happening again
|
2020-10-15 06:49:29 +00:00
|
|
|
this.requestedRevId = Math.max( docRevId || 0, this.revid );
|
2018-11-08 18:23:47 +00:00
|
|
|
this.load();
|
|
|
|
}
|
2018-11-29 03:19:08 +00:00
|
|
|
return false;
|
2018-11-08 18:23:47 +00:00
|
|
|
} else {
|
|
|
|
// Set this to false after a successful load, so we don't immediately give up
|
|
|
|
// if a subsequent load mismatches again
|
|
|
|
this.retriedRevIdConflict = false;
|
|
|
|
}
|
|
|
|
|
2021-01-14 15:59:25 +00:00
|
|
|
// Save dialog doesn't exist yet, so create an overlay for the widgets, and
|
|
|
|
// append it to the save dialog later.
|
|
|
|
this.$saveDialogOverlay = $( '<div>' ).addClass( 'oo-ui-window-overlay' );
|
2024-05-21 14:22:56 +00:00
|
|
|
const checkboxes = mw.libs.ve.targetLoader.createCheckboxFields( this.checkboxesDef, { $overlay: this.$saveDialogOverlay } );
|
2020-03-04 15:07:42 +00:00
|
|
|
this.checkboxFields = checkboxes.checkboxFields;
|
|
|
|
this.checkboxesByName = checkboxes.checkboxesByName;
|
|
|
|
|
2024-04-30 16:44:25 +00:00
|
|
|
this.checkboxFields.forEach( ( field ) => {
|
2020-03-04 15:07:42 +00:00
|
|
|
// TODO: This method should be upstreamed or moved so that targetLoader
|
|
|
|
// can use it safely.
|
|
|
|
ve.targetLinksToNewWindow( field.$label[ 0 ] );
|
|
|
|
} );
|
2018-11-29 03:19:08 +00:00
|
|
|
|
|
|
|
return true;
|
2012-06-18 20:12:32 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
2015-12-11 14:57:49 +00:00
|
|
|
* @inheritdoc
|
2012-06-18 20:12:32 +00:00
|
|
|
*/
|
2015-12-11 14:57:49 +00:00
|
|
|
ve.init.mw.ArticleTarget.prototype.documentReady = function () {
|
2016-03-01 22:03:07 +00:00
|
|
|
// We need to wait until documentReady as local notices may require special messages
|
2015-03-30 19:35:38 +00:00
|
|
|
this.editNotices = this.remoteNotices.concat(
|
2024-04-30 16:44:25 +00:00
|
|
|
this.localNoticeMessages.map( ( msgKey ) => '<p>' + ve.init.platform.getParsedMessage( msgKey ) + '</p>' )
|
2015-03-30 19:35:38 +00:00
|
|
|
);
|
|
|
|
|
2019-11-04 14:54:53 +00:00
|
|
|
this.loading = null;
|
2015-10-08 22:16:56 +00:00
|
|
|
this.edited = this.fromEditedState;
|
2015-12-11 14:57:49 +00:00
|
|
|
|
|
|
|
// Parent method
|
|
|
|
ve.init.mw.ArticleTarget.super.prototype.documentReady.apply( this, arguments );
|
2012-06-18 20:12:32 +00:00
|
|
|
};
|
|
|
|
|
2015-07-01 11:11:36 +00:00
|
|
|
/**
|
2015-12-11 14:57:49 +00:00
|
|
|
* @inheritdoc
|
2015-07-01 11:11:36 +00:00
|
|
|
*/
|
2015-12-11 14:57:49 +00:00
|
|
|
ve.init.mw.ArticleTarget.prototype.surfaceReady = function () {
|
2024-05-21 14:22:56 +00:00
|
|
|
const accessKeyPrefix = $.fn.updateTooltipAccessKeys.getAccessKeyPrefix().replace( /-/g, '+' ),
|
2018-02-21 22:58:50 +00:00
|
|
|
accessKeyModifiers = new ve.ui.Trigger( accessKeyPrefix + '-' ).modifiers,
|
|
|
|
surfaceModel = this.getSurface().getModel();
|
2016-12-13 21:55:01 +00:00
|
|
|
|
2015-12-11 14:57:49 +00:00
|
|
|
// loadSuccess() may have called setAssumeExistence( true );
|
|
|
|
ve.init.platform.linkCache.setAssumeExistence( false );
|
2018-02-21 22:58:50 +00:00
|
|
|
surfaceModel.connect( this, {
|
2015-12-11 14:57:49 +00:00
|
|
|
history: 'updateToolbarSaveButtonState'
|
|
|
|
} );
|
|
|
|
|
2022-06-15 12:41:44 +00:00
|
|
|
// Handle cancel events, i.e. pressing <escape>
|
|
|
|
this.getSurface().connect( this, {
|
|
|
|
cancel: 'onSurfaceCancel'
|
|
|
|
} );
|
|
|
|
|
2016-12-13 21:55:01 +00:00
|
|
|
// Iterate over the trigger registry and resolve any access key conflicts
|
2024-05-21 14:22:56 +00:00
|
|
|
for ( const name in ve.ui.triggerRegistry.registry ) {
|
|
|
|
const triggers = ve.ui.triggerRegistry.registry[ name ];
|
|
|
|
for ( let i = 0; i < triggers.length; i++ ) {
|
2016-12-13 21:55:01 +00:00
|
|
|
if ( ve.compare( triggers[ i ].modifiers, accessKeyModifiers ) ) {
|
|
|
|
this.disableAccessKey( triggers[ i ].primary );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-05-20 16:10:30 +00:00
|
|
|
if ( !mw.config.get( 'wgVisualEditorConfig' ).enableHelpCompletion ) {
|
|
|
|
this.getSurface().commandRegistry.unregister( 'openHelpCompletions' );
|
|
|
|
this.getSurface().commandRegistry.unregister( 'openHelpCompletionsTrigger' );
|
|
|
|
}
|
|
|
|
|
2019-05-18 15:09:13 +00:00
|
|
|
if ( !this.canEdit ) {
|
2019-02-09 19:28:34 +00:00
|
|
|
this.getSurface().setReadOnly( true );
|
2018-02-21 22:58:50 +00:00
|
|
|
} else {
|
2024-03-13 22:44:20 +00:00
|
|
|
// TODO: If the user rejects joining the collab session, start auto-save
|
|
|
|
if ( !this.currentUrl.searchParams.has( 'collabSession' ) ) {
|
|
|
|
// Auto-save
|
|
|
|
this.initAutosave();
|
|
|
|
}
|
2019-02-09 19:28:34 +00:00
|
|
|
|
2024-04-30 16:44:25 +00:00
|
|
|
setTimeout( () => {
|
2019-11-01 15:45:27 +00:00
|
|
|
mw.libs.ve.targetSaver.preloadDeflate();
|
2019-02-09 19:28:34 +00:00
|
|
|
}, 500 );
|
2018-02-21 22:58:50 +00:00
|
|
|
}
|
2018-06-10 15:04:15 +00:00
|
|
|
|
2015-12-11 14:57:49 +00:00
|
|
|
// Parent method
|
|
|
|
ve.init.mw.ArticleTarget.super.prototype.surfaceReady.apply( this, arguments );
|
2018-11-29 14:38:53 +00:00
|
|
|
|
|
|
|
mw.hook( 've.activationComplete' ).fire();
|
2015-07-01 11:11:36 +00:00
|
|
|
};
|
|
|
|
|
2022-06-15 12:41:44 +00:00
|
|
|
/**
|
|
|
|
* Handle surface cancel events
|
|
|
|
*/
|
|
|
|
ve.init.mw.ArticleTarget.prototype.onSurfaceCancel = function () {
|
|
|
|
this.tryTeardown( false, 'navigate-read' );
|
|
|
|
};
|
|
|
|
|
2021-12-21 18:41:36 +00:00
|
|
|
/**
|
|
|
|
* Runs after the surface has been made ready and visible
|
|
|
|
*
|
|
|
|
* Implementing sub-classes must call this method.
|
|
|
|
*/
|
|
|
|
ve.init.mw.ArticleTarget.prototype.afterSurfaceReady = function () {
|
|
|
|
this.restoreEditSection();
|
|
|
|
};
|
|
|
|
|
2018-02-21 22:58:50 +00:00
|
|
|
/**
|
2019-04-17 16:28:48 +00:00
|
|
|
* @inheritdoc
|
2018-02-21 22:58:50 +00:00
|
|
|
*/
|
|
|
|
ve.init.mw.ArticleTarget.prototype.storeDocState = function ( html ) {
|
2024-05-21 14:22:56 +00:00
|
|
|
const mode = this.getSurface().getMode();
|
2018-02-27 16:00:21 +00:00
|
|
|
this.getSurface().getModel().storeDocState( {
|
2018-02-21 22:58:50 +00:00
|
|
|
request: {
|
2018-05-04 13:30:10 +00:00
|
|
|
pageName: this.getPageName(),
|
2018-02-21 22:58:50 +00:00
|
|
|
mode: mode,
|
2019-02-13 13:21:26 +00:00
|
|
|
// Check true section editing is in use
|
|
|
|
section: ( mode === 'source' || this.enableVisualSectionEditing ) ? this.section : null
|
2018-02-21 22:58:50 +00:00
|
|
|
},
|
|
|
|
response: {
|
|
|
|
etag: this.etag,
|
|
|
|
fromEditedState: this.fromEditedState,
|
|
|
|
switched: this.switched,
|
|
|
|
preloaded: this.preloaded,
|
|
|
|
notices: this.remoteNotices,
|
|
|
|
protectedClasses: this.protectedClasses,
|
|
|
|
basetimestamp: this.baseTimeStamp,
|
|
|
|
starttimestamp: this.startTimeStamp,
|
|
|
|
oldid: this.revid,
|
2019-05-24 21:08:17 +00:00
|
|
|
canEdit: this.canEdit,
|
2023-06-15 00:06:42 +00:00
|
|
|
wouldautocreate: this.wouldautocreate,
|
2022-04-04 23:11:28 +00:00
|
|
|
copyrightWarning: this.copyrightWarning,
|
2018-02-21 22:58:50 +00:00
|
|
|
checkboxesDef: this.checkboxesDef,
|
2019-02-22 18:50:46 +00:00
|
|
|
checkboxesMessages: this.checkboxesMessages
|
2018-02-21 22:58:50 +00:00
|
|
|
}
|
2018-02-27 16:00:21 +00:00
|
|
|
}, html );
|
2018-02-21 22:58:50 +00:00
|
|
|
};
|
|
|
|
|
2016-12-13 21:55:01 +00:00
|
|
|
/**
|
|
|
|
* Disable an access key by removing the attribute from any element containing it
|
|
|
|
*
|
|
|
|
* @param {string} key Access key
|
|
|
|
*/
|
|
|
|
ve.init.mw.ArticleTarget.prototype.disableAccessKey = function ( key ) {
|
2024-04-30 16:44:25 +00:00
|
|
|
$( '[accesskey=' + key + ']' ).each( ( i, el ) => {
|
2024-05-21 14:22:56 +00:00
|
|
|
const $el = $( el );
|
2016-12-13 21:55:01 +00:00
|
|
|
|
2024-04-30 16:44:25 +00:00
|
|
|
$el
|
|
|
|
.attr( 'data-old-accesskey', $el.attr( 'accesskey' ) )
|
2016-12-13 21:55:01 +00:00
|
|
|
.removeAttr( 'accesskey' );
|
|
|
|
} );
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Re-enable all access keys
|
|
|
|
*/
|
|
|
|
ve.init.mw.ArticleTarget.prototype.restoreAccessKeys = function () {
|
2024-04-30 16:44:25 +00:00
|
|
|
$( '[data-old-accesskey]' ).each( ( i, el ) => {
|
2024-05-21 14:22:56 +00:00
|
|
|
const $el = $( el );
|
2016-12-13 21:55:01 +00:00
|
|
|
|
2024-04-30 16:44:25 +00:00
|
|
|
$el
|
|
|
|
.attr( 'accesskey', $el.attr( 'data-old-accesskey' ) )
|
2016-12-13 21:55:01 +00:00
|
|
|
.removeAttr( 'data-old-accesskey' );
|
|
|
|
} );
|
|
|
|
};
|
|
|
|
|
2012-06-18 20:12:32 +00:00
|
|
|
/**
|
2013-01-15 23:38:49 +00:00
|
|
|
* Handle an unsuccessful load request.
|
2012-06-18 20:12:32 +00:00
|
|
|
*
|
|
|
|
* This method is called within the context of a target instance.
|
|
|
|
*
|
2020-02-12 20:12:48 +00:00
|
|
|
* @param {string} code Error code from mw.Api
|
|
|
|
* @param {Object} errorDetails API response
|
2024-04-29 18:14:26 +00:00
|
|
|
* @fires ve.init.mw.ArticleTarget#loadError
|
2012-06-18 20:12:32 +00:00
|
|
|
*/
|
2015-12-10 16:07:50 +00:00
|
|
|
ve.init.mw.ArticleTarget.prototype.loadFail = function () {
|
2019-11-04 14:54:53 +00:00
|
|
|
this.loading = null;
|
2015-08-04 13:37:13 +00:00
|
|
|
this.emit( 'loadError' );
|
2012-06-18 20:12:32 +00:00
|
|
|
};
|
|
|
|
|
2022-02-17 22:06:14 +00:00
|
|
|
/**
|
|
|
|
* Replace the page content with new HTML.
|
|
|
|
*
|
|
|
|
* @method
|
|
|
|
* @param {string} html Rendered HTML from server
|
|
|
|
* @param {string} categoriesHtml Rendered categories HTML from server
|
|
|
|
* @param {string} displayTitle HTML to show as the page title
|
|
|
|
* @param {Object} lastModified Object containing user-formatted date
|
|
|
|
* and time strings, or undefined if we made no change.
|
|
|
|
* @param {string} contentSub HTML to show as the content subtitle
|
2022-06-03 01:37:55 +00:00
|
|
|
* @param {Array} sections Section data to display in the TOC
|
2022-02-17 22:06:14 +00:00
|
|
|
*/
|
2022-08-24 12:28:07 +00:00
|
|
|
ve.init.mw.ArticleTarget.prototype.replacePageContent = function (
|
|
|
|
html, categoriesHtml, displayTitle, lastModified, contentSub, sections
|
|
|
|
) {
|
|
|
|
// eslint-disable-next-line no-jquery/no-append-html
|
|
|
|
this.$editableContent.find( '.mw-parser-output' ).first().replaceWith( html );
|
|
|
|
mw.hook( 'wikipage.content' ).fire( this.$editableContent );
|
|
|
|
|
|
|
|
if ( displayTitle ) {
|
|
|
|
// eslint-disable-next-line no-jquery/no-html
|
|
|
|
$( '#firstHeading' ).html( displayTitle );
|
|
|
|
}
|
|
|
|
|
|
|
|
// Categories are only shown in AMC on mobile
|
|
|
|
if ( $( '#catlinks' ).length ) {
|
2024-05-21 14:22:56 +00:00
|
|
|
const $categories = $( $.parseHTML( categoriesHtml ) );
|
2022-08-24 12:28:07 +00:00
|
|
|
mw.hook( 'wikipage.categories' ).fire( $categories );
|
|
|
|
$( '#catlinks' ).replaceWith( $categories );
|
|
|
|
}
|
|
|
|
|
2022-12-09 22:37:02 +00:00
|
|
|
mw.util.clearSubtitle();
|
|
|
|
mw.util.addSubtitle( contentSub );
|
2022-08-24 12:28:07 +00:00
|
|
|
|
|
|
|
this.setRealRedirectInterface();
|
|
|
|
|
|
|
|
mw.hook( 'wikipage.tableOfContents' ).fire( sections );
|
|
|
|
};
|
2022-02-17 22:06:14 +00:00
|
|
|
|
2015-08-04 13:37:13 +00:00
|
|
|
/**
|
|
|
|
* Handle successful DOM save event.
|
|
|
|
*
|
2019-11-04 14:54:53 +00:00
|
|
|
* @param {Object} data Save data from the API
|
2023-06-03 01:19:38 +00:00
|
|
|
* @param {boolean} data.nocontent Indicates that page HTML and related properties were omitted
|
2019-11-04 14:54:53 +00:00
|
|
|
* @param {string} data.content Rendered page HTML from server
|
|
|
|
* @param {string} data.categorieshtml Rendered categories HTML from server
|
|
|
|
* @param {number} data.newrevid New revision id, undefined if unchanged
|
|
|
|
* @param {boolean} data.isRedirect Whether this page is a redirect or not
|
|
|
|
* @param {string} data.displayTitleHtml What HTML to show as the page title
|
|
|
|
* @param {Object} data.lastModified Object containing user-formatted date
|
2015-08-04 13:37:13 +00:00
|
|
|
* and time strings, or undefined if we made no change.
|
2019-11-04 14:54:53 +00:00
|
|
|
* @param {string} data.contentSub HTML to show as the content subtitle
|
|
|
|
* @param {Array} data.modules The modules to be loaded on the page
|
|
|
|
* @param {Object} data.jsconfigvars The mw.config values needed on the page
|
2022-06-03 01:37:55 +00:00
|
|
|
* @param {Array} data.sections Section data to display in the TOC
|
2023-06-15 00:06:42 +00:00
|
|
|
* @param {boolean} data.tempusercreated True if we just became logged in as a temporary user
|
|
|
|
* @param {string} data.tempusercreatedredirect URL to visit to finish creating temp account
|
2024-04-29 18:14:26 +00:00
|
|
|
* @fires ve.init.mw.ArticleTarget#save
|
2015-08-04 13:37:13 +00:00
|
|
|
*/
|
2019-11-04 14:54:53 +00:00
|
|
|
ve.init.mw.ArticleTarget.prototype.saveComplete = function ( data ) {
|
2017-03-09 23:26:08 +00:00
|
|
|
this.editSummaryValue = null;
|
|
|
|
this.initialEditSummary = null;
|
|
|
|
|
2015-08-05 21:43:23 +00:00
|
|
|
this.saveDeferred.resolve();
|
2019-11-04 14:54:53 +00:00
|
|
|
this.emit( 'save', data );
|
2022-02-17 22:06:14 +00:00
|
|
|
|
2023-06-15 00:06:42 +00:00
|
|
|
// This is a page creation, a restoration, or we loaded the editor from a non-view page,
|
|
|
|
// or we just became logged in as a temporary user: refresh the page.
|
|
|
|
if ( data.nocontent || data.tempusercreated ) {
|
2022-02-17 22:06:14 +00:00
|
|
|
// Teardown the target, ensuring auto-save data is cleared
|
2024-04-30 16:44:25 +00:00
|
|
|
this.teardown().then( () => {
|
2023-08-24 01:38:38 +00:00
|
|
|
if ( data.newrevid !== undefined ) {
|
2024-05-21 14:22:56 +00:00
|
|
|
let action;
|
2024-05-01 12:32:49 +00:00
|
|
|
if ( this.restoring ) {
|
2023-08-24 01:38:38 +00:00
|
|
|
action = 'restored';
|
2024-05-01 12:32:49 +00:00
|
|
|
} else if ( !this.pageExists ) {
|
2023-08-24 01:38:38 +00:00
|
|
|
action = 'created';
|
|
|
|
} else {
|
|
|
|
action = 'saved';
|
|
|
|
}
|
|
|
|
require( 'mediawiki.action.view.postEdit' ).fireHookOnPageReload( action, data.tempusercreated );
|
|
|
|
}
|
|
|
|
|
2023-06-15 00:06:42 +00:00
|
|
|
if ( data.tempusercreatedredirect ) {
|
|
|
|
location.href = data.tempusercreatedredirect;
|
|
|
|
} else {
|
2024-05-21 14:22:56 +00:00
|
|
|
const newUrl = new URL( this.viewUrl );
|
2023-06-15 00:06:42 +00:00
|
|
|
if ( data.newrevid !== undefined ) {
|
|
|
|
// For GrowthExperiments
|
|
|
|
newUrl.searchParams.set( 'venotify', 'saved' );
|
2022-12-16 21:05:40 +00:00
|
|
|
}
|
2023-06-15 00:06:42 +00:00
|
|
|
if ( data.isRedirect ) {
|
|
|
|
newUrl.searchParams.set( 'redirect', 'no' );
|
|
|
|
}
|
|
|
|
location.href = newUrl;
|
2022-02-17 22:06:14 +00:00
|
|
|
}
|
|
|
|
} );
|
|
|
|
} else {
|
|
|
|
// Update watch link to match 'watch checkbox' in save dialog.
|
|
|
|
// User logged in if module loaded.
|
|
|
|
if ( mw.loader.getState( 'mediawiki.page.watch.ajax' ) === 'ready' ) {
|
2024-05-21 14:22:56 +00:00
|
|
|
const watch = require( 'mediawiki.page.watch.ajax' );
|
2022-02-17 22:06:14 +00:00
|
|
|
|
|
|
|
watch.updatePageWatchStatus(
|
|
|
|
data.watched,
|
|
|
|
data.watchlistexpiry
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
// If we were explicitly editing an older version, make sure we won't
|
|
|
|
// load the same old version again, now that we've saved the next edit
|
|
|
|
// will be against the latest version.
|
|
|
|
// If there is an ?oldid= parameter in the URL, this will cause restorePage() to remove it.
|
|
|
|
this.restoring = false;
|
|
|
|
|
|
|
|
// Clear requestedRevId in case it was set by a retry or something; after saving
|
|
|
|
// we don't want to go back into oldid mode anyway
|
|
|
|
this.requestedRevId = undefined;
|
|
|
|
|
|
|
|
if ( data.newrevid !== undefined ) {
|
|
|
|
mw.config.set( {
|
|
|
|
wgCurRevisionId: data.newrevid,
|
|
|
|
wgRevisionId: data.newrevid
|
|
|
|
} );
|
|
|
|
this.revid = data.newrevid;
|
|
|
|
this.currentRevisionId = data.newrevid;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Update module JS config values and notify ResourceLoader of any new
|
|
|
|
// modules needed to be added to the page
|
|
|
|
mw.config.set( data.jsconfigvars );
|
|
|
|
mw.loader.load( data.modules );
|
|
|
|
|
|
|
|
mw.config.set( {
|
|
|
|
wgIsRedirect: !!data.isRedirect
|
|
|
|
} );
|
|
|
|
|
|
|
|
if ( this.saveDialog ) {
|
|
|
|
this.saveDialog.reset();
|
|
|
|
}
|
|
|
|
|
|
|
|
this.replacePageContent(
|
|
|
|
data.content,
|
|
|
|
data.categorieshtml,
|
|
|
|
data.displayTitleHtml,
|
|
|
|
data.lastModified,
|
2022-06-03 01:37:55 +00:00
|
|
|
data.contentSub,
|
|
|
|
data.sections
|
2022-02-17 22:06:14 +00:00
|
|
|
);
|
|
|
|
|
|
|
|
// Tear down the target now that we're done saving
|
|
|
|
// Not passing trackMechanism because this isn't an abort action
|
|
|
|
this.tryTeardown( true );
|
|
|
|
}
|
2015-08-04 13:37:13 +00:00
|
|
|
};
|
|
|
|
|
2012-06-18 20:12:32 +00:00
|
|
|
/**
|
2013-01-15 23:38:49 +00:00
|
|
|
* Handle an unsuccessful save request.
|
2012-06-18 20:12:32 +00:00
|
|
|
*
|
2014-04-23 19:30:48 +00:00
|
|
|
* @param {HTMLDocument} doc HTML document we tried to save
|
|
|
|
* @param {Object} saveData Options that were used
|
2019-11-04 14:54:53 +00:00
|
|
|
* @param {string} code Error code
|
|
|
|
* @param {Object|null} data Full API response data, or XHR error details
|
2024-04-29 18:14:26 +00:00
|
|
|
* @fires ve.init.mw.ArticleTarget#saveError
|
2012-12-07 16:23:23 +00:00
|
|
|
*/
|
2023-08-14 16:51:25 +00:00
|
|
|
ve.init.mw.ArticleTarget.prototype.saveFail = function ( doc, saveData, code, data ) {
|
2015-08-04 13:37:13 +00:00
|
|
|
this.pageDeletedWarning = false;
|
2013-11-26 19:29:14 +00:00
|
|
|
|
2024-05-21 16:40:36 +00:00
|
|
|
let handled = false;
|
2013-11-26 19:29:14 +00:00
|
|
|
// Handle empty response
|
|
|
|
if ( !data ) {
|
2015-08-04 13:37:13 +00:00
|
|
|
this.saveErrorEmpty();
|
2021-01-15 19:06:20 +00:00
|
|
|
handled = true;
|
2013-11-26 19:29:14 +00:00
|
|
|
}
|
2015-03-19 01:51:30 +00:00
|
|
|
|
2021-01-15 19:06:20 +00:00
|
|
|
if ( !handled && data.errors ) {
|
2024-05-21 14:22:56 +00:00
|
|
|
for ( let i = 0; i < data.errors.length; i++ ) {
|
|
|
|
const error = data.errors[ i ];
|
ve.init.mw.ArticleTarget: Use errorformat=html when saving
When saving fails for a reason we don't handle explicitly, the error
message will have HTML formatting and will respect any on-wiki
overridden messages, rather than being plain text generic message.
Extensions providing custom SaveErrorHandlers may need to be updated.
The only one in Gerrit that requires a fix is TitleBlacklist:
Ibeae79c95557a7af699716c9d921f34c310bee6d.
* Remove handling for errors returned in .visualeditoredit.edit.info
rather than .errors (.error in old format). AFAIK this is only used
by some extensions, it is probably incorrect to do (T229539) and all
extensions I know of that do this (AbuseFilter, SpamBlacklist,
ConfirmEdit) have custom SaveErrorHandlers.
* Remove custom error messages for 'readonly' (identical to API
response) and for 'hookaborted' (very unhelpful and there is a
chance that the API response is better, if the extension causing
this error generates any error message).
* Add a silly shim for MobileFrontend integration, because we allow it
to handle error responses, and it expects them in the old format.
This is probably subtly wrong in many ways, but MobileFrontend code
only uses this for logging, so it shouldn't explode. In the future
we will hopefully change it to use errorformat=html (T228897#5366960).
Bug: T229532
Change-Id: I3b9c4fefc0869ef7999c21cef754434febd852ec
2019-08-01 03:32:42 +00:00
|
|
|
|
2023-08-14 17:17:59 +00:00
|
|
|
if ( error.code === 'assertanonfailed' || error.code === 'assertuserfailed' || error.code === 'assertnameduserfailed' ) {
|
2024-04-30 16:44:25 +00:00
|
|
|
this.refreshUser().then( ( username ) => {
|
2024-08-13 23:27:03 +00:00
|
|
|
// Reattempt the save after successfully refreshing the
|
|
|
|
// user, but only if it's a temporary account (T345975)
|
2024-08-27 21:23:11 +00:00
|
|
|
if ( error.code === 'assertanonfailed' && mw.util.isTemporaryUser( username ) ) {
|
2024-08-13 23:27:03 +00:00
|
|
|
this.startSave( this.getSaveOptions() );
|
|
|
|
} else {
|
|
|
|
this.saveErrorNewUser( username );
|
|
|
|
}
|
2024-04-30 16:44:25 +00:00
|
|
|
}, () => {
|
2024-05-01 12:32:49 +00:00
|
|
|
this.saveErrorUnknown( data );
|
ve.init.mw.ArticleTarget: Use errorformat=html when saving
When saving fails for a reason we don't handle explicitly, the error
message will have HTML formatting and will respect any on-wiki
overridden messages, rather than being plain text generic message.
Extensions providing custom SaveErrorHandlers may need to be updated.
The only one in Gerrit that requires a fix is TitleBlacklist:
Ibeae79c95557a7af699716c9d921f34c310bee6d.
* Remove handling for errors returned in .visualeditoredit.edit.info
rather than .errors (.error in old format). AFAIK this is only used
by some extensions, it is probably incorrect to do (T229539) and all
extensions I know of that do this (AbuseFilter, SpamBlacklist,
ConfirmEdit) have custom SaveErrorHandlers.
* Remove custom error messages for 'readonly' (identical to API
response) and for 'hookaborted' (very unhelpful and there is a
chance that the API response is better, if the extension causing
this error generates any error message).
* Add a silly shim for MobileFrontend integration, because we allow it
to handle error responses, and it expects them in the old format.
This is probably subtly wrong in many ways, but MobileFrontend code
only uses this for logging, so it shouldn't explode. In the future
we will hopefully change it to use errorformat=html (T228897#5366960).
Bug: T229532
Change-Id: I3b9c4fefc0869ef7999c21cef754434febd852ec
2019-08-01 03:32:42 +00:00
|
|
|
} );
|
2021-01-15 19:06:20 +00:00
|
|
|
handled = true;
|
ve.init.mw.ArticleTarget: Use errorformat=html when saving
When saving fails for a reason we don't handle explicitly, the error
message will have HTML formatting and will respect any on-wiki
overridden messages, rather than being plain text generic message.
Extensions providing custom SaveErrorHandlers may need to be updated.
The only one in Gerrit that requires a fix is TitleBlacklist:
Ibeae79c95557a7af699716c9d921f34c310bee6d.
* Remove handling for errors returned in .visualeditoredit.edit.info
rather than .errors (.error in old format). AFAIK this is only used
by some extensions, it is probably incorrect to do (T229539) and all
extensions I know of that do this (AbuseFilter, SpamBlacklist,
ConfirmEdit) have custom SaveErrorHandlers.
* Remove custom error messages for 'readonly' (identical to API
response) and for 'hookaborted' (very unhelpful and there is a
chance that the API response is better, if the extension causing
this error generates any error message).
* Add a silly shim for MobileFrontend integration, because we allow it
to handle error responses, and it expects them in the old format.
This is probably subtly wrong in many ways, but MobileFrontend code
only uses this for logging, so it shouldn't explode. In the future
we will hopefully change it to use errorformat=html (T228897#5366960).
Bug: T229532
Change-Id: I3b9c4fefc0869ef7999c21cef754434febd852ec
2019-08-01 03:32:42 +00:00
|
|
|
} else if ( error.code === 'editconflict' ) {
|
|
|
|
this.editConflict();
|
2021-01-15 19:06:20 +00:00
|
|
|
handled = true;
|
ve.init.mw.ArticleTarget: Use errorformat=html when saving
When saving fails for a reason we don't handle explicitly, the error
message will have HTML formatting and will respect any on-wiki
overridden messages, rather than being plain text generic message.
Extensions providing custom SaveErrorHandlers may need to be updated.
The only one in Gerrit that requires a fix is TitleBlacklist:
Ibeae79c95557a7af699716c9d921f34c310bee6d.
* Remove handling for errors returned in .visualeditoredit.edit.info
rather than .errors (.error in old format). AFAIK this is only used
by some extensions, it is probably incorrect to do (T229539) and all
extensions I know of that do this (AbuseFilter, SpamBlacklist,
ConfirmEdit) have custom SaveErrorHandlers.
* Remove custom error messages for 'readonly' (identical to API
response) and for 'hookaborted' (very unhelpful and there is a
chance that the API response is better, if the extension causing
this error generates any error message).
* Add a silly shim for MobileFrontend integration, because we allow it
to handle error responses, and it expects them in the old format.
This is probably subtly wrong in many ways, but MobileFrontend code
only uses this for logging, so it shouldn't explode. In the future
we will hopefully change it to use errorformat=html (T228897#5366960).
Bug: T229532
Change-Id: I3b9c4fefc0869ef7999c21cef754434febd852ec
2019-08-01 03:32:42 +00:00
|
|
|
} else if ( error.code === 'pagedeleted' ) {
|
|
|
|
this.saveErrorPageDeleted();
|
2021-01-15 19:06:20 +00:00
|
|
|
handled = true;
|
ve.init.mw.ArticleTarget: Use errorformat=html when saving
When saving fails for a reason we don't handle explicitly, the error
message will have HTML formatting and will respect any on-wiki
overridden messages, rather than being plain text generic message.
Extensions providing custom SaveErrorHandlers may need to be updated.
The only one in Gerrit that requires a fix is TitleBlacklist:
Ibeae79c95557a7af699716c9d921f34c310bee6d.
* Remove handling for errors returned in .visualeditoredit.edit.info
rather than .errors (.error in old format). AFAIK this is only used
by some extensions, it is probably incorrect to do (T229539) and all
extensions I know of that do this (AbuseFilter, SpamBlacklist,
ConfirmEdit) have custom SaveErrorHandlers.
* Remove custom error messages for 'readonly' (identical to API
response) and for 'hookaborted' (very unhelpful and there is a
chance that the API response is better, if the extension causing
this error generates any error message).
* Add a silly shim for MobileFrontend integration, because we allow it
to handle error responses, and it expects them in the old format.
This is probably subtly wrong in many ways, but MobileFrontend code
only uses this for logging, so it shouldn't explode. In the future
we will hopefully change it to use errorformat=html (T228897#5366960).
Bug: T229532
Change-Id: I3b9c4fefc0869ef7999c21cef754434febd852ec
2019-08-01 03:32:42 +00:00
|
|
|
} else if ( error.code === 'hookaborted' ) {
|
|
|
|
this.saveErrorHookAborted( data );
|
2021-01-15 19:06:20 +00:00
|
|
|
handled = true;
|
ve.init.mw.ArticleTarget: Use errorformat=html when saving
When saving fails for a reason we don't handle explicitly, the error
message will have HTML formatting and will respect any on-wiki
overridden messages, rather than being plain text generic message.
Extensions providing custom SaveErrorHandlers may need to be updated.
The only one in Gerrit that requires a fix is TitleBlacklist:
Ibeae79c95557a7af699716c9d921f34c310bee6d.
* Remove handling for errors returned in .visualeditoredit.edit.info
rather than .errors (.error in old format). AFAIK this is only used
by some extensions, it is probably incorrect to do (T229539) and all
extensions I know of that do this (AbuseFilter, SpamBlacklist,
ConfirmEdit) have custom SaveErrorHandlers.
* Remove custom error messages for 'readonly' (identical to API
response) and for 'hookaborted' (very unhelpful and there is a
chance that the API response is better, if the extension causing
this error generates any error message).
* Add a silly shim for MobileFrontend integration, because we allow it
to handle error responses, and it expects them in the old format.
This is probably subtly wrong in many ways, but MobileFrontend code
only uses this for logging, so it shouldn't explode. In the future
we will hopefully change it to use errorformat=html (T228897#5366960).
Bug: T229532
Change-Id: I3b9c4fefc0869ef7999c21cef754434febd852ec
2019-08-01 03:32:42 +00:00
|
|
|
} else if ( error.code === 'readonly' ) {
|
|
|
|
this.saveErrorReadOnly( data );
|
2021-01-15 19:06:20 +00:00
|
|
|
handled = true;
|
2017-02-07 17:17:54 +00:00
|
|
|
}
|
ve.init.mw.ArticleTarget: Use errorformat=html when saving
When saving fails for a reason we don't handle explicitly, the error
message will have HTML formatting and will respect any on-wiki
overridden messages, rather than being plain text generic message.
Extensions providing custom SaveErrorHandlers may need to be updated.
The only one in Gerrit that requires a fix is TitleBlacklist:
Ibeae79c95557a7af699716c9d921f34c310bee6d.
* Remove handling for errors returned in .visualeditoredit.edit.info
rather than .errors (.error in old format). AFAIK this is only used
by some extensions, it is probably incorrect to do (T229539) and all
extensions I know of that do this (AbuseFilter, SpamBlacklist,
ConfirmEdit) have custom SaveErrorHandlers.
* Remove custom error messages for 'readonly' (identical to API
response) and for 'hookaborted' (very unhelpful and there is a
chance that the API response is better, if the extension causing
this error generates any error message).
* Add a silly shim for MobileFrontend integration, because we allow it
to handle error responses, and it expects them in the old format.
This is probably subtly wrong in many ways, but MobileFrontend code
only uses this for logging, so it shouldn't explode. In the future
we will hopefully change it to use errorformat=html (T228897#5366960).
Bug: T229532
Change-Id: I3b9c4fefc0869ef7999c21cef754434febd852ec
2019-08-01 03:32:42 +00:00
|
|
|
}
|
2013-11-26 19:29:14 +00:00
|
|
|
}
|
|
|
|
|
2021-01-15 19:06:20 +00:00
|
|
|
if ( !handled ) {
|
2024-05-21 16:40:36 +00:00
|
|
|
const saveErrorHandlerFactory = ve.init.mw.saveErrorHandlerFactory;
|
2024-05-21 14:22:56 +00:00
|
|
|
for ( const name in saveErrorHandlerFactory.registry ) {
|
|
|
|
const handler = saveErrorHandlerFactory.lookup( name );
|
2021-01-15 19:06:20 +00:00
|
|
|
if ( handler.static.matchFunction( data ) ) {
|
|
|
|
handler.static.process( data, this );
|
|
|
|
handled = true;
|
|
|
|
}
|
2018-11-23 17:01:25 +00:00
|
|
|
}
|
2013-11-26 19:29:14 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Handle (other) unknown and/or unrecoverable errors
|
2021-01-15 19:06:20 +00:00
|
|
|
if ( !handled ) {
|
|
|
|
this.saveErrorUnknown( data );
|
|
|
|
handled = true;
|
|
|
|
}
|
|
|
|
|
2024-05-21 14:22:56 +00:00
|
|
|
let errorCodes;
|
2021-01-15 19:06:20 +00:00
|
|
|
if ( data.errors ) {
|
2024-04-30 16:44:25 +00:00
|
|
|
errorCodes = OO.unique( data.errors.map( ( err ) => err.code ) ).join( ',' );
|
2021-01-15 19:06:20 +00:00
|
|
|
} else if ( ve.getProp( data, 'visualeditoredit', 'edit', 'captcha' ) ) {
|
|
|
|
// Eww
|
|
|
|
errorCodes = 'captcha';
|
|
|
|
} else {
|
|
|
|
errorCodes = 'http-' + ( ( data.xhr && data.xhr.status ) || 0 );
|
|
|
|
}
|
|
|
|
this.emit( 'saveError', errorCodes );
|
2012-12-07 16:23:23 +00:00
|
|
|
};
|
|
|
|
|
2015-08-04 13:37:13 +00:00
|
|
|
/**
|
|
|
|
* Show an save process error message
|
|
|
|
*
|
|
|
|
* @param {string|jQuery|Node[]} msg Message content (string of HTML, jQuery object or array of
|
|
|
|
* Node objects)
|
|
|
|
* @param {boolean} [warning=false] Whether or not this is a warning.
|
|
|
|
*/
|
2022-07-19 19:43:29 +00:00
|
|
|
ve.init.mw.ArticleTarget.prototype.showSaveError = function ( msg, warning ) {
|
|
|
|
this.saveDeferred.reject( [ new OO.ui.Error( msg, { warning: warning } ) ] );
|
2015-08-04 13:37:13 +00:00
|
|
|
};
|
|
|
|
|
ve.init.mw.ArticleTarget: Use errorformat=html when saving
When saving fails for a reason we don't handle explicitly, the error
message will have HTML formatting and will respect any on-wiki
overridden messages, rather than being plain text generic message.
Extensions providing custom SaveErrorHandlers may need to be updated.
The only one in Gerrit that requires a fix is TitleBlacklist:
Ibeae79c95557a7af699716c9d921f34c310bee6d.
* Remove handling for errors returned in .visualeditoredit.edit.info
rather than .errors (.error in old format). AFAIK this is only used
by some extensions, it is probably incorrect to do (T229539) and all
extensions I know of that do this (AbuseFilter, SpamBlacklist,
ConfirmEdit) have custom SaveErrorHandlers.
* Remove custom error messages for 'readonly' (identical to API
response) and for 'hookaborted' (very unhelpful and there is a
chance that the API response is better, if the extension causing
this error generates any error message).
* Add a silly shim for MobileFrontend integration, because we allow it
to handle error responses, and it expects them in the old format.
This is probably subtly wrong in many ways, but MobileFrontend code
only uses this for logging, so it shouldn't explode. In the future
we will hopefully change it to use errorformat=html (T228897#5366960).
Bug: T229532
Change-Id: I3b9c4fefc0869ef7999c21cef754434febd852ec
2019-08-01 03:32:42 +00:00
|
|
|
/**
|
|
|
|
* Extract the error messages from an erroneous API response
|
|
|
|
*
|
|
|
|
* @param {Object} data API response data
|
|
|
|
* @return {jQuery}
|
|
|
|
*/
|
|
|
|
ve.init.mw.ArticleTarget.prototype.extractErrorMessages = function ( data ) {
|
2024-05-21 14:22:56 +00:00
|
|
|
const $errorMsgs = ( new mw.Api() ).getErrorMessage( data );
|
2021-09-03 12:57:29 +00:00
|
|
|
// Warning, this assumes there are only Element nodes in the jQuery set
|
|
|
|
$errorMsgs.toArray().forEach( ve.targetLinksToNewWindow );
|
2019-12-12 00:38:00 +00:00
|
|
|
return $errorMsgs;
|
ve.init.mw.ArticleTarget: Use errorformat=html when saving
When saving fails for a reason we don't handle explicitly, the error
message will have HTML formatting and will respect any on-wiki
overridden messages, rather than being plain text generic message.
Extensions providing custom SaveErrorHandlers may need to be updated.
The only one in Gerrit that requires a fix is TitleBlacklist:
Ibeae79c95557a7af699716c9d921f34c310bee6d.
* Remove handling for errors returned in .visualeditoredit.edit.info
rather than .errors (.error in old format). AFAIK this is only used
by some extensions, it is probably incorrect to do (T229539) and all
extensions I know of that do this (AbuseFilter, SpamBlacklist,
ConfirmEdit) have custom SaveErrorHandlers.
* Remove custom error messages for 'readonly' (identical to API
response) and for 'hookaborted' (very unhelpful and there is a
chance that the API response is better, if the extension causing
this error generates any error message).
* Add a silly shim for MobileFrontend integration, because we allow it
to handle error responses, and it expects them in the old format.
This is probably subtly wrong in many ways, but MobileFrontend code
only uses this for logging, so it shouldn't explode. In the future
we will hopefully change it to use errorformat=html (T228897#5366960).
Bug: T229532
Change-Id: I3b9c4fefc0869ef7999c21cef754434febd852ec
2019-08-01 03:32:42 +00:00
|
|
|
};
|
|
|
|
|
2015-08-04 13:37:13 +00:00
|
|
|
/**
|
|
|
|
* Handle general save error
|
|
|
|
*/
|
2015-12-10 16:07:50 +00:00
|
|
|
ve.init.mw.ArticleTarget.prototype.saveErrorEmpty = function () {
|
2022-07-19 19:43:29 +00:00
|
|
|
this.showSaveError( this.extractErrorMessages( null ) );
|
2015-08-04 13:37:13 +00:00
|
|
|
};
|
|
|
|
|
2017-01-11 18:46:30 +00:00
|
|
|
/**
|
|
|
|
* Handle hook abort save error
|
|
|
|
*
|
ve.init.mw.ArticleTarget: Use errorformat=html when saving
When saving fails for a reason we don't handle explicitly, the error
message will have HTML formatting and will respect any on-wiki
overridden messages, rather than being plain text generic message.
Extensions providing custom SaveErrorHandlers may need to be updated.
The only one in Gerrit that requires a fix is TitleBlacklist:
Ibeae79c95557a7af699716c9d921f34c310bee6d.
* Remove handling for errors returned in .visualeditoredit.edit.info
rather than .errors (.error in old format). AFAIK this is only used
by some extensions, it is probably incorrect to do (T229539) and all
extensions I know of that do this (AbuseFilter, SpamBlacklist,
ConfirmEdit) have custom SaveErrorHandlers.
* Remove custom error messages for 'readonly' (identical to API
response) and for 'hookaborted' (very unhelpful and there is a
chance that the API response is better, if the extension causing
this error generates any error message).
* Add a silly shim for MobileFrontend integration, because we allow it
to handle error responses, and it expects them in the old format.
This is probably subtly wrong in many ways, but MobileFrontend code
only uses this for logging, so it shouldn't explode. In the future
we will hopefully change it to use errorformat=html (T228897#5366960).
Bug: T229532
Change-Id: I3b9c4fefc0869ef7999c21cef754434febd852ec
2019-08-01 03:32:42 +00:00
|
|
|
* @param {Object} data API response data
|
2017-01-11 18:46:30 +00:00
|
|
|
*/
|
ve.init.mw.ArticleTarget: Use errorformat=html when saving
When saving fails for a reason we don't handle explicitly, the error
message will have HTML formatting and will respect any on-wiki
overridden messages, rather than being plain text generic message.
Extensions providing custom SaveErrorHandlers may need to be updated.
The only one in Gerrit that requires a fix is TitleBlacklist:
Ibeae79c95557a7af699716c9d921f34c310bee6d.
* Remove handling for errors returned in .visualeditoredit.edit.info
rather than .errors (.error in old format). AFAIK this is only used
by some extensions, it is probably incorrect to do (T229539) and all
extensions I know of that do this (AbuseFilter, SpamBlacklist,
ConfirmEdit) have custom SaveErrorHandlers.
* Remove custom error messages for 'readonly' (identical to API
response) and for 'hookaborted' (very unhelpful and there is a
chance that the API response is better, if the extension causing
this error generates any error message).
* Add a silly shim for MobileFrontend integration, because we allow it
to handle error responses, and it expects them in the old format.
This is probably subtly wrong in many ways, but MobileFrontend code
only uses this for logging, so it shouldn't explode. In the future
we will hopefully change it to use errorformat=html (T228897#5366960).
Bug: T229532
Change-Id: I3b9c4fefc0869ef7999c21cef754434febd852ec
2019-08-01 03:32:42 +00:00
|
|
|
ve.init.mw.ArticleTarget.prototype.saveErrorHookAborted = function ( data ) {
|
|
|
|
this.showSaveError( this.extractErrorMessages( data ) );
|
2017-01-11 18:46:30 +00:00
|
|
|
};
|
|
|
|
|
2015-08-04 13:37:13 +00:00
|
|
|
/**
|
2020-02-15 02:50:16 +00:00
|
|
|
* Handle assert error indicating another user is logged in.
|
2015-08-04 13:37:13 +00:00
|
|
|
*
|
2023-08-14 20:29:30 +00:00
|
|
|
* @param {string|null} username Name of newly logged-in user, or a temporary account name,
|
|
|
|
* or null if logged-out and temporary accounts are disabled
|
2015-08-04 13:37:13 +00:00
|
|
|
*/
|
2020-02-15 02:50:16 +00:00
|
|
|
ve.init.mw.ArticleTarget.prototype.saveErrorNewUser = function ( username ) {
|
2024-05-21 14:22:56 +00:00
|
|
|
const $msg = mw.message(
|
2023-08-14 20:29:30 +00:00
|
|
|
username === null ?
|
|
|
|
'visualeditor-savedialog-identify-anon' :
|
|
|
|
mw.util.isTemporaryUser( username ) ?
|
|
|
|
'visualeditor-savedialog-identify-temp' :
|
2020-02-15 02:50:16 +00:00
|
|
|
'visualeditor-savedialog-identify-user',
|
2023-08-14 20:29:30 +00:00
|
|
|
username
|
|
|
|
).parseDom();
|
2016-03-05 01:34:56 +00:00
|
|
|
|
2023-01-08 11:23:28 +00:00
|
|
|
this.showSaveError( $msg, true );
|
2015-08-04 13:37:13 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Handle unknown save error
|
|
|
|
*
|
|
|
|
* @param {Object|null} data API response data
|
|
|
|
*/
|
ve.init.mw.ArticleTarget: Use errorformat=html when saving
When saving fails for a reason we don't handle explicitly, the error
message will have HTML formatting and will respect any on-wiki
overridden messages, rather than being plain text generic message.
Extensions providing custom SaveErrorHandlers may need to be updated.
The only one in Gerrit that requires a fix is TitleBlacklist:
Ibeae79c95557a7af699716c9d921f34c310bee6d.
* Remove handling for errors returned in .visualeditoredit.edit.info
rather than .errors (.error in old format). AFAIK this is only used
by some extensions, it is probably incorrect to do (T229539) and all
extensions I know of that do this (AbuseFilter, SpamBlacklist,
ConfirmEdit) have custom SaveErrorHandlers.
* Remove custom error messages for 'readonly' (identical to API
response) and for 'hookaborted' (very unhelpful and there is a
chance that the API response is better, if the extension causing
this error generates any error message).
* Add a silly shim for MobileFrontend integration, because we allow it
to handle error responses, and it expects them in the old format.
This is probably subtly wrong in many ways, but MobileFrontend code
only uses this for logging, so it shouldn't explode. In the future
we will hopefully change it to use errorformat=html (T228897#5366960).
Bug: T229532
Change-Id: I3b9c4fefc0869ef7999c21cef754434febd852ec
2019-08-01 03:32:42 +00:00
|
|
|
ve.init.mw.ArticleTarget.prototype.saveErrorUnknown = function ( data ) {
|
2022-07-19 19:43:29 +00:00
|
|
|
this.showSaveError( this.extractErrorMessages( data ) );
|
2015-08-04 13:37:13 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Handle page deleted error
|
|
|
|
*/
|
2015-12-10 16:07:50 +00:00
|
|
|
ve.init.mw.ArticleTarget.prototype.saveErrorPageDeleted = function () {
|
2015-08-04 13:37:13 +00:00
|
|
|
this.pageDeletedWarning = true;
|
ve.init.mw.ArticleTarget: Use errorformat=html when saving
When saving fails for a reason we don't handle explicitly, the error
message will have HTML formatting and will respect any on-wiki
overridden messages, rather than being plain text generic message.
Extensions providing custom SaveErrorHandlers may need to be updated.
The only one in Gerrit that requires a fix is TitleBlacklist:
Ibeae79c95557a7af699716c9d921f34c310bee6d.
* Remove handling for errors returned in .visualeditoredit.edit.info
rather than .errors (.error in old format). AFAIK this is only used
by some extensions, it is probably incorrect to do (T229539) and all
extensions I know of that do this (AbuseFilter, SpamBlacklist,
ConfirmEdit) have custom SaveErrorHandlers.
* Remove custom error messages for 'readonly' (identical to API
response) and for 'hookaborted' (very unhelpful and there is a
chance that the API response is better, if the extension causing
this error generates any error message).
* Add a silly shim for MobileFrontend integration, because we allow it
to handle error responses, and it expects them in the old format.
This is probably subtly wrong in many ways, but MobileFrontend code
only uses this for logging, so it shouldn't explode. In the future
we will hopefully change it to use errorformat=html (T228897#5366960).
Bug: T229532
Change-Id: I3b9c4fefc0869ef7999c21cef754434febd852ec
2019-08-01 03:32:42 +00:00
|
|
|
// The API error message 'apierror-pagedeleted' is poor, make our own
|
2022-07-19 19:43:29 +00:00
|
|
|
this.showSaveError( mw.msg( 'visualeditor-recreate', mw.msg( 'ooui-dialog-process-continue' ) ), true );
|
2015-08-04 13:37:13 +00:00
|
|
|
};
|
|
|
|
|
2016-03-10 18:58:41 +00:00
|
|
|
/**
|
|
|
|
* Handle read only error
|
|
|
|
*
|
ve.init.mw.ArticleTarget: Use errorformat=html when saving
When saving fails for a reason we don't handle explicitly, the error
message will have HTML formatting and will respect any on-wiki
overridden messages, rather than being plain text generic message.
Extensions providing custom SaveErrorHandlers may need to be updated.
The only one in Gerrit that requires a fix is TitleBlacklist:
Ibeae79c95557a7af699716c9d921f34c310bee6d.
* Remove handling for errors returned in .visualeditoredit.edit.info
rather than .errors (.error in old format). AFAIK this is only used
by some extensions, it is probably incorrect to do (T229539) and all
extensions I know of that do this (AbuseFilter, SpamBlacklist,
ConfirmEdit) have custom SaveErrorHandlers.
* Remove custom error messages for 'readonly' (identical to API
response) and for 'hookaborted' (very unhelpful and there is a
chance that the API response is better, if the extension causing
this error generates any error message).
* Add a silly shim for MobileFrontend integration, because we allow it
to handle error responses, and it expects them in the old format.
This is probably subtly wrong in many ways, but MobileFrontend code
only uses this for logging, so it shouldn't explode. In the future
we will hopefully change it to use errorformat=html (T228897#5366960).
Bug: T229532
Change-Id: I3b9c4fefc0869ef7999c21cef754434febd852ec
2019-08-01 03:32:42 +00:00
|
|
|
* @param {Object} data API response data
|
2016-03-10 18:58:41 +00:00
|
|
|
*/
|
ve.init.mw.ArticleTarget: Use errorformat=html when saving
When saving fails for a reason we don't handle explicitly, the error
message will have HTML formatting and will respect any on-wiki
overridden messages, rather than being plain text generic message.
Extensions providing custom SaveErrorHandlers may need to be updated.
The only one in Gerrit that requires a fix is TitleBlacklist:
Ibeae79c95557a7af699716c9d921f34c310bee6d.
* Remove handling for errors returned in .visualeditoredit.edit.info
rather than .errors (.error in old format). AFAIK this is only used
by some extensions, it is probably incorrect to do (T229539) and all
extensions I know of that do this (AbuseFilter, SpamBlacklist,
ConfirmEdit) have custom SaveErrorHandlers.
* Remove custom error messages for 'readonly' (identical to API
response) and for 'hookaborted' (very unhelpful and there is a
chance that the API response is better, if the extension causing
this error generates any error message).
* Add a silly shim for MobileFrontend integration, because we allow it
to handle error responses, and it expects them in the old format.
This is probably subtly wrong in many ways, but MobileFrontend code
only uses this for logging, so it shouldn't explode. In the future
we will hopefully change it to use errorformat=html (T228897#5366960).
Bug: T229532
Change-Id: I3b9c4fefc0869ef7999c21cef754434febd852ec
2019-08-01 03:32:42 +00:00
|
|
|
ve.init.mw.ArticleTarget.prototype.saveErrorReadOnly = function ( data ) {
|
2022-07-19 19:43:29 +00:00
|
|
|
this.showSaveError( this.extractErrorMessages( data ), true );
|
2016-03-10 18:58:41 +00:00
|
|
|
};
|
|
|
|
|
2015-08-04 13:37:13 +00:00
|
|
|
/**
|
|
|
|
* Handle an edit conflict
|
|
|
|
*/
|
2015-12-10 16:07:50 +00:00
|
|
|
ve.init.mw.ArticleTarget.prototype.editConflict = function () {
|
2015-09-25 22:59:08 +00:00
|
|
|
this.saveDialog.popPending();
|
|
|
|
this.saveDialog.swapPanel( 'conflict' );
|
2015-08-04 13:37:13 +00:00
|
|
|
};
|
|
|
|
|
2015-08-05 21:43:23 +00:00
|
|
|
/**
|
|
|
|
* Handle clicks on the review button in the save dialog.
|
|
|
|
*
|
2024-04-29 18:14:26 +00:00
|
|
|
* @fires ve.init.mw.ArticleTarget#saveReview
|
2015-08-05 21:43:23 +00:00
|
|
|
*/
|
2015-12-10 16:07:50 +00:00
|
|
|
ve.init.mw.ArticleTarget.prototype.onSaveDialogReview = function () {
|
2016-10-31 17:17:50 +00:00
|
|
|
if ( !this.saveDialog.hasDiff ) {
|
2015-08-05 21:43:23 +00:00
|
|
|
this.emit( 'saveReview' );
|
|
|
|
this.saveDialog.pushPending();
|
2023-07-18 02:14:09 +00:00
|
|
|
// Acquire a temporary user username before diffing, so that signatures and
|
|
|
|
// user-related magic words display the temp user instead of IP user in the diff. (T331397)
|
2024-04-30 16:44:25 +00:00
|
|
|
mw.user.acquireTempUserName().then( () => {
|
2024-05-01 12:32:49 +00:00
|
|
|
if ( this.pageExists ) {
|
|
|
|
// Has no callback, handled via this.showChangesDiff
|
|
|
|
this.showChanges( this.getDocToSave() );
|
2023-07-18 02:14:09 +00:00
|
|
|
} else {
|
2024-05-01 12:32:49 +00:00
|
|
|
this.serialize( this.getDocToSave() ).then( ( data ) => {
|
|
|
|
this.onSaveDialogReviewComplete( data.content );
|
2023-07-18 02:14:09 +00:00
|
|
|
} );
|
|
|
|
}
|
|
|
|
} );
|
2015-08-05 21:43:23 +00:00
|
|
|
} else {
|
|
|
|
this.saveDialog.swapPanel( 'review' );
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2016-09-01 23:54:56 +00:00
|
|
|
/**
|
|
|
|
* Handle clicks on the show preview button in the save dialog.
|
|
|
|
*
|
2024-04-29 18:14:26 +00:00
|
|
|
* @fires ve.init.mw.ArticleTarget#savePreview
|
2016-09-01 23:54:56 +00:00
|
|
|
*/
|
|
|
|
ve.init.mw.ArticleTarget.prototype.onSaveDialogPreview = function () {
|
2024-05-21 14:22:56 +00:00
|
|
|
const api = this.getContentApi();
|
2017-03-30 16:37:54 +00:00
|
|
|
|
2016-09-01 23:54:56 +00:00
|
|
|
if ( !this.saveDialog.$previewViewer.children().length ) {
|
|
|
|
this.emit( 'savePreview' );
|
|
|
|
this.saveDialog.pushPending();
|
2016-12-08 19:24:10 +00:00
|
|
|
|
2024-05-21 14:22:56 +00:00
|
|
|
const params = {};
|
2022-06-27 15:29:24 +00:00
|
|
|
|
2024-05-21 14:22:56 +00:00
|
|
|
const sectionTitle = this.sectionTitle && this.sectionTitle.getValue();
|
2022-06-27 15:29:24 +00:00
|
|
|
if ( sectionTitle ) {
|
|
|
|
params.section = 'new';
|
|
|
|
params.sectiontitle = sectionTitle;
|
|
|
|
}
|
|
|
|
if ( mw.config.get( 'wgUserVariant' ) ) {
|
|
|
|
params.variant = mw.config.get( 'wgUserVariant' );
|
2016-12-08 19:24:10 +00:00
|
|
|
}
|
|
|
|
|
2023-07-18 02:14:09 +00:00
|
|
|
// Acquire a temporary user username before previewing, so that signatures and
|
|
|
|
// user-related magic words display the temp user instead of IP user in the preview. (T331397)
|
2024-04-30 16:44:25 +00:00
|
|
|
mw.user.acquireTempUserName().then( () => api.post( ve.extendObject( params, {
|
|
|
|
action: 'parse',
|
2024-05-01 12:32:49 +00:00
|
|
|
title: this.getPageName(),
|
|
|
|
text: this.getDocToSave(),
|
2024-04-30 16:44:25 +00:00
|
|
|
pst: true,
|
|
|
|
preview: true,
|
2024-05-01 12:32:49 +00:00
|
|
|
sectionpreview: this.section !== null,
|
2024-04-30 16:44:25 +00:00
|
|
|
disableeditsection: true,
|
|
|
|
uselang: mw.config.get( 'wgUserLanguage' ),
|
|
|
|
useskin: mw.config.get( 'skin' ),
|
|
|
|
mobileformat: OO.ui.isMobile(),
|
|
|
|
prop: [ 'text', 'categorieshtml', 'displaytitle', 'subtitle', 'modules', 'jsconfigvars' ]
|
|
|
|
} ) ) ).then( ( response ) => {
|
2024-05-01 12:32:49 +00:00
|
|
|
this.saveDialog.showPreview( response );
|
2024-04-30 16:44:25 +00:00
|
|
|
}, ( errorCode, details ) => {
|
2024-05-01 12:32:49 +00:00
|
|
|
this.saveDialog.showPreview( this.extractErrorMessages( details ) );
|
2024-04-30 16:44:25 +00:00
|
|
|
} ).always( () => {
|
2024-05-01 12:32:49 +00:00
|
|
|
this.bindSaveDialogClearDiff();
|
2016-09-01 23:54:56 +00:00
|
|
|
} );
|
|
|
|
} else {
|
|
|
|
this.saveDialog.swapPanel( 'preview' );
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2016-12-08 19:24:10 +00:00
|
|
|
/**
|
|
|
|
* Clear the save dialog's diff cache when the document changes
|
|
|
|
*/
|
|
|
|
ve.init.mw.ArticleTarget.prototype.bindSaveDialogClearDiff = function () {
|
|
|
|
// Invalidate the viewer wikitext on next change
|
|
|
|
this.getSurface().getModel().getDocument().once( 'transact',
|
|
|
|
this.saveDialog.clearDiff.bind( this.saveDialog )
|
|
|
|
);
|
|
|
|
if ( this.sectionTitle ) {
|
|
|
|
this.sectionTitle.once( 'change', this.saveDialog.clearDiff.bind( this.saveDialog ) );
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2015-08-05 21:43:23 +00:00
|
|
|
/**
|
|
|
|
* Handle completed serialize request for diff views for new page creations.
|
|
|
|
*
|
2021-06-04 11:58:18 +00:00
|
|
|
* @param {string} wikitext
|
2015-08-05 21:43:23 +00:00
|
|
|
*/
|
2015-12-10 16:07:50 +00:00
|
|
|
ve.init.mw.ArticleTarget.prototype.onSaveDialogReviewComplete = function ( wikitext ) {
|
2016-12-08 19:24:10 +00:00
|
|
|
this.bindSaveDialogClearDiff();
|
2017-03-30 16:37:54 +00:00
|
|
|
this.saveDialog.setDiffAndReview(
|
2019-11-02 05:06:28 +00:00
|
|
|
ve.createDeferred().resolve( $( '<pre>' ).text( wikitext ) ).promise(),
|
2017-05-08 18:57:43 +00:00
|
|
|
this.getVisualDiffGeneratorPromise(),
|
2017-03-30 16:37:54 +00:00
|
|
|
this.getSurface().getModel().getDocument().getHtmlDocument()
|
|
|
|
);
|
2016-10-31 17:17:50 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get a visual diff object for the current document state
|
|
|
|
*
|
2017-07-17 16:38:32 +00:00
|
|
|
* @return {jQuery.Promise} Promise resolving with a generator for a ve.dm.VisualDiff visual diff
|
2016-10-31 17:17:50 +00:00
|
|
|
*/
|
2017-05-08 18:57:43 +00:00
|
|
|
ve.init.mw.ArticleTarget.prototype.getVisualDiffGeneratorPromise = function () {
|
2024-04-30 16:44:25 +00:00
|
|
|
return mw.loader.using( 'ext.visualEditor.diffLoader' ).then( () => {
|
2024-05-21 14:22:56 +00:00
|
|
|
const mode = this.getSurface().getMode();
|
2017-04-09 21:28:53 +00:00
|
|
|
|
2024-05-01 12:32:49 +00:00
|
|
|
if ( !this.originalDmDocPromise ) {
|
2021-02-22 13:42:16 +00:00
|
|
|
if ( mode === 'source' ) {
|
|
|
|
// Always load full doc in source mode for correct reference diffing (T260008)
|
2024-05-01 12:32:49 +00:00
|
|
|
this.originalDmDocPromise = mw.libs.ve.diffLoader.fetchRevision( this.revid, this.getPageName() );
|
2021-02-22 13:42:16 +00:00
|
|
|
} else {
|
2024-05-01 12:32:49 +00:00
|
|
|
if ( !this.fromEditedState ) {
|
2024-05-21 14:22:56 +00:00
|
|
|
const dmDoc = this.constructor.static.createModelFromDom( this.doc, 'visual' );
|
|
|
|
let dmDocOrNode;
|
2024-05-01 12:32:49 +00:00
|
|
|
if ( this.section !== null && this.enableVisualSectionEditing ) {
|
2021-02-22 13:42:16 +00:00
|
|
|
dmDocOrNode = dmDoc.getNodesByType( 'section' )[ 0 ];
|
|
|
|
} else {
|
|
|
|
dmDocOrNode = dmDoc;
|
|
|
|
}
|
2024-05-01 12:32:49 +00:00
|
|
|
this.originalDmDocPromise = ve.createDeferred().resolve( dmDocOrNode ).promise();
|
2019-04-25 21:43:04 +00:00
|
|
|
} else {
|
2024-05-01 12:32:49 +00:00
|
|
|
this.originalDmDocPromise = mw.libs.ve.diffLoader.fetchRevision( this.revid, this.getPageName(), this.section );
|
2019-04-25 21:43:04 +00:00
|
|
|
}
|
2017-07-17 16:38:32 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-02-22 13:42:16 +00:00
|
|
|
if ( mode === 'source' ) {
|
2023-07-18 02:14:09 +00:00
|
|
|
// Acquire a temporary user username before diffing, so that signatures and
|
|
|
|
// user-related magic words display the temp user instead of IP user in the diff. (T331397)
|
2024-05-21 14:22:56 +00:00
|
|
|
const newRevPromise = mw.user.acquireTempUserName().then( () => this.getContentApi().post( {
|
2024-04-30 16:44:25 +00:00
|
|
|
action: 'visualeditor',
|
|
|
|
paction: 'parse',
|
2024-05-01 12:32:49 +00:00
|
|
|
page: this.getPageName(),
|
|
|
|
wikitext: this.getSurface().getDom(),
|
|
|
|
section: this.section,
|
2024-04-30 16:44:25 +00:00
|
|
|
stash: 0,
|
|
|
|
pst: true
|
|
|
|
} ) ).then(
|
2021-02-22 13:42:16 +00:00
|
|
|
// Source mode always fetches the whole document, so set section=null to unwrap sections
|
2024-04-30 16:44:25 +00:00
|
|
|
( response ) => mw.libs.ve.diffLoader.getModelFromResponse( response, null )
|
|
|
|
);
|
2017-07-17 16:38:32 +00:00
|
|
|
|
2024-05-01 12:32:49 +00:00
|
|
|
return mw.libs.ve.diffLoader.getVisualDiffGeneratorPromise( this.originalDmDocPromise, newRevPromise );
|
2017-07-17 16:38:32 +00:00
|
|
|
} else {
|
2024-05-01 12:32:49 +00:00
|
|
|
return this.originalDmDocPromise.then(
|
2024-09-30 13:12:39 +00:00
|
|
|
( originalDmDoc ) => () => new ve.dm.VisualDiff( originalDmDoc, this.getSurface().getModel().getDocument().getAttachedRoot() )
|
2024-05-01 11:36:18 +00:00
|
|
|
);
|
2017-04-09 21:28:53 +00:00
|
|
|
}
|
2017-07-17 16:38:32 +00:00
|
|
|
} );
|
2015-08-05 21:43:23 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Handle clicks on the resolve conflict button in the conflict dialog.
|
|
|
|
*/
|
2015-12-10 16:07:50 +00:00
|
|
|
ve.init.mw.ArticleTarget.prototype.onSaveDialogResolveConflict = function () {
|
2024-05-21 14:22:56 +00:00
|
|
|
const fields = { wpSave: 1 };
|
2017-03-29 21:21:17 +00:00
|
|
|
|
|
|
|
if ( this.getSurface().getMode() === 'source' && this.section !== null ) {
|
2020-03-24 20:55:31 +00:00
|
|
|
// TODO: This should happen in #getSaveFields, check if moving it there breaks anything
|
2017-03-29 21:21:17 +00:00
|
|
|
fields.section = this.section;
|
|
|
|
}
|
2015-08-05 21:43:23 +00:00
|
|
|
// Get Wikitext from the DOM, and set up a submit call when it's done
|
2024-04-30 16:44:25 +00:00
|
|
|
this.serialize( this.getDocToSave() ).then( ( data ) => {
|
2024-05-01 12:32:49 +00:00
|
|
|
this.submitWithSaveFields( fields, data.content );
|
2019-11-04 14:54:53 +00:00
|
|
|
} );
|
2015-08-05 21:43:23 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Handle dialog retry events
|
|
|
|
* So we can handle trying to save again after page deletion warnings
|
|
|
|
*/
|
2015-12-10 16:07:50 +00:00
|
|
|
ve.init.mw.ArticleTarget.prototype.onSaveDialogRetry = function () {
|
2015-08-05 21:43:23 +00:00
|
|
|
if ( this.pageDeletedWarning ) {
|
|
|
|
this.recreating = true;
|
|
|
|
this.pageExists = false;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2012-06-11 06:54:41 +00:00
|
|
|
/**
|
2015-02-23 20:51:33 +00:00
|
|
|
* Load the editor.
|
2012-06-11 06:54:41 +00:00
|
|
|
*
|
2015-03-13 15:50:23 +00:00
|
|
|
* This method initiates an API request for the page data unless dataPromise is passed in,
|
|
|
|
* in which case it waits for that promise instead.
|
2012-06-11 06:54:41 +00:00
|
|
|
*
|
2015-03-13 15:50:23 +00:00
|
|
|
* @param {jQuery.Promise} [dataPromise] Promise for pending request, if any
|
2019-05-01 13:51:51 +00:00
|
|
|
* @return {jQuery.Promise} Data promise
|
2019-11-04 14:54:53 +00:00
|
|
|
*/
|
2015-12-10 16:07:50 +00:00
|
|
|
ve.init.mw.ArticleTarget.prototype.load = function ( dataPromise ) {
|
2012-06-11 06:54:41 +00:00
|
|
|
// Prevent duplicate requests
|
2012-06-18 20:12:32 +00:00
|
|
|
if ( this.loading ) {
|
2019-05-01 13:51:51 +00:00
|
|
|
return this.loading;
|
2012-06-11 06:54:41 +00:00
|
|
|
}
|
2015-04-09 03:48:46 +00:00
|
|
|
this.events.trackActivationStart( mw.libs.ve.activationStart );
|
|
|
|
mw.libs.ve.activationStart = null;
|
2013-07-05 07:56:28 +00:00
|
|
|
|
2024-05-21 14:22:56 +00:00
|
|
|
const url = new URL( location.href );
|
2019-05-01 13:51:51 +00:00
|
|
|
dataPromise = dataPromise || mw.libs.ve.targetLoader.requestPageData( this.getDefaultMode(), this.getPageName(), {
|
2018-02-21 22:58:50 +00:00
|
|
|
sessionStore: true,
|
2017-09-11 14:53:50 +00:00
|
|
|
section: this.section,
|
|
|
|
oldId: this.requestedRevId,
|
2023-06-19 20:45:02 +00:00
|
|
|
targetName: this.constructor.static.trackingName,
|
|
|
|
editintro: url.searchParams.get( 'editintro' ),
|
|
|
|
preload: url.searchParams.get( 'preload' ),
|
|
|
|
preloadparams: mw.util.getArrayParam( 'preloadparams', url.searchParams )
|
2017-09-11 14:53:50 +00:00
|
|
|
} );
|
2019-04-10 18:34:26 +00:00
|
|
|
|
2019-05-06 20:09:45 +00:00
|
|
|
this.loading = dataPromise;
|
|
|
|
dataPromise
|
2015-07-31 15:03:33 +00:00
|
|
|
.done( this.loadSuccess.bind( this ) )
|
|
|
|
.fail( this.loadFail.bind( this ) );
|
2015-02-23 20:51:33 +00:00
|
|
|
|
2019-05-01 13:51:51 +00:00
|
|
|
return dataPromise;
|
2015-02-23 20:51:33 +00:00
|
|
|
};
|
|
|
|
|
2013-12-03 02:29:11 +00:00
|
|
|
/**
|
|
|
|
* Clear the state of this target, preparing it to be reactivated later.
|
|
|
|
*/
|
2015-12-10 16:07:50 +00:00
|
|
|
ve.init.mw.ArticleTarget.prototype.clearState = function () {
|
2016-12-13 21:55:01 +00:00
|
|
|
this.restoreAccessKeys();
|
2013-12-03 02:29:11 +00:00
|
|
|
this.clearPreparedCacheKey();
|
2019-11-04 14:54:53 +00:00
|
|
|
this.loading = null;
|
|
|
|
this.saving = null;
|
2017-06-22 17:54:04 +00:00
|
|
|
this.clearDiff();
|
2013-12-03 02:29:11 +00:00
|
|
|
this.serializing = false;
|
|
|
|
this.submitting = false;
|
|
|
|
this.baseTimeStamp = null;
|
|
|
|
this.startTimeStamp = null;
|
2016-12-21 22:35:52 +00:00
|
|
|
this.checkboxes = null;
|
2018-01-23 20:51:11 +00:00
|
|
|
this.initialSourceRange = null;
|
2013-12-03 02:29:11 +00:00
|
|
|
this.doc = null;
|
2017-07-17 16:38:32 +00:00
|
|
|
this.originalDmDocPromise = null;
|
2013-12-03 02:29:11 +00:00
|
|
|
this.originalHtml = null;
|
2017-10-05 16:31:15 +00:00
|
|
|
this.toolbarSaveButton = null;
|
2016-09-06 19:16:55 +00:00
|
|
|
this.section = null;
|
2021-12-21 18:48:27 +00:00
|
|
|
this.visibleSection = null;
|
|
|
|
this.visibleSectionOffset = null;
|
2015-12-11 16:41:01 +00:00
|
|
|
this.editNotices = [];
|
2013-12-03 02:29:11 +00:00
|
|
|
this.remoteNotices = [];
|
|
|
|
this.localNoticeMessages = [];
|
2018-02-21 22:58:50 +00:00
|
|
|
this.recovered = false;
|
2018-11-30 16:53:53 +00:00
|
|
|
this.teardownPromise = null;
|
2013-12-03 02:29:11 +00:00
|
|
|
};
|
|
|
|
|
2015-07-30 11:08:56 +00:00
|
|
|
/**
|
|
|
|
* Switch to edit source mode
|
|
|
|
*
|
2019-04-03 15:43:09 +00:00
|
|
|
* Opens a confirmation dialog if the document is modified or VE wikitext mode
|
|
|
|
* is not available.
|
2015-07-30 11:08:56 +00:00
|
|
|
*/
|
2019-04-03 15:43:09 +00:00
|
|
|
ve.init.mw.ArticleTarget.prototype.editSource = function () {
|
2024-05-21 14:22:56 +00:00
|
|
|
const modified = this.fromEditedState || this.getSurface().getModel().hasBeenModified();
|
2019-04-03 15:43:09 +00:00
|
|
|
|
2019-04-18 18:38:15 +00:00
|
|
|
this.switchToWikitextEditor( modified );
|
2019-04-03 15:43:09 +00:00
|
|
|
};
|
2015-07-30 11:08:56 +00:00
|
|
|
|
2016-09-02 19:06:04 +00:00
|
|
|
/**
|
|
|
|
* Get a document to save, cached until the surface is modified
|
|
|
|
*
|
|
|
|
* The default implementation returns an HTMLDocument, but other targets
|
|
|
|
* may use a different document model (e.g. plain text for source mode).
|
|
|
|
*
|
|
|
|
* @return {Object} Document to save
|
|
|
|
*/
|
|
|
|
ve.init.mw.ArticleTarget.prototype.getDocToSave = function () {
|
|
|
|
if ( !this.docToSave ) {
|
|
|
|
this.docToSave = this.createDocToSave();
|
|
|
|
// Cache clearing events
|
2024-05-21 14:22:56 +00:00
|
|
|
const surface = this.getSurface();
|
2016-09-02 19:06:04 +00:00
|
|
|
surface.getModel().getDocument().once( 'transact', this.clearDocToSave.bind( this ) );
|
|
|
|
surface.once( 'destroy', this.clearDocToSave.bind( this ) );
|
|
|
|
}
|
|
|
|
return this.docToSave;
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Create a document to save
|
|
|
|
*
|
|
|
|
* @return {Object} Document to save
|
|
|
|
*/
|
|
|
|
ve.init.mw.ArticleTarget.prototype.createDocToSave = function () {
|
2016-11-30 18:41:50 +00:00
|
|
|
return this.getSurface().getDom();
|
2016-09-02 19:06:04 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Clear the document to save from the cache
|
|
|
|
*/
|
|
|
|
ve.init.mw.ArticleTarget.prototype.clearDocToSave = function () {
|
|
|
|
this.docToSave = null;
|
|
|
|
this.clearPreparedCacheKey();
|
|
|
|
};
|
|
|
|
|
2013-11-06 08:22:11 +00:00
|
|
|
/**
|
|
|
|
* Serialize the current document and store the result in the serialization cache on the server.
|
|
|
|
*
|
|
|
|
* This function returns a promise that is resolved once serialization is complete, with the
|
|
|
|
* cache key passed as the first parameter.
|
|
|
|
*
|
|
|
|
* If there's already a request pending for the same (reference-identical) HTMLDocument, this
|
|
|
|
* function will not initiate a new request but will return the promise for the pending request.
|
|
|
|
* If a request for the same document has already been completed, this function will keep returning
|
|
|
|
* the same promise (which will already have been resolved) until clearPreparedCacheKey() is called.
|
|
|
|
*
|
|
|
|
* @param {HTMLDocument} doc Document to serialize
|
|
|
|
*/
|
2015-12-10 16:07:50 +00:00
|
|
|
ve.init.mw.ArticleTarget.prototype.prepareCacheKey = function ( doc ) {
|
2024-05-21 16:40:36 +00:00
|
|
|
const start = ve.now();
|
2013-11-06 08:22:11 +00:00
|
|
|
|
2016-11-30 12:03:56 +00:00
|
|
|
if ( this.getSurface().getMode() === 'source' ) {
|
2016-11-14 16:07:13 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2013-11-06 08:22:11 +00:00
|
|
|
if ( this.preparedCacheKeyPromise && this.preparedCacheKeyPromise.doc === doc ) {
|
2019-04-16 15:17:29 +00:00
|
|
|
return;
|
2013-11-06 08:22:11 +00:00
|
|
|
}
|
|
|
|
this.clearPreparedCacheKey();
|
|
|
|
|
2024-05-21 14:22:56 +00:00
|
|
|
let xhr;
|
2024-05-21 16:40:36 +00:00
|
|
|
let aborted = false;
|
2019-11-01 15:45:27 +00:00
|
|
|
this.preparedCacheKeyPromise = mw.libs.ve.targetSaver.deflateDoc( doc, this.doc )
|
2024-04-30 16:44:25 +00:00
|
|
|
.then( ( deflatedHtml ) => {
|
2015-03-31 19:52:14 +00:00
|
|
|
if ( aborted ) {
|
2019-11-02 05:06:28 +00:00
|
|
|
return ve.createDeferred().reject();
|
2013-11-06 08:22:11 +00:00
|
|
|
}
|
2024-05-01 12:32:49 +00:00
|
|
|
xhr = this.getContentApi().postWithToken( 'csrf',
|
2015-03-31 19:52:14 +00:00
|
|
|
{
|
2016-09-08 04:28:59 +00:00
|
|
|
action: 'visualeditoredit',
|
2015-03-31 19:52:14 +00:00
|
|
|
paction: 'serializeforcache',
|
|
|
|
html: deflatedHtml,
|
2024-05-01 12:32:49 +00:00
|
|
|
page: this.getPageName(),
|
|
|
|
oldid: this.revid,
|
|
|
|
etag: this.etag
|
2015-03-31 19:52:14 +00:00
|
|
|
},
|
|
|
|
{ contentType: 'multipart/form-data' }
|
|
|
|
);
|
|
|
|
return xhr.then(
|
2024-04-30 16:44:25 +00:00
|
|
|
( response ) => {
|
2024-05-21 14:22:56 +00:00
|
|
|
const trackData = { duration: ve.now() - start };
|
2016-09-08 04:28:59 +00:00
|
|
|
if ( response.visualeditoredit && typeof response.visualeditoredit.cachekey === 'string' ) {
|
2024-05-01 12:32:49 +00:00
|
|
|
this.events.track( 'performance.system.serializeforcache', trackData );
|
2019-11-01 15:45:27 +00:00
|
|
|
return {
|
|
|
|
cacheKey: response.visualeditoredit.cachekey,
|
|
|
|
// Pass the HTML for retries.
|
|
|
|
html: deflatedHtml
|
|
|
|
};
|
2015-03-31 19:52:14 +00:00
|
|
|
} else {
|
2024-05-01 12:32:49 +00:00
|
|
|
this.events.track( 'performance.system.serializeforcache.nocachekey', trackData );
|
2019-11-02 05:06:28 +00:00
|
|
|
return ve.createDeferred().reject();
|
2015-03-31 19:52:14 +00:00
|
|
|
}
|
|
|
|
},
|
2024-04-30 16:44:25 +00:00
|
|
|
() => {
|
2024-05-01 12:32:49 +00:00
|
|
|
this.events.track( 'performance.system.serializeforcache.fail', { duration: ve.now() - start } );
|
2019-11-07 00:36:20 +00:00
|
|
|
return ve.createDeferred().reject();
|
2015-03-31 19:52:14 +00:00
|
|
|
}
|
|
|
|
);
|
2013-11-06 08:22:11 +00:00
|
|
|
} )
|
2015-03-31 19:52:14 +00:00
|
|
|
.promise( {
|
2024-05-01 11:36:18 +00:00
|
|
|
abort: () => {
|
2015-03-31 19:52:14 +00:00
|
|
|
if ( xhr ) {
|
|
|
|
xhr.abort();
|
|
|
|
}
|
|
|
|
aborted = true;
|
|
|
|
},
|
|
|
|
doc: doc
|
2013-11-06 08:22:11 +00:00
|
|
|
} );
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the prepared wikitext, if any. Same as prepareWikitext() but does not initiate a request
|
|
|
|
* if one isn't already pending or finished. Instead, it returns a rejected promise in that case.
|
|
|
|
*
|
|
|
|
* @param {HTMLDocument} doc Document to serialize
|
2019-11-01 15:45:27 +00:00
|
|
|
* @return {jQuery.Promise} Abortable promise, resolved with a plain object containing `cacheKey`,
|
|
|
|
* and `html` for retries.
|
2013-11-06 08:22:11 +00:00
|
|
|
*/
|
2015-12-10 16:07:50 +00:00
|
|
|
ve.init.mw.ArticleTarget.prototype.getPreparedCacheKey = function ( doc ) {
|
2013-11-06 08:22:11 +00:00
|
|
|
if ( this.preparedCacheKeyPromise && this.preparedCacheKeyPromise.doc === doc ) {
|
|
|
|
return this.preparedCacheKeyPromise;
|
|
|
|
}
|
2019-11-02 05:06:28 +00:00
|
|
|
return ve.createDeferred().reject().promise();
|
2013-11-06 08:22:11 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Clear the promise for the prepared wikitext cache key, and abort it if it's still in progress.
|
|
|
|
*/
|
2015-12-10 16:07:50 +00:00
|
|
|
ve.init.mw.ArticleTarget.prototype.clearPreparedCacheKey = function () {
|
2013-11-06 08:22:11 +00:00
|
|
|
if ( this.preparedCacheKeyPromise ) {
|
|
|
|
this.preparedCacheKeyPromise.abort();
|
|
|
|
this.preparedCacheKeyPromise = null;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Try submitting an API request with a cache key for prepared wikitext, falling back to submitting
|
|
|
|
* HTML directly if there is no cache key present or pending, or if the request for the cache key
|
|
|
|
* fails, or if using the cache key fails with a badcachekey error.
|
|
|
|
*
|
2020-02-15 02:50:16 +00:00
|
|
|
* This function will use mw.Api#postWithToken to retry automatically when encountering a 'badtoken'
|
|
|
|
* error.
|
2016-09-08 04:28:59 +00:00
|
|
|
*
|
2016-11-14 16:07:13 +00:00
|
|
|
* @param {HTMLDocument|string} doc Document to submit or string in source mode
|
2019-11-01 15:45:27 +00:00
|
|
|
* @param {Object} extraData POST parameters to send. Do not include 'html', 'cachekey' or 'format'.
|
2013-11-06 08:22:11 +00:00
|
|
|
* @param {string} [eventName] If set, log an event when the request completes successfully. The
|
|
|
|
* full event name used will be 'performance.system.{eventName}.withCacheKey' or .withoutCacheKey
|
|
|
|
* depending on whether or not a cache key was used.
|
2019-11-01 15:45:27 +00:00
|
|
|
* @return {jQuery.Promise} Promise which resolves/rejects when saving is complete/fails
|
2013-11-06 08:22:11 +00:00
|
|
|
*/
|
2019-11-01 15:45:27 +00:00
|
|
|
ve.init.mw.ArticleTarget.prototype.tryWithPreparedCacheKey = function ( doc, extraData, eventName ) {
|
2016-11-30 12:03:56 +00:00
|
|
|
if ( this.getSurface().getMode() === 'source' ) {
|
2024-05-21 14:22:56 +00:00
|
|
|
const data = ve.copy( extraData );
|
2019-11-04 14:54:53 +00:00
|
|
|
|
2020-03-24 20:55:31 +00:00
|
|
|
// TODO: This should happen in #getSaveOptions, check if moving it there breaks anything
|
2016-11-14 16:07:13 +00:00
|
|
|
if ( this.section !== null ) {
|
2019-11-01 15:45:27 +00:00
|
|
|
data.section = this.section;
|
2016-11-14 16:07:13 +00:00
|
|
|
}
|
2016-12-08 19:24:10 +00:00
|
|
|
if ( this.sectionTitle ) {
|
2019-11-01 15:45:27 +00:00
|
|
|
data.sectiontitle = this.sectionTitle.getValue();
|
|
|
|
data.summary = undefined;
|
2016-12-08 19:24:10 +00:00
|
|
|
}
|
2019-11-04 14:54:53 +00:00
|
|
|
|
|
|
|
return mw.libs.ve.targetSaver.postWikitext(
|
|
|
|
doc,
|
|
|
|
data,
|
2022-02-14 09:07:10 +00:00
|
|
|
{ api: this.getContentApi() }
|
2019-11-04 14:54:53 +00:00
|
|
|
);
|
2016-11-14 16:07:13 +00:00
|
|
|
}
|
|
|
|
|
2019-11-01 15:45:27 +00:00
|
|
|
// getPreparedCacheKey resolves with { cacheKey: ..., html: ... } or rejects.
|
|
|
|
// After modification it never rejects, just resolves with { html: ... } instead
|
2024-05-21 14:22:56 +00:00
|
|
|
const htmlOrCacheKeyPromise = this.getPreparedCacheKey( doc ).then(
|
2019-11-01 15:45:27 +00:00
|
|
|
// Success, use promise as-is.
|
|
|
|
null,
|
|
|
|
// Fail, get deflatedHtml promise
|
2024-05-01 12:32:49 +00:00
|
|
|
() => mw.libs.ve.targetSaver.deflateDoc( doc, this.doc ).then( ( html ) => ( { html: html } ) ) );
|
2024-04-30 16:44:25 +00:00
|
|
|
|
|
|
|
return htmlOrCacheKeyPromise.then( ( htmlOrCacheKey ) => mw.libs.ve.targetSaver.postHtml(
|
|
|
|
htmlOrCacheKey.html,
|
|
|
|
htmlOrCacheKey.cacheKey,
|
|
|
|
extraData,
|
|
|
|
{
|
2024-05-01 12:32:49 +00:00
|
|
|
onCacheKeyFail: this.clearPreparedCacheKey.bind( this ),
|
|
|
|
api: this.getContentApi(),
|
|
|
|
track: this.events.track.bind( this.events ),
|
2024-04-30 16:44:25 +00:00
|
|
|
eventName: eventName,
|
|
|
|
now: ve.now
|
|
|
|
}
|
|
|
|
) );
|
2013-11-06 08:22:11 +00:00
|
|
|
};
|
|
|
|
|
2015-08-04 13:37:13 +00:00
|
|
|
/**
|
2016-03-23 11:09:38 +00:00
|
|
|
* Handle the save dialog's save event
|
|
|
|
*
|
|
|
|
* Validates the inputs then starts the save process
|
2015-08-04 13:37:13 +00:00
|
|
|
*
|
2015-08-05 21:43:23 +00:00
|
|
|
* @param {jQuery.Deferred} saveDeferred Deferred object to resolve/reject when the save
|
|
|
|
* succeeds/fails.
|
2024-04-29 18:14:26 +00:00
|
|
|
* @fires ve.init.mw.ArticleTarget#saveInitiated
|
2015-08-04 13:37:13 +00:00
|
|
|
*/
|
2016-03-23 11:09:38 +00:00
|
|
|
ve.init.mw.ArticleTarget.prototype.onSaveDialogSave = function ( saveDeferred ) {
|
2015-08-05 21:43:23 +00:00
|
|
|
if ( this.deactivating ) {
|
2016-10-28 00:22:30 +00:00
|
|
|
return;
|
2015-08-05 21:43:23 +00:00
|
|
|
}
|
|
|
|
|
2024-05-21 14:22:56 +00:00
|
|
|
const saveOptions = this.getSaveOptions();
|
2015-08-17 13:06:59 +00:00
|
|
|
|
2015-08-05 21:43:23 +00:00
|
|
|
if (
|
|
|
|
+mw.user.options.get( 'forceeditsummary' ) &&
|
2016-02-21 08:35:05 +00:00
|
|
|
( saveOptions.summary === '' || saveOptions.summary === this.initialEditSummary ) &&
|
2015-08-05 21:43:23 +00:00
|
|
|
!this.saveDialog.messages.missingsummary
|
|
|
|
) {
|
|
|
|
this.saveDialog.showMessage(
|
|
|
|
'missingsummary',
|
2021-01-14 16:11:57 +00:00
|
|
|
new OO.ui.HtmlSnippet( ve.init.platform.getParsedMessage( 'missingsummary' ) )
|
2015-08-05 21:43:23 +00:00
|
|
|
);
|
|
|
|
this.saveDialog.popPending();
|
|
|
|
} else {
|
|
|
|
this.emit( 'saveInitiated' );
|
2016-03-23 11:09:38 +00:00
|
|
|
this.startSave( saveOptions );
|
2015-08-05 21:43:23 +00:00
|
|
|
this.saveDeferred = saveDeferred;
|
2015-08-04 13:37:13 +00:00
|
|
|
}
|
2015-08-05 21:43:23 +00:00
|
|
|
};
|
|
|
|
|
2016-03-23 11:09:38 +00:00
|
|
|
/**
|
|
|
|
* Start the save process
|
|
|
|
*
|
|
|
|
* @param {Object} saveOptions Save options
|
|
|
|
*/
|
|
|
|
ve.init.mw.ArticleTarget.prototype.startSave = function ( saveOptions ) {
|
2016-09-02 19:06:04 +00:00
|
|
|
this.save( this.getDocToSave(), saveOptions );
|
2016-03-23 11:09:38 +00:00
|
|
|
};
|
|
|
|
|
2015-08-05 21:43:23 +00:00
|
|
|
/**
|
|
|
|
* Get save form fields from the save dialog form.
|
|
|
|
*
|
2015-08-19 18:09:34 +00:00
|
|
|
* @return {Object} Form data for submission to the MediaWiki action=edit UI
|
2015-08-05 21:43:23 +00:00
|
|
|
*/
|
2015-12-10 16:07:50 +00:00
|
|
|
ve.init.mw.ArticleTarget.prototype.getSaveFields = function () {
|
2024-05-21 14:22:56 +00:00
|
|
|
const fields = {};
|
2020-03-24 20:55:31 +00:00
|
|
|
|
|
|
|
if ( this.section === 'new' ) {
|
|
|
|
// MediaWiki action=edit UI doesn't have separate parameters for edit summary and new section
|
|
|
|
// title. The edit summary parameter is supposed to contain the section title, and the real
|
|
|
|
// summary is autogenerated.
|
|
|
|
fields.wpSummary = this.sectionTitle ? this.sectionTitle.getValue() : '';
|
|
|
|
} else {
|
|
|
|
fields.wpSummary = this.saveDialog ?
|
|
|
|
this.saveDialog.editSummaryInput.getValue() :
|
|
|
|
( this.editSummaryValue || this.initialEditSummary );
|
|
|
|
}
|
2018-11-23 17:01:25 +00:00
|
|
|
|
2024-05-21 14:22:56 +00:00
|
|
|
let name;
|
2018-11-23 17:01:25 +00:00
|
|
|
// Extra save fields added by extensions
|
|
|
|
for ( name in this.saveFields ) {
|
|
|
|
fields[ name ] = this.saveFields[ name ]();
|
|
|
|
}
|
|
|
|
|
2015-08-05 21:43:23 +00:00
|
|
|
if ( this.recreating ) {
|
|
|
|
fields.wpRecreate = true;
|
|
|
|
}
|
2016-11-05 06:16:26 +00:00
|
|
|
|
|
|
|
for ( name in this.checkboxesByName ) {
|
2021-01-14 15:59:25 +00:00
|
|
|
// DropdownInputWidget or CheckboxInputWidget
|
|
|
|
if ( !this.checkboxesByName[ name ].isSelected || this.checkboxesByName[ name ].isSelected() ) {
|
2016-11-05 06:16:26 +00:00
|
|
|
fields[ name ] = this.checkboxesByName[ name ].getValue();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-08-05 21:43:23 +00:00
|
|
|
return fields;
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Invoke #submit with the data from #getSaveFields
|
|
|
|
*
|
|
|
|
* @param {Object} fields Fields to add in addition to those from #getSaveFields
|
|
|
|
* @param {string} wikitext Wikitext to submit
|
2015-08-19 18:09:34 +00:00
|
|
|
* @return {boolean} Whether submission was started
|
2015-08-05 21:43:23 +00:00
|
|
|
*/
|
2015-12-10 16:07:50 +00:00
|
|
|
ve.init.mw.ArticleTarget.prototype.submitWithSaveFields = function ( fields, wikitext ) {
|
2021-11-15 17:25:51 +00:00
|
|
|
return this.submit( wikitext, ve.extendObject( this.getSaveFields(), fields ) );
|
2015-08-04 13:37:13 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get edit API options from the save dialog form.
|
|
|
|
*
|
2015-08-19 18:09:34 +00:00
|
|
|
* @return {Object} Save options for submission to the MediaWiki API
|
2015-08-04 13:37:13 +00:00
|
|
|
*/
|
2015-12-10 16:07:50 +00:00
|
|
|
ve.init.mw.ArticleTarget.prototype.getSaveOptions = function () {
|
2024-05-21 14:22:56 +00:00
|
|
|
const options = this.getSaveFields(),
|
2015-08-05 21:43:23 +00:00
|
|
|
fieldMap = {
|
|
|
|
wpSummary: 'summary',
|
|
|
|
wpMinoredit: 'minor',
|
2020-02-29 17:35:29 +00:00
|
|
|
wpWatchthis: 'watchlist',
|
2015-08-05 21:43:23 +00:00
|
|
|
wpCaptchaId: 'captchaid',
|
|
|
|
wpCaptchaWord: 'captchaword'
|
|
|
|
};
|
|
|
|
|
2024-05-21 14:22:56 +00:00
|
|
|
for ( const key in fieldMap ) {
|
2015-08-19 17:33:02 +00:00
|
|
|
if ( options[ key ] !== undefined ) {
|
|
|
|
options[ fieldMap[ key ] ] = options[ key ];
|
|
|
|
delete options[ key ];
|
2015-08-05 21:43:23 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-02-29 17:35:29 +00:00
|
|
|
options.watchlist = 'watchlist' in options ? 'watch' : 'unwatch';
|
|
|
|
|
2015-08-05 21:43:23 +00:00
|
|
|
return options;
|
2015-08-04 13:37:13 +00:00
|
|
|
};
|
|
|
|
|
2012-06-11 06:54:41 +00:00
|
|
|
/**
|
2013-01-15 23:38:49 +00:00
|
|
|
* Post DOM data to the Parsoid API.
|
2012-06-11 06:54:41 +00:00
|
|
|
*
|
|
|
|
* This method performs an asynchronous action and uses a callback function to handle the result.
|
|
|
|
*
|
2024-05-01 12:32:49 +00:00
|
|
|
* this.save( dom, { summary: 'test', minor: true, watch: false } );
|
2012-06-11 06:54:41 +00:00
|
|
|
*
|
2013-02-11 19:46:58 +00:00
|
|
|
* @param {HTMLDocument} doc Document to save
|
Render check boxes from EditPage
EditPage has a lovely getCheckboxes() function which includes the
minor and watch checkboxes as rendered by MW core, as well as any
checkboxes extensions like FlaggedRevs might have added. Output
these in the API, render them, and send their values back.
ApiVisualEditor.php:
* Build a fake EditPage, get its checkboxes, and return them
ApiVisualEditorEdit.php:
* Pass through posted request data to ApiEdit, which passes it
through to EditPage thanks to Idab5b524b0e3 in core
ve.init.mw.ViewPageTarget.js:
* Remove minor and watch checkboxes from the save dialog template
and replace them with a generic checkbox container
* Have getSaveOptions() pull the state of all checkboxes in
** Special-case minor and watch, and pass the rest straight through
** Move normalization from true/false to presence/absence here, from
ve.init.mw.Target.prototype.save(), because here we know which ones
are checkboxes and we don't know that in save() without
special-casing
* Remove getSaveDialogHtml(), we don't need to hide checkboxes based on
rights anymore because in that case the API just won't send them to us.
** Moved logic for checking the watch checkbox down to where the same
logic for the minor checkbox already is
* Unwrap getSaveDialogHtml() in setupSaveDialog()
* Access minor and watch by their new IDs throughout
ve.init.mw.Target.js:
* Get and store checkboxes from the API
* Pass all keys straight through to the API
Bug: 49699
Change-Id: I09d02a42b05146bc9b7080ab38338ae869bf15e3
2013-07-24 06:39:03 +00:00
|
|
|
* @param {Object} options Saving options. All keys are passed through, including unrecognized ones.
|
JSDuck: Generated code documentation!
See CODING.md for how to run it.
Mistakes fixed:
* Warning: Unknown type function
-> Function
* Warning: Unknown type DOMElement
-> HTMLElement
* Warning: Unknown type DOM Node
-> HTMLElement
* Warning: Unknown type Integer
-> Mixed
* Warning: Unknown type Command
-> ve.Command
* Warning: Unknown type any
-> number
* Warning: Unknown type ve.Transaction
-> ve.dm.Transaction
* Warning: Unknown type ve.dm.AnnotationSet
-> ve.AnnotationSet
* Warning: Unknown type false
-> boolean
* Warning: Unknown type ve.dm.AlienNode
ve.dm doesn't have a generic AlienNode like ve.ce
-> Unknown type ve.dm.AlienInlineNode|ve.dm.AlienBlockNode
* Warning: Unknown type ve.ve.Surface
-> ve.ce.Surface
* ve.example.lookupNode:
-> Last @param should be @return
* ve.dm.Transaction.prototype.pushReplace:
-> @param {Array] should be @param {Array}
* Warning: ve.BranchNode.js:27: {@link ve.Node#hasChildren} links to non-existing member
-> (removed)
* Warning: ve.LeafNode.js:21: {@link ve.Node#hasChildren} links to non-existing member
-> (removed)
Differences fixed:
* Variadic arguments are like @param {Type...} [name]
instead of @param {Type} [name...]
* Convert all file headers from /** to /*! because JSDuck tries
to parse all /** blocks and fails to parse with all sorts of
errors for "Global property", "Unnamed property", and
"Duplicate property".
Find: \/\*\*([^@]+)(@copyright)
Replace: /*!$1$2
* Indented blocks are considered code examples.
A few methods had documentation with numbered lists that were
indented, which have now been updated to not be intended.
* The free-form text descriptions are parsed with Markdown,
which requires lists to be separated from paragraphs by an
empty line.
And we should use `backticks` instead of {braces} for inline
code in text paragraphs.
* Doc blocks for classes and their constructor have to be
in the correct order (@constructor, @param, @return must be
before @class, @abstract, @extends etc.)
* `@extends Class` must not have Class {wrapped}
* @throws must start with a {Type}
* @example means something else. It is used for an inline demo
iframe, not code block. For that simply indent with spaces.
* @member means something else.
Non-function properties are marked with @property, not @member.
* To create a link to a class or member, in most cases the name
is enough to create a link. E.g. Foo, Foo.bar, Foo.bar#quux,
where a hash stands for "instance member", so Foo.bar#quux,
links to Foo.bar.prototype.quux (the is not supported, as
"prototype" is considered an implementation detail, it only
indexes class name and method name).
If the magic linker doesn't work for some case, the
verbose syntax is {@link #target label}.
* @property can't have sub-properties (nested @param and @return
values are supported, only @static @property can't be nested).
We only have one case of this, which can be worked around by
moving those in a new virtual class. The code is unaltered
(only moved down so that it isn't with the scope of the main
@class block). ve.dm.TransactionProcessor.processors.
New:
* @mixins: Classes mixed into the current class.
* @event: Events that can be emitted by a class. These are also
inherited by subclasses. (+ @param, @return and @preventable).
So ve.Node#event-attach is inherited to ve.dm.BreakNode,
just like @method is.
* @singleton: Plain objects such as ve, ve.dm, ve.ce were missing
documentation causing a tree error. Documented those as a
JSDuck singleton, which they but just weren't documented yet.
NB: Members of @singleton don't need @static (if present,
triggers a compiler warning).
* @chainable: Shorthand for "@return this". We were using
"@return {classname}" which is ambiguous (returns the same
instance or another instance?), @chainable is specifically
for "@return this". Creates proper labels in the generated
HTML pages.
Removed:
* @mixin: (not to be confused with @mixins). Not supported by
JSDuck. Every class is standalone anyway. Where needed marked
them @class + @abstract instead.
Change-Id: I6a7c9e8ee8f995731bc205d666167874eb2ebe23
2013-01-04 08:54:17 +00:00
|
|
|
* - {string} summary Edit summary
|
|
|
|
* - {boolean} minor Edit is a minor edit
|
2013-01-15 23:38:49 +00:00
|
|
|
* - {boolean} watch Watch the page
|
2019-11-04 14:54:53 +00:00
|
|
|
* @return {jQuery.Promise} Save promise, see mw.libs.ve.targetSaver.postHtml
|
|
|
|
*/
|
2023-08-14 16:51:25 +00:00
|
|
|
ve.init.mw.ArticleTarget.prototype.save = function ( doc, options ) {
|
2012-06-11 06:54:41 +00:00
|
|
|
// Prevent duplicate requests
|
2012-06-18 20:12:32 +00:00
|
|
|
if ( this.saving ) {
|
2019-11-04 14:54:53 +00:00
|
|
|
return this.saving;
|
2012-06-11 06:54:41 +00:00
|
|
|
}
|
2013-06-23 23:09:47 +00:00
|
|
|
|
2024-05-21 14:22:56 +00:00
|
|
|
const data = ve.extendObject( {}, options, {
|
2018-05-04 13:30:10 +00:00
|
|
|
page: this.getPageName(),
|
2014-08-22 20:50:48 +00:00
|
|
|
oldid: this.revid,
|
|
|
|
basetimestamp: this.baseTimeStamp,
|
|
|
|
starttimestamp: this.startTimeStamp,
|
2016-09-08 04:28:59 +00:00
|
|
|
etag: this.etag,
|
2020-02-15 01:22:39 +00:00
|
|
|
assert: mw.user.isAnon() ? 'anon' : 'user',
|
|
|
|
assertuser: mw.user.getName() || undefined
|
Render check boxes from EditPage
EditPage has a lovely getCheckboxes() function which includes the
minor and watch checkboxes as rendered by MW core, as well as any
checkboxes extensions like FlaggedRevs might have added. Output
these in the API, render them, and send their values back.
ApiVisualEditor.php:
* Build a fake EditPage, get its checkboxes, and return them
ApiVisualEditorEdit.php:
* Pass through posted request data to ApiEdit, which passes it
through to EditPage thanks to Idab5b524b0e3 in core
ve.init.mw.ViewPageTarget.js:
* Remove minor and watch checkboxes from the save dialog template
and replace them with a generic checkbox container
* Have getSaveOptions() pull the state of all checkboxes in
** Special-case minor and watch, and pass the rest straight through
** Move normalization from true/false to presence/absence here, from
ve.init.mw.Target.prototype.save(), because here we know which ones
are checkboxes and we don't know that in save() without
special-casing
* Remove getSaveDialogHtml(), we don't need to hide checkboxes based on
rights anymore because in that case the API just won't send them to us.
** Moved logic for checking the watch checkbox down to where the same
logic for the minor checkbox already is
* Unwrap getSaveDialogHtml() in setupSaveDialog()
* Access minor and watch by their new IDs throughout
ve.init.mw.Target.js:
* Get and store checkboxes from the API
* Pass all keys straight through to the API
Bug: 49699
Change-Id: I09d02a42b05146bc9b7080ab38338ae869bf15e3
2013-07-24 06:39:03 +00:00
|
|
|
} );
|
2013-06-23 23:09:47 +00:00
|
|
|
|
2023-06-03 01:19:38 +00:00
|
|
|
if ( !this.pageExists || this.restoring || !this.isViewPage ) {
|
|
|
|
// This is a page creation, a restoration, or we loaded the editor from a non-view page.
|
|
|
|
// We can't update the interface to reflect this new state, so we're going to reload the whole page.
|
|
|
|
// Therefore we don't need the new revision's HTML content in the API response.
|
|
|
|
data.nocontent = true;
|
|
|
|
}
|
|
|
|
|
2023-06-15 00:06:42 +00:00
|
|
|
if ( this.wouldautocreate ) {
|
|
|
|
// This means that we might need to redirect to an opaque URL,
|
|
|
|
// so we must set up query parameters we want ahead of time.
|
|
|
|
// TODO: `this.isRedirect` is only set in visual mode, not in source mode
|
|
|
|
data.returntoquery = this.isRedirect ? 'redirect=no' : '';
|
|
|
|
data.returntoanchor = this.getSectionHashFromPage();
|
|
|
|
}
|
|
|
|
|
2024-05-21 14:22:56 +00:00
|
|
|
const config = mw.config.get( 'wgVisualEditorConfig' );
|
2023-03-16 15:25:06 +00:00
|
|
|
|
2024-05-21 14:22:56 +00:00
|
|
|
const taglist = data.vetags ? data.vetags.split( ',' ) : [];
|
2023-03-16 16:15:42 +00:00
|
|
|
|
|
|
|
if ( config.useChangeTagging ) {
|
|
|
|
taglist.push(
|
|
|
|
this.getSurface().getMode() === 'source' ? 'visualeditor-wikitext' : 'visualeditor'
|
|
|
|
);
|
2020-01-24 19:14:32 +00:00
|
|
|
}
|
|
|
|
|
2023-03-16 15:25:06 +00:00
|
|
|
if (
|
|
|
|
this.getSurface().getMode() === 'visual' &&
|
2023-07-03 13:34:51 +00:00
|
|
|
mw.config.get( 'wgVisualEditorConfig' ).editCheckTagging
|
2023-03-16 15:25:06 +00:00
|
|
|
) {
|
2024-10-01 11:31:17 +00:00
|
|
|
const documentModel = this.getSurface().getModel().getDocument();
|
2023-07-03 13:34:51 +00:00
|
|
|
// New content needing a reference
|
2024-10-01 11:31:17 +00:00
|
|
|
if ( mw.editcheck.hasAddedContentNeedingReference( documentModel ) ) {
|
2023-07-03 13:34:51 +00:00
|
|
|
taglist.push( 'editcheck-references' );
|
|
|
|
}
|
|
|
|
// New content, regardless of if it needs a reference
|
2024-10-01 11:31:17 +00:00
|
|
|
if ( mw.editcheck.hasAddedContentNeedingReference( documentModel, true ) ) {
|
2023-07-03 13:34:51 +00:00
|
|
|
taglist.push( 'editcheck-newcontent' );
|
|
|
|
}
|
2023-08-24 03:16:02 +00:00
|
|
|
// Rejection reasons for references
|
2024-05-21 14:22:56 +00:00
|
|
|
const rejections = mw.editcheck.getRejectionReasons();
|
2023-08-24 03:16:02 +00:00
|
|
|
if ( rejections.length > 0 ) {
|
2024-04-30 16:44:25 +00:00
|
|
|
rejections.forEach( ( reason ) => {
|
2023-08-24 03:16:02 +00:00
|
|
|
taglist.push( 'editcheck-reference-decline-' + reason );
|
|
|
|
} );
|
|
|
|
}
|
2023-03-16 15:25:06 +00:00
|
|
|
}
|
|
|
|
|
2023-03-16 16:15:42 +00:00
|
|
|
data.vetags = taglist.join( ',' );
|
|
|
|
|
2024-05-21 14:22:56 +00:00
|
|
|
const promise = this.saving = this.tryWithPreparedCacheKey( doc, data, 'save' )
|
2019-11-04 14:54:53 +00:00
|
|
|
.done( this.saveComplete.bind( this ) )
|
2023-08-14 16:51:25 +00:00
|
|
|
.fail( this.saveFail.bind( this, doc, data ) )
|
2024-04-30 16:44:25 +00:00
|
|
|
.always( () => {
|
2024-05-01 12:32:49 +00:00
|
|
|
this.saving = null;
|
2019-11-04 14:54:53 +00:00
|
|
|
} );
|
2013-10-11 22:00:10 +00:00
|
|
|
|
2019-11-04 14:54:53 +00:00
|
|
|
return promise;
|
2012-06-11 06:54:41 +00:00
|
|
|
};
|
2012-11-28 23:57:00 +00:00
|
|
|
|
2017-04-10 11:38:11 +00:00
|
|
|
/**
|
|
|
|
* Show changes in the save dialog
|
|
|
|
*
|
|
|
|
* @param {Object} doc Document
|
|
|
|
*/
|
|
|
|
ve.init.mw.ArticleTarget.prototype.showChanges = function ( doc ) {
|
|
|
|
// Invalidate the viewer diff on next change
|
2024-04-30 16:44:25 +00:00
|
|
|
this.getSurface().getModel().getDocument().once( 'transact', () => {
|
2024-05-01 12:32:49 +00:00
|
|
|
this.clearDiff();
|
2017-04-10 11:38:11 +00:00
|
|
|
} );
|
|
|
|
this.saveDialog.setDiffAndReview(
|
|
|
|
this.getWikitextDiffPromise( doc ),
|
2017-05-08 18:57:43 +00:00
|
|
|
this.getVisualDiffGeneratorPromise(),
|
2017-04-10 11:38:11 +00:00
|
|
|
this.getSurface().getModel().getDocument().getHtmlDocument()
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
2017-06-22 17:54:04 +00:00
|
|
|
/**
|
|
|
|
* Clear all state associated with the diff
|
|
|
|
*/
|
|
|
|
ve.init.mw.ArticleTarget.prototype.clearDiff = function () {
|
|
|
|
if ( this.saveDialog ) {
|
|
|
|
this.saveDialog.clearDiff();
|
|
|
|
}
|
|
|
|
this.wikitextDiffPromise = null;
|
|
|
|
};
|
|
|
|
|
2012-12-07 16:23:23 +00:00
|
|
|
/**
|
2013-11-06 08:22:11 +00:00
|
|
|
* Post DOM data to the Parsoid API to retrieve wikitext diff.
|
2012-12-07 16:23:23 +00:00
|
|
|
*
|
2013-03-20 07:09:43 +00:00
|
|
|
* @param {HTMLDocument} doc Document to compare against (via wikitext)
|
2017-04-10 11:38:11 +00:00
|
|
|
* @return {jQuery.Promise} Promise which resolves with the wikitext diff, or rejects with an error
|
2024-04-29 18:14:26 +00:00
|
|
|
* @fires ve.init.mw.ArticleTarget#showChanges
|
|
|
|
* @fires ve.init.mw.ArticleTarget#showChangesError
|
2019-11-04 14:54:53 +00:00
|
|
|
*/
|
2017-04-10 11:38:11 +00:00
|
|
|
ve.init.mw.ArticleTarget.prototype.getWikitextDiffPromise = function ( doc ) {
|
|
|
|
if ( !this.wikitextDiffPromise ) {
|
|
|
|
this.wikitextDiffPromise = this.tryWithPreparedCacheKey( doc, {
|
|
|
|
paction: 'diff',
|
2018-05-04 13:30:10 +00:00
|
|
|
page: this.getPageName(),
|
2017-04-10 11:38:11 +00:00
|
|
|
oldid: this.revid,
|
|
|
|
etag: this.etag
|
2024-04-30 16:44:25 +00:00
|
|
|
}, 'diff' ).then( ( data ) => {
|
2019-11-19 19:38:15 +00:00
|
|
|
if ( !data.diff ) {
|
2024-05-01 12:32:49 +00:00
|
|
|
this.emit( 'noChanges' );
|
2017-04-10 11:38:11 +00:00
|
|
|
}
|
2019-11-19 19:38:15 +00:00
|
|
|
return data.diff;
|
2017-04-10 11:38:11 +00:00
|
|
|
} );
|
|
|
|
this.wikitextDiffPromise
|
|
|
|
.done( this.emit.bind( this, 'showChanges' ) )
|
|
|
|
.fail( this.emit.bind( this, 'showChangesError' ) );
|
2013-11-06 08:22:11 +00:00
|
|
|
}
|
2017-04-10 11:38:11 +00:00
|
|
|
return this.wikitextDiffPromise;
|
2012-12-07 16:23:23 +00:00
|
|
|
};
|
|
|
|
|
2012-11-28 23:57:00 +00:00
|
|
|
/**
|
2013-11-15 20:30:57 +00:00
|
|
|
* Post wikitext to MediaWiki.
|
2012-11-28 23:57:00 +00:00
|
|
|
*
|
|
|
|
* This method performs a synchronous action and will take the user to a new page when complete.
|
|
|
|
*
|
2024-05-01 12:32:49 +00:00
|
|
|
* this.submit( wikitext, { wpSummary: 'test', wpMinorEdit: 1, wpSave: 1 } );
|
2012-11-28 23:57:00 +00:00
|
|
|
*
|
JSDuck: Generated code documentation!
See CODING.md for how to run it.
Mistakes fixed:
* Warning: Unknown type function
-> Function
* Warning: Unknown type DOMElement
-> HTMLElement
* Warning: Unknown type DOM Node
-> HTMLElement
* Warning: Unknown type Integer
-> Mixed
* Warning: Unknown type Command
-> ve.Command
* Warning: Unknown type any
-> number
* Warning: Unknown type ve.Transaction
-> ve.dm.Transaction
* Warning: Unknown type ve.dm.AnnotationSet
-> ve.AnnotationSet
* Warning: Unknown type false
-> boolean
* Warning: Unknown type ve.dm.AlienNode
ve.dm doesn't have a generic AlienNode like ve.ce
-> Unknown type ve.dm.AlienInlineNode|ve.dm.AlienBlockNode
* Warning: Unknown type ve.ve.Surface
-> ve.ce.Surface
* ve.example.lookupNode:
-> Last @param should be @return
* ve.dm.Transaction.prototype.pushReplace:
-> @param {Array] should be @param {Array}
* Warning: ve.BranchNode.js:27: {@link ve.Node#hasChildren} links to non-existing member
-> (removed)
* Warning: ve.LeafNode.js:21: {@link ve.Node#hasChildren} links to non-existing member
-> (removed)
Differences fixed:
* Variadic arguments are like @param {Type...} [name]
instead of @param {Type} [name...]
* Convert all file headers from /** to /*! because JSDuck tries
to parse all /** blocks and fails to parse with all sorts of
errors for "Global property", "Unnamed property", and
"Duplicate property".
Find: \/\*\*([^@]+)(@copyright)
Replace: /*!$1$2
* Indented blocks are considered code examples.
A few methods had documentation with numbered lists that were
indented, which have now been updated to not be intended.
* The free-form text descriptions are parsed with Markdown,
which requires lists to be separated from paragraphs by an
empty line.
And we should use `backticks` instead of {braces} for inline
code in text paragraphs.
* Doc blocks for classes and their constructor have to be
in the correct order (@constructor, @param, @return must be
before @class, @abstract, @extends etc.)
* `@extends Class` must not have Class {wrapped}
* @throws must start with a {Type}
* @example means something else. It is used for an inline demo
iframe, not code block. For that simply indent with spaces.
* @member means something else.
Non-function properties are marked with @property, not @member.
* To create a link to a class or member, in most cases the name
is enough to create a link. E.g. Foo, Foo.bar, Foo.bar#quux,
where a hash stands for "instance member", so Foo.bar#quux,
links to Foo.bar.prototype.quux (the is not supported, as
"prototype" is considered an implementation detail, it only
indexes class name and method name).
If the magic linker doesn't work for some case, the
verbose syntax is {@link #target label}.
* @property can't have sub-properties (nested @param and @return
values are supported, only @static @property can't be nested).
We only have one case of this, which can be worked around by
moving those in a new virtual class. The code is unaltered
(only moved down so that it isn't with the scope of the main
@class block). ve.dm.TransactionProcessor.processors.
New:
* @mixins: Classes mixed into the current class.
* @event: Events that can be emitted by a class. These are also
inherited by subclasses. (+ @param, @return and @preventable).
So ve.Node#event-attach is inherited to ve.dm.BreakNode,
just like @method is.
* @singleton: Plain objects such as ve, ve.dm, ve.ce were missing
documentation causing a tree error. Documented those as a
JSDuck singleton, which they but just weren't documented yet.
NB: Members of @singleton don't need @static (if present,
triggers a compiler warning).
* @chainable: Shorthand for "@return this". We were using
"@return {classname}" which is ambiguous (returns the same
instance or another instance?), @chainable is specifically
for "@return this". Creates proper labels in the generated
HTML pages.
Removed:
* @mixin: (not to be confused with @mixins). Not supported by
JSDuck. Every class is standalone anyway. Where needed marked
them @class + @abstract instead.
Change-Id: I6a7c9e8ee8f995731bc205d666167874eb2ebe23
2013-01-04 08:54:17 +00:00
|
|
|
* @param {string} wikitext Wikitext to submit
|
2013-11-15 20:30:57 +00:00
|
|
|
* @param {Object} fields Other form fields to add (e.g. wpSummary, wpWatchthis, etc.). To actually
|
2014-08-22 20:50:48 +00:00
|
|
|
* save the wikitext, add { wpSave: 1 }. To go to the diff view, add { wpDiff: 1 }.
|
2015-08-19 18:09:34 +00:00
|
|
|
* @return {boolean} Submitting has been started
|
2019-11-04 14:54:53 +00:00
|
|
|
*/
|
2015-12-10 16:07:50 +00:00
|
|
|
ve.init.mw.ArticleTarget.prototype.submit = function ( wikitext, fields ) {
|
2012-11-28 23:57:00 +00:00
|
|
|
// Prevent duplicate requests
|
|
|
|
if ( this.submitting ) {
|
|
|
|
return false;
|
|
|
|
}
|
2020-02-18 09:42:21 +00:00
|
|
|
// Clear autosave now that we don't expect to need it again.
|
|
|
|
// FIXME: This isn't transactional, so if the save fails we're left with no recourse.
|
|
|
|
this.clearDocState();
|
2012-11-28 23:57:00 +00:00
|
|
|
// Save DOM
|
|
|
|
this.submitting = true;
|
2024-05-21 14:22:56 +00:00
|
|
|
const $form = $( '<form>' ).attr( { method: 'post', enctype: 'multipart/form-data' } ).addClass( 'oo-ui-element-hidden' );
|
|
|
|
const params = ve.extendObject( {
|
2015-08-19 18:05:01 +00:00
|
|
|
format: 'text/x-wiki',
|
|
|
|
model: 'wikitext',
|
|
|
|
oldid: this.requestedRevId,
|
|
|
|
wpStarttime: this.startTimeStamp,
|
|
|
|
wpEdittime: this.baseTimeStamp,
|
|
|
|
wpTextbox1: wikitext,
|
2020-02-15 01:22:39 +00:00
|
|
|
wpEditToken: mw.user.tokens.get( 'csrfToken' ),
|
2018-08-02 03:32:29 +00:00
|
|
|
// MediaWiki function-verification parameters, mostly relevant to the
|
|
|
|
// classic editpage, but still required here:
|
2017-10-02 15:09:04 +00:00
|
|
|
wpUnicodeCheck: 'ℳ𝒲♥𝓊𝓃𝒾𝒸ℴ𝒹ℯ',
|
2018-08-02 03:32:29 +00:00
|
|
|
wpUltimateParam: true
|
2015-08-19 18:05:01 +00:00
|
|
|
}, fields );
|
2012-11-28 23:57:00 +00:00
|
|
|
// Add params as hidden fields
|
2024-05-21 14:22:56 +00:00
|
|
|
for ( const key in params ) {
|
2015-08-19 17:33:02 +00:00
|
|
|
$form.append( $( '<input>' ).attr( { type: 'hidden', name: key, value: params[ key ] } ) );
|
2012-11-28 23:57:00 +00:00
|
|
|
}
|
|
|
|
// Submit the form, mimicking a traditional edit
|
2013-11-15 20:30:57 +00:00
|
|
|
// Firefox requires the form to be attached
|
2024-05-21 14:22:56 +00:00
|
|
|
const submitUrl = mw.util.getUrl( this.getPageName(), {
|
2022-12-12 19:49:26 +00:00
|
|
|
action: 'submit',
|
|
|
|
veswitched: '1'
|
|
|
|
} );
|
|
|
|
$form.attr( 'action', submitUrl ).appendTo( 'body' ).trigger( 'submit' );
|
2012-11-28 23:57:00 +00:00
|
|
|
return true;
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
2013-01-15 23:38:49 +00:00
|
|
|
* Get Wikitext data from the Parsoid API.
|
2012-11-28 23:57:00 +00:00
|
|
|
*
|
|
|
|
* This method performs an asynchronous action and uses a callback function to handle the result.
|
|
|
|
*
|
2024-05-01 12:32:49 +00:00
|
|
|
* this.serialize( doc ).then( ( data ) => {
|
2019-11-04 14:54:53 +00:00
|
|
|
* // Do something with data.content (wikitext)
|
|
|
|
* } );
|
2012-11-28 23:57:00 +00:00
|
|
|
*
|
2013-02-11 19:46:58 +00:00
|
|
|
* @param {HTMLDocument} doc Document to serialize
|
2019-11-04 14:54:53 +00:00
|
|
|
* @param {Function} [callback] Optional callback to run after.
|
|
|
|
* Deprecated in favor of using the returned promise.
|
|
|
|
* @return {jQuery.Promise} Serialize promise, see mw.libs.ve.targetSaver.postHtml
|
|
|
|
*/
|
2015-12-10 16:07:50 +00:00
|
|
|
ve.init.mw.ArticleTarget.prototype.serialize = function ( doc, callback ) {
|
2012-11-28 23:57:00 +00:00
|
|
|
// Prevent duplicate requests
|
|
|
|
if ( this.serializing ) {
|
2019-11-04 14:54:53 +00:00
|
|
|
return this.serializing;
|
2012-11-28 23:57:00 +00:00
|
|
|
}
|
2024-05-21 14:22:56 +00:00
|
|
|
const promise = this.serializing = this.tryWithPreparedCacheKey( doc, {
|
2014-08-22 20:50:48 +00:00
|
|
|
paction: 'serialize',
|
2018-05-04 13:30:10 +00:00
|
|
|
page: this.getPageName(),
|
2015-10-08 22:16:56 +00:00
|
|
|
oldid: this.revid,
|
|
|
|
etag: this.etag
|
2013-11-06 08:22:11 +00:00
|
|
|
}, 'serialize' )
|
2019-11-04 14:54:53 +00:00
|
|
|
.done( this.emit.bind( this, 'serializeComplete' ) )
|
|
|
|
.fail( this.emit.bind( this, 'serializeError' ) )
|
2024-04-30 16:44:25 +00:00
|
|
|
.always( () => {
|
2024-05-01 12:32:49 +00:00
|
|
|
this.serializing = null;
|
2019-11-04 14:54:53 +00:00
|
|
|
} );
|
|
|
|
|
|
|
|
if ( callback ) {
|
|
|
|
OO.ui.warnDeprecation( 'Passing a callback to ve.init.mw.ArticleTarget#serialize is deprecated. Use the returned promise instead.' );
|
2024-04-30 16:44:25 +00:00
|
|
|
promise.then( ( data ) => {
|
2024-05-01 12:32:49 +00:00
|
|
|
callback.call( this, data.content );
|
2019-11-04 14:54:53 +00:00
|
|
|
} );
|
|
|
|
}
|
|
|
|
|
|
|
|
return promise;
|
2012-11-28 23:57:00 +00:00
|
|
|
};
|
2013-10-17 17:35:16 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Get list of edit notices.
|
|
|
|
*
|
2015-12-11 16:41:01 +00:00
|
|
|
* @return {Array} List of edit notices
|
2013-10-17 17:35:16 +00:00
|
|
|
*/
|
2015-12-10 16:07:50 +00:00
|
|
|
ve.init.mw.ArticleTarget.prototype.getEditNotices = function () {
|
2013-10-17 17:35:16 +00:00
|
|
|
return this.editNotices;
|
|
|
|
};
|
2013-10-11 18:42:46 +00:00
|
|
|
|
|
|
|
// FIXME: split out view specific functionality, emit to subclass
|
|
|
|
|
|
|
|
/**
|
2015-12-11 14:57:49 +00:00
|
|
|
* @inheritdoc
|
2013-10-11 18:42:46 +00:00
|
|
|
*/
|
2015-12-11 14:57:49 +00:00
|
|
|
ve.init.mw.ArticleTarget.prototype.track = function ( name ) {
|
2024-05-21 14:22:56 +00:00
|
|
|
const mode = this.surface ? this.surface.getMode() : this.getDefaultMode();
|
2017-12-07 11:14:00 +00:00
|
|
|
ve.track( name, { mode: mode } );
|
2015-12-11 14:57:49 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @inheritdoc
|
|
|
|
*/
|
2019-11-11 18:27:43 +00:00
|
|
|
ve.init.mw.ArticleTarget.prototype.createSurface = function ( dmDoc, config ) {
|
2024-05-21 14:22:56 +00:00
|
|
|
const sections = dmDoc.getNodesByType( 'section' );
|
|
|
|
let attachedRoot;
|
2019-11-11 18:27:43 +00:00
|
|
|
if ( sections.length && sections.length === 1 ) {
|
|
|
|
attachedRoot = sections[ 0 ];
|
|
|
|
if ( !attachedRoot.isSurfaceable() ) {
|
|
|
|
throw new Error( 'Not a surfaceable node' );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-12-11 14:57:49 +00:00
|
|
|
// Parent method
|
2024-05-21 14:22:56 +00:00
|
|
|
const surface = ve.init.mw.ArticleTarget.super.prototype.createSurface.call(
|
2019-11-11 18:27:43 +00:00
|
|
|
this,
|
|
|
|
dmDoc,
|
|
|
|
ve.extendObject( { attachedRoot: attachedRoot }, config )
|
|
|
|
);
|
2015-02-03 04:23:57 +00:00
|
|
|
|
2015-12-11 14:57:49 +00:00
|
|
|
return surface;
|
2013-10-11 18:42:46 +00:00
|
|
|
};
|
|
|
|
|
2021-05-17 21:39:22 +00:00
|
|
|
/**
|
|
|
|
* @inheritdoc
|
|
|
|
*/
|
|
|
|
ve.init.mw.ArticleTarget.prototype.getSurfaceClasses = function () {
|
2024-05-21 14:22:56 +00:00
|
|
|
const classes = ve.init.mw.ArticleTarget.super.prototype.getSurfaceClasses.call( this );
|
2024-05-29 16:03:23 +00:00
|
|
|
return [ ...classes, 'mw-body-content' ];
|
2021-05-17 21:39:22 +00:00
|
|
|
};
|
|
|
|
|
2019-04-02 14:47:45 +00:00
|
|
|
/**
|
|
|
|
* @inheritdoc
|
|
|
|
*/
|
|
|
|
ve.init.mw.ArticleTarget.prototype.getSurfaceConfig = function ( config ) {
|
|
|
|
return ve.init.mw.ArticleTarget.super.prototype.getSurfaceConfig.call( this, ve.extendObject( {
|
2019-05-27 11:39:19 +00:00
|
|
|
// Don't null selection on blur when editing a document.
|
|
|
|
// Do use it in new section mode as there are multiple inputs
|
|
|
|
// on the surface (header+content).
|
2021-05-17 21:39:22 +00:00
|
|
|
nullSelectionOnBlur: this.section === 'new',
|
|
|
|
classes: this.getSurfaceClasses()
|
|
|
|
// The following classes are used here:
|
|
|
|
// * mw-textarea-proteced
|
|
|
|
// * mw-textarea-cproteced
|
|
|
|
// * mw-textarea-sproteced
|
|
|
|
.concat( this.protectedClasses )
|
|
|
|
// addClass doesn't like empty strings
|
2024-04-30 16:44:25 +00:00
|
|
|
.filter( ( c ) => c )
|
2019-04-02 14:47:45 +00:00
|
|
|
}, config ) );
|
|
|
|
};
|
|
|
|
|
2017-10-05 16:31:15 +00:00
|
|
|
/**
|
|
|
|
* @inheritdoc
|
|
|
|
*/
|
|
|
|
ve.init.mw.ArticleTarget.prototype.teardown = function () {
|
2018-11-30 16:53:53 +00:00
|
|
|
if ( !this.teardownPromise ) {
|
2024-05-21 14:22:56 +00:00
|
|
|
const surface = this.getSurface();
|
2018-11-30 16:53:53 +00:00
|
|
|
|
|
|
|
// Restore access keys
|
|
|
|
if ( this.$saveAccessKeyElements ) {
|
|
|
|
this.$saveAccessKeyElements.attr( 'accesskey', ve.msg( 'accesskey-save' ) );
|
|
|
|
this.$saveAccessKeyElements = null;
|
|
|
|
}
|
|
|
|
if ( surface ) {
|
|
|
|
// Disconnect history listener
|
|
|
|
surface.getModel().disconnect( this );
|
|
|
|
}
|
2022-05-13 17:51:34 +00:00
|
|
|
|
2024-05-21 14:22:56 +00:00
|
|
|
let saveDialogPromise = ve.createDeferred().resolve().promise();
|
2022-05-13 17:51:34 +00:00
|
|
|
if ( this.saveDialog ) {
|
|
|
|
if ( this.saveDialog.isOpened() ) {
|
|
|
|
// If the save dialog is still open (from saving) close it
|
|
|
|
saveDialogPromise = this.saveDialog.close().closed;
|
|
|
|
}
|
|
|
|
// Release the reference
|
|
|
|
this.saveDialog = null;
|
|
|
|
}
|
|
|
|
|
2018-11-30 16:53:53 +00:00
|
|
|
// Parent method
|
2024-05-01 12:32:49 +00:00
|
|
|
this.teardownPromise = ve.init.mw.ArticleTarget.super.prototype.teardown.call( this ).then( () => saveDialogPromise.then( () => {
|
|
|
|
mw.hook( 've.deactivationComplete' ).fire( this.edited );
|
2024-04-30 16:44:25 +00:00
|
|
|
} ) );
|
2018-03-14 16:45:31 +00:00
|
|
|
}
|
2018-11-30 16:53:53 +00:00
|
|
|
return this.teardownPromise;
|
2017-10-05 16:31:15 +00:00
|
|
|
};
|
|
|
|
|
2018-03-26 14:23:56 +00:00
|
|
|
/**
|
|
|
|
* Try to tear down the target, but leave ready for re-activation later
|
|
|
|
*
|
|
|
|
* Will first prompt the user if required, then call #teardown.
|
|
|
|
*
|
|
|
|
* @param {boolean} [noPrompt] Do not display a prompt to the user
|
|
|
|
* @param {string} [trackMechanism] Abort mechanism; used for event tracking if present
|
2020-09-21 15:21:58 +00:00
|
|
|
* @return {jQuery.Promise} Promise which resolves when the target has been torn down, rejects if the target won't be torn down
|
2018-03-26 14:23:56 +00:00
|
|
|
*/
|
|
|
|
ve.init.mw.ArticleTarget.prototype.tryTeardown = function ( noPrompt, trackMechanism ) {
|
2022-03-23 14:53:32 +00:00
|
|
|
if ( !noPrompt && this.edited && mw.user.options.get( 'useeditwarning' ) ) {
|
2018-10-31 13:38:05 +00:00
|
|
|
return this.getSurface().dialogs.openWindow( 'abandonedit' )
|
2024-04-30 16:44:25 +00:00
|
|
|
.closed.then( ( data ) => {
|
2018-03-26 14:23:56 +00:00
|
|
|
if ( data && data.action === 'discard' ) {
|
2024-05-01 12:32:49 +00:00
|
|
|
return this.teardown( trackMechanism );
|
2018-03-26 14:23:56 +00:00
|
|
|
}
|
2019-11-02 05:06:28 +00:00
|
|
|
return ve.createDeferred().reject().promise();
|
2018-03-26 14:23:56 +00:00
|
|
|
} );
|
2022-03-23 14:53:32 +00:00
|
|
|
} else {
|
|
|
|
return this.teardown( trackMechanism );
|
2018-03-26 14:23:56 +00:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2016-05-26 10:56:08 +00:00
|
|
|
/**
|
|
|
|
* @inheritdoc
|
|
|
|
*/
|
|
|
|
ve.init.mw.ArticleTarget.prototype.setupToolbar = function () {
|
|
|
|
// Parent method
|
|
|
|
ve.init.mw.ArticleTarget.super.prototype.setupToolbar.apply( this, arguments );
|
|
|
|
|
|
|
|
this.setupToolbarSaveButton();
|
2019-04-15 22:39:56 +00:00
|
|
|
this.updateToolbarSaveButtonState();
|
2016-05-26 12:07:19 +00:00
|
|
|
|
|
|
|
if ( this.saveDialog ) {
|
2017-03-09 20:23:36 +00:00
|
|
|
this.editSummaryValue = this.saveDialog.editSummaryInput.getValue();
|
2016-05-26 12:07:19 +00:00
|
|
|
this.saveDialog.disconnect( this );
|
|
|
|
this.saveDialog = null;
|
|
|
|
}
|
2016-05-26 10:56:08 +00:00
|
|
|
};
|
|
|
|
|
2016-06-30 14:01:24 +00:00
|
|
|
/**
|
2016-06-30 14:04:51 +00:00
|
|
|
* Getting the message for the toolbar / save dialog save / publish button
|
2016-06-30 14:01:24 +00:00
|
|
|
*
|
2022-05-20 16:25:41 +00:00
|
|
|
* @param {boolean} [startProcess] Use version of the label for starting that process, i.e. with an ellipsis after it
|
|
|
|
* @param {boolean} [forceShort] Force the short version of the label, always used on mobile
|
2016-06-30 14:01:24 +00:00
|
|
|
* @return {Function|string} An i18n message or resolveable function
|
|
|
|
*/
|
2022-05-20 16:25:41 +00:00
|
|
|
ve.init.mw.ArticleTarget.prototype.getSaveButtonLabel = function ( startProcess, forceShort ) {
|
2024-05-21 14:22:56 +00:00
|
|
|
const suffix = startProcess ? '-start' : '';
|
2022-05-20 16:25:41 +00:00
|
|
|
|
|
|
|
if ( forceShort || OO.ui.isMobile() ) {
|
|
|
|
// The following messages can be used here:
|
|
|
|
// * visualeditor-savedialog-label-publish-short
|
|
|
|
// * visualeditor-savedialog-label-publish-short-start
|
|
|
|
// * visualeditor-savedialog-label-save-short
|
|
|
|
// * visualeditor-savedialog-label-save-short-start
|
|
|
|
if ( mw.config.get( 'wgEditSubmitButtonLabelPublish' ) ) {
|
|
|
|
return OO.ui.deferMsg( 'visualeditor-savedialog-label-publish-short' + suffix );
|
|
|
|
}
|
|
|
|
|
|
|
|
return OO.ui.deferMsg( 'visualeditor-savedialog-label-save-short' + suffix );
|
|
|
|
}
|
|
|
|
|
2018-03-19 14:49:23 +00:00
|
|
|
// The following messages can be used here
|
|
|
|
// * publishpage
|
2019-06-10 14:08:59 +00:00
|
|
|
// * publishpage-start
|
2018-03-19 14:49:23 +00:00
|
|
|
// * publishchanges
|
2019-06-10 14:08:59 +00:00
|
|
|
// * publishchanges-start
|
2018-03-19 14:49:23 +00:00
|
|
|
// * savearticle
|
|
|
|
// * savearticle-start
|
|
|
|
// * savechanges
|
|
|
|
// * savechanges-start
|
2016-08-29 17:46:18 +00:00
|
|
|
if ( mw.config.get( 'wgEditSubmitButtonLabelPublish' ) ) {
|
2018-03-19 14:49:23 +00:00
|
|
|
return OO.ui.deferMsg( ( !this.pageExists ? 'publishpage' : 'publishchanges' ) + suffix );
|
2016-06-30 14:04:51 +00:00
|
|
|
}
|
|
|
|
|
2018-03-19 14:49:23 +00:00
|
|
|
return OO.ui.deferMsg( ( !this.pageExists ? 'savearticle' : 'savechanges' ) + suffix );
|
2016-06-30 14:01:24 +00:00
|
|
|
};
|
|
|
|
|
2015-07-01 11:11:36 +00:00
|
|
|
/**
|
2019-04-15 22:39:56 +00:00
|
|
|
* Setup the toolbarSaveButton property to point to the save tool
|
2015-07-01 11:11:36 +00:00
|
|
|
*
|
2019-04-15 22:39:56 +00:00
|
|
|
* @method
|
|
|
|
* @abstract
|
2015-07-01 11:11:36 +00:00
|
|
|
*/
|
2019-04-15 22:39:56 +00:00
|
|
|
ve.init.mw.ArticleTarget.prototype.setupToolbarSaveButton = null;
|
2015-07-01 11:11:36 +00:00
|
|
|
|
|
|
|
/**
|
2018-03-10 15:26:11 +00:00
|
|
|
* Re-evaluate whether the article can be saved
|
|
|
|
*
|
|
|
|
* @return {boolean} The article can be saved
|
2015-07-01 11:11:36 +00:00
|
|
|
*/
|
2018-03-10 15:26:11 +00:00
|
|
|
ve.init.mw.ArticleTarget.prototype.isSaveable = function () {
|
2024-05-21 14:22:56 +00:00
|
|
|
const surface = this.getSurface();
|
2018-03-10 15:26:11 +00:00
|
|
|
if ( !surface ) {
|
2017-09-18 15:05:32 +00:00
|
|
|
// Called before we're attached, so meaningless; abandon for now
|
2018-03-10 15:26:11 +00:00
|
|
|
return false;
|
2017-09-18 15:05:32 +00:00
|
|
|
}
|
|
|
|
|
2018-03-10 15:26:11 +00:00
|
|
|
this.edited =
|
|
|
|
// Document was edited before loading
|
2021-05-03 13:34:25 +00:00
|
|
|
this.fromEditedState ||
|
2018-03-10 15:26:11 +00:00
|
|
|
// Document was edited
|
|
|
|
surface.getModel().hasBeenModified() ||
|
|
|
|
// Section title (if it exists) was edited
|
2018-11-29 14:38:53 +00:00
|
|
|
( !!this.sectionTitle && this.sectionTitle.getValue() !== '' );
|
2018-03-10 15:26:11 +00:00
|
|
|
|
|
|
|
return this.edited || this.restoring;
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Update the toolbar save button to reflect if the article can be saved
|
|
|
|
*/
|
|
|
|
ve.init.mw.ArticleTarget.prototype.updateToolbarSaveButtonState = function () {
|
2019-04-15 22:39:56 +00:00
|
|
|
// This should really be an emit('updateState') but that would cause
|
|
|
|
// every tool to be updated on every transaction.
|
|
|
|
this.toolbarSaveButton.onUpdateState();
|
2015-07-01 11:11:36 +00:00
|
|
|
};
|
|
|
|
|
2015-07-31 12:15:31 +00:00
|
|
|
/**
|
|
|
|
* Show a save dialog
|
2015-08-04 13:37:13 +00:00
|
|
|
*
|
2016-11-30 17:21:38 +00:00
|
|
|
* @param {string} [action] Window action to trigger after opening
|
2016-12-22 00:07:30 +00:00
|
|
|
* @param {string} [checkboxName] Checkbox to toggle after opening
|
2016-11-30 17:21:38 +00:00
|
|
|
*
|
2024-04-29 18:14:26 +00:00
|
|
|
* @fires ve.init.mw.ArticleTarget#saveWorkflowBegin
|
2015-07-31 12:15:31 +00:00
|
|
|
*/
|
2016-12-22 00:07:30 +00:00
|
|
|
ve.init.mw.ArticleTarget.prototype.showSaveDialog = function ( action, checkboxName ) {
|
2024-05-21 14:22:56 +00:00
|
|
|
let firstLoad = false;
|
2016-06-30 14:01:24 +00:00
|
|
|
|
2019-08-27 16:11:16 +00:00
|
|
|
if ( !this.isSaveable() || this.saveDialogIsOpening ) {
|
2016-11-30 17:21:38 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2024-05-21 14:22:56 +00:00
|
|
|
const currentWindow = this.getSurface().getDialogs().getCurrentWindow();
|
2018-01-08 17:52:06 +00:00
|
|
|
if ( currentWindow && currentWindow.constructor.static.name === 'mwSave' && ( action === 'save' || action === null ) ) {
|
|
|
|
// The current window is the save dialog, and we've gotten here via
|
|
|
|
// the save action. Trigger a save. We're doing this here instead of
|
|
|
|
// relying on an accesskey on the save button, because that has some
|
|
|
|
// cross-browser issues that makes it not work in Firefox.
|
|
|
|
currentWindow.executeAction( 'save' );
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2019-08-27 16:11:16 +00:00
|
|
|
this.saveDialogIsOpening = true;
|
|
|
|
|
2024-05-21 14:22:56 +00:00
|
|
|
const saveProcess = new OO.ui.Process();
|
2024-05-01 12:32:49 +00:00
|
|
|
mw.hook( 've.preSaveProcess' ).fire( saveProcess, this );
|
2023-05-09 04:32:41 +00:00
|
|
|
|
2024-05-01 12:32:49 +00:00
|
|
|
this.emit( 'saveWorkflowBegin' );
|
2023-05-09 04:32:41 +00:00
|
|
|
|
2024-04-30 16:44:25 +00:00
|
|
|
saveProcess.execute().done( () => {
|
2023-05-09 04:32:41 +00:00
|
|
|
// Preload the serialization
|
2024-05-01 12:32:49 +00:00
|
|
|
this.prepareCacheKey( this.getDocToSave() );
|
2023-05-09 04:32:41 +00:00
|
|
|
|
|
|
|
// Get the save dialog
|
2024-05-01 12:32:49 +00:00
|
|
|
this.getSurface().getDialogs().getWindow( 'mwSave' ).done( ( win ) => {
|
2024-05-21 14:22:56 +00:00
|
|
|
const windowAction = ve.ui.actionFactory.create( 'window', this.getSurface() );
|
2023-05-09 04:32:41 +00:00
|
|
|
|
2024-05-01 12:32:49 +00:00
|
|
|
if ( !this.saveDialog ) {
|
|
|
|
this.saveDialog = win;
|
2023-05-09 04:32:41 +00:00
|
|
|
firstLoad = true;
|
|
|
|
|
|
|
|
// Connect to save dialog
|
2024-05-01 12:32:49 +00:00
|
|
|
this.saveDialog.connect( this, {
|
2023-05-09 04:32:41 +00:00
|
|
|
save: 'onSaveDialogSave',
|
|
|
|
review: 'onSaveDialogReview',
|
|
|
|
preview: 'onSaveDialogPreview',
|
|
|
|
resolve: 'onSaveDialogResolveConflict',
|
|
|
|
retry: 'onSaveDialogRetry',
|
|
|
|
// The array syntax is a way to call `this.emit( 'saveWorkflowEnd' )`.
|
|
|
|
close: [ 'emit', 'saveWorkflowEnd' ]
|
|
|
|
} );
|
2021-01-14 15:59:25 +00:00
|
|
|
|
2023-05-09 04:32:41 +00:00
|
|
|
// Attach custom overlay
|
2024-05-01 12:32:49 +00:00
|
|
|
this.saveDialog.$element.append( this.$saveDialogOverlay );
|
2023-05-09 04:32:41 +00:00
|
|
|
}
|
2015-08-05 21:43:23 +00:00
|
|
|
|
2024-05-21 14:22:56 +00:00
|
|
|
const data = this.getSaveDialogOpeningData();
|
2016-12-13 21:55:01 +00:00
|
|
|
|
2023-05-09 04:32:41 +00:00
|
|
|
if (
|
|
|
|
( action === 'review' && !data.canReview ) ||
|
|
|
|
( action === 'preview' && !data.canPreview )
|
|
|
|
) {
|
2024-05-01 12:32:49 +00:00
|
|
|
this.saveDialogIsOpening = false;
|
2023-05-09 04:32:41 +00:00
|
|
|
return;
|
|
|
|
}
|
2016-12-13 21:55:01 +00:00
|
|
|
|
2023-05-09 04:32:41 +00:00
|
|
|
if ( firstLoad ) {
|
2024-05-21 14:22:56 +00:00
|
|
|
for ( const name in this.checkboxesByName ) {
|
2024-05-01 12:32:49 +00:00
|
|
|
if ( this.initialCheckboxes[ name ] !== undefined ) {
|
|
|
|
this.checkboxesByName[ name ].setSelected( this.initialCheckboxes[ name ] );
|
2023-05-09 04:32:41 +00:00
|
|
|
}
|
2020-05-26 22:08:49 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-05-21 14:22:56 +00:00
|
|
|
let checkbox;
|
2024-05-01 12:32:49 +00:00
|
|
|
if ( checkboxName && ( checkbox = this.checkboxesByName[ checkboxName ] ) ) {
|
2024-05-21 14:22:56 +00:00
|
|
|
const isSelected = !checkbox.isSelected();
|
2023-05-09 04:32:41 +00:00
|
|
|
// Wait for native access key change to happen
|
2024-04-30 16:44:25 +00:00
|
|
|
setTimeout( () => {
|
2023-05-09 04:32:41 +00:00
|
|
|
checkbox.setSelected( isSelected );
|
|
|
|
} );
|
|
|
|
}
|
2016-12-22 00:07:30 +00:00
|
|
|
|
2023-05-09 04:32:41 +00:00
|
|
|
// When calling review/preview action, switch to those panels immediately
|
|
|
|
if ( action === 'review' || action === 'preview' ) {
|
|
|
|
data.initialPanel = action;
|
|
|
|
}
|
2016-12-13 21:55:01 +00:00
|
|
|
|
2023-05-09 04:32:41 +00:00
|
|
|
// Open the dialog
|
2024-05-21 14:22:56 +00:00
|
|
|
const openPromise = windowAction.open( 'mwSave', data, action );
|
2023-05-09 04:32:41 +00:00
|
|
|
if ( openPromise ) {
|
2024-04-30 16:44:25 +00:00
|
|
|
openPromise.always( () => {
|
2024-05-01 12:32:49 +00:00
|
|
|
this.saveDialogIsOpening = false;
|
2023-05-09 04:32:41 +00:00
|
|
|
} );
|
|
|
|
}
|
|
|
|
} );
|
2024-04-30 16:44:25 +00:00
|
|
|
} ).fail( () => {
|
2024-05-01 12:32:49 +00:00
|
|
|
this.saveDialogIsOpening = false;
|
2016-12-13 21:55:01 +00:00
|
|
|
} );
|
2015-08-05 21:43:23 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
2016-06-30 14:01:24 +00:00
|
|
|
* Get opening data to pass to the save dialog
|
2019-04-16 15:17:29 +00:00
|
|
|
*
|
|
|
|
* @return {Object} Opening data
|
2015-08-05 21:43:23 +00:00
|
|
|
*/
|
2016-12-13 21:55:01 +00:00
|
|
|
ve.init.mw.ArticleTarget.prototype.getSaveDialogOpeningData = function () {
|
2024-05-21 14:22:56 +00:00
|
|
|
const mode = this.getSurface().getMode();
|
2016-06-30 14:01:24 +00:00
|
|
|
return {
|
2016-12-13 21:55:01 +00:00
|
|
|
canPreview: mode === 'source',
|
|
|
|
canReview: !( mode === 'source' && this.section === 'new' ),
|
|
|
|
sectionTitle: this.sectionTitle && this.sectionTitle.getValue(),
|
2016-11-05 06:16:26 +00:00
|
|
|
saveButtonLabel: this.getSaveButtonLabel(),
|
2022-01-24 13:06:19 +00:00
|
|
|
copyrightWarning: this.copyrightWarning,
|
2016-11-05 06:16:26 +00:00
|
|
|
checkboxFields: this.checkboxFields,
|
2016-12-13 21:55:01 +00:00
|
|
|
checkboxesByName: this.checkboxesByName
|
2016-06-30 14:01:24 +00:00
|
|
|
};
|
2015-07-31 12:15:31 +00:00
|
|
|
};
|
|
|
|
|
2014-02-06 23:13:32 +00:00
|
|
|
/**
|
|
|
|
* Move the cursor in the editor to section specified by this.section.
|
|
|
|
* Do nothing if this.section is undefined.
|
|
|
|
*/
|
2015-12-10 16:07:50 +00:00
|
|
|
ve.init.mw.ArticleTarget.prototype.restoreEditSection = function () {
|
2024-05-21 14:22:56 +00:00
|
|
|
const section = this.section !== null ? this.section : this.visibleSection;
|
|
|
|
const surface = this.getSurface();
|
|
|
|
const mode = surface.getMode();
|
2021-12-21 18:48:27 +00:00
|
|
|
|
2022-02-16 15:17:19 +00:00
|
|
|
if (
|
|
|
|
mode === 'source' ||
|
|
|
|
( this.enableVisualSectionEditing && this.section !== null )
|
|
|
|
) {
|
2022-01-24 23:36:32 +00:00
|
|
|
this.$scrollContainer.scrollTop( 0 );
|
2021-12-21 18:48:27 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if ( section === null || section === 'new' || section === '0' || section === 'T-0' ) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2024-05-21 14:22:56 +00:00
|
|
|
const setExactScrollOffset = this.section === null && this.visibleSection !== null && this.visibleSectionOffset !== null,
|
2021-12-21 18:48:27 +00:00
|
|
|
// User clicked section edit link with visual section editing not available:
|
|
|
|
// Take them to the top of the section using goToHeading
|
|
|
|
goToStartOfHeading = this.section !== null && !this.enableVisualSectionEditing,
|
|
|
|
setEditSummary = this.section !== null;
|
|
|
|
|
2024-05-21 14:22:56 +00:00
|
|
|
let headingText;
|
2021-12-21 18:48:27 +00:00
|
|
|
if ( mode === 'visual' ) {
|
2024-05-21 14:22:56 +00:00
|
|
|
const dmDoc = surface.getModel().getDocument();
|
2021-12-21 18:48:27 +00:00
|
|
|
// In mw.libs.ve.unwrapParsoidSections we copy the data-mw-section-id from the section element
|
|
|
|
// to the heading. Iterate over headings to find the one with the correct attribute
|
|
|
|
// in originalDomElements.
|
2024-05-21 14:22:56 +00:00
|
|
|
let headingModel;
|
2024-04-30 16:44:25 +00:00
|
|
|
dmDoc.getNodesByType( 'mwHeading' ).some( ( heading ) => {
|
2024-05-21 14:22:56 +00:00
|
|
|
const domElements = heading.getOriginalDomElements( dmDoc.getStore() );
|
2021-12-21 18:48:27 +00:00
|
|
|
if (
|
|
|
|
domElements && domElements[ 0 ].nodeType === Node.ELEMENT_NODE &&
|
|
|
|
domElements[ 0 ].getAttribute( 'data-mw-section-id' ) === section
|
|
|
|
) {
|
|
|
|
headingModel = heading;
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
} );
|
|
|
|
if ( headingModel ) {
|
2024-05-21 14:22:56 +00:00
|
|
|
const headingView = surface.getView().getDocument().getDocumentNode().getNodeFromOffset( headingModel.getRange().start );
|
2022-12-17 15:44:56 +00:00
|
|
|
if ( setEditSummary && !new URL( location.href ).searchParams.has( 'summary' ) ) {
|
2021-12-21 18:48:27 +00:00
|
|
|
headingText = headingView.$element.text();
|
|
|
|
}
|
|
|
|
if ( setExactScrollOffset ) {
|
|
|
|
this.scrollToHeading( headingView, this.visibleSectionOffset );
|
|
|
|
} else if ( goToStartOfHeading ) {
|
|
|
|
this.goToHeading( headingView );
|
2016-12-14 21:22:10 +00:00
|
|
|
}
|
2014-02-06 23:13:32 +00:00
|
|
|
}
|
2021-12-21 18:48:27 +00:00
|
|
|
} else if ( mode === 'source' && setEditSummary ) {
|
|
|
|
// With elements of extractSectionTitle + stripSectionName TODO:
|
|
|
|
// Arguably, we should just throw this through the API and then do
|
|
|
|
// the same extract-text pass we do in visual mode. Would save us
|
|
|
|
// having to think about wikitext here.
|
|
|
|
headingText = surface.getModel().getDocument().data.getText(
|
|
|
|
false,
|
|
|
|
surface.getModel().getDocument().getDocumentNode().children[ 0 ].getRange()
|
|
|
|
)
|
|
|
|
// Extract the title
|
|
|
|
.replace( /^\s*=+\s*(.*?)\s*=+\s*$/, '$1' )
|
|
|
|
// Remove links
|
|
|
|
.replace( /\[\[:?([^[|]+)\|([^[]+)\]\]/g, '$2' )
|
|
|
|
.replace( /\[\[:?([^[]+)\|?\]\]/g, '$1' )
|
|
|
|
.replace( new RegExp( '\\[(?:' + ve.init.platform.getUnanchoredExternalLinkUrlProtocolsRegExp().source + ')([^ ]+?) ([^\\[]+)\\]', 'ig' ), '$3' )
|
|
|
|
// Cheap HTML removal
|
|
|
|
.replace( /<[^>]+?>/g, '' );
|
|
|
|
}
|
|
|
|
if ( headingText ) {
|
|
|
|
this.initialEditSummary =
|
|
|
|
'/* ' +
|
|
|
|
ve.graphemeSafeSubstring( headingText, 0, 244 ) +
|
|
|
|
' */ ';
|
2014-02-06 23:13:32 +00:00
|
|
|
}
|
|
|
|
};
|
2014-07-23 22:30:38 +00:00
|
|
|
|
2014-08-22 00:37:12 +00:00
|
|
|
/**
|
|
|
|
* Move the cursor to a given heading and scroll to it.
|
|
|
|
*
|
|
|
|
* @param {ve.ce.HeadingNode} headingNode Heading node to scroll to
|
|
|
|
*/
|
2015-12-10 16:07:50 +00:00
|
|
|
ve.init.mw.ArticleTarget.prototype.goToHeading = function ( headingNode ) {
|
2024-05-21 16:40:36 +00:00
|
|
|
const surface = this.getSurface(),
|
|
|
|
surfaceView = surface.getView();
|
|
|
|
|
2024-05-21 14:22:56 +00:00
|
|
|
let offsetNode = headingNode,
|
2014-08-22 00:37:12 +00:00
|
|
|
lastHeadingLevel = -1;
|
2024-05-21 14:22:56 +00:00
|
|
|
let nextNode;
|
2014-08-22 00:37:12 +00:00
|
|
|
// Find next sibling which isn't a heading
|
|
|
|
while ( offsetNode instanceof ve.ce.HeadingNode && offsetNode.getModel().getAttribute( 'level' ) > lastHeadingLevel ) {
|
|
|
|
lastHeadingLevel = offsetNode.getModel().getAttribute( 'level' );
|
|
|
|
// Next sibling
|
2015-08-19 17:33:02 +00:00
|
|
|
nextNode = offsetNode.parent.children[ offsetNode.parent.children.indexOf( offsetNode ) + 1 ];
|
2014-08-22 00:37:12 +00:00
|
|
|
if ( !nextNode ) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
offsetNode = nextNode;
|
|
|
|
}
|
2024-05-21 14:22:56 +00:00
|
|
|
const startOffset = offsetNode.getModel().getOffset();
|
2016-07-06 21:11:30 +00:00
|
|
|
|
2022-01-23 01:11:51 +00:00
|
|
|
function setSelection() {
|
2022-01-23 00:39:51 +00:00
|
|
|
surfaceView.selectRelativeSelectableContentOffset( startOffset, 1 );
|
2016-07-06 21:11:30 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if ( surfaceView.isFocused() ) {
|
2022-01-23 01:11:51 +00:00
|
|
|
setSelection();
|
|
|
|
// Focussing the document triggers showSelection which calls scrollIntoView
|
|
|
|
// which uses a jQuery animation, so make sure this is aborted.
|
|
|
|
$( OO.ui.Element.static.getClosestScrollableContainer( surfaceView.$element[ 0 ] ) ).stop( true );
|
2016-07-06 21:11:30 +00:00
|
|
|
} else {
|
|
|
|
// onDocumentFocus is debounced, so wait for that to happen before setting
|
|
|
|
// the model selection, otherwise it will get reset
|
2022-01-23 01:11:51 +00:00
|
|
|
surfaceView.once( 'focus', setSelection );
|
2016-07-06 21:11:30 +00:00
|
|
|
}
|
2022-01-23 01:11:51 +00:00
|
|
|
this.scrollToHeading( headingNode );
|
2014-08-22 00:37:12 +00:00
|
|
|
};
|
|
|
|
|
2014-07-23 22:30:38 +00:00
|
|
|
/**
|
2014-07-28 21:54:12 +00:00
|
|
|
* Scroll to a given heading in the document.
|
2014-07-23 22:30:38 +00:00
|
|
|
*
|
2014-07-28 21:54:12 +00:00
|
|
|
* @param {ve.ce.HeadingNode} headingNode Heading node to scroll to
|
2021-12-21 18:48:27 +00:00
|
|
|
* @param {number} [headingOffset=0] Set the top offset of the heading to a specific amount, relative
|
|
|
|
* to the surface viewport.
|
2014-07-23 22:30:38 +00:00
|
|
|
*/
|
2021-12-21 18:48:27 +00:00
|
|
|
ve.init.mw.ArticleTarget.prototype.scrollToHeading = function ( headingNode, headingOffset ) {
|
2022-01-23 01:11:51 +00:00
|
|
|
this.$scrollContainer.scrollTop(
|
2022-01-23 01:09:37 +00:00
|
|
|
headingNode.$element.offset().top - parseInt( headingNode.$element.css( 'margin-top' ) ) -
|
|
|
|
( this.getSurface().padding.top + ( headingOffset || 0 ) ) );
|
2014-07-23 22:30:38 +00:00
|
|
|
};
|
2016-02-19 18:47:33 +00:00
|
|
|
|
2019-02-13 13:21:26 +00:00
|
|
|
/**
|
2022-12-17 15:44:56 +00:00
|
|
|
* Get the URL hash for the current section's ID using the page's HTML.
|
2019-02-13 13:21:26 +00:00
|
|
|
*
|
|
|
|
* TODO: Do this in a less skin-dependent way
|
|
|
|
*
|
2022-12-17 15:44:56 +00:00
|
|
|
* @return {string} URL hash with leading '#', or empty string if not found
|
2019-02-13 13:21:26 +00:00
|
|
|
*/
|
2022-12-17 15:44:56 +00:00
|
|
|
ve.init.mw.ArticleTarget.prototype.getSectionHashFromPage = function () {
|
2019-02-13 13:21:26 +00:00
|
|
|
// Assume there are section edit links, as the user just did a section edit. This also means
|
|
|
|
// that the section numbers line up correctly, as not every H_ tag is a numbered section.
|
2024-05-21 14:22:56 +00:00
|
|
|
const $sections = this.$editableContent.find( '.mw-editsection' );
|
2021-10-13 12:57:45 +00:00
|
|
|
|
2024-05-21 14:22:56 +00:00
|
|
|
let section;
|
2019-02-13 13:21:26 +00:00
|
|
|
if ( this.section === 'new' ) {
|
|
|
|
// A new section is appended to the end, so take the last one.
|
|
|
|
section = $sections.length;
|
|
|
|
} else {
|
|
|
|
section = this.section;
|
|
|
|
}
|
|
|
|
if ( section > 0 ) {
|
2022-11-21 21:46:13 +00:00
|
|
|
// Compatibility with pre-T13555 markup
|
2024-05-21 14:22:56 +00:00
|
|
|
const $section = $sections.eq( section - 1 )
|
2022-11-21 21:46:13 +00:00
|
|
|
.closest( '.mw-heading, h1, h2, h3, h4, h5, h6' )
|
|
|
|
.find( 'h1, h2, h3, h4, h5, h6, .mw-headline' );
|
2019-02-13 13:21:26 +00:00
|
|
|
|
|
|
|
if ( $section.length && $section.attr( 'id' ) ) {
|
2022-12-17 15:44:56 +00:00
|
|
|
return '#' + $section.attr( 'id' );
|
2019-02-13 13:21:26 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return '';
|
|
|
|
};
|
|
|
|
|
2017-01-19 22:09:20 +00:00
|
|
|
/**
|
|
|
|
* Switches to the wikitext editor, either keeping (default) or discarding changes.
|
|
|
|
*
|
2022-02-14 15:18:57 +00:00
|
|
|
* @param {boolean} [modified=false] Whether there were any changes at all.
|
2017-01-19 22:09:20 +00:00
|
|
|
*/
|
2019-04-15 22:39:04 +00:00
|
|
|
ve.init.mw.ArticleTarget.prototype.switchToWikitextEditor = function ( modified ) {
|
|
|
|
// When switching with changes we always pass the full page as changes in visual section mode
|
2019-04-03 15:43:09 +00:00
|
|
|
// can still affect the whole document (e.g. removing a reference)
|
2019-04-15 22:39:04 +00:00
|
|
|
if ( modified ) {
|
|
|
|
this.section = null;
|
|
|
|
}
|
2017-01-19 22:09:20 +00:00
|
|
|
|
2019-07-22 19:41:36 +00:00
|
|
|
if ( this.isModeAvailable( 'source' ) ) {
|
2019-04-15 22:39:04 +00:00
|
|
|
if ( !modified ) {
|
2023-06-20 08:02:23 +00:00
|
|
|
this.reloadSurface( 'source' );
|
2017-01-19 22:09:20 +00:00
|
|
|
} else {
|
2024-05-21 14:22:56 +00:00
|
|
|
const dataPromise = this.getWikitextDataPromiseForDoc( modified );
|
2023-06-20 08:02:23 +00:00
|
|
|
this.reloadSurface( 'source', dataPromise );
|
2017-01-19 22:09:20 +00:00
|
|
|
}
|
|
|
|
} else {
|
2019-04-15 22:39:04 +00:00
|
|
|
this.switchToFallbackWikitextEditor( modified );
|
2017-01-19 22:09:20 +00:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2019-04-03 15:43:09 +00:00
|
|
|
/**
|
|
|
|
* Get a data promise for wikitext editing based on the current doc state
|
|
|
|
*
|
|
|
|
* @param {boolean} modified Whether there were any changes
|
|
|
|
* @return {jQuery.Promise} Data promise
|
|
|
|
*/
|
|
|
|
ve.init.mw.ArticleTarget.prototype.getWikitextDataPromiseForDoc = function ( modified ) {
|
2024-04-30 16:44:25 +00:00
|
|
|
return this.serialize( this.getDocToSave() ).then( ( data ) => {
|
2019-04-03 15:43:09 +00:00
|
|
|
// HACK - add parameters the API doesn't provide for a VE->WT switch
|
2024-05-01 12:32:49 +00:00
|
|
|
data.etag = this.etag;
|
2019-04-03 15:43:09 +00:00
|
|
|
data.fromEditedState = modified;
|
2024-05-01 12:32:49 +00:00
|
|
|
data.notices = this.remoteNotices;
|
|
|
|
data.protectedClasses = this.protectedClasses;
|
|
|
|
data.basetimestamp = this.baseTimeStamp;
|
|
|
|
data.starttimestamp = this.startTimeStamp;
|
|
|
|
data.oldid = this.revid;
|
|
|
|
data.canEdit = this.canEdit;
|
|
|
|
data.wouldautocreate = this.wouldautocreate;
|
|
|
|
data.checkboxesDef = this.checkboxesDef;
|
2019-11-04 14:54:53 +00:00
|
|
|
// Wrap up like a response object as that is what dataPromise is expected to be
|
|
|
|
return { visualeditoredit: data };
|
2019-04-03 15:43:09 +00:00
|
|
|
} );
|
|
|
|
};
|
|
|
|
|
2017-01-19 22:09:20 +00:00
|
|
|
/**
|
|
|
|
* Switches to the fallback wikitext editor, either keeping (default) or discarding changes.
|
|
|
|
*
|
2022-02-14 15:18:57 +00:00
|
|
|
* @param {boolean} [modified=false] Whether there were any changes at all.
|
2022-04-26 15:31:12 +00:00
|
|
|
* @return {jQuery.Promise} Promise which rejects if the switch fails
|
2017-01-19 22:09:20 +00:00
|
|
|
*/
|
2022-04-26 15:31:12 +00:00
|
|
|
ve.init.mw.ArticleTarget.prototype.switchToFallbackWikitextEditor = function () {
|
|
|
|
return ve.createDeferred().resolve().promise();
|
|
|
|
};
|
2017-01-19 22:09:20 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Switch to the visual editor.
|
|
|
|
*/
|
|
|
|
ve.init.mw.ArticleTarget.prototype.switchToVisualEditor = function () {
|
2018-08-01 02:07:44 +00:00
|
|
|
if ( !this.edited ) {
|
|
|
|
this.reloadSurface( 'visual' );
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2024-05-21 14:22:56 +00:00
|
|
|
const url = new URL( location.href );
|
|
|
|
const dataPromise = mw.libs.ve.targetLoader.requestParsoidData( this.getPageName(), {
|
2023-06-24 10:21:30 +00:00
|
|
|
oldId: this.revid,
|
|
|
|
targetName: this.constructor.static.trackingName,
|
|
|
|
modified: this.edited,
|
|
|
|
wikitext: this.getDocToSave(),
|
|
|
|
section: this.section,
|
|
|
|
editintro: url.searchParams.get( 'editintro' ),
|
|
|
|
preload: url.searchParams.get( 'preload' ),
|
|
|
|
preloadparams: mw.util.getArrayParam( 'preloadparams', url.searchParams )
|
|
|
|
} );
|
2017-01-19 22:09:20 +00:00
|
|
|
|
2023-06-24 10:21:30 +00:00
|
|
|
this.reloadSurface( 'visual', dataPromise );
|
2017-01-19 22:09:20 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Switch to a different wikitext section
|
|
|
|
*
|
2020-03-31 20:04:30 +00:00
|
|
|
* @param {string|null} section Section to switch to: a number, 'T-'-prefixed number, 'new'
|
|
|
|
* or null (whole document)
|
2022-03-23 14:53:32 +00:00
|
|
|
* @param {boolean} [noPrompt=false] Switch without prompting (changes will be lost either way)
|
2017-01-19 22:09:20 +00:00
|
|
|
*/
|
2022-03-23 14:53:32 +00:00
|
|
|
ve.init.mw.ArticleTarget.prototype.switchToWikitextSection = function ( section, noPrompt ) {
|
2017-01-19 22:09:20 +00:00
|
|
|
if ( section === this.section ) {
|
|
|
|
return;
|
|
|
|
}
|
2024-05-21 14:22:56 +00:00
|
|
|
let promise;
|
2022-03-23 14:53:32 +00:00
|
|
|
if ( !noPrompt && this.edited && mw.user.options.get( 'useeditwarning' ) ) {
|
2019-05-06 19:57:51 +00:00
|
|
|
promise = this.getSurface().dialogs.openWindow( 'abandonedit' )
|
2024-04-30 16:44:25 +00:00
|
|
|
.closed.then( ( data ) => data && data.action === 'discard' );
|
2017-01-19 22:09:20 +00:00
|
|
|
} else {
|
2019-11-02 05:06:28 +00:00
|
|
|
promise = ve.createDeferred().resolve( true ).promise();
|
2017-01-19 22:09:20 +00:00
|
|
|
}
|
2024-04-30 16:44:25 +00:00
|
|
|
promise.then( ( confirmed ) => {
|
2017-01-19 22:09:20 +00:00
|
|
|
if ( confirmed ) {
|
2019-06-21 12:07:24 +00:00
|
|
|
// Section has changed and edits have been discarded, so edit summary is no longer valid
|
|
|
|
// TODO: Preserve summary if document changes can be preserved
|
2024-05-01 12:32:49 +00:00
|
|
|
if ( this.saveDialog ) {
|
|
|
|
this.saveDialog.reset();
|
2019-06-21 12:07:24 +00:00
|
|
|
}
|
|
|
|
// TODO: If switching to a non-null section, get the new section title
|
2024-05-01 12:32:49 +00:00
|
|
|
this.initialEditSummary = null;
|
|
|
|
this.section = section;
|
|
|
|
this.reloadSurface( 'source' );
|
|
|
|
this.updateTabs();
|
2017-01-19 22:09:20 +00:00
|
|
|
}
|
|
|
|
} );
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Reload the target surface in the new editor mode
|
|
|
|
*
|
|
|
|
* @param {string} newMode New mode
|
|
|
|
* @param {jQuery.Promise} [dataPromise] Data promise, if any
|
|
|
|
*/
|
|
|
|
ve.init.mw.ArticleTarget.prototype.reloadSurface = function ( newMode, dataPromise ) {
|
|
|
|
this.setDefaultMode( newMode );
|
2017-06-22 17:54:04 +00:00
|
|
|
this.clearDiff();
|
2024-05-21 14:22:56 +00:00
|
|
|
const promise = this.load( dataPromise );
|
2017-01-19 22:09:20 +00:00
|
|
|
this.getSurface().createProgress(
|
2022-04-26 15:31:12 +00:00
|
|
|
promise,
|
2017-01-19 22:09:20 +00:00
|
|
|
ve.msg( newMode === 'source' ? 'visualeditor-mweditmodesource-progress' : 'visualeditor-mweditmodeve-progress' ),
|
|
|
|
true /* non-cancellable */
|
|
|
|
);
|
|
|
|
};
|
2018-04-02 16:28:01 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Display the given redirect subtitle and redirect page content header on the page.
|
|
|
|
*
|
|
|
|
* @param {jQuery} $sub Redirect subtitle, see #buildRedirectSub
|
|
|
|
* @param {jQuery} $msg Redirect page content header, see #buildRedirectMsg
|
|
|
|
*/
|
|
|
|
ve.init.mw.ArticleTarget.prototype.updateRedirectInterface = function ( $sub, $msg ) {
|
|
|
|
// For the subtitle, replace the real one with ours.
|
|
|
|
// This is more complicated than it should be because we have to fiddle with the <br>.
|
2024-05-21 14:22:56 +00:00
|
|
|
const $currentSub = $( '#redirectsub' );
|
2018-04-02 16:28:01 +00:00
|
|
|
if ( $currentSub.length ) {
|
|
|
|
if ( $sub.length ) {
|
|
|
|
$currentSub.replaceWith( $sub );
|
|
|
|
} else {
|
|
|
|
$currentSub.prev().filter( 'br' ).remove();
|
|
|
|
$currentSub.remove();
|
|
|
|
}
|
|
|
|
} else {
|
2024-05-21 14:22:56 +00:00
|
|
|
const $subtitle = $( '#contentSub' );
|
2018-04-02 16:28:01 +00:00
|
|
|
if ( $sub.length ) {
|
|
|
|
if ( $subtitle.children().length ) {
|
|
|
|
$subtitle.append( $( '<br>' ) );
|
|
|
|
}
|
|
|
|
$subtitle.append( $sub );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if ( $msg.length ) {
|
|
|
|
$msg
|
|
|
|
// We need to be able to tell apart the real one and our fake one
|
|
|
|
.addClass( 've-redirect-header' )
|
2024-04-30 16:44:25 +00:00
|
|
|
.on( 'click', ( e ) => {
|
2024-05-21 14:22:56 +00:00
|
|
|
const windowAction = ve.ui.actionFactory.create( 'window', this.getSurface() );
|
2018-04-02 16:28:01 +00:00
|
|
|
windowAction.open( 'meta', { page: 'settings' } );
|
|
|
|
e.preventDefault();
|
|
|
|
} );
|
|
|
|
}
|
|
|
|
// For the content header, the real one is hidden, insert ours before it.
|
2024-05-21 14:22:56 +00:00
|
|
|
const $currentMsg = $( '.ve-redirect-header' );
|
2018-04-02 16:28:01 +00:00
|
|
|
if ( $currentMsg.length ) {
|
|
|
|
$currentMsg.replaceWith( $msg );
|
|
|
|
} else {
|
|
|
|
// Hack: This is normally inside #mw-content-text, but that's hidden while editing.
|
|
|
|
$( '#mw-content-text' ).before( $msg );
|
|
|
|
}
|
|
|
|
};
|
2018-05-09 20:12:26 +00:00
|
|
|
|
2022-02-17 22:06:14 +00:00
|
|
|
/**
|
|
|
|
* Set temporary redirect interface to match the current state of redirection in the editor.
|
|
|
|
*
|
|
|
|
* @param {string|null} title Current redirect target, or null if none
|
|
|
|
*/
|
|
|
|
ve.init.mw.ArticleTarget.prototype.setFakeRedirectInterface = function ( title ) {
|
2023-06-15 00:06:42 +00:00
|
|
|
this.isRedirect = !!title;
|
2022-02-17 22:06:14 +00:00
|
|
|
this.updateRedirectInterface(
|
|
|
|
title ? this.constructor.static.buildRedirectSub() : $(),
|
|
|
|
title ? this.constructor.static.buildRedirectMsg( title ) : $()
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Set the redirect interface to match the page's redirect state.
|
|
|
|
*/
|
|
|
|
ve.init.mw.ArticleTarget.prototype.setRealRedirectInterface = function () {
|
|
|
|
this.updateRedirectInterface(
|
|
|
|
mw.config.get( 'wgIsRedirect' ) ? this.constructor.static.buildRedirectSub() : $(),
|
|
|
|
// Remove our custom content header - the original one in #mw-content-text will be shown
|
|
|
|
$()
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
2018-05-09 20:12:26 +00:00
|
|
|
/**
|
|
|
|
* Render a list of categories
|
|
|
|
*
|
2019-02-12 18:00:59 +00:00
|
|
|
* Duplicate items are not shown.
|
|
|
|
*
|
2018-05-09 20:12:26 +00:00
|
|
|
* @param {ve.dm.MetaItem[]} categoryItems Array of category metaitems to display
|
|
|
|
* @return {jQuery.Promise} A promise which will be resolved with the rendered categories
|
|
|
|
*/
|
|
|
|
ve.init.mw.ArticleTarget.prototype.renderCategories = function ( categoryItems ) {
|
2024-05-21 14:22:56 +00:00
|
|
|
const promises = [],
|
2019-02-12 18:00:59 +00:00
|
|
|
categories = { hidden: {}, normal: {} };
|
2024-04-30 16:44:25 +00:00
|
|
|
categoryItems.forEach( ( categoryItem, index ) => {
|
2024-05-21 14:22:56 +00:00
|
|
|
const attributes = ve.copy( ve.getProp( categoryItem, 'element', 'attributes' ) );
|
2018-07-10 17:22:33 +00:00
|
|
|
attributes.index = index;
|
2024-04-30 16:44:25 +00:00
|
|
|
promises.push( ve.init.platform.linkCache.get( attributes.category ).done( ( result ) => {
|
2024-05-21 14:22:56 +00:00
|
|
|
const group = result.hidden ? categories.hidden : categories.normal;
|
2019-02-12 18:00:59 +00:00
|
|
|
// In case of duplicates, first entry wins (like in MediaWiki)
|
|
|
|
if ( !group[ attributes.category ] || group[ attributes.category ].index > attributes.index ) {
|
|
|
|
group[ attributes.category ] = attributes;
|
2018-05-09 20:12:26 +00:00
|
|
|
}
|
|
|
|
} ) );
|
|
|
|
} );
|
2024-04-30 16:44:25 +00:00
|
|
|
return ve.promiseAll( promises ).then( () => {
|
2024-05-21 14:22:56 +00:00
|
|
|
const $output = $( '<div>' ).addClass( 'catlinks' );
|
2018-05-09 20:12:26 +00:00
|
|
|
function renderPageLink( page ) {
|
2024-05-21 14:22:56 +00:00
|
|
|
const title = mw.Title.newFromText( page ),
|
2020-07-13 19:00:25 +00:00
|
|
|
$link = $( '<a>' ).attr( 'rel', 'mw:WikiLink' ).attr( 'href', title.getUrl() ).text( title.getMainText() );
|
|
|
|
// Style missing links. The data should already have been fetched
|
|
|
|
// as part of the earlier processing of categoryItems.
|
|
|
|
ve.init.platform.linkCache.styleElement( title.getPrefixedText(), $link, false );
|
|
|
|
return $link;
|
2018-05-09 20:12:26 +00:00
|
|
|
}
|
|
|
|
function renderPageLinks( pages ) {
|
2024-05-21 16:40:36 +00:00
|
|
|
const $list = $( '<ul>' );
|
|
|
|
for ( let i = 0; i < pages.length; i++ ) {
|
2024-05-21 14:22:56 +00:00
|
|
|
const $link = renderPageLink( pages[ i ] );
|
2021-10-25 15:51:29 +00:00
|
|
|
$list.append( $( '<li>' ).append( $link ) );
|
2018-05-09 20:12:26 +00:00
|
|
|
}
|
|
|
|
return $list;
|
|
|
|
}
|
2019-02-12 18:00:59 +00:00
|
|
|
function categorySort( group, a, b ) {
|
|
|
|
return group[ a ].index - group[ b ].index;
|
2018-07-10 17:22:33 +00:00
|
|
|
}
|
2024-05-21 14:22:56 +00:00
|
|
|
const categoriesNormal = Object.keys( categories.normal );
|
2019-02-12 18:00:59 +00:00
|
|
|
if ( categoriesNormal.length ) {
|
|
|
|
categoriesNormal.sort( categorySort.bind( null, categories.normal ) );
|
2024-05-21 14:22:56 +00:00
|
|
|
const $normal = $( '<div>' ).addClass( 'mw-normal-catlinks' );
|
|
|
|
const $pageLink = renderPageLink( ve.msg( 'pagecategorieslink' ) ).text( ve.msg( 'pagecategories', categoriesNormal.length ) );
|
|
|
|
const $pageLinks = renderPageLinks( categoriesNormal );
|
2018-05-09 20:12:26 +00:00
|
|
|
$normal.append(
|
2021-10-25 15:51:29 +00:00
|
|
|
$pageLink,
|
|
|
|
$( document.createTextNode( ve.msg( 'colon-separator' ) ) ),
|
|
|
|
$pageLinks
|
2018-05-09 20:12:26 +00:00
|
|
|
);
|
|
|
|
$output.append( $normal );
|
|
|
|
}
|
2024-05-21 14:22:56 +00:00
|
|
|
const categoriesHidden = Object.keys( categories.hidden );
|
2019-02-12 18:00:59 +00:00
|
|
|
if ( categoriesHidden.length ) {
|
|
|
|
categoriesHidden.sort( categorySort.bind( null, categories.hidden ) );
|
2024-05-21 14:22:56 +00:00
|
|
|
const $hidden = $( '<div>' ).addClass( 'mw-hidden-catlinks' );
|
2018-05-09 20:12:26 +00:00
|
|
|
if ( mw.user.options.get( 'showhiddencats' ) ) {
|
|
|
|
$hidden.addClass( 'mw-hidden-cats-user-shown' );
|
|
|
|
} else if ( mw.config.get( 'wgNamespaceIds' ).category === mw.config.get( 'wgNamespaceNumber' ) ) {
|
|
|
|
$hidden.addClass( 'mw-hidden-cats-ns-shown' );
|
|
|
|
} else {
|
|
|
|
$hidden.addClass( 'mw-hidden-cats-hidden' );
|
|
|
|
}
|
2024-05-21 14:22:56 +00:00
|
|
|
const $hiddenPageLinks = renderPageLinks( categoriesHidden );
|
2018-05-09 20:12:26 +00:00
|
|
|
$hidden.append(
|
2021-10-25 15:51:29 +00:00
|
|
|
$( document.createTextNode( ve.msg( 'hidden-categories', categoriesHidden.length ) ) ),
|
|
|
|
$( document.createTextNode( ve.msg( 'colon-separator' ) ) ),
|
|
|
|
$hiddenPageLinks
|
2018-05-09 20:12:26 +00:00
|
|
|
);
|
|
|
|
$output.append( $hidden );
|
|
|
|
}
|
|
|
|
return $output;
|
|
|
|
} );
|
|
|
|
};
|
2018-10-31 13:38:05 +00:00
|
|
|
|
|
|
|
// Used in tryTeardown
|
|
|
|
ve.ui.windowFactory.register( mw.widgets.AbandonEditDialog );
|