User:Mikevoir/betterLinkSuggest.js

mw.loader.using('mediawiki.api').then(function {	var betterLinkSuggest = {		config: mw.config.get(['wgAction', 'wgNamespaceNumber']),		waitFor: function(query, callback, extraDelay) {			// Delay until element exists to run function			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) { extraDelay = 0;} setTimeout(callback, extraDelay); me.disconnect; // stop observing return; }				});				// start observing				observer.observe(document, { childList: true, subtree: true });			}		},		init: function {			// Check we're in the editor screen			if (['edit', 'submit'].includes(this.config.wgAction)) {				this.waitFor('.wikiEditor-ui-linkSuggest', this.noBlankLinkSuggest);			}			// Check we're in MessageWall or Main namespace			if (this.config.wgNamespaceNumber == 1200 || this.config.wgNamespaceNumber == 0) {				this.waitFor('#MessageWall', this.messageLinkSuggest);			}		},

// Fix the link suggestion popup in editor screen to hide when blank always noBlankLinkSuggest: function { console.log('reached: noBlankLinkSuggest'); 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 { console.log('reached messageLinkSuggest'); 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]); var methods = { SEARCH: {}, initEvents: function { // 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') ) {							methods.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')) {console.log('mouseup triggered, this: ', methods); methods.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')) {								methods.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;								methods.suggestLink(event);							} else if (!box.classList.contains('hidden') && ['ArrowDown', 'ArrowUp'].includes(event.key)) {								event.preventDefault;								methods.handleOOUI(event);							}						} else if (event.key == 'Enter' && event.target.classList.contains('ProseMirror') && suggestBox.getAttribute('aria-activedescendant')) { event.preventDefault; dispatchLink; } 					});				},				dispatchLink: function {					console.log('dispatchLink triggered'); 					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 = methods.SEARCH.node.splitText(methods.SEARCH.offset);					newNode.splitText(methods.SEARCH.str.length+2);					linkNode.href = clickedNode.getAttribute('link-to');					linkNode.append(targetPage);					methods.SEARCH.node.parentNode.append(dummyText); // need to add some text so that the link doesnt get stripped bc prosemirror is cancer modify					methods.SEARCH.node.parentNode.insertBefore(linkNode, methods.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; },				handleOOUI: function(event) { console.log('handleOOUI triggered: ', 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); }					}				},				suggestLink: function(event) { console.log('suggestLink triggered: ', event); var caret; var link_regex = /^.*\[\[([^\[\]]+)(\]\]|$)/; var ext_link_regex = /^.*\[([^\[\]]+)\]$/; var raw_str = ''; var link_str = ''; while (true) { caret = methods.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'); methods.SEARCH.str = link_str; methods.SEARCH.offset = raw_str.indexOf('[['+link_str);							methods.SEARCH.node = caret.data.focusNode;							if (caret.data.type == 'Caret' && caret.position > 0 && link_str.length > 0 && raw_str.length > link_str.length) {								methods.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') {								methods.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; }							methods.buildSuggestions([{								title: link_str,								linkto: url							}]);						}					}				},				getCaret: function(element) {					console.log('getCaret triggered: ', 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					};				},				getPages: function(prefix) {					console.log('getPages triggered: ', prefix); 					api.get({						action: 'query',						list: 'prefixsearch',						pssearch: prefix					}).then(function(data) {						console.log('data obtained: ', data);						if (data && data.query && data.query.prefixsearch.length > 0) {							methods.buildSuggestions(data.query.prefixsearch);						} else {							return;						}					});				},				buildSuggestions: function(pages) {					console.log('buildSuggestions triggered: ', 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');				}			};			methods.initEvents;		}	};	betterLinkSuggest.init; });