User:Zhaofeng Li/Scratchpad.js

// 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( "\

\

\

    \

  • x
  • \

    \

\

    \

      \

    • +
    • \

    • ?
    • \

    • fork
    • \

    • preview
    • \

    • rename
    • \

    \

    \

    \

    \

    \

    \

    " );

    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( "

    Preferences

    " );

    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 strike insert\n\

    link caption\n\

    File:Bad Title Example.png File:Bad Title Example.png\n\

    [http://example.com Reference]\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( "

    Preview

    Loading, please wait...

    " );

    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( "[Close preview][Expand to page]

    " + html + "
    " ); // 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( "

  • " + name + " [x]
  • " );

    $( "#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, strike, 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 template subsitutions and 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 );