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.
// Scratchpad - A sandbox in your browser
// by Zhaofeng Li
// Code is very ugly, I know. :D

var scratch_var_curtab = ''; // contains a guid
// from http://stackoverflow.com/questions/105034/how-to-create-a-guid-uuid-in-javascript
function scratch_guid_s4() {
    return Math.floor((1 + Math.random()) * 0x10000)
       .toString(16)
       .substring(1);
};
function scratch_guid() {
    return scratch_guid_s4() + scratch_guid_s4() + '-' + scratch_guid_s4() + '-' + scratch_guid_s4() + '-' + scratch_guid_s4() + '-' + scratch_guid_s4() + scratch_guid_s4() + scratch_guid_s4();
}
function scratch_ui_setup() { // call scratch_setup() instead
    $( "#mw-content-text" ).prepend( "\
<div id='scratch'>\
    <div id='scratch-top' style='margin-bottom:3px;padding-top:5px;'>\
       <ul id='scratch-controls-left' class='scratch-controls' style='display:inline;'>\
          <li id='scratch-teardown' title='Close Scratchpad'>x</li>\
          <!--<li id='scratch-prefs' title='Toggle settings'>prefs</li>-->\
       </ul>\
       <ul id='scratch-tabs' style='display:inline;white-space:nowrap;'></ul>\
       <ul id='scratch-controls' class='scratch-controls' style='display:inline;'>\
           <li id='scratch-newtab' title='Create new tab'>+</li>\
           <li id='scratch-help' title='Show help information'>?</li>\
           <li id='scratch-fork' title='Fork current page'>fork</li>\
           <li id='scratch-preview' title='Preview current tab'>preview</li>\
           <li id='scratch-rename' title='Rename current tab'>rename</li>\
       </ul>\
    </div>\
<div id='scratch-body' style='padding:10px;border:1px solid rgba(0,0,0,0.2);border-radius:5px;background:rgba(0,0,0,0.2);min-height:100px;'>\
<textarea id='scratch-textarea' style='min-height:500px;border:0;border-radius:3px;'></textarea>\
    </div>\
</div>\
    " );
    var height = $( "#scratch" ).height();
    $( "#scratch" ).css( { 'opacity':'0', 'height':'0' } ); // hide it from view for now
    
    $( ".scratch-controls li" ).css( { 'cursor':'pointer', 'display':'inline', 'background':'#999', 'border-radius':'3px', 'padding':'2px 5px' } );
    $( ".scratch-controls li a" ).css( { 'color':'white' } );
    $( "#scratch-top a" ).css( { 'color':'rgba(0,0,0,0.8)' } )
    $( "#scratch-teardown" ).click( scratch_teardown );
    $( "#scratch-prefs" ).click( function() {
        $( "#scratch-body" ).append( "<div id='scratch-prefs-area' style='background:white;border-radius:3px;padding:10px;margin-top:10px;'><h1>Preferences</h1><button id='scratch-prefs-close'>Close</button><button id='scratch-prefs-reset'>Reset Scratchpad</button></div>" );
        scratch_ui_scrollTo( "#scratch-prefs-area" );
        $( "#scratch-prefs-close" ).click( function() {
            $( "#scratch-prefs-area" ).remove();
            scratch_ui_scrollTo( "#scratch" );
        } );
        $( "#scratch-prefs-reset" ).click( function() { // remove everything
            if( confirm( "Doing this will delete all tabs by clearing the Scratchpad storage. This is *irreversible*! Do you want to continue?" ) ) {
                scratch_teardown();
                var del = 0;
                for ( var i = localStorage.length - 1; i >= 0; i-- ) {
                    if ( localStorage.key( i ).substring( 0, 8 ) == "scratch_" ) {
                        console.log( "Removed " + localStorage.key( i ) );
                        localStorage.removeItem( localStorage.key( i ) );
                        del++;
                    }
                }
                alert( "Reset complete. " + del + " items removed from localStorage. Click the toolbox link to start a fresh Scratchpad." );
            }
        } );
    } );
    $( "#scratch-newtab" ).click( function() {
        scratch_storage_newTab( scratch_guid(), "New tab", "" );
        scratch_ui_refreshTabs();
    } );
    $( "#scratch-help" ).click( function(){
        var guid = scratch_guid();
        scratch_storage_newTab( guid, "Help", "\
== Basic wikitext ==\n\
''italic'' '''bold''' <s>strike</s> <ins>insert</ins>\n\
[[link]] [[link|caption]]\n\
[[File:Bad Title Example.png]] [[File:Bad Title Example.png|thumb|Text]]\n\
<ref>[http://example.com Reference]</ref>\n\
== Scratchpad ==\n\
* [+]: Create a new tab\n\
* [?]: Show help information\n\
* [fork]: Create a new tab with current page's source\n\
* [preview]: Preview the tab\n\
== References ==\n\
{{Reflist}}\
" );
        scratch_ui_refreshTabs();
        scratch_ui_selectTab( guid );
    } );
    $( "#scratch-fork" ).click( function() {
        console.log( "Generating preview..." );
        var title = mw.config.get( 'wgPageName' ); // get page name
        var indexphp = mw.config.get( 'wgServer' ) + mw.config.get( 'wgScript' ); // get url to index.php
        var url = indexphp + "?action=raw&title=" + encodeURIComponent(title);
        var guid = scratch_guid();
        scratch_storage_newTab( guid, title, "Loading source from [[" + title + "]], please wait...");
        scratch_ui_refreshTabs();
        scratch_ui_selectTab( guid );
        $.get( url, function( data ){
            scratch_storage_setTabContent( guid, data );
            scratch_ui_selectTab( guid );
        } );
    } );
    $( "#scratch-preview" ).click( function() { // really messy...
        $( "#scratch-preview-area" ).remove();
        var wikitext = scratch_storage_getTabContent( scratch_var_curtab );
        var api = mw.config.get( 'wgServer' ) + mw.config.get( 'wgScriptPath' ) + "/api.php"; // get url to index.php
        var url = api + "?action=parse&format=json&text=" + encodeURIComponent(wikitext);
        $( "#scratch-body" ).append( "<div id='scratch-preview-area' style='background:white;border-radius:3px;padding:10px;margin-top:10px;'><h1>Preview</h1><p>Loading, please wait...</p></div>" );
        scratch_ui_scrollTo( "#scratch-preview-area" );
        $.getJSON( url, function( data ) {
            html = data.parse.text['*'];
            $( "#scratch-preview-area p" ).remove(); // remove notice
            $( "#scratch-preview-area" ).append( "<a id='scratch-preview-close'>[Close preview]</a><a id='scratch-preview-expand'>[Expand to page]</a><div id='scratch-preview-page'>" + html + "</div>" ); // add preview
            $( "#scratch-preview-area .mw-editsection" ).hide(); // remove useless edit section links
            $( "#scratch-preview-close" ).click( function() {
                $( "#scratch-preview-area" ).remove();
                scratch_ui_scrollTo( "#scratch" );
            } );
            $( "#scratch-preview-expand" ).click( function() {
                var preview = $( "#scratch-preview-page" ).html();
                $( "#mw-content-text" ).html( preview );
                scratch_teardown();
            } )
        } );
    } );
    $( "#scratch-rename" ).click( function() {
        console.log( "Rename clicked on " + scratch_var_curtab );
        var curname = scratch_storage_getTabName( scratch_var_curtab );
        var newname = prompt( "Enter a new name for the tab:", curname );
        scratch_storage_setTabName( scratch_var_curtab, newname );
        scratch_ui_refreshTabs();
        scratch_ui_selectTab( scratch_var_curtab );
    } );
    $( "#scratch-textarea" ).keyup( function() {
        scratch_storage_setTabContent( scratch_var_curtab, $( "#scratch-textarea" ).val() );
    } );
    
    $( "#scratch" ).animate( { 'height':height }, 300, function() { // toggle height
        $( "#scratch" ).css( 'height', 'auto' ); // set height to auto
        $( "#scratch" ).animate( { 'opacity':'1' }, 300 );
    } );
}
function scratch_ui_teardown() { // call scratch_teardown() instead
    $( "#scratch" ).animate( { "opacity": "0" }, 300, function() {
        $( "#scratch" ).animate( { "height": "toggle" }, 300, function() {
            $( "#scratch" ).remove();
        } );
    } ); // close elegantly
}
function scratch_ui_scrollTo( element ) {
    // from http://www.abeautifulsite.net/blog/2010/01/smoothly-scroll-to-an-element-without-a-jquery-plugin/
    $( "html, body" ).animate( {
        scrollTop: $( element ).offset().top
    }, 500 );
}
function scratch_ui_addTab( guid ) { // add a tab to ui
    var name = scratch_storage_getTabName( guid );
    $( "#scratch-tabs" ).append( "<li id='scratch-tab-" + guid + "' style='cursor:pointer;display:inline;padding:5px;border:1px solid rgba(0,0,0,0.2);border-top-left-radius:5px;border-top-right-radius:5px;margin-right:5px;'><span style='display:inline-block;max-width:80px;text-overflow:ellipsis;overflow:hidden;vertical-align:middle;'>" + name + "</span> <a class='delete'>[x]</a></li>" );
    $( "#scratch-tab-" + guid ).click( function(){ // tab switching
        var guid = this.id.substring( 12 ); // "scratch-tab-123456"
        scratch_ui_selectTab( guid );
    } );  // bind the click event
    $( "#scratch-tab-" + guid + " .delete" ).click( function(){ // delete
        guid = this.parentNode.id.substring( 12 );
        scratch_storage_deleteTab( guid ); // bye
        scratch_ui_refreshTabs();
    } );  // bind the click event
}
function scratch_ui_selectTab( guid ) {
    console.log( "Selecting tab " + guid );
    tablist = scratch_storage_getTabList(); // get the tab list
    $( "#scratch-tabs li" ).css( { 'background':'white', 'border':'1px solid rgba(0,0,0,0.2)' } );
    if ( tablist.indexOf( guid ) == -1 ) { // not found...
        guid = tablist[0]; // there will always be one tab, scratch_storage_deleteTab() ensures this
    }
    scratch_var_curtab = guid; // set the current tab
    $( "#scratch-tab-" + guid ).css( { 'background':'rgba(0,0,0,0.2)', 'border-bottom':'none' } ); // highlight it
    $( "#scratch-textarea" ).val( scratch_storage_getTabContent( guid ) ); // populate the textarea
}
function scratch_ui_refreshTabs() {
    tablist = scratch_storage_getTabList();
    $( "#scratch-tabs li" ).remove(); // clear tabs
    for ( var i = 0; i < tablist.length; i++ ) {
        scratch_ui_addTab( tablist[i] );
    }
    if ( tablist.indexOf( scratch_var_curtab ) == -1 ) { // the current tab is gone
        scratch_ui_selectTab( tablist[0] ); // reset it
    } else {
        scratch_ui_selectTab( scratch_var_curtab ); // refresh the current tab
    }
}
function scratch_storage_access( key, value ) { // low-level access
    if ( typeof value === "undefined" ) {
        return localStorage.getItem( "scratch_" + key ); // web storage can only store strings, beware!
    } else {
        console.log( "Setting localStorage key 'scratch_" + key + "'' to " + value );
        localStorage.setItem( "scratch_" + key, value );
    }
}
function scratch_storage_delete( key ) { // low-level delete
    localStorage.removeItem( "scratch_" + key );
}
function scratch_storage_getTabList() {
    tablist = scratch_storage_access( "tabs" );
    if ( tablist != null ) {
        return JSON.parse( tablist );
    } else {
        return [];
    }
}
function scratch_storage_setTabList( value ) {
    return scratch_storage_access( "tabs", JSON.stringify( value ) );
}
function scratch_storage_accessTab( guid, value ) { // low-level tab access
    if ( typeof value === "undefined" ) {
        return JSON.parse( scratch_storage_access( "tab_" + guid ) );
    } else {
        scratch_storage_access( "tab_" + guid, JSON.stringify( value ) );
    }
}
function scratch_storage_deleteTab( guid ) { // delete tab in storage, does not refresh ui!
    scratch_storage_delete( "tab_" + guid );
    tablist = scratch_storage_getTabList();
    index = tablist.indexOf( guid );
    if ( index != -1 ) { // exists in tab list
        tablist.splice( index, 1 );
        scratch_storage_setTabList( tablist );
    } // if it doesn't exist in tablist, no actions are needed
    if ( tablist.length === 0 ) { // empty tablist?
        scratch_storage_newTab( scratch_guid(), "New tab", "" ); // create a new tab to prevent tablist being empty
    }
    // do scratch_ui_refreshTabs() after you call this
}
function scratch_storage_getTabName( guid ) {
    return scratch_storage_accessTab( guid ).name;
}
function scratch_storage_getTabContent( guid ) {
    return scratch_storage_accessTab( guid ).content;
}
function scratch_storage_setTabContent( guid, content ) { // tabs not in tablist are inaccessible (hidden tab is coming soon)!
    var tab = scratch_storage_accessTab( guid );
    tab.content = content;
    scratch_storage_accessTab( guid, tab );
}
function scratch_storage_setTabName( guid, name ) { // tabs not in tablist are inaccessible (hidden tab is coming soon)!
    var tab = scratch_storage_accessTab( guid );
    tab.name = name;
    scratch_storage_accessTab( guid, tab );
}
function scratch_storage_newTab( guid, name, content ){ // create or overwrite a tab
    console.log( "Creating new tab: GUID='" + guid + "', name='" + name + "', content='" + content + "'");
    var tab = {
        "name": name,
        "content": content
    }; // create the tab object
    scratch_storage_accessTab( guid, tab ); // put the tab into the storage (not in the tab list yet)
    tablist = scratch_storage_getTabList(); // get the tab list
    if ( tablist.indexOf( guid ) == -1 ) { // non-existant
        tablist.push( guid ); // push it into the list
        scratch_storage_setTabList( tablist );
    } // if it exists, no actions are needed
}
function scratch_storage_init() {
    console.log( "Initializing storage..." );
    if ( scratch_storage_getTabList().length == 0 ) { // first time user
        console.log( "First time user, creating a welcome tab..." );
        var guid = scratch_guid(); // generate a guid
        scratch_storage_newTab( guid, "Welcome", "\
== Welcome to Scratchpad! ==\n\
Want to try out a new template quickly without sandboxes? Want to keep a track of your patrol progress? Want to have a lot of sandboxes but don't want get your userspace messy? Scratchpad is the answer!\n\n\
Scratchpad is a sandbox which resides entirely in your browser, saved automatically as you type. Changes are echoed between multiple pages with Scratchpad opened in your browser. You can create multiple tabs, experiment with wikitext, preview and delete them, without making any changes to the actual wiki.\n\
=== Try it out! ===\n\
Everything you expect from a normal sandbox is here. ''Italic'', '''bold''', <s>strike</s>, [[WP:SANDBOX|links]] and all other magic works on Scratchpad. Hit ''Preview'' and see it for yourself!\n\
{{mbox|text=Of course, templates work as well.}}\n\n\
You can even use [[WP:SUBST|template subsitutions]] and [[WP:SIGN|signatures]]! Try them out!\n\n\
Open another browser tab, fire up Scratchpad and change this line - does the other one reflect the changes?\n\n\
=== Before you continue... ===\n\
Remember that your Scratchpad is visible to all other scripts and sent to the server every time you use preview. It's not completely private.\n\
=== Any suggestions? ===\n\
If you have any suggestions, please send them to [[User talk:Zhaofeng Li]], Scratchpad's maker.\n\
\n\
Enjoy Scratchpad!\n\
" );
    }
    scratch_ui_refreshTabs();
    // listen for storage changes
    $( window ).bind( 'storage', scratch_storage_listener );
}
function scratch_storage_listener( event ) { // storage update event
    console.log( "Storage event catched!" );
    event = event.originalEvent; // remove jquery's wrapper
    key = event.key.substring( 8 ); // get the part after "scratch_"
    if ( key == "tabs" ) { // the tablist has changed
        scratch_ui_refreshTabs();
    } else { // one of the tabs has changed
        guid = key.substring( 4 ); // get the part after "tab_"
        if ( guid == scratch_var_curtab ) { // that's what we are looking at as well!
            scratch_ui_selectTab( guid ); // refresh the tab content
        }
    }
}
function scratch_setup() {
    $( scratch_var_portlet ).hide();
    scratch_ui_setup();
    console.log( "Scratchpad is starting..." );
    if ( typeof( localStorage ) == "undefined" ) {
        console.log( "No Web Storage support! Aborting..." );
        $( "#scratch-body" ).append( "Failed to initialize storage! Scratchpad needs HTML5 Web Storage support to work, try changing to a newer browser. Sorry about that." );
        return;
    }
    scratch_storage_init();
}
function scratch_teardown() {
    console.log( "Scratchpad is closing down..." );
    // remove listeners
    $( window ).unbind( 'storage', scratch_storage_listener );
    scratch_ui_teardown(); // remove the ui
    $( scratch_var_portlet ).show();
}

// Add portlet link
var scratch_var_portlet = mw.util.addPortletLink( "p-tb", "#", "Scratchpad");
$( scratch_var_portlet ).click( scratch_setup );