User:AlternateTorg/PersonalNotes/code.js

/* * This script adds a "Personal Notes" editor to the right rail on the page. * Anything written in this editor is stored locally in the user's browser, and * is only visible to that user. */ (function { 'use strict'; var NAMESPACE = '__com_walkertribe_rjw_fandom-personal-notes__'; var COMMUNITY_ID = NAMESPACE + mw.config.get('wgDBname'); var PAGE_NAME = mw.config.get('wgPageName'); var RIGHT_RAIL_SELECTOR = 'aside .right-rail-wrapper'; var WIDGET_TEXTAREA_ID = NAMESPACE + 'widget-textarea'; var DIALOG_ID = NAMESPACE + 'dialog'; var DIALOG_TEXTAREA_ID = NAMESPACE + 'dialog-textarea'; var MIN_TEXTAREA_HEIGHT = 100; var AUTO_SAVE_DELAY_MS = 1000; var SEARCH_TYPE_DELAY_MS = 500;

var editorTimer, searchTimer, selectedPage;

/** * Loads all the notes for this community. The return value is an object where * the notes for each page are keyed under that page's name. * * @returns {Object} - the notes */ function loadNotes { var data = localStorage.getItem(COMMUNITY_ID); return data ? JSON.parse(data) : {}; }

/** * Loads any stored data for this page into the widget editor. */ function loadWidget { document.getElementById(WIDGET_TEXTAREA_ID).value = loadNotes[PAGE_NAME] || ''; }

/** * Saves the contents of the widget into local storage. */ function saveWidget { var value = document.getElementById(WIDGET_TEXTAREA_ID).value; var notes = loadNotes;

if (value.length) { notes[PAGE_NAME] = value; } else { delete notes[PAGE_NAME]; }

localStorage.setItem(COMMUNITY_ID, JSON.stringify(notes)); }

/** * Saves the contents of the dialog editor into local storage. */ function saveDialogEditor { var value = document.getElementById(DIALOG_TEXTAREA_ID).value; var notes = loadNotes;

if (value.length) { notes[selectedPage] = value; } else { delete notes[selectedPage]; }

localStorage.setItem(COMMUNITY_ID, JSON.stringify(notes));

if (selectedPage === PAGE_NAME) { document.getElementById(WIDGET_TEXTAREA_ID).value = value; } }

/** * Fires when the widget textarea loses focus. Saves the contents. */ function onWidgetBlur { clearTimeout(editorTimer); saveWidget; }

/** * Fires when the widget textarea contents change. Auto-adjusts the height of * the field, and sets a timer to save the contents if the user stops typing. */ function onWidgetInput { clearTimeout(editorTimer); var editor = document.getElementById(WIDGET_TEXTAREA_ID); editor.style.height = 'auto'; editor.style.height = Math.max(editor.scrollHeight, MIN_TEXTAREA_HEIGHT) + 'px'; editorTimer = setTimeout(saveWidget, AUTO_SAVE_DELAY_MS); }

/** * Fires when the search field contents change. Set a timer to filter the pages * list. */ function onSearchInput { clearTimeout(searchTimer); searchTimer = setTimeout(onSearch, SEARCH_TYPE_DELAY_MS); }

/** * Filters the pages list. */ function onSearch { var q = document.querySelector('#' + DIALOG_ID + ' input').value.trim.toLowerCase; Array.from(document.querySelectorAll('#' + DIALOG_ID + ' li')).forEach(function(li) { 	var match = li.textContent.toLowerCase.includes(q);  	li.style.display = match ? 'list-item' : 'none';  }); }

/** * Fires when the dialog textarea loses focus. Saves the contents. */ function onDialogEditorBlur { clearTimeout(editorTimer); saveDialogEditor; }

/** * Fires when the dialog textarea contents change. Sets a timer to save the * contents if the user stops typing. */ function onDialogEditorInput { clearTimeout(editorTimer); editorTimer = setTimeout(saveDialogEditor, AUTO_SAVE_DELAY_MS); }

/** * Fires when a page is clicked from the notes list. */ function onClickPage(ev) { onSelectPage(ev.target.dataset.page); }

/** * Fires when a page is selected. */ function onSelectPage(page) { var li = document.getElementById(NAMESPACE + 'page__' + selectedPage);

if (li) { li.classList.remove('selected'); }

li = document.getElementById(NAMESPACE + 'page__' + page);

if (li) { li.classList.add('selected'); }

var textarea = document.getElementById(DIALOG_TEXTAREA_ID); textarea.disabled = !page;

if (page) { textarea.value = loadNotes[page] || ''; textarea.disabled = false; textarea.focus; } else { textarea.value = ''; textarea.disabled = true; }

selectedPage = page; document.getElementById(DIALOG_ID).showModal; }

/** * Injects the stylesheet into the page. */ function buildStylesheet { var style = document.createElement('style'); style.textContent = [ '/* PersonalNotes styles */', '.' + NAMESPACE + 'widget {', ' margin-top: 24px;', ' border-bottom: solid var(--theme-border-color) 1px;', '}', 	'.' + NAMESPACE + 'widget textarea {', ' display: block;', ' box-sizing: border-box;', ' width: 100%;', ' resize: none;', ' overflow: hidden;', ' font-family: inherit;', ' font-size: 100%;', ' color: var(--theme-page-text-color);', ' background-color: transparent;', ' border-color: transparent;', '}',   '.' + NAMESPACE + 'widget textarea:focus {', ' border-color: unset;', '}',   '.' + NAMESPACE + 'widget .button-panel {', ' text-align: right;', ' margin: 3px 0 24px 0;', '}',   '.' + NAMESPACE + 'widget button {', ' border: none;', ' color: var(--theme-page-background-color);', ' background-color: var(--theme-accent-color);', ' border-radius: 6px;', ' padding: 3px 12px;', ' cursor: pointer;', '}',   '#' + DIALOG_ID + ' {', ' box-sizing: border-box;', ' position: fixed;', ' height: 50%;', ' width: 60em;', ' background-color: var(--theme-page-background-color);', ' box-shadow: 0 0 3em #000;', ' border: solid var(--theme-accent-color) 3px;', ' border-radius: 6px;', ' padding: 0;', ' overflow: hidden;', '}',   '#' + DIALOG_ID + '::backdrop {', ' background-color: rgba(0, 0, 0, 0.7);', '}',   '#' + DIALOG_ID + ' h2 {', ' box-sizing: border-box;', ' height: 2.5em;', ' margin: 0;', ' padding: 12px 24px;', ' border-bottom: solid var(--theme-accent-color) 1px;', ' color: var(--theme-page-text-color);', '}',   '#' + DIALOG_ID + ' h2 > div {', ' box-sizing: border-box;', ' position: absolute;', ' top: 0;', ' right: 0;', '}',   '#' + DIALOG_ID + ' h2 > div > * {', ' display: inline-block;', ' vertical-align: middle;', '}',   '#' + DIALOG_ID + ' h2 input {', ' font-size: 0.8em;', ' color: var(--theme-page-text-color);', ' background-color: rgba(0, 0, 0, 0.5);', ' border: solid var(--theme-border-color) 1px;', ' border-radius: 1.5em;', ' padding: 0 0.75em;', '}',   '#' + DIALOG_ID + ' .close {', ' box-sizing: border-box;', ' width: 1.5em;', ' text-align: center;', ' font-size: 1.5em;', ' color: var(--theme-page-text-color);', ' cursor: pointer;', '}',   '#' + DIALOG_ID + ' .dialog-body {', ' box-sizing: border-box;', ' height: calc(100% - 2.5em - 18px);', ' position: relative;', '}',   '#' + DIALOG_ID + ' .pages-list {', ' box-sizing: border-box;', ' position: absolute;', ' top: 0;', ' left: 0;', ' width: 25em;', ' height: 100%;', ' overflow-x: hidden;', ' overflow-y: auto;', ' color: var(--theme-page-text-color);', '}',   '#' + DIALOG_ID + ' .pages-list li {', ' padding: 6px 48px 6px 24px;', ' line-height: 0.95;', ' text-indent: -12px;', ' cursor: pointer;', ' position: relative;', '}',   '#' + DIALOG_ID + ' .pages-list li.selected {', ' color: var(--theme-page-background-color);', ' background-color: var(--theme-accent-color);', '}',   '#' + DIALOG_ID + ' .pages-list li a {', ' position: absolute;', ' top: 0;', ' right: 0;', ' font-size: 2em;', ' display: none;', '}',   '#' + DIALOG_ID + ' .pages-list a:hover {', ' text-decoration: none;', '}',   '#' + DIALOG_ID + ' .pages-list li:not(.current):hover a {', ' display: block;', '}',   '#' + DIALOG_ID + ' .pages-list li.selected a {', ' color: var(--theme-page-background-color);', '}',   '#' + DIALOG_ID + ' .pages-list li.selected a:hover {', ' color: var(--theme-page-background-color);', '}',   '#' + DIALOG_ID + ' textarea {', ' display: block;', ' position: absolute;', ' top: 0;', ' right: 0;', ' box-sizing: border-box;', ' width: calc(35em - 6px);', ' height: 100%;', ' padding: 0.5em;', ' border: none;', ' border-left: solid var(--theme-accent-color) 1px;', ' resize: none;', ' font-family: inherit;', ' font-size: 100%;', ' color: var(--theme-page-text-color);', ' background-color: transparent;', '}',   '#' + DIALOG_ID + ' textarea:focus {', ' outline: none;', '}', ].join('\n'); document.head.appendChild(style); }

/** * Builds the notes editor. */ function buildWidget(railEl) { var widgetDiv = document.createElement('div'); widgetDiv.className = NAMESPACE + 'widget'; var h2 = document.createElement('h2'); h2.className = 'rail-module__header'; h2.textContent = 'Personal Notes'; widgetDiv.appendChild(h2); var textarea = document.createElement('textarea'); textarea.id = WIDGET_TEXTAREA_ID; textarea.autocomplete = false; textarea.placeholder = 'Whatever you write here is visible only to you.'; widgetDiv.appendChild(textarea); var buttonPanel = document.createElement('div'); buttonPanel.className = 'button-panel'; var button = document.createElement('button'); button.textContent = 'View notes list'; button.addEventListener('click', showDialog); buttonPanel.appendChild(button); widgetDiv.appendChild(buttonPanel); railEl.insertBefore(widgetDiv, railEl.firstChild); loadWidget; onWidgetInput; textarea.addEventListener('input', onWidgetInput); textarea.addEventListener('blur', onWidgetBlur); }

/** * Build the notes dialog. */ function buildDialog { var dialog = document.createElement('dialog'); dialog.id = DIALOG_ID; buildDialogHeader(dialog); var dialogBody = document.createElement('div'); dialogBody.className = 'dialog-body'; buildDialogContent(dialogBody); dialog.appendChild(dialogBody); document.body.appendChild(dialog); }

/** * Builds the dialog header bar. */ function buildDialogHeader(dialog) { var h2 = document.createElement('h2'); h2.className = 'mw-headline'; h2.textContent = 'Notes List'; var rightPanel = document.createElement('div'); var input = document.createElement('input'); input.type = 'search'; input.placeholder = 'Search notes'; input.addEventListener('input', onSearchInput); rightPanel.appendChild(input); var closeButton = document.createElement('div'); closeButton.className = 'close'; closeButton.textContent = '×'; closeButton.addEventListener('click', function { 	dialog.close;  }); rightPanel.appendChild(closeButton); h2.appendChild(rightPanel); dialog.appendChild(h2); }

/** * Build the contents of the dialog. */ function buildDialogContent(dialogBody) { var pagesList = document.createElement('div'); pagesList.className = 'pages-list'; var ul = document.createElement('ul'); pagesList.appendChild(ul); dialogBody.appendChild(pagesList); var textarea = document.createElement('textarea'); textarea.id = DIALOG_TEXTAREA_ID; textarea.autocomplete = false; textarea.placeholder = 'Whatever you write here is visible only to you.'; textarea.addEventListener('input', onDialogEditorInput); textarea.addEventListener('blur', onDialogEditorBlur); dialogBody.appendChild(textarea); }

/** * Displays the notes list dialog. */ function showDialog { clearTimeout(editorTimer); var keys = Array.from(Object.keys(loadNotes)).sort; selectedPage = keys.indexOf(PAGE_NAME) === -1 ? keys[0] : PAGE_NAME; var ul = document.querySelector('#' + DIALOG_ID + ' ul'); ul.innerHTML = ''; var li;

for (var i = 0; i < keys.length; i++) { var title = keys[i]; li = document.createElement('li'); li.id = NAMESPACE + 'page__' + title; li.dataset.page = title; li.textContent = title.replaceAll('_', ' '); li.addEventListener('click', onClickPage); li.classList.toggle('current', title === PAGE_NAME); li.classList.toggle('selected', title === selectedPage); var a = document.createElement('a'); a.href = mw.util.getUrl(title); a.textContent = '⮫'; a.title = 'Open page'; a.addEventListener('click', function(ev) {     ev.stopPropagation;    }); li.appendChild(a); ul.appendChild(li); }

onSelectPage(selectedPage); }

/** * Bootstraps the module. */ function init { var railEl = document.querySelector(RIGHT_RAIL_SELECTOR);

if (!railEl) { return; }

buildStylesheet; buildWidget(railEl); buildDialog; }

init; });