diff --git src/wp-admin/css/dashboard.css src/wp-admin/css/dashboard.css
index d467f0c..80f0292 100644
--- src/wp-admin/css/dashboard.css
+++ src/wp-admin/css/dashboard.css
@@ -518,6 +518,10 @@ form.initial-form.quickpress-open input#title {
 	resize: none;
 }
 
+#quick-press.is-saving .spinner {
+	visibility: inherit;
+}
+
 /* Dashboard Quick Draft - Drafts list */
 
 .js #dashboard_quick_press .drafts {
@@ -541,9 +545,68 @@ form.initial-form.quickpress-open input#title {
 	margin: 0 12px;
 }
 
+#dashboard_quick_press .drafts ul.is-placeholder li {
+	padding: 3px 0;
+	color: transparent;
+}
+
+@-webkit-keyframes loading-fade {
+
+	0% {
+		opacity: 0.5;
+	}
+
+	50% {
+		opacity: 1;
+	}
+
+	100% {
+		opacity: 0.5;
+	}
+}
+
+@keyframes loading-fade {
+
+	0% {
+		opacity: 0.5;
+	}
+
+	50% {
+		opacity: 1;
+	}
+
+	100% {
+		opacity: 0.5;
+	}
+}
+
+#dashboard_quick_press .drafts ul.is-placeholder li:before,
+#dashboard_quick_press .drafts ul.is-placeholder li:after {
+	content: "";
+	display: block;
+	height: 13px;
+	background: #eee;
+	-webkit-animation: loading-fade 1.6s ease-in-out infinite;
+	animation: loading-fade 1.6s ease-in-out infinite;
+}
+
+#dashboard_quick_press .drafts ul.is-placeholder li:before {
+	margin-bottom: 5px;
+	width: 40%;
+}
+
+#dashboard_quick_press .drafts ul.is-placeholder li:after {
+	width: 80%;
+}
+
 #dashboard_quick_press .drafts li {
 	margin-bottom: 1em;
 }
+
+#dashboard_quick_press .drafts li.is-new {
+	background-color: #fffbe5;
+}
+
 #dashboard_quick_press .drafts li time {
 	color: #72777c;
 }
diff --git src/wp-admin/includes/dashboard.php src/wp-admin/includes/dashboard.php
index 40035f8..7c0f4b3 100644
--- src/wp-admin/includes/dashboard.php
+++ src/wp-admin/includes/dashboard.php
@@ -494,46 +494,54 @@ function wp_dashboard_quick_press( $error_msg = false ) {
 	$post_ID = (int) $post->ID;
 ?>
 
-	<form name="post" action="<?php echo esc_url( admin_url( 'post.php' ) ); ?>" method="post" id="quick-press" class="initial-form hide-if-no-js">
-
-		<?php if ( $error_msg ) : ?>
-		<div class="error"><?php echo $error_msg; ?></div>
-		<?php endif; ?>
-
+	<form name="post" method="post" id="quick-press" class="initial-form hide-if-no-js">
+		<div class="notice notice-error notice-alt inline hidden"><p></p></div>
 		<div class="input-text-wrap" id="title-wrap">
-			<label class="screen-reader-text prompt" for="title" id="title-prompt-text">
+			<label class="prompt" for="title" id="title-prompt-text">
 
 				<?php
 				/** This filter is documented in wp-admin/edit-form-advanced.php */
 				echo apply_filters( 'enter_title_here', __( 'Title' ), $post );
 				?>
 			</label>
-			<input type="text" name="post_title" id="title" autocomplete="off" />
+			<input type="text" name="title" id="title" autocomplete="off" />
 		</div>
 
 		<div class="textarea-wrap" id="description-wrap">
-			<label class="screen-reader-text prompt" for="content" id="content-prompt-text"><?php _e( 'What&#8217;s on your mind?' ); ?></label>
+			<label class="prompt" for="content" id="content-prompt-text"><?php _e( 'What&#8217;s on your mind?' ); ?></label>
 			<textarea name="content" id="content" class="mceEditor" rows="3" cols="15" autocomplete="off"></textarea>
 		</div>
-
 		<p class="submit">
-			<input type="hidden" name="action" id="quickpost-action" value="post-quickdraft-save" />
-			<input type="hidden" name="post_ID" value="<?php echo $post_ID; ?>" />
-			<input type="hidden" name="post_type" value="post" />
-			<?php wp_nonce_field( 'add-post' ); ?>
+			<div class="spinner no-float"></div>
 			<?php submit_button( __( 'Save Draft' ), 'primary', 'save', false, array( 'id' => 'save-post' ) ); ?>
 			<br class="clear" />
 		</p>
 
 	</form>
+	<div id="quick-press-drafts" class="drafts">
+		<p class="view-all" style="display: none;">
+			<a href="<?php echo esc_url( admin_url( 'edit.php?post_status=draft' ) ) ?>" aria-label="<?php esc_attr_e( 'View all drafts' ) ?>"><?php _ex( 'View all', 'drafts' ) ?></a>
+		</p>
+		<h2 class="hide-if-no-js"><?php _e( 'Drafts' ) ?></h2>
+		<ul class="drafts-list is-placeholder">
+			<li><span class="screen-reader-text"><?php _e( 'Loading&hellip;' ) ?></span></li>
+		</ul>
+	</div>
+	<script id="tmpl-item-quick-press-draft" type="text/template">
+		<div class="draft-title">
+			<a href="<?php echo ( esc_url( admin_url( 'post.php?post={{ data.id }}&action=edit' ) ) ); ?>" aria-label="<?php esc_attr_e( 'Edit Post' ) ?>">{{ data.formattedTitle }}</a>
+			<time datetime="{{ data.date }}">{{ data.formattedDate }}</time>
+		</div>
+		{{{ data.formattedContent }}}
+	</script>
 	<?php
-	wp_dashboard_recent_drafts();
 }
 
 /**
  * Show recent drafts of the user on the dashboard.
  *
  * @since 2.7.0
+ * @deprecated 4.8.0
  *
  * @param array $drafts
  */
@@ -548,13 +556,7 @@ function wp_dashboard_recent_drafts( $drafts = false ) {
 			'order'          => 'DESC'
 		);
 
-		/**
-		 * Filters the post query arguments for the 'Recent Drafts' dashboard widget.
-		 *
-		 * @since 4.4.0
-		 *
-		 * @param array $query_args The query arguments for the 'Recent Drafts' dashboard widget.
-		 */
+		/** This filter is documented in wp-includes/rest-api.php */
 		$query_args = apply_filters( 'dashboard_recent_drafts_query_args', $query_args );
 
 		$drafts = get_posts( $query_args );
diff --git src/wp-admin/js/dashboard.js src/wp-admin/js/dashboard.js
index fa100dd..6dee942 100644
--- src/wp-admin/js/dashboard.js
+++ src/wp-admin/js/dashboard.js
@@ -1,5 +1,5 @@
-/* global pagenow, ajaxurl, postboxes, wpActiveEditor:true */
-var ajaxWidgets, ajaxPopulateWidgets, quickPressLoad;
+/* global _, wp, quickDraft, pagenow, ajaxurl, postboxes */
+var ajaxWidgets, ajaxPopulateWidgets, QuickDraft = {};
 
 jQuery(document).ready( function($) {
 	var welcomePanel = $( '#welcome-panel' ),
@@ -59,69 +59,13 @@ jQuery(document).ready( function($) {
 	};
 	ajaxPopulateWidgets();
 
-	postboxes.add_postbox_toggles(pagenow, { pbshow: ajaxPopulateWidgets } );
-
-	/* QuickPress */
-	quickPressLoad = function() {
-		var act = $('#quickpost-action'), t;
-
-		$( '#quick-press .submit input[type="submit"], #quick-press .submit input[type="reset"]' ).prop( 'disabled' , false );
-
-		t = $('#quick-press').submit( function( e ) {
-			e.preventDefault();
-			$('#dashboard_quick_press #publishing-action .spinner').show();
-			$('#quick-press .submit input[type="submit"], #quick-press .submit input[type="reset"]').prop('disabled', true);
-
-			$.post( t.attr( 'action' ), t.serializeArray(), function( data ) {
-				// Replace the form, and prepend the published post.
-				$('#dashboard_quick_press .inside').html( data );
-				$('#quick-press').removeClass('initial-form');
-				quickPressLoad();
-				highlightLatestPost();
-				$('#title').focus();
-			});
-
-			function highlightLatestPost () {
-				var latestPost = $('.drafts ul li').first();
-				latestPost.css('background', '#fffbe5');
-				setTimeout(function () {
-					latestPost.css('background', 'none');
-				}, 1000);
-			}
-		} );
-
-		$('#publish').click( function() { act.val( 'post-quickpress-publish' ); } );
-
-		$('#title, #tags-input, #content').each( function() {
-			var input = $(this), prompt = $('#' + this.id + '-prompt-text');
-
-			if ( '' === this.value ) {
-				prompt.removeClass('screen-reader-text');
-			}
-
-			prompt.click( function() {
-				$(this).addClass('screen-reader-text');
-				input.focus();
-			});
+	postboxes.add_postbox_toggles(pagenow, { pbshow: function( id ) {
+		ajaxPopulateWidgets();
 
-			input.blur( function() {
-				if ( '' === this.value ) {
-					prompt.removeClass('screen-reader-text');
-				}
-			});
-
-			input.focus( function() {
-				prompt.addClass('screen-reader-text');
-			});
-		});
-
-		$('#quick-press').on( 'click focusin', function() {
-			wpActiveEditor = 'content';
-		});
-
-		autoResizeTextarea();
-	};
-	quickPressLoad();
+		if ( 'dashboard_quick_press' === id ) {
+			QuickDraft.init();
+		}
+	} } );
 
 	$( '.meta-box-sortables' ).sortable( 'option', 'containment', '#wpwrap' );
 
@@ -186,4 +130,375 @@ jQuery(document).ready( function($) {
 		});
 	}
 
+	autoResizeTextarea();
+
+	if ( jQuery( '#dashboard_quick_press' ).is( ':visible' ) ) {
+		QuickDraft.init();
+	}
+});
+
+// Set up the QuickDraft views.
+QuickDraft.Views = {};
+
+/**
+ * Set up a view object for the quick draft form.
+ *
+ * @since 4.8.0
+ *
+ * @augments wp.Backbone.View
+ */
+QuickDraft.Views.Form = wp.Backbone.View.extend( {
+
+	// Set up our default action handlers.
+	events: {
+
+		// Hide the prompt whena field receives focus.
+		'focus :input': 'hidePrompt',
+
+		// Possibly re-display the prompt when a field looses focus.
+		'blur :input':  'showPrompt',
+
+		// Listen for a reset event, showing all prompts.
+		'reset':        'showAllPrompts',
+
+		// Listed for and handle form submissions.
+		'submit':       'handleFormSubmission'
+	},
+
+	// Initialize the QuickDraft Form view.
+	initialize: function() {
+
+		// Show all prompts in the form to begin.
+		this.showAllPrompts();
+
+		// Rerender if the error state changes.
+		quickDraft.state.on( 'change:errorState', _.bind( this.render, this ) );
+	},
+
+	/**
+	 * Handle toggling of the helper prompts shown inside each field.
+	 *
+	 * Will only show the prompt if the field is empty.
+	 *
+	 * @param  {object}  element Field element containing the helper prompt.
+	 * @param  {boolean} visible Should the prompt be visible?
+	 */
+	togglePrompt: function( element, visible ) {
+		var $input = jQuery( element ),
+			hasContent = $input.val().length > 0;
+
+		// Set the visibility of the elements nearest prompt to passed 'visible' value.
+		jQuery( element ).siblings( '.prompt' ).toggleClass( 'screen-reader-text', ! visible || hasContent );
+	},
+
+	// Show all of the field promts.
+	showAllPrompts: function() {
+
+		// Show all of the field prompts.
+		this.$el.find( ':input' ).each( _.bind( function( i, input ) {
+			_.defer( _.bind( this.togglePrompt, this, input, true ) );
+		}, this ) );
+	},
+
+	/**
+	 * Show the prompt inside a field.
+	 *
+	 * @param {object} event The event triggering this show request.
+	 */
+	showPrompt: function( event ) {
+		this.togglePrompt( event.target, true );
+	},
+
+	/**
+	 * Hide the prompt inside a field.
+	 *
+	 * @param {object} event The event triggering this hide request.
+	 */
+	hidePrompt: function( event ) {
+		this.togglePrompt( event.target, false );
+	},
+
+	/**
+	 * Handle error conditions.
+	 *
+	 * {string} error The error condition, or false to reset.
+	 */
+	setErrorState: function( error ) {
+
+		// Set or reset the app state error condition.
+		quickDraft.state.set( 'errorState', error );
+
+		if ( false !== error ) {
+
+			// Alert screen readers that an error occurred.
+			wp.a11y.speak( error, 'assertive' );
+		}
+
+	},
+
+	/**
+	 * Handle the form submission event.
+	 *
+	 * @param {object} event The form submission event.
+	 */
+	handleFormSubmission: function( event ) {
+		var values,
+			hasValuesToSave = false;
+
+		// Prevent the browser's default form submission handling.
+		event.preventDefault();
+
+		// Prevent double submissions by checking the submitting state.
+		if ( quickDraft.state.get( 'submitting' ) ) {
+			return;
+		}
+
+		// Reset the error state.
+		this.setErrorState( false );
+
+		// Extract the form field values.
+		values = _.reduce( this.$el.serializeArray(), function( memo, field ) {
+			memo[ field.name ] = field.value;
+			hasValuesToSave    = hasValuesToSave || ( '' !== field.value );
+			return memo;
+		}, {} );
+
+		// If the values are all blank, show an error.
+		if ( ! hasValuesToSave ) {
+
+			// Set the error.
+			this.setErrorState( quickDraft.l10n.errorEmptyFields );
+			return;
+		}
+
+		// Save the new values to the model and confirm they are valid.
+		this.model.set( values );
+		if ( ! this.model.isValid() ) {
+			return;
+		}
+
+		// Show a spinner during the callback.
+		this.$el.addClass( 'is-saving' );
+
+
+		// Set the state sbmitting to avoid double saves
+		quickDraft.state.set( 'submitting', true );
+
+		// Trigger the model save.
+		this.model.save()
+
+			// Always remove the spinner.
+			.always(
+				_.bind( function() {
+					this.$el.removeClass( 'is-saving' );
+
+					// Submission complete
+					quickDraft.state.set( 'submitting', false );
+
+				}, this )
+			)
+
+			// Handle save success.
+			.success(
+				_.bind( function() {
+					// Success! Clear any previous error state.
+					 this.render();
+
+					// Add the post model to the head of our collection.
+					this.collection.add( this.model, { at: 0 } );
+
+					// Create a new post model to contain the form data and reset the form.
+					this.model = new wp.api.models.Post();
+					this.model.on( 'change:errorState', _.bind( this.render, this ) );
+
+					this.el.reset();
+				}, this )
+			)
+
+			// Handle save failure.
+			.error(
+				_.bind( function( model, error ) {
+					var message = '';
+
+					// Try to parse and use the response message.
+					try {
+						message = JSON.parse( error.responseText ).message;
+					} catch( e ) {
+
+						// Fall back to a default error string if the parse fails.
+						message = quickDraft.l10n.error;
+					}
+
+					// Set the app error condition.
+					this.setErrorState( message );
+				}, this )
+			);
+	},
+
+	// Render the form view.
+	render: function() {
+		var $error    = this.$el.find( '.notice-alt' ),
+			errorText = quickDraft.state.get( 'errorState' );
+
+		// Error notice is only visible if error text is set.
+		$error.toggleClass( 'hidden', ! errorText );
+		if ( errorText ) {
+
+			// Note: The inner text transform prevents XSS via html().
+			$error.html( jQuery( '<p />', { text: errorText } ) );
+		}
+	}
+} );
+
+/**
+ * Set up a view object for the Quick Draft list of drafts.
+ *
+ * @since 4.8.0
+ *
+ * @augments wp.Backbone.View
+ */
+QuickDraft.Views.DraftList = wp.Backbone.View.extend( {
+
+	// Initialize the draft list view.
+	initialize: function() {
+
+		// Render the view once the drafts have loaded.
+		this.listenTo( this.collection, 'sync', this.onDraftsLoaded );
+	},
+
+	// Once the drafts have loaded, complete the setup.
+	onDraftsLoaded: function() {
+
+		// Add a listener for new items added to the underlying (draft) post collection.
+		this.listenTo( this.collection, 'add', this.renderNew );
+
+		// Render the view!
+		this.render();
+	},
+
+	// Handle a new item being added to the collection.
+	renderNew: function() {
+
+		// Display highlight effect to first (added) item for one second.
+		var $newEl = this.render().$el.find( 'li:first' ).addClass( 'is-new' );
+		setTimeout( function() {
+			$newEl.removeClass( 'is-new' );
+		}, 1000 );
+
+		// Alert screen readers that a new draft has been added.
+		wp.a11y.speak( quickDraft.l10n.newDraftCreated, 'assertive' );
+	},
+
+	// Render the draft post list view.
+	render: function() {
+
+		// Hide drafts list entirely if no drafts exist.
+		this.$el.toggle( this.collection.length > 0 );
+
+		// Display a 'View All' link if there are more drafts available.
+		this.$el.find( '.view-all' ).toggle( this.collection.hasMore() );
+
+		// Remove the placeholder class and render the models.
+		this.$el.find( '.drafts-list' )
+			.removeClass( 'is-placeholder' )
+			.html(
+				_.map( this.collection.models, function( draft ) {
+					return new QuickDraft.Views.DraftListItem( {
+						model: draft
+					} ).render().el;
+				} )
+			);
+
+		return this;
+	}
 } );
+
+/**
+ * Set up a view object an individual draft in the draft list.
+ *
+ * @since 4.8.0
+ *
+ * @augments wp.Backbone.View
+ */
+QuickDraft.Views.DraftListItem = wp.Backbone.View.extend( {
+	tagName: 'li',
+
+	// Render beased on the passed template.
+	template: wp.template( 'item-quick-press-draft' ),
+
+	// Render a single draft list item.
+	render: function() {
+
+		// Clone the original model attributes, so we can leave the model untouched.
+		var attributes = _.clone( this.model.attributes );
+
+		// Trim the content to 10 words.
+		attributes.formattedContent = wp.formatting.trimWords( attributes.content.rendered, 10 );
+
+		// If the title is missing entirely, add a no title placeholder.
+		attributes.formattedTitle = attributes.title.rendered.length > 0 ? attributes.title.rendered : quickDraft.l10n.noTitle;
+
+		// Format the data using Intl.DateTimeFormat with a fallback to date.toLocaleDateString.
+		var date = new Date( wp.api.utils.parseISO8601( attributes.date + quickDraft.timezoneOffset ) );
+		if ( 'undefined' !== typeof Intl && Intl.DateTimeFormat ) {
+			attributes.formattedDate = new Intl.DateTimeFormat( undefined, {
+				month: 'long',
+				day: 'numeric',
+				year: 'numeric'
+			} ).format( date );
+		} else {
+			attributes.formattedDate = date.toLocaleDateString();
+		}
+
+		// Output the rendered template.
+		this.$el.html( this.template( attributes ) );
+
+		// Continue the rendering chain.
+		return this;
+	}
+} );
+
+
+/**
+ * Initialize the Quick Draft feature.
+ *
+ * @since 4.8.0
+ *
+ */
+QuickDraft.init = function() {
+
+	// Set up a state model to track the application state.
+	quickDraft.state = new Backbone.Model({
+		'errorState': false
+	});
+
+	// Wait for the wp-api client to initialize.
+	wp.api.loadPromise.done( function() {
+
+		// Fetch up to 4 of the current user's recent drafts by extending wp.api.collections.Posts.
+		var draftsCollection = new wp.api.collections.Posts();
+		draftsCollection.fetch( {
+			data: wp.hooks.applyFilters(
+				'dashboard_recent_drafts_fetch_args',
+				{
+					status: 'draft',
+					author: quickDraft.currentUserId,
+					per_page: 4,
+					order_by: 'date',
+				}
+			)
+		} );
+
+		// Drafts list is initialized but not rendered until drafts load.
+		new QuickDraft.Views.DraftList( {
+			el: '#quick-press-drafts',
+			collection: draftsCollection
+		} );
+
+		new QuickDraft.Views.Form( {
+			el: '#quick-press',
+			model: new wp.api.models.Post(),
+			collection: draftsCollection
+		} ).render();
+	});
+};
diff --git src/wp-includes/js/wp-hooks.js src/wp-includes/js/wp-hooks.js
new file mode 100644
index 0000000..c7b2d79
--- /dev/null
+++ src/wp-includes/js/wp-hooks.js
@@ -0,0 +1,276 @@
+( function( wp ) {
+	'use strict';
+
+	/**
+	 * Contains the registered hooks, keyed by hook type. Each hook type is an
+	 * array of objects with priority and callback of each registered hook.
+	 */
+	var HOOKS = {};
+
+	/**
+	 * Returns a function which, when invoked, will add a hook.
+	 *
+	 * @param  {string}   type Type for which hooks are to be added
+	 * @return {Function}      Hook added
+	 */
+	function createAddHookByType( type ) {
+		/**
+		 * Adds the hook to the appropriate hooks container
+		 *
+		 * @param {string}   hook     Name of hook to add
+		 * @param {Function} callback Function to call when the hook is run
+		 * @param {?number}  priority Priority of this hook (default=10)
+		 */
+		return function( hook, callback, priority ) {
+			var hookObject, hooks;
+			if ( typeof hook !== 'string' || typeof callback !== 'function' ) {
+				return;
+			}
+
+			// Assign default priority
+			if ( 'undefined' === typeof priority ) {
+				priority = 10;
+			} else {
+				priority = parseInt( priority, 10 );
+			}
+
+			// Validate numeric priority
+			if ( isNaN( priority ) ) {
+				return;
+			}
+
+			// Check if adding first of type
+			if ( ! HOOKS[ type ] ) {
+				HOOKS[ type ] = {};
+			}
+
+			hookObject = {
+				callback: callback,
+				priority: priority
+			};
+
+			if ( HOOKS[ type ].hasOwnProperty( hook ) ) {
+				// Append and re-sort amongst existing
+				hooks = HOOKS[ type ][ hook ];
+				hooks.push( hookObject );
+				hooks = sortHooks( hooks );
+			} else {
+				// First of its type needs no sort
+				hooks = [ hookObject ];
+			}
+
+			HOOKS[ type ][ hook ] = hooks;
+		};
+	}
+
+	/**
+	 * Returns a function which, when invoked, will remove a specified hook.
+	 *
+	 * @param  {string}   type Type for which hooks are to be removed
+	 * @return {Function}      Hook remover
+	 */
+	function createRemoveHookByType( type ) {
+		/**
+		 * Removes the specified hook by resetting its value.
+		 *
+		 * @param {string}    hook     Name of hook to remove
+		 * @param {?Function} callback The specific callback to be removed. If
+		 *                             omitted, clears all callbacks.
+		 */
+		return function( hook, callback ) {
+			var handlers, i;
+
+			// Baily early if no hooks exist by this name
+			if ( ! HOOKS[ type ] || ! HOOKS[ type ].hasOwnProperty( hook ) ) {
+				return;
+			}
+
+			if ( callback ) {
+				// Try to find specified callback to remove
+				handlers = HOOKS[ type ][ hook ];
+				for ( i = handlers.length - 1; i >= 0; i-- ) {
+					if ( handlers[ i ].callback === callback ) {
+						handlers.splice( i, 1 );
+					}
+				}
+			} else {
+				// Reset hooks to empty
+				delete HOOKS[ type ][ hook ];
+			}
+		};
+	}
+
+	/**
+	 * Returns a function which, when invoked, will execute all registered
+	 * hooks of the specified type by calling upon runner with its hook name
+	 * and arguments.
+	 *
+	 * @param  {string}   type   Type for which hooks are to be run, one of 'action' or 'filter'.
+	 * @param  {Function} runner Function to invoke for each hook callback
+	 * @return {Function}        Hook runner
+	 */
+	function createRunHookByType( type, runner ) {
+		/**
+		 * Runs the specified hook.
+		 *
+		 * @param  {string} hook The hook to run
+		 * @param  {...*}   args Arguments to pass to the action/filter
+		 * @return {*}           Return value of runner, if applicable
+		 * @private
+		 */
+		return function( /* hook, ...args */ ) {
+			var args, hook;
+
+			args = Array.prototype.slice.call( arguments );
+			hook = args.shift();
+
+			if ( typeof hook === 'string' ) {
+				return runner( hook, args );
+			}
+		};
+	}
+
+	/**
+	 * Performs an action if it exists.
+	 *
+	 * @param {string} action The action to perform.
+	 * @param {...*}   args   Optional args to pass to the action.
+	 * @private
+	 */
+	function runDoAction( action, args ) {
+		var handlers, i;
+		if ( HOOKS.actions ) {
+			handlers = HOOKS.actions[ action ];
+		}
+
+		if ( ! handlers ) {
+			return;
+		}
+
+		HOOKS.actions.current = action;
+
+		for ( i = 0; i < handlers.length; i++ ) {
+			handlers[ i ].callback.apply( null, args );
+			HOOKS.actions[ action ].runs = HOOKS.actions[ action ].runs ? HOOKS.actions[ action ].runs + 1 : 1;
+		}
+	}
+
+	/**
+	 * Performs a filter if it exists.
+	 *
+	 * @param  {string} filter The filter to apply.
+	 * @param  {...*}   args   Optional args to pass to the filter.
+	 * @return {*}             The filtered value
+	 * @private
+	 */
+	function runApplyFilters( filter, args ) {
+		var handlers, i;
+		if ( HOOKS.filters ) {
+			handlers = HOOKS.filters[ filter ];
+		}
+
+		if ( ! handlers ) {
+			return args[ 0 ];
+		}
+
+		HOOKS.filters.current = filter;
+		HOOKS.filters[ filter ].runs = HOOKS.filters[ filter ].runs ? HOOKS.filters[ filter ].runs + 1 : 1;
+
+		for ( i = 0; i < handlers.length; i++ ) {
+			args[ 0 ] = handlers[ i ].callback.apply( null, args );
+		}
+
+		return args[ 0 ];
+	}
+
+	/**
+	 * Use an insert sort for keeping our hooks organized based on priority.
+	 *
+	 * @see http://jsperf.com/javascript-sort
+	 *
+	 * @param  {Array} hooks Array of the hooks to sort
+	 * @return {Array}       The sorted array
+	 * @private
+	 */
+	function sortHooks( hooks ) {
+		var i, tmpHook, j, prevHook;
+		for ( i = 1; i < hooks.length; i++ ) {
+			tmpHook = hooks[ i ];
+			j = i;
+			while ( ( prevHook = hooks[ j - 1 ] ) && prevHook.priority > tmpHook.priority ) {
+				hooks[ j ] = hooks[ j - 1 ];
+				--j;
+			}
+			hooks[ j ] = tmpHook;
+		}
+
+		return hooks;
+	}
+
+	/**
+	 * Checks to see if an action is currently being executed.
+	 *
+	 * @param  {string} type   Type of hooks to check, one of 'action' or 'filter'.
+	 * @param {string}  action The name of the action to check for, if omitted will check for any action being performed.
+	 *
+	 * @return {[type]}      [description]
+	 */
+	function createDoingHookByType( type ) {
+		return function( action ) {
+
+			// If the action was not passed, check for any current hook.
+			if ( 'undefined' === typeof action ) {
+				return 'undefined' !== typeof HOOKS[ type ].current;
+			}
+
+			// Return the current hook.
+			return HOOKS[ type ] && HOOKS[ type ].current ?
+				action === HOOKS[ type ].current :
+				false;
+		};
+	}
+
+	/**
+	 * Retrieve the number of times an action is fired.
+	 *
+	 * @param  {string} type   Type for which hooks to check, one of 'action' or 'filter'.
+	 * @param {string}  action The action to check.
+	 *
+	 * @return {[type]}      [description]
+	 */
+	function createDidHookByType( type ) {
+		return function( action ) {
+			return HOOKS[ type ] && HOOKS[ type ][ action ] && HOOKS[ type ][ action ].runs ?
+				HOOKS[ type ][ action ].runs :
+				0;
+		};
+	}
+
+	/**
+	 * Check to see if an action is registered for a hook.
+	 *
+	 * @param  {string} type   Type for which hooks to check, one of 'action' or 'filter'.
+	 * @param {string}  action  The action to check.
+	 *
+	 * @return {bool}      Whether an action has been registered for a hook.
+	 */
+	function createHasHookByType( type ) {
+		return function( action ) {
+			return HOOKS[ type ] && HOOKS[ type ][ action ] ?
+				!! HOOKS[ type ][ action ] :
+				false;
+		};
+	}
+
+	wp.hooks = {
+		removeFilter: createRemoveHookByType( 'filters' ),
+		applyFilters: createRunHookByType( 'filters', runApplyFilters ),
+		addFilter: createAddHookByType( 'filters' ),
+		removeAction: createRemoveHookByType( 'actions' ),
+		doAction: createRunHookByType( 'actions', runDoAction ),
+		addAction: createAddHookByType( 'actions' ),
+		doingAction: createDoingHookByType( 'actions' ),
+		didAction: createDidHookByType( 'actions' ),
+		hasAction: createHasHookByType( 'actions' )
+	};
+} )( window.wp = window.wp || {} );
diff --git src/wp-includes/js/wp-util.js src/wp-includes/js/wp-util.js
index 527441d..bdc7531 100644
--- src/wp-includes/js/wp-util.js
+++ src/wp-includes/js/wp-util.js
@@ -121,4 +121,50 @@ window.wp = window.wp || {};
 		}
 	};
 
+	// wp.formatting
+	// ------
+	//
+	// Tools for formatting strings
+	wp.formatting = {
+		settings: settings.formatting || {},
+
+		/**
+		 * Trims text to a certain number of words.
+		 *
+		 * @see wp_trim_words
+		 *
+		 * @param  {string} text     Text to trim.
+		 * @param  {number} numWords Number of words. Optional, default is 55.
+		 * @param  {string} more     What to append if text needs to be trimmed. Optional, default is '…'.
+		 * @return {string}          Trimmed text.
+		 */
+		trimWords: function( text, numWords, more ) {
+			var words, separator;
+
+			if ( 'undefined' === typeof numWords ) {
+				numWords = 55;
+			}
+
+			if ( 'undefined' === typeof more ) {
+				more = wp.formatting.settings.trimWordsMore;
+			}
+
+			text = text.replace( /[\n\r\t ]+/g, ' ' ).replace( /^ | $/g, '' );
+
+			if ( wp.formatting.settings.trimWordsByCharacter ) {
+				separator = '';
+			} else {
+				separator = ' ';
+			}
+
+			words = text.split( separator );
+
+			if ( words.length <= numWords ) {
+				return words.join( separator );
+			}
+
+			return words.slice( 0, numWords ).join( separator ) + more;
+		}
+	};
+
 }(jQuery));
diff --git src/wp-includes/plugin.php src/wp-includes/plugin.php
index 86f1c3b..86f9db8 100644
--- src/wp-includes/plugin.php
+++ src/wp-includes/plugin.php
@@ -363,7 +363,7 @@ function doing_filter( $filter = null ) {
 }
 
 /**
- * Retrieve the name of an action currently being processed.
+ * Retrieve whether action currently being processed.
  *
  * @since 3.9.0
  *
diff --git src/wp-includes/script-loader.php src/wp-includes/script-loader.php
index def438c..1067e4b 100644
--- src/wp-includes/script-loader.php
+++ src/wp-includes/script-loader.php
@@ -85,6 +85,8 @@ function wp_default_scripts( &$scripts ) {
 
 	$scripts->add( 'wp-a11y', "/wp-includes/js/wp-a11y$suffix.js", array( 'jquery' ), false, 1 );
 
+	$scripts->add( 'wp-hooks', "/wp-includes/js/wp-hooks$suffix.js", array(), false, 1 );
+
 	$scripts->add( 'sack', "/wp-includes/js/tw-sack$suffix.js", array(), '1.6.1', 1 );
 
 	$scripts->add( 'quicktags', "/wp-includes/js/quicktags$suffix.js", array(), false, 1 );
@@ -336,6 +338,15 @@ function wp_default_scripts( &$scripts ) {
 		'ajax' => array(
 			'url' => admin_url( 'admin-ajax.php', 'relative' ),
 		),
+		'formatting' => array(
+			'trimWordsMore'  => __( '&hellip;' ),
+			/*
+			 * translators: If your word count is based on single characters (e.g. East Asian characters),
+			 * enter 'characters_excluding_spaces' or 'characters_including_spaces'. Otherwise, enter 'words'.
+			 * Do not translate into your own language.
+			 */
+			'trimWordsByCharacter' => strpos( _x( 'words', 'Word count type. Do not translate!' ), 'characters' ) === 0 && preg_match( '/^utf\-?8$/i', get_option( 'blog_charset' ) ),
+		),
 	) );
 
 	$scripts->add( 'wp-backbone', "/wp-includes/js/wp-backbone$suffix.js", array('backbone', 'wp-util'), false, 1 );
@@ -724,7 +735,17 @@ function wp_default_scripts( &$scripts ) {
 			'current' => __( 'Current Color' ),
 		) );
 
-		$scripts->add( 'dashboard', "/wp-admin/js/dashboard$suffix.js", array( 'jquery', 'admin-comments', 'postbox' ), false, 1 );
+		$scripts->add( 'dashboard', "/wp-admin/js/dashboard$suffix.js", array( 'jquery', 'admin-comments', 'postbox', 'wp-api', 'wp-backbone', 'wp-a11y', 'wp-util', 'wp-hooks' ), false, 1 );
+		did_action( 'init' ) && $scripts->localize( 'dashboard', 'quickDraft', array(
+			'currentUserId'  => get_current_user_id(),
+			'l10n' => array(
+				'error'            => __( 'An error has occurred. Please reload the page and try again.' ),
+				'newDraftCreated'  => __( 'Success. A new draft was created.' ),
+				'errorEmptyFields' => __( 'Error. All fields were empty.' ),
+				'noTitle'          => __( '(no title)' ),
+			),
+			'timezoneOffset' => ( get_option( 'gmt_offset' ) >= 0 ? '+' : '-' ) . date( 'H:i', abs( get_option( 'gmt_offset' ) * 3600 ) ),
+		) );
 
 		$scripts->add( 'list-revisions', "/wp-includes/js/wp-list-revisions$suffix.js" );
 
diff --git tests/qunit/index.html tests/qunit/index.html
index 0c9d820..a707e13 100644
--- tests/qunit/index.html
+++ tests/qunit/index.html
@@ -50,6 +50,7 @@
 		<script src="../../src/wp-includes/js/customize-base.js"></script>
 		<script src="../../src/wp-includes/js/customize-models.js"></script>
 		<script src="../../src/wp-includes/js/shortcode.js"></script>
+		<script src="../../src/wp-includes/js/wp-hooks.js"></script>
 		<script src="../../src/wp-admin/js/customize-controls.js"></script>
 		<script src="../../src/wp-includes/js/wp-api.js"></script>
 
@@ -70,6 +71,7 @@
 		<script src="wp-admin/js/customize-base.js"></script>
 		<script src="wp-admin/js/customize-header.js"></script>
 		<script src="wp-includes/js/shortcode.js"></script>
+		<script src="wp-includes/js/wp-hooks.js"></script>
 		<script src="wp-includes/js/wp-api.js"></script>
 		<script src="wp-admin/js/customize-controls.js"></script>
 		<script src="wp-admin/js/customize-controls-utils.js"></script>
@@ -77,6 +79,7 @@
 		<script src="wp-admin/js/customize-widgets.js"></script>
 		<script src="wp-admin/js/word-count.js"></script>
 		<script src="wp-admin/js/nav-menu.js"></script>
+		<script src="wp-includes/js/wp-util.js"></script>
 
 		<!-- Customizer templates for sections -->
 		<script type="text/html" id="tmpl-customize-section-default">
diff --git tests/qunit/wp-includes/js/wp-hooks.js tests/qunit/wp-includes/js/wp-hooks.js
new file mode 100644
index 0000000..8f894b2
--- /dev/null
+++ tests/qunit/wp-includes/js/wp-hooks.js
@@ -0,0 +1,191 @@
+/* global wp */
+( function( QUnit ) {
+	QUnit.module( 'wp-hooks' );
+
+	function filter_a( str ) {
+		return str + 'a';
+	}
+	function filter_b( str ) {
+		return str + 'b';
+	}
+	function filter_c( str ) {
+		return str + 'c';
+	}
+	function action_a() {
+		window.actionValue += 'a';
+	}
+	function action_b() {
+		window.actionValue += 'b';
+	}
+	function action_c() {
+		window.actionValue += 'c';
+	}
+	window.actionValue = '';
+
+	QUnit.test( 'add and remove a filter', function() {
+		expect( 1 );
+		wp.hooks.addFilter( 'test.filter', filter_a );
+		wp.hooks.removeFilter( 'test.filter' );
+		equal( wp.hooks.applyFilters( 'test.filter', 'test' ), 'test' );
+	} );
+
+	QUnit.test( 'add a filter and run it', function() {
+		expect( 1 );
+		wp.hooks.addFilter( 'test.filter', filter_a );
+		equal( wp.hooks.applyFilters( 'test.filter', 'test' ), 'testa' );
+		wp.hooks.removeFilter( 'test.filter' );
+	} );
+
+	QUnit.test( 'add 2 filters in a row and run them', function() {
+		expect( 1 );
+		wp.hooks.addFilter( 'test.filter', filter_a );
+		wp.hooks.addFilter( 'test.filter', filter_b );
+		equal( wp.hooks.applyFilters( 'test.filter', 'test' ), 'testab' );
+		wp.hooks.removeFilter( 'test.filter' );
+	} );
+
+	QUnit.test( 'add 3 filters with different priorities and run them', function() {
+		expect( 1 );
+		wp.hooks.addFilter( 'test.filter', filter_a );
+		wp.hooks.addFilter( 'test.filter', filter_b, 2 );
+		wp.hooks.addFilter( 'test.filter', filter_c, 8 );
+		equal( wp.hooks.applyFilters( 'test.filter', 'test' ), 'testbca' );
+		wp.hooks.removeFilter( 'test.filter' );
+	} );
+
+	QUnit.test( 'add and remove an action', function() {
+		expect( 1 );
+		window.actionValue = '';
+		wp.hooks.addAction( 'test.action', action_a );
+		wp.hooks.removeAction( 'test.action' );
+		wp.hooks.doAction( 'test.action' );
+		equal( window.actionValue, '' );
+	} );
+
+	QUnit.test( 'add an action and run it', function() {
+		expect( 1 );
+		window.actionValue = '';
+		wp.hooks.addAction( 'test.action', action_a );
+		wp.hooks.doAction( 'test.action' );
+		equal( window.actionValue, 'a' );
+		wp.hooks.removeAction( 'test.action' );
+	} );
+
+	QUnit.test( 'add 2 actions in a row and then run them', function() {
+		expect( 1 );
+		window.actionValue = '';
+		wp.hooks.addAction( 'test.action', action_a );
+		wp.hooks.addAction( 'test.action', action_b );
+		wp.hooks.doAction( 'test.action' );
+		equal( window.actionValue, 'ab' );
+		wp.hooks.removeAction( 'test.action' );
+	} );
+
+	QUnit.test( 'add 3 actions with different priorities and run them', function() {
+		expect( 1 );
+		window.actionValue = '';
+		wp.hooks.addAction( 'test.action', action_a );
+		wp.hooks.addAction( 'test.action', action_b, 2 );
+		wp.hooks.addAction( 'test.action', action_c, 8 );
+		wp.hooks.doAction( 'test.action' );
+		equal( window.actionValue, 'bca' );
+		wp.hooks.removeAction( 'test.action' );
+	} );
+
+	QUnit.test( 'pass in two arguments to an action', function() {
+		var arg1 = 10,
+			arg2 = 20;
+
+		expect( 4 );
+
+		wp.hooks.addAction( 'test.action', function( a, b ) {
+			equal( arg1, a );
+			equal( arg2, b );
+		} );
+		wp.hooks.doAction( 'test.action', arg1, arg2 );
+		wp.hooks.removeAction( 'test.action' );
+
+		equal( arg1, 10 );
+		equal( arg2, 20 );
+	} );
+
+	QUnit.test( 'fire action multiple times', function() {
+		var func;
+		expect( 2 );
+
+		func = function() {
+			ok( true );
+		};
+
+		wp.hooks.addAction( 'test.action', func );
+		wp.hooks.doAction( 'test.action' );
+		wp.hooks.doAction( 'test.action' );
+		wp.hooks.removeAction( 'test.action' );
+	} );
+
+	QUnit.test( 'remove specific action callback', function() {
+		window.actionValue = '';
+		wp.hooks.addAction( 'test.action', action_a );
+		wp.hooks.addAction( 'test.action', action_b, 2 );
+		wp.hooks.addAction( 'test.action', action_c, 8 );
+
+		wp.hooks.removeAction( 'test.action', action_b );
+		wp.hooks.doAction( 'test.action' );
+		equal( window.actionValue, 'ca' );
+		wp.hooks.removeAction( 'test.action' );
+	} );
+
+	QUnit.test( 'remove specific filter callback', function() {
+		wp.hooks.addFilter( 'test.filter', filter_a );
+		wp.hooks.addFilter( 'test.filter', filter_b, 2 );
+		wp.hooks.addFilter( 'test.filter', filter_c, 8 );
+
+		wp.hooks.removeFilter( 'test.filter', filter_b );
+		equal( wp.hooks.applyFilters( 'test.filter', 'test' ), 'testca' );
+		wp.hooks.removeFilter( 'test.filter' );
+	} );
+
+	// Test doingAction, didAction, hasAction.
+	QUnit.test( 'Test doingAction, didAction and hasAction.', function() {
+
+		// Reset state for testing.
+		wp.hooks.removeAction( 'test.action' );
+		wp.hooks.addAction( 'another.action', function(){} );
+		wp.hooks.doAction( 'another.action' );
+
+		// Verify no action is running yet.
+		ok( ! wp.hooks.doingAction( 'test.action' ), 'The test.action is not running.' );
+		equal( wp.hooks.didAction( 'test.action' ), 0, 'The test.action has not run.' );
+		ok( ! wp.hooks.hasAction( 'test.action' ), 'The test.action is not registered.' );
+
+		wp.hooks.addAction( 'test.action', action_a );
+
+		// Verify action added, not running yet.
+		ok( ! wp.hooks.doingAction( 'test.action' ), 'The test.action is not running.' );
+		equal( wp.hooks.didAction( 'test.action' ), 0, 'The test.action has not run.' );
+		ok( wp.hooks.hasAction( 'test.action' ), 'The test.action is registered.' );
+
+		wp.hooks.doAction( 'test.action' );
+
+		// Verify action added and running.
+		ok( wp.hooks.doingAction( 'test.action' ), 'The test.action is running.' );
+		equal( wp.hooks.didAction( 'test.action' ), 1, 'The test.action has run once.' );
+		ok( wp.hooks.hasAction( 'test.action' ), 'The test.action is registered.' );
+
+		wp.hooks.doAction( 'test.action' );
+		equal( wp.hooks.didAction( 'test.action' ), 2, 'The test.action has run twice.' );
+
+		wp.hooks.removeAction( 'test.action' );
+
+		// Verify state is reset appropriately.
+		ok( wp.hooks.doingAction( 'test.action' ), 'The test.action is running.' );
+		equal( wp.hooks.didAction( 'test.action' ), 0, 'The test.action has not run.' );
+		ok( ! wp.hooks.hasAction( 'test.action' ), 'The test.action is not registered.' );
+
+		wp.hooks.doAction( 'another.action' );
+		ok( ! wp.hooks.doingAction( 'test.action' ), 'The test.action is running.' );
+
+
+	} );
+
+} )( window.QUnit );
diff --git tests/qunit/wp-includes/js/wp-util.js tests/qunit/wp-includes/js/wp-util.js
new file mode 100644
index 0000000..6d42228
--- /dev/null
+++ tests/qunit/wp-includes/js/wp-util.js
@@ -0,0 +1,41 @@
+/* global wp */
+( function( QUnit ) {
+	wp.formatting.settings.trimWordsMore = '&hellip;';
+	QUnit.module( 'wp-util' );
+	var longText = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce varius lacinia vehicula. Etiam sapien risus, ultricies ac posuere eu, convallis sit amet augue. Pellentesque urna massa, lacinia vel iaculis eget, bibendum in mauris. Aenean eleifend pulvinar ligula, a convallis eros gravida non. Suspendisse potenti. Pellentesque et odio tortor. In vulputate pellentesque libero, sed dapibus velit mollis viverra. Pellentesque id urna euismod dolor cursus sagittis.';
+
+	QUnit.test( 'wp.formatting.trimWords', function( assert ) {
+		_.each( [
+			{
+				'trimmed': 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce varius lacinia vehicula. Etiam sapien risus, ultricies ac posuere eu, convallis sit amet augue. Pellentesque urna massa, lacinia vel iaculis eget, bibendum in mauris. Aenean eleifend pulvinar ligula, a convallis eros gravida non. Suspendisse potenti. Pellentesque et odio tortor. In vulputate pellentesque libero, sed dapibus velit&hellip;',
+				'text': longText,
+				'description': 'Trims to 55 by default.'
+			},
+			{
+				'trimmed': 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce varius&hellip;',
+				'text': longText,
+				'length': 10,
+				'description': 'Trims to 10.'
+			},
+			{
+				'trimmed': 'Lorem ipsum dolor sit amet,[...] Read on!',
+				'text': longText,
+				'description': 'Trims to 5 and uses custom more.',
+				'length': 5,
+				'more': '[...] Read on!'
+			},
+			{
+				'trimmed': 'This is some short text.',
+				'text': 'This is some short text.',
+				'description': 'Doesn\'t strip short text.'
+			}
+
+		], function( test ) {
+			assert.equal(
+				wp.formatting.trimWords( test.text, test.length, test.more ),
+				test.trimmed,
+				test.description
+			);
+		} );
+	} );
+} )( window.QUnit );
