Many games (e.g. labeler) have several data sets and courses have multiple lessons. Manifests are the way to deliver these different data to a single app.
Each Manifest has several standard fields, with an arbitrarily defined payload
(which is an SQL blob
type).
Here are the fields in a manifest record in the database:
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`ord` int(10) unsigned COMMENT "old id - use for ordering",
`otype` smallint(1) unsigned NOT NULL DEFAULT 4096 COMMENT 'obj type',
`title` varchar(255) NOT NULL,
`desc` text,
`mid` int(10) unsigned NOT NULL default 0 COMMENT "image",
`access` smallint NOT NULL COMMENT "owner-only trial free member purchase",
`flags` int(3) unsigned NOT NULL default 0 COMMENT "publish draft current featured",
`payload` blob,
`owner` int(10) unsigned NOT NULL default 1,
`uid_lmod` int(10) unsigned NULL COMMENT "author",
`debut` int(10) unsigned NOT NULL,
`lmod` int(10) unsigned NULL,
OTYPE Object Types
The otype
— short for Object TYPE — field is used to identity what kind of manifest this is and this can be mapped
to a set of apis or apps that knows how to encode/decode the payload
field.
Pages vs Manifests
There is some overlap in the functionality of manifests and pages. Both can present the user with a choice of related dynamic content. For example if there is a course with multiple lessons, then each lesson could be saved as either a manifest or as a separate page.
Sometimes it's unclear which is the better solution — pages or manifests. Here are some of the key differences to help the choice between for a given situation:
- Pages can offer lists of manifests (or other pages) tor the user to choose from. Manifests do not.
- Page content is searchable in the site's search bar, while manifest payloads (and other fields) are not.
- Pages are tagged and categorized while manifests are organized by otype.
- Manifests have an ord field which can easily be used to group or order manifests within a given otype.
So, back to the course example. We could make a category for Courses and a subcategory for each course. Then we might make pages for each lesson. In this case the lessons would be searchable.
On the other hand we might make on page for each course and pull in the lessons as manifests. There is no right answer.
Selecting Manifests
The image below shows a panel that allows a ThaiDrills user to choose which manifest to load for a labeling game.
Contrast that with the page summaries in the side bar which allow the user to choose from distinct pages all categorized as CMS pages.
Note that there are option to show descriptions and owner for the manifests, so both manifests and pages can be presented to the user in a very similar fashion.
A paged list consisting of manifest summaries — typically, thumbnail images with a title underneath —
can easily be fetched into a <div id="man-summaries"></div>
filtered by OTYPE
via the UBOW.ManifestSummaries
class. The following example shows the basic usage:
new UBOW.ManifestSummaries('#man-summaries', {
per_page: 24,
extra: {otype: TD.OTYPE.DECK.value, admin:1},
onClick: (idManifest) => {
console.log('loading manifest:', idManifest);
// do something with the selected manifest
_manifest.load(idManifest);
},
});
Paging is handled automatically, and you can do whatever you like with the data when the user clicks on a manifest to select it.
In the above example from ThaiDrills.com we present all of the
the manifests that have otype TD.OTYPE.DECK
with 24 manifests per page.
Since the payload of the manifest depends on it's otype, you will need to write a parser for each otype.
Editing Manifests
We manage manifests in the back-end. There is a standard form — shown left — for creating and editing manifests that contains all the fields.
The form will populate a <div>
whose css selector is passed into the constructor.
A settings object is the second argument passed to the constuctor. There are a wide range
of options including the available otypes
and urlLoad
which specifies the API responsible
fetching the data.
Usually, the basic editor is extended or augmented with separate code to process the payload.
For instance, in the case of ThaiDrill's Deck Editor, we are creating decks of flash cards to study Thai vocaulary. Each manifest holds a different word list which is representated as a table where each row has columns for the Thai word and its English translation, as well as an image and links to audio files for the Thai and English pronunciations.
Here's a typical use case, from the ThaiDrills back-end:
const _manifest = new UBOW.ManifestEditor('#mani-editor', {
urlLoad: '/api/deck/load',
otype: TD.OTYPE.DECK,
isValid: () => true,
getExtra: () => {
return {
payload: tbl.toCSV(), // a csv of the labels
};
},
onReset: reset, // create new manifest
onLoaded: (mani) => {
mani.cards.forEach((word) => tbl.append(word));
},
});
Notice how the getExtra
setting is used to easily override the payload field (which can be hidden
in the DOM). It is called after the form is validated, but before it is posted to the server.
Any additional fields may be also be posted.
Also, the onLoaded
setting is particularly useful for performing client actions after the manifest
is retrieved from the server. In the above code we populate the aforementioned table of flash cards
from the 'extra' field (cards
) that the server adds to the manifest object. The server expands the
payload field into an array of cards via a database looking up of ids stored in the payload.
Because this is 'non-standard' behavior, we override the urlLoad
setting to point to a controller
that knows how to process this particular otype (i.e. TD.OTYPE.DECK in this example).
Using another image
This is an unusual use case, but one that I have used more than once: Sometimes the manifest image (which is displayed in the list of manifests) is also the background image for the game you are editing.
You can take the image out of the form proper and put it someplace else on the page. The following
settings for the UBOW.ManifestEditor
put the image into <div id="drops></div>
, that may be changed
by clicking on <button id="img-pick"">Change Background</button>
.
The imgChanged
setting fixes everything up internally for the image editor to work as
expected.
imgDiv: '#drops',
btnPickImg: '#img-pick',
imgChanged: ($img) => {
const mid = $img.attr('mid');
$('#img-info').val( `#${mid}, ${$img[0].naturalWidth} x ${$img[0].naturalHeight}`);
ed.imgChanged($img);
},
~~~ ~~~