TheDJ has uploaded a new change for review. https://gerrit.wikimedia.org/r/159626
Change subject: Drafts: this time using indexeddb ...................................................................... Drafts: this time using indexeddb Advantadges: - doesn't fill up storage - fully async Disadvantadge: - requires WebSQL based shim in order to support Safari and older IE. Other changes - Does 30 day expiry - No longer a max # of drafts - Seperate module - Added a preference for this Could also easily add a Special:Drafts page to manage these. Wondering if this thing should be renamed from drafts to 'backups' or something. Change-Id: I0ea580e4acc6ed539f4314ab3f5378f95a451c1c --- M includes/DefaultSettings.php M includes/EditPage.php M includes/Preferences.php M languages/i18n/en.json M resources/Resources.php A resources/src/mediawiki.action/mediawiki.action.edit.drafts.js M resources/src/mediawiki.action/mediawiki.action.edit.js 7 files changed, 192 insertions(+), 195 deletions(-) git pull ssh://gerrit.wikimedia.org:29418/mediawiki/core refs/changes/26/159626/1 diff --git a/includes/DefaultSettings.php b/includes/DefaultSettings.php index 26ce83c..1ce75fd 100644 --- a/includes/DefaultSettings.php +++ b/includes/DefaultSettings.php @@ -4283,6 +4283,7 @@ 'wllimit' => 250, 'useeditwarning' => 1, 'prefershttps' => 1, + 'localdrafts' => 1, ); /** diff --git a/includes/EditPage.php b/includes/EditPage.php index a14191a..8c55dcb 100644 --- a/includes/EditPage.php +++ b/includes/EditPage.php @@ -2066,6 +2066,10 @@ $wgOut->addModules( 'mediawiki.action.edit.editWarning' ); } + if ( $wgUser->getOption( 'localdrafts', false ) ) { + $wgOut->addModules( 'mediawiki.action.edit.drafts' ); + } + $wgOut->setRobotPolicy( 'noindex,nofollow' ); # Enabled article-related sidebar, toplinks, etc. diff --git a/includes/Preferences.php b/includes/Preferences.php index eb29e41..20adb40 100644 --- a/includes/Preferences.php +++ b/includes/Preferences.php @@ -834,6 +834,11 @@ 'section' => 'editing/editor', 'label-message' => 'tog-useeditwarning', ); + $defaultPreferences['localdrafts'] = array( + 'type' => 'toggle', + 'section' => 'editing/editor', + 'label-message' => 'tog-localdrafts', + ); $defaultPreferences['showtoolbar'] = array( 'type' => 'toggle', 'section' => 'editing/editor', diff --git a/languages/i18n/en.json b/languages/i18n/en.json index 196b491..9ced786 100644 --- a/languages/i18n/en.json +++ b/languages/i18n/en.json @@ -42,6 +42,7 @@ "tog-norollbackdiff": "Omit diff after performing a rollback", "tog-useeditwarning": "Warn me when I leave an edit page with unsaved changes", "tog-prefershttps": "Always use a secure connection when logged in", + "tog-localdrafts": "Back-up unsaved changes using drafts on your computer", "underline-always": "Always", "underline-never": "Never", "underline-default": "Skin or browser default", diff --git a/resources/Resources.php b/resources/Resources.php index 7a3a011..b566216 100644 --- a/resources/Resources.php +++ b/resources/Resources.php @@ -991,19 +991,25 @@ 'mediawiki.action.edit.styles', 'jquery.textSelection', 'jquery.byteLimit', - 'mediawiki.notification', - 'jquery.throttle-debounce', ), 'position' => 'top', + ), + 'mediawiki.action.edit.styles' => array( + 'styles' => 'resources/src/mediawiki.action/mediawiki.action.edit.styles.css', + 'position' => 'top', + ), + 'mediawiki.action.edit.drafts' => array( + 'scripts' => 'resources/src/mediawiki.action/mediawiki.action.edit.drafts.js', + 'dependencies' => array( + 'mediawiki.notification', + 'jquery.throttle-debounce', + 'jquery.indexeddb', + ), 'messages' => array( 'textarea-draft-found', 'textarea-use-draft', 'textarea-use-current-version', ), - ), - 'mediawiki.action.edit.styles' => array( - 'styles' => 'resources/src/mediawiki.action/mediawiki.action.edit.styles.css', - 'position' => 'top', ), 'mediawiki.action.edit.collapsibleFooter' => array( 'scripts' => 'resources/src/mediawiki.action/mediawiki.action.edit.collapsibleFooter.js', diff --git a/resources/src/mediawiki.action/mediawiki.action.edit.drafts.js b/resources/src/mediawiki.action/mediawiki.action.edit.drafts.js new file mode 100644 index 0000000..a716882 --- /dev/null +++ b/resources/src/mediawiki.action/mediawiki.action.edit.drafts.js @@ -0,0 +1,168 @@ +/** + * Interface for the classic edit toolbar. + * + * @class mw.toolbar + * @singleton + */ +( function ( mw, $ ) { + var dbPromise, + indexedDB = window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window. +msIndexedDB, + conf = mw.config.get( [ + 'wgCookiePrefix', + 'wgAction', + 'wgPageName' + ] ), + draftsDBName = conf.wgCookiePrefix + "-mediawiki-drafts", + draftsTable = "drafts"; + + /** + * Open the database, do an update of the schema if required + */ + function openDraftsDatabase() { + dbOpenPromise = $.indexedDB( draftsDBName, { + "schema" : { + "1" : function( transaction ) { + var store = transaction.createObjectStore( draftsTable, { + keyPath: 'wgPageName' + } ); + store.createIndex( 'lastUpdated' ); + } + } + } ).done( function(db) { + mw.log( 'Database ' + draftsDBName + ' opened or created' ); + // Check if there is a draft for the current page and show window + } ).fail( function( db ) { + mw.log.warn( 'Database could not be opened' ); + } ); + return dbOpenPromise; + } + + function purgeDb() { + var parsed, starttime, threshold = new Date(); + + // We purge stuff after a draft has not been used in a month + threshold = threshold.getTime() - ( 30 * 24 * 60 * 60 * 1000 ); + + return $.indexedDB( draftsDBName ) + .transaction( draftsTable ) + .progress( function ( trans ) { + trans.objectStore( draftsTable ).index( 'lastUpdated' ).each( function ( item ) { + parsed = new Date( ) + parsed.setTime( item.value.lastUpdated ); + if ( threshold > parsed ) { + item.delete(); + } + } ) + } ) + .fail( function(error) { + mw.log.warn( 'something went wrong in the purging: ' + error); + }); + } + + function saveToDb( draftEntry ) { + return $.indexedDB( draftsDBName ) + .transaction( draftsTable ) + .progress( function ( trans ) { + trans.objectStore( draftsTable ) + .get( draftEntry.wgPageName ) + .done( function ( result /*, event*/ ) { + draftEntry.lastUpdated = (new Date()).getTime(); + + if ( !result ) { + mw.log( 'successfully saved: ' + draftEntry.wgPageName ); + trans.objectStore( draftsTable ).add( draftEntry ) + // TODO Check max size + } else { + mw.log( 'successfully updated: ' + draftEntry.wgPageName ); + trans.objectStore( draftsTable ).put( draftEntry ) + } + } ) + }); + } + + function getFromDb( pageName ) { + return $.indexedDB( draftsDBName ) + .objectStore(draftsTable) + .get( wgPageName ); + } + + function removeFromDb( pageName ) { + return $.indexedDB( draftsDBName ) + .objectStore(draftsTable) + .delete( wgPageName ); + } + + function textAreaAutoSaveInit() { + var pageName = conf.wgPageName, + $textArea = $( '#wpTextbox1' ), + $editForm = $( '#editform' ), + $wpStarttime = $editForm.find( '[name="wpStarttime"]' ), + $wpEdittime = $editForm.find( '[name="wpEdittime"]' ), + $wpSection = $editForm.find( '[name="wpSection"]' ); + + $textArea.on( 'input', $.debounce( 300, function () { + var draftEntry = { + wgPageName: pageName, + wpStarttime: $wpStarttime.val(), + wpEdittime: $wpEdittime.val(), + wpSection: $wpSection.val(), + content: $textArea.val() + }; + $.when( dbPromise, saveToDb( draftEntry ) ) + .fail( function ( error /*, errorEvent*/ ) { + mw.log.warn( 'could not save entry: ' + draftEntry.wgPageName + ' ' + error ); + }); + } ) ); + + $( '#wpSave, #mw-editform-cancel' ).click( function () { + $.when( dbPromise, removeFromDb( pageName ) ) + .done( function () { + mw.log('removed from DB: ' + pageName ); + } ); + } ); + + $.when( dbPromise ).done( function () { + getFromDb( pageName ).done( function ( currentDraft /*, event */ ) { + var node, notif, $replaceLink, $discardLink; + if ( currentDraft && $.trim( currentDraft ) !== $.trim( $textArea.val() ) ) { + $replaceLink = $( '<a>' ) + .text( mw.msg( 'textarea-use-draft' ) ) + .click( function ( e ) { + $textArea.val( currentDraft.content ); + $wpStarttime.val( currentDraft.wpStarttime ); + $wpEdittime.val( currentDraft.wpEdittime ); + $wpSection.val( currentDraft.wpSection ); + e.preventDefault(); + notif.close(); + } ); + + $discardLink = $( '<a>' ) + .text( mw.msg( 'textarea-use-current-version' ) ) + .click( function ( e ) { + $.when( dbPromise, removeFromDb( pageName ) ); + e.preventDefault(); + notif.close(); + } ); + + node = $( '<span>' ).append( + $( '<span>' ).text( mw.msg( 'textarea-draft-found' ) ), + $( '<br>' ), + $replaceLink, + document.createTextNode( ' · ' ), + $discardLink + ); + notif = mw.notification.notify( node, { autoHide: false } ); + } + } ); + } ); + } + + // TODO can be moved before document ready.. + if ( indexedDB && ( conf.wgAction === 'edit' || conf.wgAction === 'submit' ) ) { + dbPromise = openDraftsDatabase(); + $.when( dbPromise, purgeDb() ); + $.when( dbPromise, $.ready ).done( textAreaAutoSaveInit ); + } + +}( mediaWiki, jQuery ) ); \ No newline at end of file diff --git a/resources/src/mediawiki.action/mediawiki.action.edit.js b/resources/src/mediawiki.action/mediawiki.action.edit.js index 36b5cee..43f39f1 100644 --- a/resources/src/mediawiki.action/mediawiki.action.edit.js +++ b/resources/src/mediawiki.action/mediawiki.action.edit.js @@ -5,7 +5,7 @@ * @singleton */ ( function ( mw, $ ) { - var toolbar, isReady, $toolbar, queue, slice, $currentFocused, draftsIdx, + var toolbar, isReady, $toolbar, queue, slice, $currentFocused, conf = mw.config.get( [ 'wgCookiePrefix', 'wgAction', @@ -155,193 +155,6 @@ // Expose API publicly mw.toolbar = toolbar; - function saveToLocalStorage( draftEntry ) { - var oldestPage, - d, - size = 0, - draftsLimit = 10, - newEntry = { - 'a': draftEntry.wpStarttime, // Age, updated for each edit session - 't': draftEntry.wpStarttime, - 'e': draftEntry.wpEdittime, - 's': draftEntry.wpSection - }; - if ( !( draftEntry.pageName in draftsIdx ) ) { - for ( d in draftsIdx ) { - if ( draftsIdx.hasOwnProperty( d ) ) { - size++; - } - } - if ( size === draftsLimit ) { - oldestPage = oldestDraft(); - mw.log( 'Maximum of ' + draftsLimit + ' drafts reached. Removing a draft for ' + oldestPage ); - removeFromLocalStorage( oldestPage, false ); - } - mw.log( 'Adding a draft for ' + draftEntry.pageName ); - draftsIdx[draftEntry.pageName] = newEntry; - } - localStorage.setItem( conf.wgCookiePrefix + '-drafts' + draftEntry.pageName, draftEntry.content ); - writeDraftsIndex(); - } - - function oldestDraft() { - var pageName, - draftEntry, - oldestPage; - for ( pageName in draftsIdx ) { - draftEntry = draftsIdx[pageName]; - if ( oldestPage === undefined || draftEntry.a < draftsIdx[oldestPage].a ) { - oldestPage = pageName; - } - } - return oldestPage; - } - - function getLocalStorage( pageName ) { - var draftEntry = draftsIdx[pageName]; - if ( draftEntry ) { - return { - wpStarttime: draftEntry.t, - wpEdittime: draftEntry.e, - wpSection: draftEntry.s, - content: localStorage.getItem( conf.wgCookiePrefix + '-drafts' + pageName ) - }; - } - return null; - } - - /** - * @private - * @param {string} pageName - * @param {boolean} [updateIndex=true] - */ - function removeFromLocalStorage( pageName, updateIndex ) { - delete draftsIdx[pageName]; - localStorage.removeItem( conf.wgCookiePrefix + '-drafts' + pageName ); - if ( updateIndex === undefined || updateIndex === true ) { - writeDraftsIndex(); - } - } - - function readDraftsIndex() { - try { - draftsIdx = JSON.parse( localStorage.getItem( conf.wgCookiePrefix + '-draftsIdx' ) ); - } catch ( e ) { - mw.log.error( e ); - } - if ( !draftsIdx ) { - draftsIdx = {}; - } - } - - function writeDraftsIndex() { - localStorage.setItem( conf.wgCookiePrefix + '-draftsIdx', JSON.stringify( draftsIdx ) ); - } - - function purgeOldDrafts() { - var draftEntry, pageName, i, - parsed, - starttime, - toDelete = [], - threshold = new Date(); - - // We purge stuff after a draft has not been used in a month - threshold.setTime( threshold.getTime() - ( 30 * 24 * 60 * 60 * 1000 ) ); - for ( pageName in draftsIdx ) { - draftEntry = draftsIdx[pageName]; - parsed = draftEntry.a.match( /^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})$/ ); - starttime = new Date( Date.UTC( parsed[1], parsed[2] - 1, parsed[3], parsed[4], parsed[5], parsed[6] ) ); - if ( threshold > starttime ) { - toDelete.push( pageName ); - } - } - if ( toDelete.length ) { - for ( i = 0; i < toDelete.length; i++ ) { - pageName = toDelete[i]; - mw.log.warn( 'Removing draft older than 30 days for ' + pageName ); - removeFromLocalStorage( pageName, false ); - } - writeDraftsIndex(); - } - } - - function textAreaAutoSaveInit() { - // If there's no localStorage, there's no point in doing the checks or adding listeners. - if ( 'localStorage' in window && localStorage !== null && - ( conf.wgAction === 'edit' || conf.wgAction === 'submit' ) ) { - var pageName = conf.wgPageName, - currentDraft, - notif, - $replaceLink, - $discardLink, - $textArea = $( '#wpTextbox1' ), - $editForm = $( '#editform' ), - $wpStarttime = $editForm.find( '[name="wpStarttime"]' ), - $wpEdittime = $editForm.find( '[name="wpEdittime"]' ), - $wpSection = $editForm.find( '[name="wpSection"]' ), - node = $( '<span>' ); - - readDraftsIndex(); - purgeOldDrafts(); - - $textArea.on( 'input', $.debounce( 300, function () { - var draftEntry = { - pageName: pageName, - wpStarttime: $wpStarttime.val(), - wpEdittime: $wpEdittime.val(), - wpSection: $wpSection.val(), - content: $textArea.val() - }; - try { - saveToLocalStorage( draftEntry ); - } catch ( e ) { - // Assume any error is due to the quota being exceeded, - // per http://chrisberkhout.com/blog/localstorage-errors/ - mw.log.warn( 'Unable to save draft. localStorage is full.', e ); - removeFromLocalStorage( pageName ); - } - } ) ); - - $( '#wpSave, #mw-editform-cancel' ).click( function () { - removeFromLocalStorage( pageName ); - } ); - - currentDraft = getLocalStorage( pageName ); - if ( currentDraft && $.trim( currentDraft ) !== $.trim( $textArea.val() ) ) { - $replaceLink = $( '<a>' ) - .text( mw.msg( 'textarea-use-draft' ) ) - .click( function ( e ) { - var draftEntry = getLocalStorage( pageName ); - draftsIdx[pageName].a = $wpStarttime.val(); - writeDraftsIndex(); - $textArea.val( draftEntry.content ); - $wpStarttime.val( draftEntry.wpStarttime ); - $wpEdittime.val( draftEntry.wpEdittime ); - $wpSection.val( draftEntry.wpSection ); - e.preventDefault(); - notif.close(); - } ); - - $discardLink = $( '<a>' ) - .text( mw.msg( 'textarea-use-current-version' ) ) - .click( function ( e ) { - removeFromLocalStorage( pageName ); - e.preventDefault(); - notif.close(); - } ); - - node.append( - $( '<span>' ).text( mw.msg( 'textarea-draft-found' ) ), - $( '<br>' ), - $replaceLink, - document.createTextNode( ' · ' ), - $discardLink - ); - notif = mw.notification.notify( node, { autoHide: false } ); - } - } - } - $( function () { var i, b, editBox, scrollTop, $editForm; @@ -393,7 +206,6 @@ $currentFocused = $( this ); } ); - textAreaAutoSaveInit(); } ); }( mediaWiki, jQuery ) ); -- To view, visit https://gerrit.wikimedia.org/r/159626 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: newchange Gerrit-Change-Id: I0ea580e4acc6ed539f4314ab3f5378f95a451c1c Gerrit-PatchSet: 1 Gerrit-Project: mediawiki/core Gerrit-Branch: master Gerrit-Owner: TheDJ <hartman.w...@gmail.com> _______________________________________________ MediaWiki-commits mailing list MediaWiki-commits@lists.wikimedia.org https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits