User:Mikevoir/betterLinkSuggest.js

mw.loader.using('mediawiki.api').then(function {	var betterLinkSuggest = {		config: mw.config.get(['wgAction', 'wgNamespaceNumber']),		lib: $.extend({ // Delay until element exists to run function waitFor: function(query, callback, extraDelay) { if ('function' == typeof callback && 'string' == typeof query) { // set up the mutation observer var observer = new MutationObserver(function (mutations, me) {						// `mutations` is an array of mutations that occurred						// `me` is the MutationObserver instance						var targetNode = document.querySelector(query);						if (targetNode) {							if (extraDelay && 'number' == typeof extraDelay) {setTimeout(callback, extraDelay);}							else {callback;}							me.disconnect; // stop observing							return;						}					}); // start observing observer.observe(document, {					 childList: true,					  subtree: true					}); }			}		}, window.betterLinkSuggest),		init: function {			console.log(this);			// Check we're in the editor screen			if (['edit', 'submit'].includes(this.config.wgAction)) {				this.waitFor('.wikiEditor-ui-linkSuggest', noBlankLinkSuggest);			}			// Check we're in MessageWall or Main namespace			if (this.config.wgNamespaceNumber == 1200 || this.config.wgNamespaceNumber == 0) {				this.waitFor('body', messageLinkSuggest);			}		},		waitFor: function(query, THEN, extraDelay) {			// set up the mutation observer			var observer = new MutationObserver(function (mutations, me) { // `mutations` is an array of mutations that occurred // `me` is the MutationObserver instance var targetNode = document.querySelector(query); if (targetNode) { if (extraDelay) {setTimeout(THEN, extraDelay);} else {THEN;} me.disconnect; // stop observing return; }			});			// start observing			observer.observe(document, { childList: true, subtree: true });		},

// Fix the link suggestion popup in editor screen to hide when blank always noBlankLinkSuggest: function { var parent = document.querySelector('.wikiEditor-ui-linkSuggest'); if (parent) { var targetNode = parent.querySelector('.oo-ui-selectWidget'); // console.log(targetNode); var observer = new MutationObserver(function(mutations) {					mutations.forEach(function(mutation){ var node = mutation.target; if (node.childElementCount == 0) { parent.classList.add('oo-ui-element-hidden'); }					});				});				observer.observe(targetNode, { childList: true }); }		},		// Adds link suggest functionality to message walls and comment section text editors messageLinkSuggest: function { var api = new mw.Api; var ooui = 0; var SEARCH = {}; // Add necessary styles that dont load outside editor screen var styleEl = document.createElement('style'); document.head.appendChild(styleEl); styleEl.sheet.insertRule(				'.wikiEditor-ui-linkSuggest-suggestion.oo-ui-optionWidget-highlighted, .wikiEditor-ui-linkSuggest-suggestion:hover {\n' +				'background-color: rgba(var(--theme-link-color--rgb),.15);\n' +				'color: var(--theme-link-color);\n' +				'}',				0			); // Suggestion list var suggestBox = document.createElement('div'); suggestBox.className = 'oo-ui-widget oo-ui-widget-enabled oo-ui-selectWidget oo-ui-selectWidget-unpressed'; suggestBox.setAttribute('role', 'list'); suggestBox.setAttribute('aria-multiselectable', 'false'); // Build suggestion list's box var box__ = document.createElement('div'); box__.className = 'oo-ui-clippableElement-clippable oo-ui-popupWidget-body'; box__.style.width = '320px'; box__.style['max-width'] = '1889px'; box__.style.height = 'auto'; box__.style['max-height'] = '250px'; box__.style['overflow-y'] = 'scroll'; box__.appendChild(suggestBox); var box_ = document.createElement('div'); box_.className = 'oo-ui-popupWidget-popup wikiEditor-ui-linkSuggest-popup'; box_.style.padding = 0; box_.appendChild(box__); var anchorArrow = document.createElement('div'); anchorArrow.className = 'oo-ui-popupWidget-anchor'; anchorArrow.style.left = '12px'; var box = document.createElement('div'); box.className = 'oo-ui-widget oo-ui-widget-enabled oo-ui-floatableElement-floatable oo-ui-popupWidget-anchored oo-ui-popupWidget wikiEditor-ui-linkSuggest oo-ui-popupWidget-anchored-top'; box.style.position = 'absolute'; box.style['margin-top'] = '21px'; box.appendChild(box_); box.appendChild(anchorArrow); // Hide by default box.classList.add('mobile-only', 'hidden'); // Append box to area document.body.insertBefore(box, document.body.children[0]); this.waitFor('.ProseMirror', initEvents); function initEvents { // Hide and empty out when unfocusing list document.addEventListener('click', function(event) {					// console.log(event);					if (!event.target.closest('.wikiEditor-ui-linkSuggest')) {						suggestBox.removeAttribute('aria-activedescendant');						box.classList.add('mobile-only', 'hidden');						suggestBox.replaceChildren;					}				}); // Suggestion list hovering logic suggestBox.addEventListener('mouseover', function(event){					if ( event.target.classList.contains('oo-ui-labelElement-label') && event.target.closest('.wikiEditor-ui-linkSuggest-suggestion') ) {						handleOOUI(event);						event.target.closest('.wikiEditor-ui-linkSuggest-suggestion').classList.add('oo-ui-optionWidget-highlighted');					}				}); suggestBox.addEventListener('mouseout', function(event){					if ( event.target.classList.contains('oo-ui-labelElement-label') && event.target.closest('.wikiEditor-ui-linkSuggest-suggestion') && event.target.closest('.wikiEditor-ui-linkSuggest-suggestion').classList.contains('oo-ui-optionWidget-highlighted') ) {						event.target.closest('.wikiEditor-ui-linkSuggest-suggestion').classList.remove('oo-ui-optionWidget-highlighted');					}				}); // Start looking document.addEventListener('mouseup', function(event){					// console.log('mouse event: ', event);					if ( event.target.classList.contains('oo-ui-labelElement-label') || (							event.target.parentNode &&							event.target.parentNode.classList.contains('wikiEditor-ui-linkSuggest-suggestion')						) ) {dispatchLink;}					else if (event.target.closest('.ProseMirror')) {suggestLink(event);}				}); document.addEventListener('keydown', function(event){					if (['ArrowDown', 'ArrowUp', 'ArrowLeft', 'ArrowRight'].includes(event.key) && event.target.classList.contains('ProseMirror')) {						//console.log('key event: ', event);						if (box.classList.contains('hidden')) {							suggestLink(event);						} else if (!box.classList.contains('hidden') && ['ArrowLeft', 'ArrowRight'].includes(event.key)) {							box.classList.add('mobile-only', 'hidden');							suggestBox.removeAttribute('aria-activedescendant');							suggestBox.replaceChildren;							suggestLink(event);						} else if (!box.classList.contains('hidden') && ['ArrowDown', 'ArrowUp'].includes(event.key)) {							event.preventDefault;							handleOOUI(event);						}					} else if (event.key == 'Enter' && event.target.classList.contains('ProseMirror') && suggestBox.getAttribute('aria-activedescendant')) {						event.preventDefault; dispatchLink; } 				});			}			function dispatchLink {				var clickedNode = suggestBox.querySelector('#'+suggestBox.getAttribute('aria-activedescendant'));				var targetPage = document.createTextNode(clickedNode.children[0].innerHTML);				var dummyText = document.createTextNode(' ');				var linkNode = document.createElement('a');				var newNode = SEARCH.node.splitText(SEARCH.offset);				newNode.splitText(SEARCH.str.length+2);				linkNode.href = clickedNode.getAttribute('link-to');				linkNode.append(targetPage);				SEARCH.node.parentNode.append(dummyText); // need to add some text so that the link doesnt get stripped bc prosemirror is cancer modify				SEARCH.node.parentNode.insertBefore(linkNode, SEARCH.node.nextSibling);				newNode.remove; // Remove original link text used for search				// Close suggestion list				suggestBox.removeAttribute('aria-activedescendant');				box.classList.add('mobile-only', 'hidden'); suggestBox.replaceChildren; }			function handleOOUI(event) { if (!suggestBox.getAttribute('aria-activedescendant')) { var firstOpt = suggestBox.querySelector('.oo-ui-labelElement'); if (!firstOpt.id) { ooui = ooui + 1; firstOpt.id = 'ooui-' + ooui; }					firstOpt.classList.add('oo-ui-optionWidget-highlighted'); suggestBox.setAttribute('aria-activedescendant', firstOpt.id); } else if (suggestBox.getAttribute('aria-activedescendant')) { var Tooui = suggestBox.getAttribute('aria-activedescendant'); var currentNode = suggestBox.querySelector('#'+Tooui); var newNode; var scrollChange = 0; if (event.type == 'keydown' && event.key == 'ArrowUp' && currentNode && currentNode.previousSibling) { newNode = currentNode.previousSibling; }					else if (event.type == 'keydown' && event.key == 'ArrowDown' && currentNode && currentNode.nextSibling) { newNode = currentNode.nextSibling; } else if (event.type == 'mouseover' && currentNode && event.target.closest('.wikiEditor-ui-linkSuggest-popup .oo-ui-selectWidget')){ newNode = event.target; if (newNode.classList.contains('oo-ui-labelElement-label')) { newNode = event.target.parentNode; } }					if (newNode) { if (box__.clientHeight < (newNode.offsetTop+newNode.clientHeight)) {scrollChange = newNode.clientHeight;} else if (box__.scrollTop > newNode.offsetTop) {scrollChange = -currentNode.clientHeight;} currentNode.classList.remove('oo-ui-optionWidget-highlighted'); newNode.classList.add('oo-ui-optionWidget-highlighted'); if (!newNode.id) { ooui = ooui + 1; newNode.id = 'ooui-' + ooui; }						box__.scrollTop = box__.scrollTop + scrollChange; suggestBox.setAttribute('aria-activedescendant', newNode.id); }				}			}			function suggestLink(event) { var caret; var link_regex = /^.*\[\[([^\[\]]+)(\]\]|$)/; var ext_link_regex = /^.*\[([^\[\]]+)\]$/; var raw_str = ''; var link_str = ''; while (true) { caret = getCaret(event.target); if ( caret && caret.data && caret.data.focusNode ) { break; }				}				if ( caret && caret.data && caret.data.focusNode && caret.data.focusNode.nodeValue ) { if (link_regex.test(caret.data.focusNode.nodeValue)) { raw_str = caret.data.focusNode.nodeValue; link_str = raw_str.slice(0, caret.position).replace(link_regex, '$1'); SEARCH.str = link_str; SEARCH.offset = raw_str.indexOf('[['+link_str);						SEARCH.node = caret.data.focusNode;						if (caret.data.type == 'Caret' && caret.position > 0 && link_str.length > 0 && raw_str.length > link_str.length) {							getPages(link_str);							document.querySelector('.wikiEditor-ui-linkSuggest').style.top = (event.pageY) + 'px';							document.querySelector('.wikiEditor-ui-linkSuggest').style.left = (event.pageX-12) + 'px';						} else if (caret.data.type == 'Range') {							getPages(link_str);							document.querySelector('.wikiEditor-ui-linkSuggest').style.top  = (event.pageY) + 'px';							document.querySelector('.wikiEditor-ui-linkSuggest').style.left = (event.pageX-12) + 'px';						}					} else if (ext_link_regex.test(caret.data.focusNode.nodeValue.slice(0, caret.position))) {						raw_str = caret.data.focusNode.nodeValue.slice(0, caret.position);						link_str = raw_str.replace(ext_link_regex, '$1').split(' ');						var url = link_str.shift;						if (link_str.length > 0) { link_str = link_str.join(' '); }						else { link_str = url; }						buildSuggestions([{							title: link_str,							linkto: url						}]);					}				}			}			function getCaret(element) {				//console.log(element);				var caretOffset = 0;				var doc = element.ownerDocument || element.document;				var win = doc.defaultView || doc.parentWindow;				var sel;				if (typeof win.getSelection != "undefined") {				    sel = win.getSelection;				    if (sel.rangeCount > 0) {				        var range = win.getSelection.getRangeAt(0);				        var preCaretRange = range.cloneRange;				        preCaretRange.selectNodeContents(element);				        preCaretRange.setEnd(range.endContainer, range.endOffset);				        caretOffset = preCaretRange.toString.length;				    }				} else if ( (sel = doc.selection) && sel.type != "Control") {				    var textRange = sel.createRange;				    var preCaretTextRange = doc.body.createTextRange;				    preCaretTextRange.moveToElementText(element);				    preCaretTextRange.setEndPoint("EndToEnd", textRange);				    caretOffset = preCaretTextRange.text.length;				}				return {					data: sel,					position: caretOffset				};			}			function getPages(prefix) {				api.get({					action: 'query',					list: 'prefixsearch',					pssearch: prefix				}).then(function(data) {					if (data && data.query && data.query.prefixsearch.length > 0) {						buildSuggestions(data.query.prefixsearch);					} else {						return;					}				});			}			function buildSuggestions(pages) {				// Build list				pages.forEach(					function(page){						var link = document.createElement('div');						link.className = 'oo-ui-widget oo-ui-widget-enabled oo-ui-labelElement oo-ui-optionWidget wikiEditor-ui-linkSuggest-suggestion';						link.role = 'option';						link.tabindex = -1;						link.setAttribute('link-to', (page.linkto || ('https://genshin-impact.fandom.com/wiki/' + page.title)));						var label = document.createElement('span');						label.className = 'oo-ui-labelElement-label';						label.innerHTML = page.title;						link.appendChild(label);						suggestBox.appendChild(link);					}				);				document.querySelector('.wikiEditor-ui-linkSuggest-suggestion').focus;				box.classList.remove('mobile-only', 'hidden');			}		}	};	betterLinkSuggest.init; });