WP Media UI: States, StateMachine, and Frame

(It seems like I got the AJAX server working, at least somewhat. So I needed to implement the editors. This is really difficult because it’s all in the wp.media hierarchy, and with keywords like “wp” and “media”, it’s hard to find the documentation. I have been digging around in the code trying to understand it, and came up with notes, which I’ll post occasionally. These are mainly for myself, to review when I need to restart the exploration, but they’re posted because someone else might find this useful.)

These are incomplete, and probably incorrect notes.

WP Media UI: States, StateMachine, and Frame

“A frame is a composite view consisting of
one or more regions and one or more
states.”

A frame is a composite view, meaning that it is
a view that contains other views.

A frame has “state”, which is a way to orchestrate
what views are visible. The State and StateMachine
fire Events when the current state changes, or
any attributes on the State change.

A Frame has the StateMachine class mixed in, so
it has a collection of States.

State is a model that stores attributes about the
current state. These attributes are used by the
subclasses of Frame to determine which regions
are visible in a specific state, and to store
values in a view.

For example, MediaFrame is a subclass of Frame,
and it manages the media library. It comprises
five regions. The Select workflow extends MediaFrame
and adds handlers to respond to events thrown by
these regions.

bindHandlers: function() {
    this.on( 'router:create:browse', this.createRouter, this );
    this.on( 'router:render:browse', this.browseRouter, this ); 
    this.on( 'content:create:browse', this.browseContent, this );
    this.on( 'content:render:upload', this.uploadContent, this );
    this.on( 'toolbar:create:select', this.createSelectToolbar, this );
},

The “content:create:browse” events means, the content region was created,
and the browse mode was activated. The pattern is “region:event:mode”.
Mode seems to be a hack – a region has a mode, and that determines
which view is displayed in the region. A region doesn’t need to have
a mode.

By the way, Regions fire off two events: create and render. First,
if fires create, and a handler in Select creates the view.

But look at the ‘content:render:browse’ and ‘content:render:upload’
events: where does the “browse” or “upload” mode come from?

The mode is set in the Library controller. Library extends State
(which makes sense, because controllers hold state).
The Library controller manages the state in the ‘content’ region.
The Library controller is created by the Select view, and then added
to Select’s states — remember, that Select is a Frame, and Frames
have the StateMachine controller mixed in.

The Select’s initialize() is:

initialize: function() {
    // Call 'initialize' directly on the parent class.
    MediaFrame.prototype.initialize.apply( this, arguments );

    _.defaults( this.options, {
        selection: [],
        library:   {},
        multiple:  false,
        state:    'library'
    });

    this.createSelection();
    this.createStates();  // look here
    this.bindHandlers();
},

When createStates() is called, it creates the Library controller:

createStates: function() {
    var options = this.options;

    if ( this.options.states ) {
        return;
    }

    // Add the default states.
    this.states.add([
        // Main states.
        new wp.media.controller.Library({
            library:   wp.media.query( options.library ),
            multiple:  options.multiple,
            title:     options.title,
            priority:  20
        })
    ]);
},

So when the Select view is initialized, the Library controller is also
created and a reference to it is saved in the view’s StateMachine.
Later, when browseContent() is called, it retreives state information
from the Library controller instance, and that renders the
AttachmentBrowser view:

browseContent: function( contentRegion ) {
    var state = this.state();

    this.$el.removeClass('hide-toolbar');

    // Browse our library of attachments.
    contentRegion.view = new wp.media.view.AttachmentsBrowser({
        controller: this,
        collection: state.get('library'),
        selection:  state.get('selection'),
        model:      state,
        sortable:   state.get('sortable'),
        search:     state.get('searchable'),
        filters:    state.get('filterable'),
        date:       state.get('date'),
        display:    state.has('display') ? state.get('display') : state.get('displaySettings'),
        dragInfo:   state.get('dragInfo'),

        idealColumnWidth: state.get('idealColumnWidth'),
        suggestedWidth:   state.get('suggestedWidth'),
        suggestedHeight:  state.get('suggestedHeight'),

        AttachmentView: state.get('AttachmentView')
    });
},

(This feels like coupling to me.)

When the mode is “upload”, Select displays UploaderInline:

uploadContent: function() {
    this.$el.removeClass( 'hide-toolbar' );
    this.content.set( new wp.media.view.UploaderInline({
        controller: this
    }) );
},

So the Select frame and Library controller work in tandem to display two different
views: AttachemsntBrwoser and UploaderInline.

In addition the view juggling above, Select also has a selection.

Select’s selection

A Selection is subclass of Attachements. Attachments is a collection
of “Attachment”s. An Attachment is a Model. So, Select has a collection
to hold the selection. Here’s the code to create the selection in Select:

createSelection: function() {
    var selection = this.options.selection;

    if ( ! (selection instanceof wp.media.model.Selection) ) {
        this.options.selection = new wp.media.model.Selection( selection, {
            multiple: this.options.multiple
        });
    }

    this._selection = {
        attachments: new wp.media.model.Attachments(),
        difference: []
    };
},

Note that it’s stored in this.options. That was weird, so I did a grep on
“this.options.selection” and found it used in some views, like the
Attachment view. The Attachment view seems to manage the manipulation
of selection lists.

Library’s selection

In the “browse” mode in Select, an AttachmentsBrowser is created
and it has a property ‘selection’:

        selection:  state.get('selection'),

Remember that state is the Library controller. Up in the Library controller,
the code to create the selection is:

initialize: function() {
    var selection = this.get('selection'),
        props;

    if ( ! this.get('library') ) {
        this.set( 'library', wp.media.query() );
    }

    if ( ! ( selection instanceof wp.media.model.Selection ) ) {
        props = selection;

        if ( ! props ) {
            props = this.get('library').props.toJSON();
            props = _.omit( props, 'orderby', 'query' );
        }

        this.set( 'selection', new wp.media.model.Selection( null, {
            multiple: this.get('multiple'),
            props: props
        }) );
    }

    this.resetDisplays();
},

So it’s similar code, but the selection is kept in the Library controller’s
state.

It looks like these two instances of Selection are separate. The Select’s selection
is a global value, and the Library controller’s selection is local to the
library. This feels weird, but I’ll assume there’s a good reason. (Or a bad reason.)

BTW, this.options?

this.options.selection is defined up in the initialize(). It’s a little hard to spot
because it uses the _.defaults() function:

    _.defaults( this.options, {
        selection: [],
        library:   {},
        multiple:  false,
        state:    'library'
    });

But where is this.options set? It might be in Backbone.History. That’s the only place
I saw “this.options =”. So this is still a mystery to me. The only thing I see
is that this.options is like a global between different views.

Additionally, the only place I found Backbone.history.start was in
views/frame/manage.js.