MediaWiki:Gadget-CommentWidget.js
Questa pagina definisce alcuni parametri di aspetto e comportamento generale di tutte le pagine. Per personalizzarli vedi Aiuto:Stile utente.
Nota: dopo aver salvato è necessario pulire la cache del proprio browser per vedere i cambiamenti (per le pagine globali è comunque necessario attendere qualche minuto). Per Mozilla / Firefox / Safari: fare clic su Ricarica tenendo premuto il tasto delle maiuscole, oppure premere Ctrl-F5 o Ctrl-R (Command-R su Mac); per Chrome: premere Ctrl-Shift-R (Command-Shift-R su un Mac); per Konqueror: premere il pulsante Ricarica o il tasto F5; per Opera può essere necessario svuotare completamente la cache dal menù Strumenti → Preferenze; per Internet Explorer: mantenere premuto il tasto Ctrl mentre si preme il pulsante Aggiorna o premere Ctrl-F5.
/**
* Classe CommentWidget
*
* Consente di creare un'area di testo simile a quella che si usa nelle pagine
* di discussione quando si clicca sul pulsante 'Rispondi'.
* Il codice è frutto di una rielaborazione delle classi ReplyWidget,
* ReplyWidgetVisual e CommentController dell'estensione DiscussionTools
* (consultare il repository linkato di seguito per trovare i rispettivi autori).
* Commit: a74c00ba8c256c3aa2f416f189c4256a59e37a0b
* https://phabricator.wikimedia.org/diffusion/EDTO/browse/master/;a74c00ba8c256c3aa2f416f189c4256a59e37a0b
*/
const STORAGE_EXPIRY = 60 * 60 * 24 * 30;
/**
* Costruttore della classe CommentWidget
*
* @param {Object} [config] Opzioni di configurazione
* @param {Object} [config.cancelButtonLabel] Etichetta del pulsante di annullamento
* @param {Object} [config.commentIndent] Indentazione del commento, indicare '*' o '#'
* @param {Object} [config.confirmButtonLabel] Etichetta del pulsante di conferma
* @param {Object} [config.placeholder] Testo segnaposto
* @param {Object} [config.previewLabel] Etichetta dell'anteprima
* @param {Object} [config.storageId] Identificativo per la memorizzazione del commento
*/
function CommentWidget( config ) {
config = config || {};
CommentWidget.super.call( this, {} );
this.defaultMode = mw.user.options.get( 'discussiontools-editmode' ) || 'visual';
this.pending = false;
this.isTornDown = false;
this.pageName = mw.config.get( 'wgPageName' );
this.storagePrefix = config.storageId ? 'userjs-gadget-comment-widget' + config.storageId : null;
this.storage = ve.init.platform.localStorage;
this.contentDir = $( '#mw-content-text' ).css( 'direction' );
if ( [ '*', '#' ].includes( config.commentIndent ) ) {
this.commentIndent = config.commentIndent;
}
// hack: sfruttiamo la funzione della classe ReplyWidgetVisual per creare un
// CommentTargetWidget senza duplicare il sorgente salvato in cache;
// non essendo previsto potrebbe essere instabile
this.commentBodyWidget = this.createReplyBodyWidget( {
placeholder: config.placeholder,
authors: [],
defaultMode: this.defaultMode
} );
this.confirmButtonLabel = config.confirmButtonLabel || 'Conferma';
this.confirmButton = new OO.ui.ButtonWidget( {
flags: [ 'primary', 'progressive' ],
label: this.confirmButtonLabel,
title: this.confirmButtonLabel + ' [' +
new ve.ui.Trigger( ve.ui.commandHelpRegistry.lookup( 'dialogConfirm' ).shortcuts[ 0 ] ).getMessage() +
']',
accessKey: mw.msg( 'discussiontools-replywidget-publish-accesskey' )
} );
this.cancelButtonLabel = config.cancelButtonLabel || 'Annulla';
this.cancelButton = new OO.ui.ButtonWidget( {
flags: [ 'destructive' ],
label: this.cancelButtonLabel,
framed: false,
title: this.cancelButtonLabel + ' [' +
new ve.ui.Trigger( ve.ui.commandHelpRegistry.lookup( 'dialogCancel' ).shortcuts[ 0 ] ).getMessage() +
']'
} );
this.$headerWrapper = $( '<div>' ).addClass( 'ext-discussiontools-ui-replyWidget-headerWrapper' );
if ( !OO.ui.isMobile() ) {
this.modeTabSelect = new ModeTabSelectWidget( {
classes: [ 'ext-discussiontools-ui-replyWidget-modeTabs' ],
items: [
new ModeTabOptionWidget( {
label: mw.msg( 'discussiontools-replywidget-mode-visual' ),
data: 'visual'
} ),
new ModeTabOptionWidget( {
label: mw.msg( 'discussiontools-replywidget-mode-source' ),
data: 'source'
} )
],
framed: false
} );
this.modeTabSelect.$element.attr( 'aria-label', mw.msg( 'visualeditor-mweditmode-tooltip' ) );
this.modeTabSelect.selectItemByData( this.getMode() );
this.modeTabSelect.connect( this, {
choose: 'onModeTabSelectChoose'
} );
this.$headerWrapper.append(
this.modeTabSelect.$element
);
}
this.$bodyWrapper = $( '<div>' ).addClass( 'ext-discussiontools-ui-replyWidget-bodyWrapper' ).append(
this.commentBodyWidget.$element
);
this.$preview = $( '<div>' )
.addClass( 'ext-discussiontools-ui-replyWidget-preview' )
.attr( 'data-label', config.previewLabel || mw.msg( 'discussiontools-replywidget-preview' ) )
.attr( 'dir', this.contentDir );
this.$actionsWrapper = $( '<div>' ).addClass( 'ext-discussiontools-ui-replyWidget-actionsWrapper' );
this.$actions = $( '<div>' ).addClass( 'ext-discussiontools-ui-replyWidget-actions' ).append(
this.cancelButton.$element,
this.confirmButton.$element
);
this.$footer = $( '<div>' ).addClass( 'ext-discussiontools-ui-replyWidget-footer' );
new mw.Api().loadMessagesIfMissing( [
'wikimedia-discussiontools-replywidget-terms-click',
'wikimedia-license-links'
] ).then( () => {
this.$footer.append(
$( '<p>' ).addClass( 'plainlinks' ).html(
mw.message( 'wikimedia-discussiontools-replywidget-terms-click',
this.confirmButtonLabel,
mw.message( 'wikimedia-license-links' ).parseDom()
).parse()
)
);
} );
this.$actionsWrapper.append( this.$footer, this.$actions );
this.connect( this, { confirm: 'teardown' } );
this.confirmButton.connect( this, { click: 'onConfirmClick' } );
this.cancelButton.connect( this, { click: 'tryTeardown' } );
this.beforeUnloadHandler = this.onBeforeUnload.bind( this );
this.onInputChangeThrottled = OO.ui.throttle( this.onInputChange.bind( this ), 1000 );
this.commentBodyWidget.connect( this, { change: this.onInputChangeThrottled } );
this.$element.addClass( 'ext-discussiontools-ui-replyWidget ext-discussiontools-ui-replyWidget-ve' ).append(
this.$headerWrapper,
this.$bodyWrapper,
this.$preview,
this.$actionsWrapper
);
this.setup();
}
OO.inheritClass( CommentWidget, OO.ui.Widget );
OO.mixinClass( CommentWidget, require( 'ext.discussionTools.ReplyWidget' ).ReplyWidgetVisual );
CommentWidget.prototype.detach = function () {
this.$element.detach();
this.emit( 'detach' );
};
CommentWidget.prototype.focus = function () {
const target = this.commentBodyWidget;
setTimeout( () => {
if ( target.getSurface() ) {
target.getSurface().getView().selectLastSelectableContentOffset();
target.focus();
}
} );
return this;
};
CommentWidget.prototype.getMode = function () {
return this.commentBodyWidget.target.getSurface() ?
this.commentBodyWidget.target.getSurface().getMode() :
this.defaultMode;
};
CommentWidget.prototype.clear = function ( preserveStorage ) {
if ( !preserveStorage ) {
this.clearStorage();
}
this.commentBodyWidget.clear();
if ( this.previewRequest ) {
this.previewRequest.abort();
this.previewRequest = null;
}
this.$preview.empty();
this.previewWikitext = null;
if ( preserveStorage && this.storagePrefix ) {
this.storage.set(
this.storagePrefix + '/saveable',
this.isEmpty() ? '' : '1',
STORAGE_EXPIRY
);
}
this.emit( 'clear' );
};
CommentWidget.prototype.clearStorage = function () {
if ( !this.storagePrefix ) {
return;
}
this.commentBodyWidget.target.clearDocState();
this.storage.remove( this.storagePrefix + '/saveable' );
this.emit( 'clearStorage' );
};
CommentWidget.prototype.getValue = function () {
if ( this.getMode() === 'source' ) {
const wikitext = this.commentBodyWidget.target.getSurface().getModel().getDom();
return $.Deferred().resolve( this.sanitizeWikitextLinebreaks( wikitext ) ).promise();
} else {
const target = this.commentBodyWidget.target;
return target.getWikitextFragment( target.getSurface().getModel().getDocument() )
.then( wikitext => {
if ( this.commentIndent && wikitext.startsWith( this.commentIndent ) ) {
wikitext = wikitext.replace( this.commentIndent, '' ).trimStart();
}
return this.htmlTrim( wikitext );
} );
}
};
CommentWidget.prototype.isEmpty = function () {
const surface = this.commentBodyWidget.target.getSurface();
if ( !( surface && surface.getModel().getDocument().data.hasContent() ) ) {
return true;
} else if ( !this.commentIndent || this.getMode() === 'source' ) {
return false;
} else {
const firstChild = surface.getView().getDocument().getBranchNodeFromOffset( 1 ),
nodeData = surface.getModel().getDocument().getDataFromNode( firstChild );
return firstChild.type === 'commentindent' &&
( typeof nodeData[ 2 ] === 'object' || nodeData[ 2 ].type === '/paragraph' );
}
};
CommentWidget.prototype.isDisabled = function () {
return this.disabled;
};
CommentWidget.prototype.setDisabled = function ( disabled ) {
CommentWidget.super.prototype.setDisabled.call( this, disabled );
if ( this.commentBodyWidget ) {
this.confirmButton.setDisabled( disabled );
this.cancelButton.setDisabled( disabled );
this.setReadOnly( disabled );
}
return this;
};
CommentWidget.prototype.isPending = function () {
return this.pending;
};
CommentWidget.prototype.setPending = function ( pending ) {
this.pending = pending;
this.setDisabled( pending );
if ( pending ) {
this.commentBodyWidget.pushPending();
} else {
this.commentBodyWidget.popPending();
}
return this;
};
CommentWidget.prototype.isReadOnly = function () {
this.commentBodyWidget.isReadOnly();
};
CommentWidget.prototype.setReadOnly = function ( readonly ) {
this.commentBodyWidget.setReadOnly( readonly );
this.modeTabSelect.$element.css( 'pointer-events', readonly ? 'none' : '' );
return this;
};
CommentWidget.prototype.onBeforeUnload = function ( e ) {
if ( !this.isEmpty() ) {
e.preventDefault();
return '';
}
};
CommentWidget.prototype.onConfirmClick = function () {
if ( this.pending ) {
return;
}
this.setPending( true );
this.getValue().always( () => {
this.setPending( false );
} ).then( value => {
$( window ).off( 'beforeunload', this.beforeUnloadHandler );
if ( value && !value.includes( '~~' + '~~' ) ) {
value += ' --~~' + '~~';
}
this.emit( 'confirm', {
value: value
} );
} );
};
CommentWidget.prototype.tryTeardown = function () {
let promise;
if ( !this.isEmpty() ) {
const messageDialog = new OO.ui.MessageDialog();
const windowManager = new OO.ui.WindowManager();
$( 'body' ).append( windowManager.$element );
windowManager.addWindows( [ messageDialog ] );
promise = windowManager.openWindow( messageDialog, {
title: 'Annullare il commento?',
message: 'Confermi di voler annullare il commento che stavi scrivendo?',
actions: [
{
action: 'discard',
label: 'Annulla commento',
flags: 'destructive'
},
{
action: 'keep',
label: 'Continua a scrivere',
flags: [ 'primary', 'safe' ]
}
]
} ).closed.then( ( data ) => {
if ( !( data && data.action === 'discard' ) ) {
return $.Deferred().reject().promise();
}
} );
} else {
promise = $.Deferred().resolve().promise();
}
promise = promise.then( () => {
this.teardown( 'abandoned' );
} );
return promise;
};
CommentWidget.prototype.teardown = function ( mode ) {
this.commentBodyWidget.disconnect( this );
this.onInputChange();
if ( this.modeTabSelect ) {
this.modeTabSelect.blur();
}
$( window ).off( 'beforeunload', this.beforeUnloadHandler );
this.isTornDown = true;
this.clear( mode === 'refresh' );
this.$element.remove();
this.emit( 'teardown', mode );
return this;
};
CommentWidget.prototype.onInputChange = function () {
if ( this.isTornDown ) {
return;
}
if ( this.storagePrefix ) {
this.storage.set(
this.storagePrefix + '/saveable',
this.isEmpty() ? '' : '1',
STORAGE_EXPIRY
);
}
if ( this.getMode() === 'source' ) {
this.getValue().then( wikitext => this.preparePreview( wikitext ) );
}
};
CommentWidget.prototype.onModeTabSelectChoose = function ( option ) {
const mode = option.getData();
if ( mode === this.getMode() ) {
return;
}
this.modeTabSelect.setDisabled( true );
this.switch( mode ).then(
null,
() => {
this.modeTabSelect.selectItemByData( this.getMode() );
}
).always( () => {
this.modeTabSelect.setDisabled( false );
this.modeTabSelect.findSelectedItem().$element.mouseleave();
} );
};
CommentWidget.prototype.htmlTrim = function ( str ) {
return str.replace( /^[\t\n\f\r ]+/, '' ).replace( /[\t\n\f\r ]+$/, '' );
};
CommentWidget.prototype.sanitizeWikitextLinebreaks = function ( wikitext ) {
return this.htmlTrim( wikitext )
.replace( /\r/g, '\n' )
.replace( /\n\n\n+/g, '\n\n' )
.replace( /([^\n])\n([^\n:#\*;])/, '$1\n\n$2' );
};
CommentWidget.prototype.switch = function ( mode ) {
if ( mode === this.getMode() ) {
return $.Deferred().reject().promise();
}
let promise;
this.setPending( true );
switch ( mode ) {
case 'source':
promise = this.switchToWikitext();
break;
case 'visual':
promise = this.switchToVisual();
break;
}
return promise.always( () => {
this.setPending( false );
} );
};
CommentWidget.prototype.switchToWikitext = function () {
const target = this.commentBodyWidget.target,
previewDeferred = $.Deferred();
return this.getValue().then( wikitext => {
this.preparePreview( wikitext ).then( previewDeferred.resolve );
setTimeout( previewDeferred.resolve, 500 );
return previewDeferred.then( () => {
this.setup( {
value: wikitext,
mode: 'source'
} ).focus();
} );
} );
};
CommentWidget.prototype.switchToVisual = function () {
return this.getValue().then( wikitext => {
wikitext = wikitext.replace(
/([^~]|^)~[~]~~([^~]|$)/g,
'$1<span data-dtsignatureforswitching="1"></span>$2'
);
if ( wikitext ) {
wikitext = this.sanitizeWikitextLinebreaks( wikitext );
return new mw.Api( {
parameters: {
formatversion: 2,
uselang: mw.config.get( 'wgUserLanguage' )
}
} ).post( {
action: 'visualeditor',
paction: 'parsefragment',
page: this.pageName,
wikitext: ( this.commentIndent || '' ) + wikitext,
pst: true
} ).then( ( response ) => {
return response && response.visualeditor.content;
} );
} else {
parsePromise = $.Deferred().resolve( '' ).promise();
}
} ).then( html => {
const unsupportedSelectors = {
table: 'table',
template: '[typeof*="mw:Transclusion"]',
extension: '[typeof*="mw:Extension"]'
};
let doc = '<p></p>';
if ( html ) {
doc = this.commentBodyWidget.target.parseDocument( html );
mw.libs.ve.stripRestbaseIds( doc );
for ( let type in unsupportedSelectors ) {
if ( doc.querySelector( unsupportedSelectors[ type ] ) ) {
const $msg = $( '<div>' ).html(
mw.message(
'discussiontools-error-noswitchtove',
// The following messages are used here:
// * discussiontools-error-noswitchtove-extension
// * discussiontools-error-noswitchtove-table
// * discussiontools-error-noswitchtove-template
mw.msg( 'discussiontools-error-noswitchtove-' + type )
).parse()
);
$msg.find( 'a' ).attr( {
target: '_blank',
rel: 'noopener'
} );
OO.ui.alert(
$msg.contents(),
{
title: mw.msg( 'discussiontools-error-noswitchtove-title' ),
size: 'medium'
}
);
return $.Deferred().reject().promise();
}
}
}
this.preparePreview( '' );
this.setup( {
value: doc,
mode: 'visual'
} ).focus();
} );
};
CommentWidget.prototype.isCommentIndentNode = function ( node ) {
return node.type === 'commentindent' && node.getRoot() && node.getOffset() === 0;
};
CommentWidget.prototype.setup = function ( data ) {
const target = this.commentBodyWidget.target;
data = data || {};
let htmlOrDoc;
if ( data.value === undefined && this.storagePrefix && this.storage.get( this.storagePrefix + '/saveable' ) ) {
const docstate = this.storage.getObject( this.storagePrefix + '/ve-docstate' );
if ( typeof docstate === 'object' && docstate.mode ) {
htmlOrDoc = this.storage.get( this.storagePrefix + '/ve-dochtml' );
this.modeTabSelect.selectItemByData( docstate.mode );
target.setDefaultMode( docstate.mode );
target.recovered = true;
}
} else {
htmlOrDoc = data.value;
if ( data.mode ) {
target.setDefaultMode( data.mode );
target.getSurface().getModel().updateDocState( data.mode );
}
target.recovered = false;
}
htmlOrDoc = htmlOrDoc || ( target.getDefaultMode() === 'visual' ? '<p></p>' : '' );
target.originalHtml = htmlOrDoc instanceof HTMLDocument ? ve.properInnerHtml( htmlOrDoc.body ) : htmlOrDoc;
target.fromEditedState = !!data.value;
this.commentBodyWidget.setDocument( htmlOrDoc );
target.once( 'surfaceReady', () => {
const surface = target.getSurface(),
documentNode = surface.getView().getDocument().getDocumentNode();
surface.getView().connect( this, {
focus: [ 'emit', 'bodyFocus' ]
} );
if ( this.storagePrefix ) {
target.initAutosave( {
docId: this.storagePrefix,
storage: this.storage,
storageExpiry: STORAGE_EXPIRY
} );
}
if ( this.commentIndent ) {
let firstChild = documentNode.getChildren()[ 0 ],
isDocumentUpdating = false;
surface.getModel().on( 'documentUpdate', () => {
if (
isDocumentUpdating ||
this.getMode() === 'source' ||
this.isCommentIndentNode( firstChild )
) {
return;
}
isDocumentUpdating = true;
surface.getModel().getLinearFragment( new ve.Range( 1 ) ).insertContent( [
{
type: 'commentindent',
attributes: {
style: this.commentIndent === '*' ? 'bullet' : 'number'
}
},
{ type: 'listItem' },
{ type: 'paragraph' }, { type: '/paragraph' },
{ type: '/listItem' },
{ type: '/commentindent' }
] );
firstChild = documentNode.getChildren()[ 0 ];
this.focus();
isDocumentUpdating = false;
} );
surface.getModel().emit( 'documentUpdate' );
}
if ( data.value === undefined ) {
$( window ).on( 'beforeunload', this.beforeUnloadHandler );
}
this.afterSetup();
} );
this.commentBodyWidget.connect( this, {
change: 'onInputChangeThrottled',
cancel: 'teardown',
submit: 'onConfirmClick'
} );
return this;
};
CommentWidget.prototype.afterSetup = function () {
this.onInputChange();
};
CommentWidget.prototype.preparePreview = function ( wikitext ) {
if ( this.getMode() !== 'source' ) {
return $.Deferred().resolve().promise();
}
wikitext = wikitext !== undefined ? wikitext : '';
wikitext = this.sanitizeWikitextLinebreaks( wikitext );
if ( this.previewWikitext === wikitext ) {
return $.Deferred().resolve().promise();
}
this.previewWikitext = wikitext;
if ( this.previewRequest ) {
this.previewRequest.abort();
this.previewRequest = null;
}
if ( wikitext && !wikitext.includes( '~~' + '~~' ) ) {
wikitext += ' <span style="opacity:.6;">--~~' + '~~</span>';
}
let parsePromise;
if ( !wikitext ) {
parsePromise = $.Deferred().resolve( null ).promise();
} else {
parsePromise = mw.user.acquireTempUserName().then( () => {
this.previewRequest = new mw.Api().get( {
action: 'parse',
text: ( this.commentIndent || '' ) + wikitext,
prop: [ 'text', 'modules', 'jsconfigvars' ],
pst: true,
disableeditsection: true,
preview: true,
useskin: mw.config.get( 'skin' ),
contentmodel: 'wikitext',
mobileformat: OO.ui.isMobile()
} );
return this.previewRequest;
} );
}
return parsePromise.then( result => {
this.$preview.html( result ? result.parse.text[ '*' ] : '' );
this.$preview.find( '.mw-parser-output' ).css( 'margin-left', '0' );
if ( result ) {
mw.config.set( result.parse.jsconfigvars );
mw.loader.load( result.parse.modulestyles );
mw.loader.load( result.parse.modules );
}
} );
};
/**
* Classe ModeTabSelectWidget
*
* Copiata dall'estensione DiscussionTools.
* Commit: 58c078437d183cd6f51d7e90ae88033ae659bf2f
* https://gerrit.wikimedia.org/r/c/mediawiki/extensions/DiscussionTools/+/667029
*/
function ModeTabSelectWidget() {
ModeTabSelectWidget.super.apply( this, arguments );
}
OO.inheritClass( ModeTabSelectWidget, OO.ui.TabSelectWidget );
ModeTabSelectWidget.prototype.onDocumentKeyDown = function ( e ) {
if ( e.keyCode === OO.ui.Keys.SPACE ) {
e = $.Event( e, { keyCode: OO.ui.Keys.ENTER } );
}
ModeTabSelectWidget.super.prototype.onDocumentKeyDown.call( this, e );
};
/**
* Classe ModeTabOptionWidget
*
* Copiata dall'estensione DiscussionTools.
* Commit 58c078437d183cd6f51d7e90ae88033ae659bf2f
* https://gerrit.wikimedia.org/r/c/mediawiki/extensions/DiscussionTools/+/667029
*/
function ModeTabOptionWidget() {
ModeTabOptionWidget.super.apply( this, arguments );
this.$element.addClass( 'ext-discussiontools-ui-modeTab' );
}
OO.inheritClass( ModeTabOptionWidget, OO.ui.TabOptionWidget );
ModeTabOptionWidget.static.highlightable = true;
/**
* Classe dmCommentIndentNode
*/
dmCommentIndentNode = function VeDmCommentIndentNode() {
dmCommentIndentNode.super.apply( this, arguments );
};
OO.inheritClass( dmCommentIndentNode, ve.dm.ListNode );
dmCommentIndentNode.static.name = 'commentindent';
dmCommentIndentNode.prototype.canHaveSlugBefore = function () {
return false;
};
ve.dm.modelRegistry.register( dmCommentIndentNode );
/**
* Classe ceCommentIndentNode
*/
ceCommentIndentNode = function VeCeCommentIndentNode() {
ceCommentIndentNode.super.apply( this, arguments );
};
OO.inheritClass( ceCommentIndentNode, ve.ce.ListNode );
ceCommentIndentNode.static.name = 'commentindent';
ve.ce.nodeFactory.register( ceCommentIndentNode );
ve.dm.ListItemNode.static.parentNodeTypes = [ 'list', 'commentindent' ];
module.exports = CommentWidget;