HTML5 Client Code Technical Details - Izza/so_client_html5 GitHub Wiki
The ContentNode object hierarchy provides a data model around which obtaining, assimilating, and presenting the !SpiderOak-managed user content data is organized - storage devices versus share rooms, and their contained folders and file "nodes".
-
Node
- Abstract basis for all nodes.-
ContentNode
- Abstract basis representing !SpiderOak-managed content-
RootContentNode
- Consolidated storage and share roots container -
FolderContentNode
- General behaviors used by storage and share folders -
FileContentNode
- General behaviors used by storage and share files -
StorageNode
- Backups contentRootStorageNode
DeviceStorageNode
DirectoryStorageNode
FileStorageNode
-
ShareNode
- Share Rooms and their contents-
OriginalRootShareNode
- List of share rooms published by an account -
PublicRootShareNode
- Any share rooms visited -
RoomShareNode
- a public or original share room DirectoryShareNode
FileShareNode
-
-
RecentContentNode
- recently visited items list, most recent first
-
-
PanelNode
- abstract basis for console/UI panels-
SettingsPanelNode
- settings collection -
AccountPanelNode
- details of the remote account
-
-
Content model elements are address by the URLs of their JSON content items:
- as the '#' fragment identifiers of the app's URL, for jQuery Mobile changePage navigation, and
- as the key for retrieval (and allocation) within the app's internal node database, mediated by the
node_manager
object.
The respective content-specific roots are encompassed by a consolidated
root, the RootContentNode
, which provides a central pivot for
navigation. Incorporation of this consolidated root provides the basis for
what I think is the most coherent and clear navigation model possible,
given the various constituents. (See
docs/HTML5ClientProjectConsolidatedRoot.txt
for the principal issues.)
- The actual content items are indexed in the
node_manager
by their JSON access URLs, and addressed in app URLs as the fragment identifiers.- The
RootStorageNode
contains the backup storage items, with a URL consisting of the storage server host (which can vary) and a trimmed, b32 encoding of the account name. Access to the contained items depends on login, at which point the storage host server is determined.
- The
- The list of account-originated share rooms is collected within the
OriginalRootShareNode
. It has a special JSON address on the server within each account's storage root. The original share rooms are situated with all the public share rooms, in... - The
PublicRootShareNode
has a URL that is the common stem of all visited !SpiderOak share rooms. - A encompassing root node, the
RootContentNode
, contains all the other roots, and presents their contents in a consolidated view. The app uses an arbitrary, internal address for it, sufficiently URL-like to be recognized as such by the jQm URL traversal machinery in changePage().
node_manager
is the broker by which everything obtains nodes, given
their URL handles. It also is the node allocator, fabricating new ones
when first referenced. For the latter purpose, it requires identification
of the parent node in any situations that might be first encounters, in an
entire session or since a logout.
There are a few kinds of structural connections by which users navigate the content hierarchies.
- Navigation into contained items: Downwards navigation is by clicking on the items presented as the contents of some kind of folder, including share room and storage devices, their contained folders, and the various root containers.
- The items have URLs consisting of the application's address followed by a fragment identifier consisting of the content's address. The app's [#DynamicPageTraversal] (below) retrieves the JSON data for content nodes and populates and presents an application node for it.
- Besides content items and the special provisions listed below, fragment addresses are simple locations of jQuery mobile page divs ([data-role="page"]), conveyed to the regular jQm pageChange if none of the specific conditions obtain.
- Some of the encompassing root nodes, like the
RootContentNode
andPublicRootShareNode
, use static DOM templates, with corresponding static document addresses. Intra-document (fragment) links to those static template addresses are mapped to the addresses of corresponding root content nodes using ahandle_content_visit()
helper,internalize_url()
. - The special consolidated root node,
RootContentNode
, arranges to present the contents of the respective actual content roots, theRootStorageNode
,OriginalRootShareNode
, andPublicRootShareNode
. Thus theRootContentNode
effectively shares downward-navigation containment of the actual content roots. - Some addresses are of application-managed, non-content-specific facilities, like 'logout'. These are listed in the
document_addrs
object, which is used byhandle_content_visit()
to dispatch the listed functions. - Navigation from more contained nodes to less contained ones depends on registration of the parent node URLs in the offspring when the offspring nodes are allocated.
The respective actual content root's items are produced so that the consolidated root is registered as their parent. Thus, outward navigation from the top level content items (storage devices and share rooms) goes to theRootContentNode
consolidated root, rather than the actual content roots.
-
RootStorageNode
: (eventually) adjusting backup coverage -
OriginalRootShareNode
: (eventually) managing the share rooms published by the account -
PublicRootShareNode
: adding to and omitting from the collection of share rooms being visited.
- so_init_manager (in
custom-scripting.js
) has a roster of frameworks that need to complete their initialization before the app can be launched. Calls toso_init_manager.ready()
are included in initialization-completion routines for the various frameworks, and when the last one completes,spideroak.init()
(below) is invoked.- jQuery Mobile: an anonymous function that is bound (in (
custom-scripting.js
) to the jQmmobileinit
event callsso_init_manager.ready('jQm')
- !PhoneGap:
onDeviceReady()
(also incustom-scripting.js
) is either registered (inindex.html
) on the !PhoneGapdeviceready
event, if !PhoneGap is observed present. If we are running outside of !PhoneGap (useful for a few purposes), thenonDeviceReady()
is invoked directly.onDeviceReady()
callsso_init_manager.ready('PhoneGap')
- Document loaded/DOM: The
index.html
body
element has anonload
trigger which callsso_init_manager.ready('DOM')
. - Application code: The last statement in
SpiderOak.js
isso_init_manager.ready('app')
.
This arrangement is insensitive to the order in which the frameworks happen to initialize, or interdependencies between them. When the initialization of each has been accounted for, the main app inits.
- jQuery Mobile: an anonymous function that is bound (in (
-
spideroak.init()
:- establishes the traversal handler,
handle_content_visit()
on jQuery Mobile'spagebeforechange
traversal event. - instantiates the
RootContentNode
,RecentContentNode
, andPublicRootShareNode
. - makes
index.html
DOM adjustments for branding - prepares the various credentials forms submit callback and visibility controls
- arranges the combo-root (
RootContentNode
) intial fade-in does initial fetch of persistent settings - does a traversal to the combo-root
RootContentNode
, to be dispatched by ourhandle_content_visit()
jQmpagebeforechange
handler.
- establishes the traversal handler,
- the
RootContentNode
and other content node.visit()
methods, which handle traversal to them are described [#DyanamicPageTraversal].
Persistent settings are managed via two facilities:
- the
persistence_manager
: provides a simple API for managing persistently stored values javascript, preserving structures by using JSON transparently:.set(name, value)
.get(name)
.remove(name)
.keys()
.length()
- the
remember_manager
, a layer on top of thepersistence_manager
for managing non-sensitive user account info, and whether or not retention of user account info is elected ("remembering").- The maintained fields:
username
storage_host
storage_web_url
-
.unset()
: True if no persistentremember_manager
fields are available. -
.active()
: Establish UI "Remember Me" if passed a truthy value, return whether or not it the mode is active if passed no value, else deactivate if passed a non-truthy (false or related) value. -
.fetch()
: Return an object with the currently remembered field values -
.store(values_obj)
: Persist the set of field values, passed in an object. The object must have settings for all the maintained fields. -
remove_storage_host()
: Remove the storage_host persisted value. This is the way to inhibit auto-login, without losing the convenience of a remembered username (in the absence of a way to remove the authentication cookies).
- The maintained fields:
Most content nodes ([#ContentNodeDataModel]) are
presented as jQuery Mobile "pages" - <div data-role="page>
. Most are
cloned copies of static index.html
template nodes, upon in-app
traversal of content URLs (see [#ContentAddressing],
above).
- The spideroak object's
handle_content_visit()
function is assigned to handle the jQuery Mobilepagebeforechange
event, which is triggered by changes to the browser'slocation.hash
setting and$.mobile.changePage()
invocations.
Ourhandle_content_visit()
routine only discontinues default jQm traversal machinery for handling of string addresses. That way, the standard jQm traversal facilities handles traversal to the jQm objects that our machinery fabricates, by passing them to$.mobile.changePage()
as jQm objects. - A few different classes of addresses are used in the application, as described in the [#OrganizationofContentTraversal] section, above.
- For navigation URLs that go to application content, including the user's !SpiderOak-managed content, we:
- Fetch a suitable
ContentNode
-based object for the node usingnode_manager.get()
, which returns an already existing node, if any, or else allocates a new one with suitable initial settings. - The obtained
ContentNode
-derived object uses its.visit()
method to handle the visit - cloning or directly using a (jQm ) page object and recursively using the traversal machinery to visit it.
- Fetch a suitable
- In most cases,
ContentNode.visit()
:- Fetches the node contents from the server if necessary (intially, always necessary)
- Gets a jQuery object for the node's data-role="page", cloning the !index.html id="content-page-template" <div> if it hasn't already gotten a copy this way, and adjusting the basic structure.
- Fills in the page header and footer according to the node's navigation context. Different types of nodes have different actions and relative ascending navigation routes associated with them.
- Populates the page listview with entries for the node's contents, or special activities associated with the node.
- The
RootContentNode
's.visit()
method is different. It dispatches a visit to thePublicRootShareNode
,RootStorageNode
andOriginalRootShareNode
. All of those visits are delegated such that:- The delegated presentations do not take browser focus (using the 'passive' mode option), leaving focus on the
RootContentNode
, and - the
RootContentNode
is notified of success, along with an indicator of the transaction that succeeded, so it can redisplay its consolidated content view, as it notifications are received. - The visit is contingent to receiving success status notification from the
RootStorageNode
visit, since both depend on successful authentication, while access to contents of thePublicRootShareNode
does not.- If the
RootStorageNode
visit succeeds, theOriginalRootShareNode
visit is dispatched. - If the
RootStorageNode
visit fails, theRootContentNode
presents the login form, with the username filled in and Remember Me is activated, as appropriate.
- If the
- The delegated presentations do not take browser focus (using the 'passive' mode option), leaving focus on the
Adding an exceptional node type, to maintain the roster of recently visited content items, involves unusually many of the various traversal arrangements.
Here are the changes to plug the new object in to the surrounding traversal infrastrcuture:
- In the
spideroak
objectgeneric
settings object, add a contrivedrecents_url
, for anode_manager
address to the object. - Add an
is_recents_url()
predicate routine,- and include that among those checked in
is_content_root_url()
. (The recents collection is the root of a single-node hierarchy.)
- and include that among those checked in
- Provide a root-type case in
node_manager.get()
- and, to accommodate exceptionally frequent access, a dedicated
.get_recents()
method, like.get_combo_root()
.
- and, to accommodate exceptionally frequent access, a dedicated
- Also in
generic
settings, add 'recents_page_id'- and, in
internalize_url()
, provide for translating therecents_page_id
to therecents_url
, so traversals to thepage_id
are handled as node traversal.
- and, in
- Add
RecentContentsNode
, basing its prototype onContentNode
- Give it a distinct
.visit()
prototype method, skipping any provisioning (unlike normal items, its contents aren't constituted from remote data). - Add to it a unique
.add_visited_url()
, for use byhandle_content_visit()
to register each traversal to a content item.
no_dividers
mode_option
and respect for it in
ContentNode.layout_content()
, or adding a generic
recents_max_size
setting, but that is essentially it.
The traversal provisions are clearly more elaborate than the actual object implementation, and may bear some scrutiny for simplification and consolidation. The above details should be good fodder for such scrutiny.
Special navigation behaviors are communicated internally using specific operational parameters.
- Modes are conveyed in URL addresses as URL '?' query string parameters.
- Modes are conveyed across method calls using the mode_opts parameter. The mode_opts settings are passed through the
.visit()
,.provision*()
, and.show()
/.layout*()
node methods, which may have behaviors conditioned by the options. (Some modes can only be used via the mode_opts method parameters, and are not recognized as URL query parameters.) -
handle_content_visit()
translates from URL to mode_opts usingquery_params(url)
(defined injs_aux/misc.js
).
Currently implemented modes include:
-
refresh
: refetch the data for the node. -
logout
: return to the dashboard/combo root and logout from storage -
passive
: when showing a node, just adjust the DOM$page, don't transfer browser focus by doing a $ .mobile.changePage(). Useful for page updates in the background, or for theRootContentNode
composite page, which is informed by combined subnode updates. -
notify_callback
: report visit success or failure to this callback function. The callback requires: the visit success (true or false), the accompanying value ofnotify_token
(see below, and on failure, the XMLHttpResponse (xhr) object. -
notify_token
: object to be passed back tonotify_callback
. A means for the initiator to identify the transaction and convey useful state. -
actions_menu_link_creator
: Pass in a function to be used by.layout_item$()
to get a link to a context-specific actions-menu, for inclusion on the item's list entry as, eg, a split-button. -
no_dividers
: tellContentNode.layout_content()
to not inhibit creation of dividers even when content is plentiful. -
icon
: Name a particular icon, for use in.layout_item$()
-
transition
: Name a specific transition effect, for use in.layout_item$()
-
alt_page_selector
: A means for a content node to make adjustments to pages in addition to its own. Currently only supported by.layout_header()
, it is a selector passed in to identify the pages to adjust. (As of writing, used byRootContentNode
to set the header of some static info pages.)
Content urls are recognized by virtue of beginning with one of the registered content roots.
The storage root is registered when the user logs in.
The client keeps track of two share rooms roots:
- the collection of share rooms that belongs to the logged-in account, known here as "personally owned share rooms", or "personal share rooms",
- the collection of share rooms that the client has been used to visit, or familiar "public share rooms".
The html5 app, as cloned from the git repository, is arranged so that development is done in the files organized around the clone's root. A few scripts in the ./tools subdirectory compose the releases, and build the platform-specific application packages, using the development copies and resources collected in specific subdirs.
- The cloned repo's top level files, including
index.html
,SpiderOak.js
, and the other files included in the index.html header, are what the developer edits.
Some of the top level files are actually symlinks into specific versions of resources that vary for different renderings of the app.
This top level is organized so you can run by pointing your browser at the index.html (though you need to specifically enable your browser to allow cross-origin operation for file-based pages), for immediate feedback, or you can view release versions produced by some scripts, described next. - The
tools/prep_release
script produces various, complete-unto-themselves "release" collections of the html/javascript/css files, in thereleases
subdirectory. These releases vary according to branding, color scheme, and platform theme, and are named accordingly.
The releases created byprep_release
are not platform-specific executables. (As of this writing, only the iOS platform theme is implemented, Android will be added soon).
Thisprep_release
script is configured with lists of the variations within a set of categories, currently brand, color scheme, and platform style. The script takes selection of no variants within a category to mean doing all the variants for that category. You can see the available variants (without any work done) by invoking the script with--help
.
The script assembles the releases from a combination of the development copies and resources residing in therelease_artifacts
subdirectory. - The
tools/build_platforms
script uses the results of (and machinery from) thetools/prep_release
script to assemble and compile platform-specific executable application packages in thereleases/PhoneGap
subdirectory. It takes the same set of variant specifiers as theprep_release
script.
Unlike the latter,build_platforms
does nothing if no variants at all are selected. You can explicitly get all variants built, without enumerating all the variant selectors, by passing the flag--all
.
build_platforms
currently uses the respective platform SDK to build the platform-specific packages. There is also a relatively new !PhoneGap resource that provides cloud build services for all our concerned platforms. There are administrative project, propriety, and monetary decisions to address before making the technical arrangements. I've checked a preliminary, untestedconfig.xml
in to therelease_artifacts/PhoneGap
subdirectory to build on, if we decide to use this service.
The script tools/prep_release produces various application renditions, varying branding, color scheme, and platform look and feel. The script is also useful as guidance, indicating the locations of rendition-specific elements.
The script has external configuration variables towards to top, for easy incorporation of new elements.
Currently the code is only viewable by running from local files, or as a fully built and side-loaded application. (We are working on organizing a facility for making viewing the latest development code, via a web visit from a regular browser session. In the meanwhile, going through these hoops is necessary to try it out.)
To run the client from local files you must have to run your browser with
flags that reduce browser security, including allowing local file visits to
code (javascript) and also disregarding restrictions on Cross-Origin
Resource Sharing (CORS - see
docs/HTML5ClientAppSameOriginIssues
). Be
aware that this means drastically reduced security!
I use a colleagues suggestions for doing so with Google Chrome. We haven't figured out a way to do so with other browsers.
Here are the options you need to pass to Chrome:
--allow-file-access-from-files --allow-http-access-from-files --disable-web-security --enable-file-cookies
Note: I've added a tiny app to the same share room with the canary dmg. Also useful only in Mac os, "Chrome Permissive.app" launches canary (if you install canary in the default location, as <code>/Applications/Google Chrome Canary.app</code>). Put a copy of this whole directory hierarchy in your <code>/Applications</code> folder and launch it to run Canary enabled to run local files with CORS restrictions eased.
For more details about CORS restrictions and the project, see
docs/HTML5ClientAppSameOriginIssues
.
The default set of jQuery Mobile theme swatches get us near enough to Mike's designs to be worth building on. The jQm theme roller gets us closer, but unfortunately its' resolution is too low to avoid the need for custom tailoring and contortions. It's necessary to know some details about using the tool in order to continue to leverage its benefits while preserving our custom tailoring.
The most essential maintenance routine is feeding our tailored css back into the tool in order to use its minification feature, use the tool for the limited adjustments it can do, and eventually, use it to upgrade to subsequent jQm versions.
To do so:
- Visit the tool at http://jquerymobile.com/themeroller/
- Open the "Import (or Upgrade)" activity
- Enter our adjusted css/themes/iphone.css in the text box and hit Import
- Make adjustments
- Use the "Download (theme zip file)" activity to get the zip file
- Put in place copies of the desired artifacts - .css, .min.css, and incidentals
Because button and list item styles are used across theme swatch page elements (header, content, footer), we sometimes have to use different swatches for different parts of the same page.
In case we need to re-derive our theme, it's based closely on the default jQm theme, using the following procedure:
- Visit http://jquerymobile.com/themeroller/
- Use "Import or Upgrade" tab and "Import default theme"
- Turn swatch A to stark white:
- - Copy the white patch to the header
- - Set header/footer background gradient to be #ffffff to #ffffff
- - Copy the white patch to the content body
- - Set content background gradient to be #ffffff to #ffffff
- Download (with name "spideroak")
- Unzip in css subdir, or elsewhere and copy out the specific artifacts.
At this point there are enough tweaks that you probably want to do any theme rolling [#Maintainingourthemethroughthethemeroller], rather than starting again from scratch.
jQuery Mobile DOM "enhancements" don't just involve adding class (and other) attributes to tags - structures are injected around things, as well. This means that elements location in the DOM may be different, before and after enhancement. For example, the header button labels and icons get enveloped in structures, so that you have to look in the new spot before just changing the location where they were originally found.
As of 2012-05-30, ContentNode.prototype.layout_header_fields()
shows
examples of this elaboration.
Default caching of typical $.ajax() requests - including json ones - means that normal traversal of the content hierarchy won't update after first pass without explicitly asking for a refresh. We specifically override the cache on explicit refreshes, but not on return by traversal to already visited nodes. This seems like a good tradeoff, reducing server load by requiring that the user explicitly ask for an update, but we should warn the user about it.
The (new in jQuery Mobile 1.2) popup machinery requires special provisions
to work properly with our handle_content_visit()
pagebeforechange
handler.
It appears that the URL that launched the popup is fed back through changePage when the popup is dismissed, and the default machinery (evidently) needs to see that traversal to account for closing of the popup.
In order to recognize and properly pass through the second traversal, we
treat URLs that are going to trigger popups with
transit_manager.distinguish_url()
. That adds a query parameter for
handle_content_visit()
to recognize, using
transit_manager.is_repeat_url()
, and pass it through to the standard jQm
traversal mechanism.
Despite all this, the popup menu on the public share rooms split-button (in the public share rooms visit root) misbehaves if it is dismissed twice without any intervening selections or page travesals. In that case, the system will traverse back to the prior page visited. This needs to be fixed, but it is somewhat obscure, and darned stubborn.