User:Qwerfjkl/scripts/CFDlister.js

From Wikipedia, the free encyclopedia
Note: After saving, you have to bypass your browser's cache to see the changes. Google Chrome, Firefox, Microsoft Edge and Safari: Hold down the ⇧ Shift key and click the Reload toolbar button. For details and instructions about other browsers, see Wikipedia:Bypass your cache.
// Fork of [[User:Writ Keeper/Scripts/autoCloser.js]]
//<nowiki>


mw.loader.using(["oojs-ui-core", "oojs-ui-windows", "oojs-ui-widgets"], function () {
	function escapeRegexp(string) {
	    return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
	}
	
	function parseHTML(html) {
	    // Create a temporary div to parse the HTML
	    var tempDiv = $('<div>').html(html);
	
	    // Find all li elements
	    var liElements = tempDiv.find('li');
	
	    // Array to store extracted hrefs
	    var hrefs = [];
	
	    let existinghrefRegexp = /^https:\/\/en\.wikipedia.org\/wiki\/(Category:[^?&]+?)$/;
	    let nonexistinghrefRegexp = /^https:\/\/en\.wikipedia\.org\/w\/index\.php\?title=(Category:[^&?]+?)&action=edit&redlink=1$/;
	
	    // Iterate through each li element
	    liElements.each(function () {
	        // Find all anchor (a) elements within the current li
	        let hrefline = [];
	        var anchorElements = $(this).find('a');
	
	        // Extract href attribute from each anchor element
	        anchorElements.each(function () {
	            var href = $(this).attr('href');
	            if (href) {
	                var existingMatch = existinghrefRegexp.exec(href);
	                var nonexistingMatch = nonexistinghrefRegexp.exec(href);
	
	                if (existingMatch) {
	                    hrefline.push(decodeURIComponent(existingMatch[1]).replaceAll('_', ' '));
	                }
	                if (nonexistingMatch) {
	                    hrefline.push(decodeURIComponent(nonexistingMatch[1]).replaceAll('_', ' '));
	                }
	            }
	        });
	        hrefs.push(hrefline);
	    });
	
	    return hrefs;
	}
	
	function handlepaste(widget, e) {
	    var types, pastedData, parsedData;
	    // Browsers that support the 'text/html' type in the Clipboard API (Chrome, Firefox 22+)
	    if (e && e.clipboardData && e.clipboardData.types && e.clipboardData.getData) {
	        // Check for 'text/html' in types list
	        types = e.clipboardData.types;
	        if (((types instanceof DOMStringList) && types.contains("text/html")) ||
	            ($.inArray && $.inArray('text/html', types) !== -1)) {
	            // Extract data and pass it to callback
	            pastedData = e.clipboardData.getData('text/html');
	
	            parsedData = parseHTML(pastedData);
	
	            // Check if it's an empty array
	            if (!parsedData || parsedData.length === 0) {
	                // Allow the paste event to propagate for plain text or empty array
	                return true;
	            }
	            let confirmed = confirm( 'You have pasted formatted text. Do you want this to be converted into wikitext?' );
				if (!confirmed) return true;
	            processPaste(widget, pastedData);
	
	            // Stop the data from actually being pasted
	            e.stopPropagation();
	            e.preventDefault();
	            return false;
	        }
	    }
	
	    // Allow the paste event to propagate for plain text
	    return true;
	}
	
	function waitForPastedData(widget, savedContent) {
	    // If data has been processed by the browser, process it
	    if (widget.getValue() !== savedContent) {
	        // Retrieve pasted content via widget's getValue()
	        var pastedData = widget.getValue();
	
	        // Restore saved content
	        widget.setValue(savedContent);
	
	        // Call callback
	        processPaste(widget, pastedData);
	    }
	    // Else wait 20ms and try again
	    else {
	        setTimeout(function () {
	            waitForPastedData(widget, savedContent);
	        }, 20);
	    }
	}
	
	function processPaste(widget, pastedData) {
	    // Parse the HTML
	    var parsedArray = parseHTML(pastedData);
	    let stringOutput = '';
	    for (const cats of parsedArray) {
		    if (cats.length === 1) stringOutput += `* [[:${cats[0]}]]\n`;
		    if (cats.length === 2) stringOutput += `* [[:${cats[0]}]] to [[:${cats[1]}]]\n`;
		    if (cats.length === 3) stringOutput += `* [[:${cats[0]}]] to [[:${cats[1]}]] and [[:${cats[2]}]]\n`;
		    if (cats.length > 3) {
		    	let firstCat = cats.pop(0);
		    	let lastCat = cats.pop(0);
		    	stringOutput += `* [[:${firstCat}}]] to [[:${cats.join(']], [[:')}]] and [[:${lastCat}]]\n`;
		    }
		}
	    widget.insertContent(stringOutput);
	}
	
	// Code from https://doc.wikimedia.org/oojs-ui/master/js/#!/api/OO.ui.Dialog
	
	// Add interface shell
	function CfdDialog( config ) {
	    CfdDialog.super.call( this, config );
	    this.InputText = config.InputText;
	    this.sectionIndex = config.sectionIndex || null;
		CfdDialog.static.title = config.dialogTitle;
		CfdDialog.static.actions = config.dialogActions;
	}
	OO.inheritClass( CfdDialog, OO.ui.ProcessDialog );
	CfdDialog.static.name = 'CfdDialog';
	
	CfdDialog.prototype.initialize = function () {
	    CfdDialog.super.prototype.initialize.call( this );
	    this.content = new OO.ui.PanelLayout( { padded: false, expanded: false } );
	    this.content.$element.append( '<p style="padding-left: 5px">Make any changes necessary:</p>' );
	    CfdDialog.prototype.cfdlisterTextBox = new OO.ui.MultilineTextInputWidget( {
			autosize: true,
			value: this.InputText,
			id: "CFD-lister-text",
			rows: Math.min((this.InputText.match(/\n/g)||[]).length+2, 10),
	        maxrows: 25
		} );
		
		let textInputElement = CfdDialog.prototype.cfdlisterTextBox.$element.get(0);
		let handler = handlepaste.bind(this, CfdDialog.prototype.cfdlisterTextBox);
		// Modern browsers. Note: 3rd argument is required for Firefox <= 6
		if (textInputElement.addEventListener) {
		    textInputElement.addEventListener('paste', handler, false);
		}
		// IE <= 8
		else {
		    textInputElement.attachEvent('onpaste', handler);
		}
	
	
		mw.loader.using('ext.wikiEditor', function() {
			mw.addWikiEditor(CfdDialog.prototype.cfdlisterTextBox.$input);
	
		});
		CfdDialog.prototype.cfdlisterTextBox.$input.on('input', () => this.updateSize() );
		
	    this.content.$element.append(CfdDialog.prototype.cfdlisterTextBox.$element);
	    this.$body.append( this.content.$element );
	};
	
	CfdDialog.prototype.getActionProcess = function ( action ) {
	    var dialog = this;
	    if ( action ) {
	        return new OO.ui.Process( function () {
	            dialog.close( { text: '\n\n'+CfdDialog.prototype.cfdlisterTextBox.value, sectionIndex: this.sectionIndex } );
	        } );
	    }
	    return CfdDialog.super.prototype.getActionProcess.call( this, action );
	};	
	
	function editDiscussions() {
		var text = localStorage.getItem('CFDNAClist');
		if (!text) {
			mw.notify('Error, no discussions listed yet.', {type:'error'});
			return;
		}
		var windowManager = new OO.ui.WindowManager();
		var cfdDialog = new CfdDialog(
			{
				InputText: text.trim(), // newlines will be stripped here and added back implicitly in the closing call
				dialogTitle: 'Edit listed discussions',
				dialogActions: [
					{ action: 'add', label: 'Save', flags: ['primary', 'progressive'] },
					{ label: 'Cancel', flags: ['destructive', 'safe'] }
					]
			});
		
		windowManager.defaultSize = 'full';
		$( document.body ).append( windowManager.$element );
		windowManager.addWindows( [ cfdDialog ] );
		windowManager.openWindow( cfdDialog );
		windowManager.on('closing', (win, closing, data) => {
			if (!data) return;
			if (!data.text.trim()) {
				OO.ui.confirm('Are you sure you want to delete all listed discussions?').done((response) => {
					if (response) {
						localStorage.setItem('CFDNAClist', '');
						localStorage.setItem('CFDNAClist-count', '');
					} else mw.notify('Aborted changes to listed discussions.');
				});
			} else {
				localStorage.setItem('CFDNAClist', data.text);
				mw.notify('Listed discussions updated.');
			}
			});
	}
	
	var editDiscussionslink = mw.util.addPortletLink( 'p-cactions', '#','Edit discussions', 'pt-cfdnaceditlist', 'Edit listed discussions', null, listDiscussionslink); 
    $( editDiscussionslink ).click( function ( event ) {
        event.preventDefault();
        editDiscussions();
    });
	

		
    function quickClose(option, editLink, headerElement) {
		if (typeof editLink !== "undefined") {
			var regexResults = /title=([^&]+).*&section=[\D]*(\d+)/.exec(editLink.href);
			if(regexResults === null)
			{
				return false;
			}
			var pageTitle = regexResults[1].replaceAll('_', ' '); // prettify
			var sectionIndex = regexResults[2];
			const params = {
				action: "parse",
				format: "json",
				page: pageTitle,
				prop: "wikitext|sections",
				section: sectionIndex
			
			};
			const api = new mw.Api();
			
			api.get(params).done(data => {
				sectionTitle = data.parse.sections[0].line.replaceAll('_', ' ');
				wikitext = data.parse.wikitext["*"];
				const closedRegexp = /<div class="boilerplate cfd vfd xfd-closed mw-archivedtalk"/i;
				if ( closedRegexp.test(wikitext) ) { // already closed
					mw.notify('Discussion already closed, aborted closure.', {type:'error'});
					return;
				}
				const newWikitext = `\n${wikitext.replace(/====.+====/, `$&\n{{subst:cfd top|'''${option}'''}}${mw.config.get('wgUserGroups').includes('sysop') ? '' : ' {{subst:nac}}'} ~~~~\n`)}\n{{subst:cfd bottom}}`;
				var requestData =
					{
						action: "edit",
						title: pageTitle,
						format: "json",
						section: sectionIndex,
						text: newWikitext,
						summary: `/* ${sectionTitle} */ Quick close as ${option} via [[User:Qwerfjkl/scripts/CFDlister.js|script]]`,
						notminor: 1,
						nocreate: 1,
						token: mw.user.tokens.get( 'csrfToken' )
				
					};
					$.ajax({
						url: mw.util.wikiScript( 'api' ),
						type: 'POST',
						dataType: 'json',
						data: requestData
					})
					.then (function( data ) {
						if ( data && data.edit && data.edit.result && data.edit.result == 'Success' ) {
							mw.notify( `Discussion closed as ${option}.` );
							
							// Now use wikitext from before, don't bother refetching
							let result = option;
							const categoryRegex = /====.+====\s+([\s\S]+)\s+[:\*]* *'''(?:Nominator'?s )?rationale?/i;
							if ( categoryRegex.test(wikitext) ) { // correctly formatted
								wikitext = wikitext.match(categoryRegex)[1];
							} else {
								alert("This nomination is missing a '''Nominator's rationale''': and so the script cannot recognise the categories nominated. Please manually fix this by adding '''Nominator's rationale''': just before the nominator's rationale.");
								return;
							}
							// Cleanup
							wikitext = wikitext.replace(/\{\{(?:lc|cl|cls|lcs|clc)\|(.+?)\}\}/gi, '[[:Category:$1]]'); // fix category templates
			                wikitext = wikitext.replaceAll(':Category:Category:', ':Category:'); // fix double category - can be caused by above
							wikitext = wikitext.replace(/'''(propose|delet|renam|split|(?:up)?merg|container).*?'''/gi, '');
							wikitext = wikitext.replace(/^ *[:#\*]* */gm, '* '); // fix indentation
			                wikitext = wikitext.replace(/^\s*[\*\: ]*\s*\n/gm, ''); // remove lines with just *, : and whitespace
			                wikitext = wikitext.replace(/\n?\s*\*\s*$/, ''); // remove trailing asterisk
			                wikitext = wikitext.replace(/<br ?\/?>/g, ''); // remove br tags
							wikitext = `* [[${pageTitle}#${sectionTitle}]]\nResult: '''${result}'''\n<syntaxhighlight lang="wikitext">\n; [[${pageTitle}]]\n${wikitext}\n</syntaxhighlight>`;
							var incorrectOptionRegexp;
							switch (option) {
								case 'rename':
								case 'merge':
									incorrectOptionRegexp = /\* *\[\[:Category:[^\]\n]+?\]\]$/gim;
									if ( incorrectOptionRegexp.test(wikitext) ) {
										mw.notify('Error parsing nomination. Please click "list discussion" to list it manually.', {type:'error'});
										return;
									}
									break;
								case 'delete':
									incorrectOptionRegexp = /\* *\[\[:Category:[^\]\n]+?\]\].+\[\[:Category:/gim;
									if ( incorrectOptionRegexp.test(wikitext) ) {
										mw.notify('Error parsing nomination. Please click "list discussion" to list it manually.', {type:'error'});
										return;
									}
									break;
								default: // shouldn't happen unless the user has modified their html
								    mw.notify(`Error handling closure: ${option}. Please click "list discussion" to list it manually.`, {type:'error'});
								    return;
							}
							if ( wikitext.includes('<s>') || wikitext.includes('{{') || wikitext.includes('<!--')) {
								// something probably needs manual review
								mw.notify('Error parsing nomination. Please click "list discussion" to list it manually.', {type:'error'});
								return;
							}
							
							addCFD(null, null, {text: '\n\n'+wikitext, sectionIndex: sectionIndex});

						} else {
							console.error( 'The edit query returned an error. =(\n\n' + JSON.stringify(data) );
							mw.notify('Error closing discussion, see console for details', {type:'error'});
						}
					})
					.catch ( function(e) {
						console.error( 'The ajax request failed.\n\n' + JSON.stringify(e)  );
					});
			});
    }
    }
    
	function listDiscussion() {
		editLink = $(this).siblings("a.sectionEditLink")[0];
		if (typeof editLink !== "undefined") {
			var regexResults = /title=([^&]+).*&section=[\D]*(\d+)/.exec(editLink.href);
			if(regexResults === null)
			{
				return false;
			}
			var pageTitle = regexResults[1].replaceAll('_', ' ');
			var sectionIndex = regexResults[2];
			const params = {
				action: "parse",
				format: "json",
				page: pageTitle,
				prop: "wikitext|sections",
				section: sectionIndex
			
			};
			const api = new mw.Api();
			
			api.get(params).done(data => {
				sectionTitle = data.parse.sections[0].line.replaceAll('_', ' ');
				wikitext = data.parse.wikitext["*"];
				resultRegexp = /<div class="boilerplate cfd vfd xfd-closed(?: mw-archivedtalk)?" style="background(?:-color)?:#bff9fc; margin:0 auto; padding:0 10px 0 10px; border:1px solid #AAAAAA;">\n:''The following is an archived discussion concerning one or more categories\. <span style="color:red"\>'''Please do not modify it\.'''<\/span> Subsequent comments should be made on an appropriate discussion page \(such as the category's \[\[Help:Using talk pages\|talk page\]\] or in a \[\[Wikipedia:Deletion review\|deletion review\]\]\)\. No further edits should be made to this section\.''\n\n:''The result of the discussion was:'' ?(?:<!-- ?Template:Cfd top ?-->)? ?'''(.+?)'''/i;
				if ( resultRegexp.test(wikitext) ) { // match
					result = wikitext.match(resultRegexp)[1];
				} else {
					result = 'RESULT';
				}
				wikitext = wikitext.replace(new RegExp(resultRegexp.source + '.+', 'i'), ''); // remove closure text, unneeded
				const categoryRegex = /====.+====\s+([\s\S]+)\s+[:\*]* *'''(?:Nominator'?s )?rationale?/i;
				if ( categoryRegex.test(wikitext) ) { // correctly formatted
					wikitext = wikitext.match(categoryRegex)[1];
				} else {
					alert("This nomination is missing a '''Nominator's rationale''': and so the script cannot recognise the categories nominated. Please manually fix this by adding '''Nominator's rationale''': just before the nominator's rationale.");
					return;
				}
				// Cleanup
				wikitext = wikitext.replace(/\{\{(?:lc|cl|cls|lcs|clc)\|(.+?)\}\}/gi, '[[:Category:$1]]'); // fix category templates
                wikitext = wikitext.replaceAll(':Category:Category:', ':Category:'); // fix double category - can be caused by above
				wikitext = wikitext.replace(/'''(propose|delet|renam|split|(?:up)?merg|container).*?'''/gi, '');
				wikitext = wikitext.replace(/^ *[:#\*]* */gm, '* '); // fix indentation
                wikitext = wikitext.replace(/^\s*[\*\: ]*\s*\n/gm, ''); // remove lines with just *, : and whitespace
                wikitext = wikitext.replace(/\n?\s*\*\s*$/, ''); // remove trailing asterisk
                wikitext = wikitext.replace(/<br ?\/?>/g, ''); // remove br tags
				wikitext = "* [["+pageTitle+"#"+sectionTitle+"]]\nResult: '''"+result+"'''\n<syntaxhighlight lang=\"wikitext\">\n; [["+pageTitle+"]]\n"+wikitext+"\n</syntaxhighlight>";
				
				var windowManager = new OO.ui.WindowManager();

				var cfdDialog = new CfdDialog(
					{
						InputText: wikitext,
						sectionIndex: sectionIndex,
						dialogTitle: 'List discussion for processing',
						dialogActions: [
							{ action: 'add', label: 'Add', flags: ['primary', 'progressive'] },
							{ label: 'Cancel', flags: ['destructive', 'safe'] }
							]
					});
				
				windowManager.defaultSize = 'full';
				$( document.body ).append( windowManager.$element );
				windowManager.addWindows( [ cfdDialog ] );
				windowManager.openWindow( cfdDialog );
				windowManager.on('closing', addCFD);
	        });
		}
	}
		
    function addCFD(win, closing, data) {
				if ( data == null || data === undefined || data.text == '' || !data.text ) {
					return;
				}
				var wikitext = data.text;
				var text = localStorage.getItem('CFDNAClist');
				var count = localStorage.getItem('CFDNAClist-count') || 0;
				if (text == '' || text == null) {
					localStorage.setItem('CFDNAClist', wikitext);
				}
				else {
					localStorage.setItem('CFDNAClist', text+wikitext);
				}
				localStorage.setItem('CFDNAClist-count', Number(count)+1);
				mw.notify('Added discussion');
				// double strike through handled sections
				if (data.sectionIndex) {
					// apply styles to show discussion has been closed (like XFDCloser)
					$(`h4:nth-of-type(${data.sectionIndex-1})`).find('.mw-headline').css({'text-decoration': 'line-through', 'text-decoration-style': 'double'});
					var startH4 = document.querySelector(`h4:nth-of-type(${data.sectionIndex-1})`);
					if (startH4) {
					      var elementsBetweenH4 = [];
					      var currentElement = startH4.nextElementSibling;
					
					      while (currentElement) {
					          if (currentElement.tagName.toLowerCase() === 'h4') {
					              break; 
					          }
					
					          elementsBetweenH4.push(currentElement);
					          currentElement = currentElement.nextElementSibling;
					      }
					
					      elementsBetweenH4.forEach(function(element) {
					          $(element).css('opacity', '50%');
					      });
					}
				}
	}
	
	function discussionListerSetup() {
		function createDropdownLink(text, clickHandler) {
		    var link = document.createElement("a");
		    link.href = "#";
		    link.innerHTML = text;
		    link.onclick = function (event) {
		        event.preventDefault();
		        clickHandler();
		    };
		    return link;
		}
		
	    var sectionHeaders = $("h4 .mw-editsection"); // where parent is h4
	    $('.mw-heading, h1, h2, h3, h4, h5, h6').css('overflow', 'visible'); // allow for dropdown, no idea what damage it could do
	    sectionHeaders.each(function (index, element) {
	        var editLink = $(element).children("a")[0];
	        if (typeof editLink !== "undefined" && /&section=[\D]*(\d+)/.exec(editLink.href)) {
	            $(editLink).addClass("sectionEditLink");
	
	            var discussionLister = $("<a>List discussion</a>");
				discussionLister.click(listDiscussion);

			    // Create dropdown elements
                var dropdownContainer = $(`<span class='dropdown-container' style="position:relative; display:inline-block; "></span>`);
			    

				var dropdownTrigger = $(`<a class='dropdown-trigger' style="color: #0645AD; text-decoration: none; cursor: pointer">One click close</a>`);
	

				var dropdownMenu = $(`<span class='dropdown-menu' style="display: none; position: absolute; background-color: #fff; border: 1px solid #ddd; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); padding: 5px; min-width: 6em; z-index: 1; left: 0px; top: 0.8em;" ></span>`);


			    var actions = ['delete', 'rename', 'merge']; // needs to correspond with switch statement in quickClose()
			    for (var i = 0; i < actions.length; i++) {
			      const action = actions[i];
			      var menuItem = $(`<a style="display: block; color: #0645AD; text-decoration: none; padding: 10px; margin-top: 5px; font-size: 150%" class='dropdown-item'>${action}</a>`);
			      menuItem.click(function () {
		                quickClose(action, editLink, element);
		                dropdownMenu.hide();
		            });
		            // make red on hover
				  menuItem.on( "mouseenter", function () {$(this).css('color', 'red')} ).on( "mouseleave", function () {$(this).css('color', 'color: #0645AD')} )
			      dropdownMenu.append(menuItem);
			    }
				
			    dropdownTrigger.click(function () {
		            dropdownMenu.toggle();
		        });
				
			    // Append elements to the existing element
			    dropdownContainer.append(dropdownTrigger, dropdownMenu);
			  
		
			
			  // Close the dropdown if the user clicks outside of it
  		      $(document).click(function (event) {
	            if (!$(event.target).closest('.dropdown-container').length) {
	                dropdownMenu.hide();
	            }
	          });
				
				let bracket = $(element).find('.mw-editsection-bracket:last-child');
				$(bracket).before(' | ', discussionLister, " | ", dropdownContainer);

	        }
	    });
	

	}
		if (mw.config.get("wgPageName").match('Wikipedia:Categories_for_discussion')) $(document).ready(discussionListerSetup);
});




function listPages() {
	var text = localStorage.getItem('CFDNAClist');
	var count = localStorage.getItem('CFDNAClist-count');
	if (text == '' || text == null) {
		 mw.notify('No discussions to list, aborting');
		return;
	}
	text = "\n\nPlease can an admin add the following:"+text+"\n~~~~";
	var requestData =
	{
		action: "edit",
		title: "Wikipedia talk:Categories for discussion/Working",
		format: "json",
		//section: "new",
		//sectiontitle: "NAC request (~~~~~)",
		appendtext: text,
		summary: "Add NAC request ("+count+" dicussions listed) via [[User:Qwerfjkl/scripts/CFDlister.js|script]]",
		notminor: 1,
		nocreate: 1,
		redirect: 1,
		token: mw.user.tokens.get( 'csrfToken' )

	};
	mw.notify('Editing WT:CFDW...', {tag:'CFDListerEdit'});
	$.ajax({
		url: mw.util.wikiScript( 'api' ),
		type: 'POST',
		dataType: 'json',
		data: requestData
	})
	.then (function( data ) {
		if ( data && data.edit && data.edit.result && data.edit.result == 'Success' ) {
			mw.notify( 'Discussions listed.', {tag:'CFDListerEdit'});
			localStorage.setItem('CFDNAClist', '');
			localStorage.setItem('CFDNAClist-count', 0);
            window.location.href = "https://en.wikipedia.org/wiki/Wikipedia_talk:Categories_for_discussion/Working#footer"; // redirect
		} else {
			alert( 'The edit query returned an error. =(' );
		}
	})
	.catch ( function() {
		alert( 'The ajax request failed.' );
	});
}

var listDiscussionslink = mw.util.addPortletLink( 'p-cactions', '#','List discussions', 'pt-cfdnaclist', 'List closed discussions for admins to handle'); 
    $( listDiscussionslink ).click( function ( event ) {
        event.preventDefault();
        listPages();
    });

//</nowiki>