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.
(function( $, mw ) {
    'use strict';

    var api = new mw.Api({});

    var app = {
        width: 30,
        styleSheet: mw.util.addCSS(`
            /* VARIABLES */
            :root {
                --notepad-width: 30vw;
                --speed: 50ms;
            }

            /* TRANSITIONS */
            body, 
            #p-personal,
            #right-navigation,
            .mw-header,
            .mw-workspace-container {
                transition: all var(--speed) ease-out;
            }
            #notepad-window {
                z-index: 101;
                transition: margin var(--speed) ease-out, 
                    width var(--speed) ease-out;
            }

            /* RULES FOR LEGACY VECTOR */    
            body.notepad.skin-vector-legacy {
                margin-right: var(--notepad-width);
            }
            body.notepad.skin-vector-legacy #p-personal {
                margin-right: var(--notepad-width);
            }
            body.notepad.skin-vector-legacy #right-navigation {
                margin-right: var(--notepad-width);
            }            
            body.notepad.skin-vector-legacy #simpleSearch {
                width: 10em;
            }

            /* RULES FOR NEW VECTOR */
            @media (max-width:2000px) {
                body.notepad.skin-vector:not(.skin-vector-legacy) {
                    margin-right: var(--notepad-width);
                }
                body.notepad.skin-vector:not(.skin-vector-legacy) .mw-header #p-search {
                    min-width: inherit;
                    width: inherit;
                }
            }   
            @media (min-width:2000px) {
                body.notepad.skin-vector:not(.skin-vector-legacy) .mw-header {
                    padding-right: max(calc(20px + var(--notepad-width) - ((100vw - 100%) / 2)), 0px);
                }
                body.notepad.skin-vector:not(.skin-vector-legacy) .mw-workspace-container {
                    padding-right: max(calc(20px + var(--notepad-width) - ((100vw - 100%) / 2)), 0px);
                }
            }

            /* NOTEPAD STYLES */
            body:not(.notepad) #notepad-window {
                margin-right: calc(-1 * var(--notepad-width));
                visibility: hidden;;
                transition: 
                    visibility var(--speed) ease-out,
                    margin var(--speed) ease-out, 
                    width var(--speed) ease-out;
            }
            #notepad-icon {
                position:fixed;
                right:-2px;
                top:40px;
                transform-origin: 100% 0;
                transform: rotate(90deg) translateX(100%);
                border: 2px solid #a7d7f9;
                border-radius: 0 0 5px 5px;
                box-shadow: 0 0 4px 2px #eee;
                padding: 0px 10px 1px;
                background-color: #f5faff;
                color: #0645ad;
                font-size: 13px;
                z-index:102;
                transition: all var(--speed) ease-out;
            }
            #notepad-icon:hover {
                cursor: pointer;
                background-color: #a7d7f9;
            }
            body.notepad #notepad-icon {
                right: var(--notepad-width)
            }
            body.notepad #notepad-icon, #notepad-icon.selected {
                background-color: #a7d7f9;
                font-weight: bold;
            }            
            #notepad-window {
                position:fixed; 
                top:0; 
                right:0; 
                bottom: 0; 
                width: var(--notepad-width); 
                border-left: 2px solid #a7d7f9; 
                box-shadow: 0px 0px 4px 2px #eee; 
                background-color: #fcfdfe; 
            }
            #notepad-window:before {
                top: -100px; 
                content: ''; 
                position:absolute; 
                left: -30px; 
                width: 0; 
                height: 0; 
                border: 15px solid transparent; 
                border-right-color: #a7d7f9; 
                margin-top:-15px;
            }
            #notepad-window:after {
                top: -100px; 
                content: ''; 
                position:absolute; 
                left: -25px; 
                width: 0; 
                height: 0; 
                border: 15px solid transparent; 
                border-right-color: transparent; 
                margin-top:-15px;
            }
            #notepad-window #notepad-slider {
                position:absolute; 
                top: 20px; 
                left: -10px; 
                bottom: 0; 
                width: 15px; 
                opacity: 0; 
                cursor: ew-resize; 
            }
            #notepad-window #notepad-container {
                height: 100%; 
                width: 100%; 
                padding: 0px 10px; 
                line-height: 1.6; 
                font-size: 0.875em;
            }
            #notepad-window article {
                position:absolute;
                top: 50px;
                bottom: 0px;
                width: calc(100% - 15px);
                overflow: auto;
            }
            #notepad-window textarea:focus {
                outline-color: transparent;
            }

            #notepad-container:not([status=saving]) #notepad-saving {
                display:none;
            }
            #notepad-container #notepad-saving {
                position:fixed;
                top: 0px;
                right: 0px;
                font-size: 10px;
                padding: 2px;
                width: 40px;
                text-align:center;
                background-color: #fef6e7;
            }

            #notepad-container:not([tab=edit]) #notepad-edit,
            #notepad-container:not([tab=render]) #notepad-render,
            #notepad-container:not([tab=all]) #notepad-all {
                visibility: hidden;
                opacity: 0;
            }
            #notepad-container article {
                transition: visibility var(--speed) ease-out, 
                    opacity var(--speed) ease-out;
            }

            #notepad-container #notepad-header {
                border-bottom: 1px solid #ccc;
                padding: 8px 0;
                margin: 0;
                white-space: nowrap;
            }
            #notepad-container #notepad-header a {
                padding: 10px 20px;
                font-weight: bold;
                color: black;
                font-size: 12px;
                border-bottom: 2px solid #0645ad00;
                transition: color var(--speed) ease-out, 
                    border var(--speed) ease-out;            
            }
            #notepad-container[tab=edit] #notepad-link-edit,
            #notepad-container[tab=render] #notepad-link-render,
            #notepad-container[tab=all] #notepad-link-all,
            #notepad-container #notepad-header a:hover {
                color: #0645ad;
                border-bottom: 2px solid #0645ad;
                text-decoration: none;
            }

            #notepad-edit textarea {
                height: 100%;
                width: 100%;
                background-color: transparent;
                border: none;
                resize: none;
            }
            #notepad-container #loading {
                position:absolute;
                top: 50%;
                left: 50%;
                transform: translate(-50%, -50%);
                color: #a7d7f9;
            }
            #notepad-container #loading img {
                width: 30px;
                height: 30px;
                opacity:0.5;
            }

            #notepad-container #notepad-all section {
                box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2);
                width: calc(100% - 32px);
                margin:10px auto;
                padding:10px;
				clear: both;
                overflow: auto;
            }
            #notepad-container #notepad-all section:hover {
                box-shadow: 0 4px 8px 0 rgba(6,69,173,0.2);
                outline: 2px solid #a7d7f9;
            }
            #notepad-container #notepad-all section h4 {
                color: #0645ad;
                padding: 0;
            }
            #notepad-container #notepad-all section > .content {
                font-family: monospace;
                overflow: hidden;
				margin-bottom: 5px;
            }
            #notepad-container #notepad-all section:not([expanded]) > .content {
                max-height: 90px;
            }
			#notepad-container #notepad-all section > .updated {
				font-size: 0.8em;
				color: gray;
				float: left;
			}
			#notepad-container #notepad-all section > .links {
				font-size: 0.8em;
				float: right;
				margin-left: 10px;
			}
			#notepad-container #notepad-all section > .links.confirm {
				color: red;
				font-weight: bold;
			}

        `),
        init: function(oninit, onload) {
            app.onload = onload;
            mw.loader.using(["mediawiki.util", "mediawiki.user"]).then(function () {
                app.$link = $('<div>', {'id': 'notepad-icon', 'title': 'Open notepad [ctrl+alt+n]'})
                    .append($('<span>').text('Note'))
                    .append($('<span>').text('pad'))
                    .appendTo("body");

                app.$link.click(app.click);

                document.addEventListener('keydown', function(e) {
                    if (e.altKey && e.ctrlKey && e.code == 'KeyN') {
                        app.$link.click(); //Ctrl+Alt+N
                    }
                });

                oninit();
                app.load();
            })
        },
        click: function(e) {
            e.preventDefault();
			app.start();
        },
		start: function(tab) {
            app.load();
			app.resize();
			if (tab) {
				$('#notepad-link-' + tab).click();
			}
			$("body").toggleClass('notepad');
		},
        load: function() {
            if (!app.loaded) {
                app.loaded=true;
                app.$window = $("<div>", {'id': 'notepad-window'}).appendTo("body");
                app.$container = $("<div>", {'id': 'notepad-container'}).appendTo(app.$window);

                app.$window.append(
                    $("<div>", {'id': 'notepad-slider'})
                    .mousedown(function(e) {
                        if ($('body').hasClass('notepad')) {
                            function mouseup() {
                                $(document).off("mousemove", app.mousemove);
                                app.resize();
                            }
                            if (e.which==1) {
                                mouseup();
                                app.mousemove = function(e) {
                                    e.preventDefault();
                                    var x = e.pageX;
                                    if (x * 2 < window.innerWidth) x = (window.innerWidth/2)+1
                                    if (window.innerWidth - x < 100) x = window.innerWidth - 99
                                    var vw = ((1 - ((x+15) / window.innerWidth)) * 100);
                                    app.resize(vw + "vw");
                                }
                                $(document).mousemove(app.mousemove);
                                $(document).mouseup(mouseup);
                            }
                        } else {
                            app.start();
                        }
                    })
                );
                app.onload();
            }
        },
        resize: function(val, suppresstrigger) {
            if (val) { 
                $(':root').css('--notepad-width', val);
            }

            if (!suppresstrigger) window.dispatchEvent(new Event('resize'));
        }
    }

    var util = {
        get pagename() {
            var ns = mw.config.get('wgNamespaceNumber');
            var page = mw.config.get('wgPageName');
            var user = mw.config.get('wgRelevantUserName');
            if (ns % 2 == 1) {
                var p1 = mw.config.get('wgFormattedNamespaces')[ns].replace(' ', '_');
                var p0 = mw.config.get('wgFormattedNamespaces')[ns-1].replace(' ', '_');
                var r = new RegExp("^" + p1 + ':');
                page = page.replace(r, ns==1 ? p0 : p0 + ':');
                console.log(page);
            }
            if (ns == -1 && user) {                
                page = 'User:' + user.replace(' ', '_');
            }

            return page;
        },
        get now() {
            function pad(n) {
                if (n<10) {
                    return '0' + n;
                } else {
                    return '' + n;
                }
            }
            var d = new Date()
            return d.getUTCFullYear() + '-' + pad(d.getUTCMonth()+1) + '-' + pad(d.getUTCDate()) + 'T' 
            + pad(d.getUTCHours()) + ':' + pad(d.getUTCMinutes()) + ':' + pad(d.getUTCSeconds()) + 'Z';
        }
    }

    var notes = {
        get: function() {
            var s = mw.user.options.get("userjs-pagenotes");
            if (s && s.length) {
                var obj = JSON.parse(s);
                return obj[util.pagename]
            } else {
                return "";
            }
        },
        refresh: function() {
            var deferred = new $.Deferred();
            api.get({
                action: 'query',
                meta: 'userinfo',
                uiprop: 'options'
            }).done(function (response, data) {
                var s = data.responseJSON.query.userinfo.options['userjs-pagenotes'];
                mw.user.options.set('userjs-pagenotes', s);
                deferred.resolve(s);
            });
            return deferred.promise();
        },
        save: function(content) {
            var deferred = new $.Deferred();
            var cur = mw.user.options.get("userjs-pagenotes");
            var obj = {};
            if (cur && cur.length) {
                obj = JSON.parse(cur);
            }
            if (!obj[util.pagename] || (obj[util.pagename] && (content != obj[util.pagename].content))) {
                if (content) {
                    obj[util.pagename] = {
                        'updated': util.now,
                        'content': content
                    };
                } else {
                    delete obj[util.pagename]
                }
                var out = JSON.stringify(obj);
                mw.user.options.set("userjs-pagenotes", out);
                api.saveOption("userjs-pagenotes", out).then(function() {
                    deferred.resolve();
                });
            } else {
                deferred.resolve();
            }
            return deferred.promise();
        },
		delete: function(key) {
			var deferred = new $.Deferred();
			var cur = mw.user.options.get("userjs-pagenotes");
			var obj = {};
			if (cur && cur.length) {
				obj = JSON.parse(cur);
			}
			if (obj[key]) {
				delete obj[key];
				var out = JSON.stringify(obj);
				mw.user.options.set("userjs-pagenotes", out);
				api.saveOption("userjs-pagenotes", out).then(function() {
					deferred.resolve();
				});
			} else {
				deferred.resolve();
			}
			return deferred.promise();
		},
        init: function() {
            var $link = app.$link;
            if (notes.get()) {
                $link.addClass('selected');
            }

            var param = mw.util.getParamValue('notepad');
            if (param) {
                app.start(param);
            }
        },
        render: function(wikitext, $target) {
            var deferred = new $.Deferred();
            if (wikitext && wikitext.length) {
                api.parse(wikitext, {
                    'contentmodel': 'wikitext', 
                    'preview': true, 
                    'pst': true
                })
                .then(function(data) {
                    $target.empty().append($(data));
                    $target.find('.mw-editsection').remove();
                    deferred.resolve();
                })
            } else {
                $target.empty();
                deferred.resolve();
            }
            return deferred.promise();
        },
        onload: function() {
            var $container = app.$container
            $container.attr('tab', 'edit');

            var $topbar = $("<nav>", {'id': 'notepad-header'})
                .appendTo($container)
                .append($("<a>", {'id': 'notepad-link-edit'}).text("Notes")
                    .click(function () {
                        $container.attr('tab', 'edit')                        
                    }))
                .append($("<a>", {'id': 'notepad-link-render'}).text("Preview")
                    .click(function() {
                        $container.attr('tab', 'render');
                        tabs.render.draw();
                    }))
                .append($("<a>", {'id': 'notepad-link-all'}).text("Recent notes")
                    .click(function() {
                        $container.attr('tab', 'all');
                        tabs.edit.save()
                            .then(tabs.allnotes.draw);
                    }))

            var tabs = {
                edit: new function() {
                    //draw edit window
                    var $editwindow = $("<article>", {'id': 'notepad-edit'}).appendTo($container);
                    var $saving = $('<div>', {'id': 'notepad-saving'}).appendTo($editwindow);
                    $saving.append('Saving...');
                    var $textarea = $("<textarea>", 
                        {'id': 'notepad-text', 'placeholder': 'Record a private note here...'})
                        .appendTo($editwindow);

                    this.draw = function() {
                        if (notes.get()) {
                            $textarea.val(notes.get().content);
                        } else {
                            $textarea.val('');
                        }
                    }
                    this.draw();
        
                    this.save = function() {
                        var deferred = new $.Deferred();
                        var val = $textarea.val();
                        notes.save(val).then(function() {
                            $container.attr('status', 'saved');
                            deferred.resolve();
                        });
                        return deferred.promise();
                    }
                    
                    var edit = this;
                    var timer = 0;
                    $textarea.change(function() {
                        clearTimeout(timer);
                        $container.attr('status', 'saving');
                        edit.save();
                    });
                    $textarea.keyup(function() {
                        clearTimeout(timer);
                        $container.attr('status', 'saving');
                        timer = setTimeout(function() {
                            edit.save()
                        }, 1000);
                    });        
                },
                render: new function() {
                    //draw render window
                    var $renderwindow = $("<article>", {'id': 'notepad-render'}).appendTo($container);
                    $('<div>', {'id': 'loading'})
                        .append($('<img>', {'src': "https://upload.wikimedia.org/wikipedia/commons/3/30/Chromiumthrobber.svg"}))
                        .appendTo($renderwindow);

                    this.draw = function() {
                        var s = "";
                        if (notes.get()) s = notes.get().content;
                        notes.render(s, $renderwindow);
                    }
                    this.draw();
                },
                allnotes: new function() {
                    //draw all notes window
                    var $allnotes = $("<article>", {'id': 'notepad-all'}).appendTo($container);
                    $('<div>', {'id': 'loading'})
                        .append($('<img>', {'src': "https://upload.wikimedia.org/wikipedia/commons/3/30/Chromiumthrobber.svg"}))
                        .appendTo($allnotes);

                    this.draw = function() {
                        $allnotes.empty();
                        var s = mw.user.options.get("userjs-pagenotes");
                        if (s && s.length) {
                            var list = JSON.parse(s);
                            var keys = Object.keys(list).sort(
                                function(a,b) {
                                    if (list[a].updated > list[b].updated) return -1;
                                    if (list[a].updated < list[b].updated) return 1;
                                    return 0;
                                }
                            );
        
                            for (var i=0; i<keys.length; i++) {
                                var $sec = $('<section>', {'key': keys[i]}).appendTo($allnotes);
                                var $h4 = $('<h4>').appendTo($sec);
                                if (keys[i] === util.pagename) {
                                    $h4.append(
                                        $('<a>', {'href': '#'})
                                        .text(keys[i].replace(/_/g, ' '))
                                        .click(function (e) {
                                            e.preventDefault();
                                            $('#notepad-link-edit').click();
                                        })
                                    );
                                } else {
                                    $h4.append(
                                        $('<a>', {'href': '/wiki/' + keys[i] + '?notepad=notes'})
                                        .text(keys[i].replace(/_/g,' '))
                                    );
                                }
                                var $preview = $('<div>', {'class': 'content'}).appendTo($sec);
        
                                var content = list[keys[i]].content;
                                $preview.append(content);
        
                                var updated = list[keys[i]].updated.replace(/[TZ]/g,' ');
                                $('<div>', {'class': 'updated'}).text(updated).appendTo($sec);
        
                                $('<a>', {'class': 'links', 'href': '#'})
                                    .text('delete')
                                    .click(function (e) {
                                        e.preventDefault();
                                        var $link = $(e.target);
                                        var $section = $link.parent();
                                        if ($link.text() === 'delete') {
                                            $link.text('confirm delete');
                                            $link.addClass('confirm');
                                            $section.focusout(function () {
                                                $link.text('delete');
                                                $link.removeClass('confirm');
                                                $section.off('focusout');
                                            });
                                        } else {
                                            var key = $section.attr('key');
                                            notes.delete($section.attr('key'))
                                            .then(function() {
                                                if (key === util.pagename) {
                                                    tabs.edit.draw();
                                                }    
                                            });
                                            $section.remove();
                                        }							
                                    })
                                    .appendTo($sec);
        
                                if ($preview[0].scrollHeight > $preview[0].offsetHeight) {                            
                                    $('<a>', {'class': 'links', 'href': '#'})
                                        .text('expand')
                                        .click(function (e) {
                                            e.preventDefault();
                                            var $section = $(e.target.parentNode);
                                            if ($section.attr('expanded')==='') {
                                                $(e.target).text('expand');
                                                $section.removeAttr('expanded');
                                            } else {
                                                $(e.target).text('collapse');
                                                $section.attr('expanded', '');
                                            }
                                        })
                                        .appendTo($sec);
                                }
                            }
                        }
                    }
                }
            }

            //refresh from server on focus
            window.addEventListener('focus', function() {
                notes.refresh().then(function () {
                    tabs.edit.draw();
                    tabs.render.draw();
                    tabs.allnotes.draw();
                });                
            });
        },
    }

    app.init(notes.init, notes.onload);

} (jQuery, mediaWiki ));