ACC SHELL
/**
* View (Lightbox) functionality
* @package Simple Lightbox
* @subpackage View
* @author Archetyped
*/
/* global SLB */
if ( !!window.SLB && !!SLB.attach ) { (function ($) {
/*-** Controller **-*/
var View = {
/* Properties */
/**
* Media item properties
* > Item key: Link URI
* > Base properties
* > id: WP Attachment ID
* > source: Source URI
* > title: Media title (generally WP attachment title)
* > desc: Media description (generally WP Attachment content)
* > type: Asset type (attachment, image, etc.)
*/
assets: {},
/**
* Component types that can have default instances
* @var array
*/
component_defaults: [],
/**
* Collection of jQuery.Deferred instances added during loading routine
* @var array
*/
loading: [],
/**
* Cache
* @var object
*/
cache: {},
/**
* Temporary component instances
* For use by controller when no component instance is available
* > Key: Component slug
* > Value: Component instance
*/
component_temps: {},
/* Options */
options: {},
/* Methods */
/* Init */
/**
* Instance initialization
*/
_init: function() {
this._super();
// Component References
this.init_refs();
// Components
this.init_components();
},
/**
* Update component references in component definitions
*/
init_refs: function() {
var r;
var ref;
var prop;
for ( prop in this ) {
prop = this[prop];
// Process only components
if ( !this.is_component(prop) ) {
continue;
}
// Update component references
if ( !this.util.is_empty(prop.prototype._refs) ) {
for ( r in prop.prototype._refs ) {
ref = prop.prototype._refs[r];
if ( this.util.is_string(ref) && ref in this ) {
ref = prop.prototype._refs[r] = this[ref];
}
if ( !this.util.is_class(ref) ) {
delete prop.prototype_refs[r];
}
}
}
}
},
/**
* Initialize Components
*/
init_components: function() {
this.component_defaults = [
this.Viewer
];
},
/**
* Client Initialization
* @param obj options Global options
*/
init: function(options) {
var t = this;
// Defer initialization until all components loaded
$.when.apply($, this.loading).always(function() {
// Set options
$.extend(true, t.options, options);
/* Event handlers */
// History
$(window).on('popstate', function(e) {
var state = e.originalEvent.state;
if ( t.util.in_obj(state, ['item', 'viewer']) ) {
var v = t.get_viewer(state.viewer);
v.history_handle(e);
return e.preventDefault();
}
});
/* Set defaults */
// Items
t.init_items();
});
},
/* Components */
/**
* Check if default component instance can be created
* @uses View.component_defaults
* @param func type Component type to check
* @return bool TRUE if default component instance creation is allowed
*/
can_make_default_component: function(type) {
return ( -1 !== $.inArray(type, this.component_defaults) );
},
/**
* Check if object is valid component class
* @param func comp Component to check
* @return bool TRUE if object is valid component class
*/
is_component: function(comp) {
return ( this.util.is_class(comp, this.Component) );
},
/**
* Retrieve collection of components of specified type
* @param func type Component type
* @return object Component collection (Default: Empty object)
*/
get_components: function(type) {
var ret = {};
if ( this.is_component(type) ) {
// Determine collection based on component slug
var coll = type.prototype._slug + 's';
// Create default collection
if ( ! ( coll in this.cache ) ) {
this.cache[coll] = {};
}
ret = this.cache[coll];
}
return ret;
},
/**
* Retrieve component from specific collection
* @param function type Component type
* @param string id Component ID
* @return object|null Component reference (NULL if invalid)
*/
get_component: function(type, id) {
var ret = null;
// Validate parameters
if ( !this.util.is_func(type) ) {
return ret;
}
// Sanitize id
if ( !this.util.is_string(id) ) {
id = null;
}
// Get component from collection
var coll = this.get_components(type);
if ( this.util.is_obj(coll) ) {
var tid = ( this.util.is_string(id) ) ? id : this.util.add_prefix('default');
if ( tid in coll ) {
ret = coll[tid];
}
}
// Default: Create default component
if ( this.util.is_empty(ret) ) {
if ( this.util.is_string(id) || this.can_make_default_component(type) ) {
ret = this.add_component(type, id);
}
}
// Return component
return ret;
},
/**
* Create new component instance and save to appropriate collection
* @param function type Component type to create
* @param string id ID of component
* @param object options Component initialization options (Default options used if default component is allowed)
* @return object|null New component (NULL if invalid)
*/
add_component: function(type, id, options) {
// Validate type
if ( !this.util.is_func(type) ) {
return false;
}
// Validate request
if ( this.util.is_empty(id) && !this.can_make_default_component(type) ) {
return false;
}
// Defaults
var ret = null;
if ( this.util.is_empty(id) ) {
id = this.util.add_prefix('default');
}
if ( !this.util.is_obj(options) ) {
options = {};
}
// Check if specialized method exists for component type
var m = ( 'component' !== type.prototype._slug ) ? 'add_' + type.prototype._slug : null;
if ( !this.util.is_empty(m) && ( m in this ) && this.util.is_func(this[m]) ) {
ret = this[m](id, options);
}
// Default process
else {
ret = new type(id, options);
}
// Add new component to collection
if ( this.util.is_type(ret, type) ) {
// Get collection
var coll = this.get_components(type);
// Add to collection
switch ( $.type(coll) ) {
case 'object' :
coll[id] = ret;
break;
case 'array' :
coll.push(ret);
break;
}
} else {
ret = null;
}
// Return new component
return ret;
},
/**
* Create new temporary component instance
* @param function type Component type
* @return New temporary instance
*/
add_component_temp: function(type) {
var ret = null;
if ( this.is_component(type) ) {
// Create new instance
ret = new type('');
// Save to collection
this.component_temps[ret._slug] = ret;
}
return ret;
},
/**
* Retrieve temporary component instance
* Creates new temp component instance for type if not previously created
* @param function type Component type to retrieve temp instance for
* @return obj Temporary component instance
*/
get_component_temp: function(type) {
return ( this.has_component_temp(type) ) ? this.component_temps[type.prototype._slug] : this.add_component_temp(type);
},
/**
* Check if temporary component instance exists
* @param function type Component type to check for
* @return bool TRUE if temp instance exists, FALSE otherwise
*/
has_component_temp: function(type) {
return ( this.is_component(type) && ( type.prototype._slug in this.component_temps ) ) ? true : false;
},
/* Properties */
/**
* Retrieve specified options
* @param array opts Array of option names
* @return object Specified options (Default: empty object)
*/
get_options: function(opts) {
var ret = {};
// Validate
if ( this.util.is_string(opts) ) {
opts = [opts];
}
if ( !this.util.is_array(opts) ) {
return ret;
}
// Get specified options
for ( var x = 0; x < opts.length; x++ ) {
// Skip if option not set
if ( !( opts[x] in this.options ) ) {
continue;
}
ret[ opts[x] ] = this.options[ opts[x] ];
}
return ret;
},
/**
* Retrieve option
* @uses View.options
* @param string opt Option to retrieve
* @param mixed def (optional) Default value if option does not exist (Default: NULL)
* @return mixed Option value
*/
get_option: function(opt, def) {
var ret = this.get_options(opt);
if ( this.util.is_obj(ret) && ( opt in ret ) ) {
ret = ret[opt];
} else {
ret = ( this.util.is_set(def) ) ? def : null;
}
return ret;
},
/* Viewers */
/**
* Add viewer instance to collection
* @param string id Viewer ID
* @param obj options Viewer options
* @return Viewer New viewer instance
*/
add_viewer: function(id, options) {
// Create viewer
var v = new this.Viewer(id, options);
// Save viewer
this.get_viewers()[v.get_id()] = v;
// Return viewer
return v;
},
/**
* Retrieve all viewer instances
* @return obj Viewer instances
*/
get_viewers: function() {
return this.get_components(this.Viewer);
},
/**
* Check if viewer exists
* @param string v Viewer ID
* @return bool TRUE if viewer exists, FALSE otherwise
*/
has_viewer: function(v) {
return ( this.util.is_string(v) && v in this.get_viewers() ) ? true : false;
},
/**
* Retrieve Viewer instance
* Default viewer retrieved if specified viewer does not exist
* > Default viewer created if necessary
* @param string v Viewer ID to retrieve
* @return Viewer Viewer instance
*/
get_viewer: function(v) {
// Retrieve default viewer if specified viewer not set
if ( !this.has_viewer(v) ) {
v = this.util.add_prefix('default');
// Create default viewer if necessary
if ( !this.has_viewer(v) ) {
v = this.add_viewer(v);
v = v.get_id();
}
}
return this.get_viewers()[v];
},
/* Items */
/**
* Set event handlers
*/
init_items: function() {
// Define handler
var t = this;
var handler = function() {
var ret = t.show_item(this);
if ( !t.util.is_bool(ret) ) {
ret = true;
}
return !ret;
};
// Get activated links
var sel = this.util.format('a[href][%s="%s"]', this.util.get_attribute('active'), 1);
// Add event handler
$(document).on('click', sel, null, handler);
},
/**
* Retrieve cached items
* @return obj Items collection
*/
get_items: function() {
return this.get_components(this.Content_Item);
},
/**
* Retrieve specific Content_Item instance
* @param mixed Item reference
* > Content_Item: Item instance (returned immediately)
* > DOM element: DOM element to get item for
* > int: Index of cached item
* @return Content_Item Item instance for DOM node
*/
get_item: function(ref) {
// Evaluate reference type
// Content Item instance
if ( this.util.is_type(ref, this.Content_Item) ) {
return ref;
}
// Retrieve item instance
var item = null;
// DOM element
if ( this.util.in_obj(ref, 'nodeType') ) {
// Check if item instance attached to element
var key = this.get_component_temp(this.Content_Item).get_data_key();
item = $(ref).data(key);
}
// Cached item (index)
else if ( this.util.is_string(ref, false) ) {
var items = this.get_items();
if ( ref in items ) {
item = items[ref];
}
}
// Create default item instance
if ( !this.util.is_instance(item, this.Content_Item) ) {
item = this.add_item(ref);
}
return item;
},
/**
* Create new item instance
* @param obj el DOM element representing item
* @return Content_Item New item instance
*/
add_item: function(el) {
return ( new this.Content_Item(el) );
},
/**
* Display item in viewer
* @param obj el DOM element representing item
* @return bool Display result (TRUE if item displayed without issues)
*/
show_item: function(el) {
return this.get_item(el).show();
},
/**
* Cache item instance
* @uses View.get_items() to retrieve item cache
* @param Content_Item item Item to cache
* @return Content_item Item instance
*/
save_item: function(item) {
if ( !this.util.is_instance(item, this.Content_Item) ) {
return item;
}
// Save item
this.get_items()[item.get_id()] = item;
// Return item instance
return item;
},
/* Content Handler */
/**
* Retrieve content handlers
* @return object Content handlers
*/
get_content_handlers: function() {
return this.get_components(this.Content_Handler);
},
/**
* Find matching content handler for item
* @param Content_Item|string item Item to find handler for (or ID of Handler)
* @return Content_Handler|null Matching content handler (NULL if no matching handler found)
*/
get_content_handler: function(item) {
// Determine handler to retrieve
var type = ( this.util.is_instance(item, this.Content_Item) ) ? item.get_attribute('type', '') : item.toString();
// Retrieve handler
var types = this.get_content_handlers();
return ( type in types ) ? types[type] : null;
},
/**
* Add/Update Content Handler
* @param string id Handler ID
* @param obj attr Handler attributes
* @return obj|null Handler instance (NULL on failure)
*/
extend_content_handler: function(id, attr) {
var hdl = null;
if ( !this.util.is_string(id) || !this.util.is_obj(attr) ) {
return hdl;
}
hdl = this.get_content_handler(id);
// Add new content handler
if ( null === hdl ) {
var hdls = this.get_content_handlers();
hdls[id] = hdl = new this.Content_Handler(id, attr);
}
// Update existing handler
else {
hdl.set_attributes(attr);
}
// Load styles
if ( this.util.in_obj(attr, 'styles') ) {
this.load_styles(attr.styles);
}
return hdl;
},
/* Group */
/**
* Add new group
* @param string g Group ID
* > If group with same ID already set, new group replaces existing one
* @param object attrs (optional) Group attributes
* @return Group New group
*/
add_group: function(g, attrs) {
// Create new group
g = new this.Group(g, attrs);
// Cache group
this.get_groups()[g.get_id()] = g;
return g;
},
/**
* Retrieve groups
* @uses groups property
* @return object Registered groups
*/
get_groups: function() {
return this.get_components(this.Group);
},
/**
* Retrieve specified group
* New group created if not yet set
* @uses View.has_group()
* @uses View.add_group()
* @uses View.get_groups()
* @param string g Group ID
* @return Group Group instance
*/
get_group: function(g) {
return ( !this.has_group(g) ) ? this.add_group(g) : this.get_groups()[g];
},
/**
* Checks if group is registered
* @uses get_groups() to retrieve registered groups
* @return bool TRUE if group exists, FALSE otherwise
*/
has_group: function(g) {
return ( this.util.is_string(g) && ( g in this.get_groups() ) );
},
/* Theme */
/**
* Add/Update theme
* @param string name Theme name
* @param obj attr (optional) Theme options
* @return obj|bool Theme model
*/
extend_theme: function(id, attr) {
// Validate
if ( !this.util.is_string(id) ) {
return false;
}
var dfr = $.Deferred();
this.loading.push(dfr);
// Get model if it already exists
var model = this.get_theme_model(id);
// Create default attributes for new theme
if ( this.util.is_empty(model) ) {
// Save default model
model = this.save_theme_model( {'parent': null, 'id': id} );
}
// Add custom attributes
if ( this.util.is_obj(attr) ) {
// Sanitize
if ( 'id' in attr ) {
delete(attr['id']);
}
$.extend(model, attr);
}
// Load styles
if ( this.util.in_obj(attr, 'styles') ) {
this.load_styles(attr.styles);
}
// Link parent model
if ( !this.util.is_obj(model.parent) ) {
model.parent = this.get_theme_model(model.parent);
}
// Complete loading when all components loaded
dfr.resolve();
return model;
},
/**
* Retrieve theme models
* @return obj Theme models
*/
get_theme_models: function() {
// Retrieve matching theme model
return this.Theme.prototype._models;
},
/**
* Retrieve theme model
* @param string id Theme to retrieve
* @return obj Theme model (Default: empty object)
*/
get_theme_model: function(id) {
var ms = this.get_theme_models();
return ( this.util.in_obj(ms, id) ) ? ms[id] : {};
},
/**
* Save theme model
* @uses View.get_theme_models() to retrieve Theme model collection
* @param obj Theme model to save
* @return obj Saved model
*/
save_theme_model: function(model) {
if ( this.util.in_obj(model, 'id') && this.util.is_string(model.id) ) {
// Save model
this.get_theme_models()[model.id] = model;
}
return model;
},
/**
* Add/Update Template Tag Handler
* @param string id Handler ID
* @param obj attr Handler attributes
* @return obj|bool Handler instance (FALSE on failure)
*/
extend_template_tag_handler: function(id, attr) {
if ( !this.util.is_string(id) || !this.util.is_obj(attr) ) {
return false;
}
var hdl;
var hdls = this.get_template_tag_handlers();
if ( this.util.in_obj(hdls, id) ) {
// Update existing handler
hdl = hdls[id];
hdl.set_attributes(attr);
} else {
// Add new content handler
hdl = new this.Template_Tag_Handler(id, attr);
hdls[hdl.get_id()] = hdl;
}
// Load styles
if ( this.util.in_obj(attr, 'styles') ) {
this.load_styles(attr.styles);
}
// Set hooks
if ( this.util.in_obj(attr, '_hooks') ) {
attr._hooks.call(hdl);
}
return hdl;
},
/**
* Retrieve Template Tag Handler collection
* @return obj Template_Tag_Handler objects
*/
get_template_tag_handlers: function() {
return this.Template_Tag.prototype.handlers;
},
/**
* Retrieve template tag handler
* @param string id ID of tag handler to retrieve
* @return Template_Tag_Handler|null Tag Handler instance (NULL for invalid ID)
*/
get_template_tag_handler: function(id) {
var handlers = this.get_template_tag_handlers();
// Retrieve existing handler or return new handler
return ( this.util.in_obj(handlers, id) ) ? handlers[id] : null;
},
/**
* Load styles
* @param array styles Styles to load
*/
load_styles: function(styles) {
if ( this.util.is_array(styles) ) {
var out = [];
var style;
for ( var x = 0; x < styles.length; x++ ) {
style = styles[x];
if ( !this.util.in_obj(style, 'uri') || !this.util.is_string(style.uri) ) {
continue;
}
out.push('<link rel="stylesheet" type="text/css" href="' + style.uri + '" />');
}
$('head').append(out.join(''));
}
}
};
/* Components */
var Component = {
/*-** Properties **-*/
/* Internal/Configuration */
/**
* Base name of component type
* @var string
*/
_slug: 'component',
/**
* Component namespace
* @var string
*/
_ns: null,
/**
* Valid component references for current component
* @var object
* > Key (string): Property name that stores reference
* > Value (function): Data type of component
*/
_refs: {},
/**
* Whether DOM element and component are connected in 1:1 relationship
* Some components will be assigned to different DOM elements depending on usage
* @var bool
*/
_reciprocal: false,
/**
* DOM Element tied to component
* @var DOM Element
*/
_dom: null,
/**
* Component attributes
* @var object
* > Key: Attribute ID
* > Value: Attribute value
*/
_attributes: false,
/**
* Default attributes
* @var object
*/
_attr_default: {},
/**
* Flag indicates whether default attributes have previously been parsed
* @var bool
*/
_attr_default_parsed: false,
/**
* Attributes passed to constructor
* @var obj
*/
_attr_init: null,
/**
* Defines how parent properties should be remapped to component properties
* @var object
*/
_attr_map: {},
/**
* Event handlers
* @var object
* > Key: string Event type
* > Value: array Handlers
*/
_events: {},
/**
* Status management
* @var object
* > Key: Status ID
* > Value: Status value
*/
_status: null,
/**
* Component ID
* @var string
*/
_id: '',
/* Init */
/**
* Constructor
* @param string id (optional) Component ID (Default ID will be generated if no valid ID provided)
* @param object attributes (optional) Component attributes
*/
_c: function(id, attributes) {
// Set ID
this._set_id(id);
// Save init attributes
if ( this.util.is_obj(attributes) ) {
this._attr_init = attributes;
}
// Call hooks
this._hooks();
},
/**
* Set Component parent to View module
* @uses `_super._set_parent()`
*/
_set_parent: function() {
this._super(View);
},
/**
* Register hooks on init
* Placeholder method to be overridden by child classes
*/
_hooks: function() {},
/* Methods */
/* Properties */
/**
* Set instance ID
* Instance ID can only be set once (will not change ID if valid ID already set)
* Generates random GUID if no valid ID provided
* @uses Utilities.guid()
* @param string id Unique ID
* @return string Instance ID
*/
_set_id: function(id) {
// Set ID only once
if ( this.util.is_empty(this._id) ) {
this._id = ( this.util.is_string(id) ) ? id : this.util.guid();
}
return this._id;
},
/**
* Retrieve instance ID
* @uses id as ID base
* @uses _slug to add namespace (if necessary)
* @param bool ns (optional) Whether or not to namespace ID (Default: FALSE)
* @return string Instance ID
*/
get_id: function(ns) {
// Get raw ID
var id = this._id;
// Namespace ID
if ( this.util.is_bool(ns) && ns ) {
id = this.add_ns(id);
}
return id;
},
/**
* Get namespace
* @uses _slug for namespace segment
* @uses Util.add_prefix() to prefix slug
* @return string Component namespace
*/
get_ns: function() {
if ( null === this._ns ) {
this._ns = this.util.add_prefix(this._slug);
}
return this._ns;
},
/**
* Add namespace to value
* @param string val Value to namespace
* @return string Namespaced value (Empty string if invalid value provided)
*/
add_ns: function(val) {
return ( this.util.is_string(val) ) ? this.get_ns() + '_' + val : '';
},
/**
* Retrieve status
* @param string id Status to retrieve
* @param bool raw (optional) Retrieve raw value (Default: FALSE)
* @return mixed Status value (Default: bool)
*/
get_status: function(id, raw) {
var ret = false;
if ( this.util.in_obj(this._status, id) ) {
ret = ( !!raw ) ? this._status[id] : !!this._status[id];
}
return ret;
},
/**
* Set status
* @param string id Status to retrieve
* @param mixed val Status value (Default: TRUE)
* @return mixed Status value
*/
set_status: function(id, val) {
// Validate ID
if ( this.util.is_string(id) ) {
// Validate value
if ( !this.util.is_set(val) ) {
val = true;
}
// Initialize status collection
if ( !this.util.is_obj(this._status, false) ) {
this._status = {};
}
// Set status
this._status[id] = val;
} else if ( !this.util.is_set(val) ) {
val = false;
}
return val;
},
/**
* Get controller object
* @uses get_parent() (alias)
* @return object Controller object (SLB.View)
*/
get_controller: function() {
return this.get_parent();
},
/* Components */
/**
* Check if reference exists in object
* @param string ref Reference ID
* @return bool TRUE if reference exists, FALSE otherwise
*/
has_reference: function(ref) {
return ( this.util.is_string(ref) && ( ref in this ) && ( ref in this.get_references() ) ) ? true : false;
},
/**
* Retrieve object references
* @uses _refs
* @return obj References object
*/
get_references: function() {
return this._refs;
},
/**
* Retrieve reference data type
* @param string ref Reference ID
* @return function Reference data type (NULL if invalid)
*/
get_reference: function(ref) {
return ( this.has_reference(ref) ) ? this._refs[ref] : null;
},
/**
* Retrieve component reference from current object
* > Procedure:
* > Check if top-level property already set
* > Check attributes
* > Check parent object (controller)
* @param string cname Component name
* @param object options (optional) Request options
* > check_attr bool Whether or not to check instance attributes for component (Default: TRUE)
* > get_default bool Whether or not to retrieve default object from controller if none exists in current instance (Default: FALSE)
* @return object|null Component reference (NULL if no component found)
*/
get_component: function(cname, options) {
var c = null;
// Validate request
if ( !this.has_reference(cname) ) {
return c;
}
// Initialize options
var opt_defaults = {
check_attr: true,
get_default: false
};
options = $.extend({}, opt_defaults, options);
// Get component type
var ctype = this.get_reference(cname);
// Phase 1: Check if component reference previously set
if ( this.util.is_type(this[cname], ctype) ) {
return this[cname];
}
// If reference not set, iterate through component hierarchy until reference is found
c = this[cname] = null;
// Phase 2: Check attributes
if ( options.check_attr ) {
c = this.get_attribute(cname);
// Save object-specific component reference
if ( !this.util.is_empty(c) ) {
c = this.set_component(cname, c);
}
}
// Phase 3: From controller (optional)
if ( this.util.is_empty(c) && options.get_default ) {
c = this.get_controller().get_component(ctype);
}
return c;
},
/**
* Sets component reference on current object
* > Component property reset (set to NULL) if invalid component supplied
* @param string name Name of property to set component on object
* @param string|object ref Component or Component ID (to be retrieved from controller)
* @param function validate (optional) Additional validation to be performed (Must return bool: TRUE (Valid)/FALSE (Invalid))
* @return object Component (NULL if invalid)
*/
set_component: function(name, ref, validate) {
var invalid = null;
// Make sure component property exists
if ( !this.has_reference(name) ) {
return invalid;
}
// Validate reference
if ( this.util.is_empty(ref) ) {
ref = invalid;
} else {
var ctype = this.get_reference(name);
// Get component from controller when ID supplied
if ( this.util.is_string(ref, false) ) {
ref = this.get_controller().get_component(ctype, ref);
}
// Validation callback
if ( !this.util.is_type(ref, ctype) || ( this.util.is_func(validate) && !validate.call(this, ref) ) ) {
ref = invalid;
}
}
// Set (or clear) component reference
this[name] = ref;
// Return value for confirmation
return this[name];
},
/**
* Clear component reference
* @uses set_component() to handle component manipulation
* @param string Component name
*/
clear_component: function(name) {
this.set_component(name, null);
},
/* Attributes */
/**
* Initialize attributes
* @param bool force (optional) Force full initialization of attributes (Default: FALSE)
* @return void
*/
init_attributes: function(force) {
if ( !this.util.is_bool(force) ) {
force = false;
}
if ( force || !this.util.is_obj(this._attributes) ) {
var a = this._attributes = {};
// Default attributes
$.extend(a, this.init_default_attributes());
// Instantiation attributes
if ( this.util.is_obj(this._attr_init) ) {
$.extend(a, this._attr_init);
}
// DOM attributes
$.extend(a, this.get_dom_attributes());
}
},
/**
* Generate default attributes for component
* @uses View.get_options() to get values from controller
* @uses _attr_map to Remap controller attributes to instance attributes
* @uses _attr_default to Store default attributes
* @return obj Default attributes
*/
init_default_attributes: function() {
// Get options from controller
if ( !this._attr_default_parsed && this.util.is_obj(this._attr_map) ) {
var opts = this.get_controller().get_options(this.util.obj_keys(this._attr_map));
if ( this.util.is_obj(opts) ) {
// Remap
for ( var opt in this._attr_map ) {
if ( opt in opts && null !== this._attr_map[opt]) {
// Move value to new property
opts[this._attr_map[opt]] = opts[opt];
// Delete old property
delete opts[opt];
}
}
// Merge with default attributes
$.extend(true, this._attr_default, opts);
}
this._attr_default_parsed = true;
}
return this._attr_default;
},
/**
* Retrieve DOM attributes
* @return obj DOM Attributes
*/
get_dom_attributes: function() {
var attrs = {};
var el = this.dom_get(null, {'init': false});
if ( el.length > 0 ) {
// Get attributes from element
var attrs_full = $(el).get(0).attributes;
if ( this.util.is_obj(attrs_full) ) {
var attr_prefix = this.util.get_attribute();
var attr_key;
$.each(attrs_full, function(idx, attr) {
if ( attr.name.indexOf( attr_prefix ) === -1 ) {
return true;
}
// Process custom attributes (Strip prefix)
attr_key = attr.name.substr(attr_prefix.length + 1);
attrs[attr_key] = attr.value;
});
}
}
return attrs;
},
/**
* Retrieve all instance attributes
* @uses init_attributes() to initialize attributes (if necessary)
* @uses _attributes object
* @return obj Component attributes
*/
get_attributes: function() {
// Initilize attributes
this.init_attributes();
// Return attributes
return this._attributes;
},
/**
* Retrieve value of specified attribute for value
* @param string key Attribute to retrieve
* @param mixed def (optional) Default value if attribute is not set
* @param bool enforce_type (optional) Whether data type should match default value (Default: TRUE)
* > If possible, attribute value will be converted to match default data type
* > If attribute value cannot match default data type, default value will be used
* @return mixed Attribute value (NULL if attribute is not set)
*/
get_attribute: function(key, def, enforce_type) {
// Validate
if ( !this.util.is_set(def) ) {
def = null;
}
if ( !this.util.is_string(key) ) {
return def;
}
if ( !this.util.is_bool(enforce_type) ) {
enforce_type = true;
}
// Get attribute value
var ret = ( this.has_attribute(key) ) ? this.get_attributes()[key] : def;
// Validate type
if ( enforce_type && ret !== def && null !== def && !this.util.is_type(ret, $.type(def), false) ) {
// Convert type
// Scalar default
if ( this.util.is_scalar(def, false) ) {
if ( !this.util.is_scalar(ret, false) ) {
// Non-scalar attribute
ret = def;
} else if ( this.util.is_string(def, false) ) {
// Convert to string
ret = ret.toString();
} else if ( this.util.is_num(def, false) && !this.util.is_num(ret, false) ) {
// Convert to number
ret = ( this.util.is_int(def, false) ) ? parseInt(ret) : parseFloat(ret);
if ( !this.util.is_num(ret, false) ) {
ret = def;
}
} else if ( this.util.is_bool(def, false) ) {
// Convert to boolean
ret = ( this.util.is_string(ret) || ( this.util.is_num(ret) ) );
} else {
// Fallback: Set to default
ret = def;
}
}
// Non-scalar default
else {
ret = def;
}
}
return ret;
},
/**
* Call attribute as method
* @param string attr Attribute to call
* @param arguments (optional) Additional arguments to pass to method
* @return mixed Attribute return value (if attribute is not a function, attribute's value is returned)
*/
call_attribute: function(attr, args) {
attr = this.get_attribute(attr);
if ( this.util.is_func(attr) ) {
// Get arguments
args = Array.prototype.slice.call(arguments, 1);
// Pass arguments to user-defined method
attr = attr.apply(this, args);
}
return attr;
},
/**
* Check if attribute exists
* @param string key Attribute name
* @return bool TRUE if exists, FALSE otherwise
*/
has_attribute: function(key) {
return ( this.util.is_string(key) && ( key in this.get_attributes() ) );
},
/**
* Set component attributes
* @param obj attributes Attributes to set
* @param bool full (optional) Whether to fully replace or merge component's attributes with new values (Default: Merge)
*/
set_attributes: function(attributes, full) {
// Validate
if ( !this.util.is_bool(full) ) {
full = false;
}
// Initialize attributes
this.init_attributes(full);
// Merge new/existing attributes
if ( this.util.is_obj(attributes) ) {
$.extend(this._attributes, attributes);
}
},
/**
* Set value for a component attribute
* @uses get_attributes() to retrieve attributes
* @param string key Attribute to set
* @param mixed val Attribute value
* @return mixed Attribute value
*/
set_attribute: function(key, val) {
if ( this.util.is_string(key) && this.util.is_set(val) ) {
this.get_attributes()[key] = val;
}
return val;
},
/* DOM */
/**
* Generate selector for retrieving child element
* @param string element Class name of child element
* @return string Element selector
*/
dom_get_selector: function(element) {
return ( this.util.is_string(element) ) ? '.' + this.add_ns(element) : '';
},
dom_get_attribute: function() {
return this.util.get_attribute(this._slug);
},
/**
* Set reference of instance on DOM element
* @uses _reciprocal to determine if DOM element should also be attached to instance
* @param string|obj (jQuery) el DOM element to attach instance to
* @return jQuery DOM element set
*/
dom_set: function(el) {
el = $(el);
// Save instance to DOM object
el.data(this.get_data_key(), this);
// Save DOM object to instance
if ( this._reciprocal ) {
this._dom = el;
}
return el;
},
/**
* Retrieve attached DOM element
* @uses _dom to retrieve attached DOM element
* @uses dom_put() to insert child element
* @param string element (optional) ID of child element to retrieve (Default: Main element)
* @param bool put (optional) Whether to insert element if it does not exist (Default: FALSE)
* @param obj options (optional) Runtime options
* @return obj jQuery DOM element
*/
dom_get: function(element, options) {
// Build options
var opts_default = {
'init': true,
'put': false
};
options = ( this.util.is_obj(options) ) ? $.extend({}, opts_default, options) : opts_default;
// Init Component DOM
if ( options.init && !this.get_status('dom_init') ) {
this.set_status('dom_init');
this.dom_init();
}
// Check for main DOM element
var ret = this._dom;
if ( !!ret && this.util.is_string(element) ) {
var ch = $(ret).find( this.dom_get_selector(element) );
// Check for child element
if ( ch.length ) {
ret = ch;
} else if ( true === options.put || this.util.is_obj(options.put) ) {
// Insert child element
ret = this.dom_put(element, options.put);
}
}
return $(ret);
},
/**
* Initialize DOM element
* To be overridden by child classes
*/
dom_init: function() {},
/**
* Wrap output in DOM element
* Wrapper element created and added to main DOM element if not yet created
* @param string element ID for DOM element (Used as class name for wrapper)
* @param string|jQuery|obj content Content to add to DOM (Object contains element properties)
* > tag : Element tag name
* > content : Element content
* @return jQuery Inserted element(s)
*/
dom_put: function(element, content) {
var r = null;
// Stop processing if main DOM element not set or element is not valid
if ( !this.dom_has() || !this.util.is_string(element) ) {
return $(r);
}
// Setup options
var strip = ['tag', 'content', 'success'];
var options = {
'tag': 'div',
'content': '',
'class': this.add_ns(element)
};
// Setup content
if ( !this.util.is_empty(content) ) {
if ( this.util.is_type(content, jQuery, false) || this.util.is_string(content, false) ) {
options.content = content;
}
else if ( this.util.is_obj(content, false) ) {
$.extend(options, content);
}
}
var attrs = $.extend({}, options);
for ( var x = 0; x < strip.length; x++ ) {
delete attrs[strip[x]];
}
// Retrieve existing element
var d = this.dom_get();
r = $(this.dom_get_selector(element), d);
// Create element (if necessary)
if ( !r.length ) {
r = $(this.util.format('<%s />', options.tag), attrs).appendTo(d);
if ( r.length && this.util.is_method(options, 'success') ) {
options['success'].call(r, r);
}
}
// Set content
$(r).append(options.content);
return $(r);
},
/**
* Check if DOM element is set for instance
* DOM is initialized before evaluation
* @return bool TRUE if DOM element set, FALSE otherwise
*/
dom_has: function() {
return ( !!this.dom_get().length );
},
/* Data */
/**
* Retrieve key used to store data in DOM element
* @return string Data key
*/
get_data_key: function() {
return this.get_ns();
},
/* Events */
/**
* Register event handler for custom event
* Structure
* > Events (obj)
* > Event-Name (array)
* > Handlers (functions)
* @param mixed event Custom event to register handler for
* > string: Standard event handler
* > array: Multiple events to register single handler on
* > object: Map of events/handlers
* @param function fn Event handler
* @param obj options Handler registration options
* > clear (bool) Clear existing event handlers before setting current handler (Default: FALSE)
* @return obj Component instance (allows chaining)
*/
on: function(event, fn, options) {
// Handle request types
if ( !this.util.is_string(event) || !this.util.is_func(fn) ) {
var t = this;
var args = Array.prototype.slice.call(arguments, 1);
if ( this.util.is_array(event) ) {
// Events array
$.each(event, function(idx, val) {
t.on.apply(t, [val].concat(args));
});
} else if ( this.util.is_obj(event) ) {
// Events map
$.each(event, function(ev, hdl) {
t.on.apply(t, [ev, hdl].concat(args));
});
}
return this;
}
// Options
// Default options
var options_std = {
clear: false
};
if ( !this.util.is_obj(options, false) ) {
// Reset options
options = {};
}
// Build options
options = $.extend({}, options_std, options);
// Initialize events bucket
if ( !this.util.is_obj(this._events, false) ) {
this._events = {};
}
// Setup event
var es = this._events;
if ( !( event in es ) || !this.util.is_obj(es[event], false) || !!options.clear ) {
es[event] = [];
}
// Add event handler
es[event].push(fn);
return this;
},
/**
* Trigger custom event
* Event handlers are executed in the context of the current component instance
* Event handlers are passed parameters
* > ev (obj) Event object
* > type (string) Event name
* > data (mixed) Data to pass to handlers (if supplied)
* > component (obj) Current component instance
* @param string event Custom event to trigger
* @param mixed data (optional) Data to pass to event handlers
* @return jQuery.Promise Promise that is resolved once event handlers are resolved
*/
trigger: function(event, data) {
var dfr = $.Deferred();
var dfrs = [];
var t = this;
// Handle array of events
if ( this.util.is_array(event) ) {
$.each(event, function(idx, val) {
// Collect promises from triggered events
dfrs.push( t.trigger(val, data) );
});
// Resolve trigger when all events have been resolved
$.when.apply(t, dfrs).done(function() {
dfr.resolve();
});
return dfr.promise();
}
// Validate
if ( !this.util.is_string(event) || !( event in this._events ) ) {
dfr.resolve();
return dfr.promise();
}
// Create event object
var ev = { 'type': event, 'data': null };
// Add data to event object
if ( this.util.is_set(data) ) {
ev.data = data;
}
// Fire handlers for event
$.each(this._events[event], function(idx, fn) {
// Call handler (`this` set to current instance)
// Collect promises from event handlers
dfrs.push( fn.call(t, ev, t) );
});
// Resolve trigger when all handlers have been resolved
$.when.apply(this, dfrs).done(function() {
dfr.resolve();
});
return dfr.promise();
}
};
View.Component = Component = SLB.Class.extend(Component);
/**
* Content viewer
* @param obj options Init options
*/
var Viewer = {
/* Configuration */
_slug: 'viewer',
_refs: {
item: 'Content_Item',
theme: 'Theme'
},
_reciprocal: true,
_attr_default: {
loop: true,
animate: true,
autofit: true,
overlay_enabled: true,
overlay_opacity: '0.8',
title_default: false,
container: null,
slideshow_enabled: true,
slideshow_autostart: false,
slideshow_duration: 2,
slideshow_active: false,
slideshow_timer: null,
labels: {
close: 'close',
nav_prev: '« prev',
nav_next: 'next »',
slideshow_start: 'start slideshow',
slideshow_stop: 'stop slideshow',
group_status: 'Image %current% of %total%',
loading: 'loading'
}
},
_attr_map: {
'theme': null,
'group_loop': 'loop',
'ui_autofit': 'autofit',
'ui_animate': 'animate',
'ui_overlay_opacity': 'overlay_opacity',
'ui_labels': 'labels',
'ui_title_default': 'title_default',
'slideshow_enabled': null,
'slideshow_autostart': null,
'slideshow_duration': null
},
/* References */
/**
* Item currently loaded in viewer
* @var object Content_Item
*/
item: null,
/**
* Queued item to be loaded once viewer is available
* @var object Content_Item
*/
item_queued: null,
/**
* Theme used by viewer
* @var object Theme
*/
theme: null,
/* Properties */
item_working: null,
active: false,
init: false,
open: false,
loading: false,
/* Methods */
/* Init */
_hooks: function() {
var t = this;
this
.on(['item-prev', 'item-next'], function() {
t.trigger('item-change');
})
.on(['close', 'item-change'], function() {
t.unload().done(function() {
t.unlock();
});
});
},
/* References */
/**
* Retrieve item instance current attached to viewer
* @return Content_Item|NULL Current item instance
*/
get_item: function() {
return this.get_component('item');
},
/**
* Set item reference
* Validates item before setting
* @param obj item Content_Item instance
* @return bool TRUE if valid item set, FALSE otherwise
*/
set_item: function(item) {
// Clear existing item
this.clear_item(false);
var i = this.set_component('item', item, function(item) {
return ( item.has_type() );
});
return ( !this.util.is_empty(i) );
},
/**
* Clear item from viewer
* Resets item state and removes reference (if necessary)
* @param bool full (optional) Fully remove item? (Default: TRUE)
*/
clear_item: function(full) {
// Validate
if ( !this.util.is_bool(full) ) {
full = true;
}
var item = this.get_item();
if ( !!item ) {
item.reset();
}
if ( full ) {
this.clear_component('item');
}
},
/**
* Retrieve theme reference
* @return object Theme reference
*/
get_theme: function() {
// Get saved theme
var ret = this.get_component('theme', {check_attr: false});
if ( this.util.is_empty(ret) ) {
// Theme needs to be initialized
ret = this.set_component('theme', new View.Theme(this));
}
return ret;
},
/**
* Set viewer's theme
* @param object theme Theme object
*/
set_theme: function(theme) {
this.set_component('theme', theme);
},
/* Properties */
/**
* Lock the viewer
* Indicates that item is currently being processed
* @return jQuery.Deferred Resolved when item processing is complete
*/
lock: function() {
return this.set_status('item_working', $.Deferred());
},
/**
* Retrieve lock
* @param bool simple (optional) Whether to return a simple status of the locked status (Default: FALSE)
* @param bool full (optional) Whether to return Deferred (TRUE) or Promise (FALSE) object (Default: FALSE)
* @return jQuery.Promise Resolved when item processing is complete
*/
get_lock: function(simple, full) {
// Validate
if ( !this.util.is_bool(simple) ) {
simple = false;
}
if ( !this.util.is_bool(full) ) {
full = false;
}
var s = 'item_working';
// Simple status
if ( simple ) {
return this.get_status(s);
}
// Full value
var r = this.get_status(s, true);
if ( !this.util.is_promise(r) ) {
// Create default
r = this.lock();
}
return ( full ) ? r : r.promise();
},
is_locked: function() {
return this.get_lock(true);
},
/**
* Unlock the viewer
* Any callbacks registered for this action will be executed
* @return jQuery.Deferred Resolved instance
*/
unlock: function() {
return this.get_lock(false, true).resolve();
},
/**
* Set Viewer active status
* @param bool mode (optional) Activate or deactivate status (Default: TRUE)
* @return bool Active status
*/
set_active: function(mode) {
if ( !this.util.is_bool(mode) ) {
mode = true;
}
return this.set_status('active', mode);
},
/**
* Check Viewer active status
* @return bool Active status
*/
is_active: function() {
return this.get_status('active');
},
/**
* Set loading mode
* @param bool mode (optional) Set (TRUE) or unset (FALSE) loading mode (Default: TRUE)
* @return jQuery.Promise Promise that resolves when loading mode is set
*/
set_loading: function(mode) {
var dfr = $.Deferred();
if ( !this.util.is_bool(mode) ) {
mode = true;
}
this.loading = mode;
// Pause/Resume slideshow
if ( this.slideshow_active() ) {
this.slideshow_pause(mode);
}
// Set CSS class on DOM element
var m = ( mode ) ? 'addClass' : 'removeClass';
$(this.dom_get())[m]('loading');
if ( mode ) {
// Loading transition
this.get_theme().transition('load').always(function() {
dfr.resolve();
});
} else {
dfr.resolve();
}
return dfr.promise();
},
/**
* Unset loading mode
* @see set_loading()
* @return jQuery.Promise Promise that resovles when loading mode is set
*/
unset_loading: function() {
return this.set_loading(false);
},
/**
* Retrieve loading status
* @return bool Loading status (Default: FALSE)
*/
get_loading: function() {
return ( this.util.is_bool(this.loading) ) ? this.loading : false;
},
/**
* Check if viewer is currently loading content
* @return bool Loading status (Default: FALSE)
*/
is_loading: function() {
return this.get_loading();
},
/* Display */
/**
* Display content in viewer
* @param Content_Item item Item to show
* @param obj options (optional) Display options
*/
show: function(item) {
this.item_queued = item;
var fin_set = 'show_deferred';
// Validate theme
var vt = 'theme_valid';
var valid = true;
if ( this.has_attribute(vt)) {
valid = this.get_attribute(vt, true);
} else {
valid = ( this.get_theme() && this.get_theme().get_template().get_layout(false) !== "" ) ? true : false;
this.set_attribute(vt, valid);
}
if ( !valid ) {
this.close();
return false;
}
var v = this;
var fin = function() {
// Lock viewer
v.lock();
// Reset callback flag (for new lock)
v.set_status(fin_set, false);
// Validate request
if ( !v.set_item(v.item_queued) ) {
v.close();
return false;
}
// Add item to history stack
v.history_add();
// Activate
v.set_active();
// Display
v.render();
};
if ( !this.is_locked() ) {
fin();
} else if ( !this.get_status(fin_set) ) {
// Set flag to avoid duplicate callbacks
this.set_status(fin_set);
this.get_lock().always(function() {
fin();
});
}
},
/* History Management */
history_handle: function(e) {
var state = e.originalEvent.state;
// Load item
if ( this.util.is_string(state.item, false) ) {
this.get_controller().get_item(state.item).show({'event': e});
this.trigger('item-change');
} else {
var count = this.history_get(true);
// Reset count
this.history_set(0);
// Close viewer
if ( -1 !== count ) {
this.close();
}
}
},
history_get: function(full) {
return this.get_status('history_count', full);
},
history_set: function(val) {
return this.set_status('history_count', val);
},
history_add: function() {
if ( !history.pushState ) {
return false;
}
// Get display options
var item = this.get_item();
var opts = item.get_attribute('options_show');
// Save history state
var count = ( this.history_get() ) ? this.history_get(true) : 0;
if ( !this.util.in_obj(opts, 'event') ) {
// Create state
var state = {
'viewer': this.get_id(),
'item': null,
'count': count
};
// Init: Save viewer state
if ( !count ) {
history.replaceState(state, null);
}
// Always: Save item state
state.item = this.get_controller().save_item(item).get_id();
state.count = ++count;
history.pushState(state, '');
} else {
var e = opts.event.originalEvent;
if ( this.util.in_obj(e, 'state') && this.util.in_obj(e.state, 'count') ) {
count = e.state.count;
}
}
// Save history item count
this.history_set(count);
},
history_reset: function() {
var count = this.history_get(true);
if ( count ) {
// Clear history status
this.history_set(-1);
// Restore history stack
history.go( -1 * count );
}
},
/**
* Check if viewer is currently open
* Checks if node is actually visible in DOM
* @return bool TRUE if viewer is open, FALSE otherwise
*/
is_open: function() {
return ( this.dom_get().css('display') === 'none' ) ? false : true;
},
/**
* Load output into DOM
*/
render: function() {
// Get theme output
var v = this;
var thm = this.get_theme();
v.dom_prep();
// Register theme event handlers
if ( !this.get_status('render-events') ) {
this.set_status('render-events');
thm
// Loading
.on('render-loading', function(ev, thm) {
var dfr = $.Deferred();
if ( !v.is_active() ) {
dfr.reject();
return dfr.promise();
}
var set_pos = function() {
// Set position
v.dom_get().css('top', $(window).scrollTop());
};
var always = function() {
// Set loading flag
v.set_loading().always(function() {
dfr.resolve();
});
};
if ( v.is_open() ) {
thm.transition('unload')
.fail(function() {
set_pos();
thm.dom_get_tag('item', 'content').attr('style', '');
})
.always(always);
} else {
thm.transition('open')
.always(function() {
always();
v.events_open();
v.open = true;
})
.fail(function() {
set_pos();
// Fallback open
v.get_overlay().show();
v.dom_get().show();
});
}
return dfr.promise();
})
// Complete
.on('render-complete', function(ev, thm) {
// Stop if viewer not active
if ( !v.is_active() ) {
return false;
}
// Set classes
var d = v.dom_get();
var classes = ['item_single', 'item_multi'];
var ms = ['addClass', 'removeClass'];
if ( !v.get_item().get_group().is_single() ) {
ms.reverse();
}
$.each(ms, function(idx, val) {
d[val](classes[idx]);
});
// Bind events
v.events_complete();
// Transition
thm.transition('complete')
.fail(function() {
// Autofit content
if ( v.get_attribute('autofit', true) ) {
var dims = $.extend({'display': 'inline-block'}, thm.get_item_dimensions());
thm.dom_get_tag('item', 'content').css(dims);
}
})
.always(function() {
// Unset loading flag
v.unset_loading();
// Trigger event
v.trigger('render-complete');
// Set viewer as initialized
v.init = true;
});
});
}
// Render
thm.render();
},
/**
* Retrieve container element
* Creates default container element if not yet created
* @return jQuery Container element
*/
dom_get_container: function() {
var sel = this.get_attribute('container');
// Set default container
if ( this.util.is_empty(sel) ) {
sel = '#' + this.add_ns('wrap');
}
// Add default container to DOM if not yet present
var c = $(sel);
if ( !c.length ) {
// Prepare ID
var id = ( sel.indexOf('#') === 0 ) ? sel.substr(1) : sel;
// Add element
c = $('<div />', {'id': id}).appendTo('body');
}
return c;
},
/**
* Custom Viewer DOM initialization
*/
dom_init: function() {
// Create element & add to DOM
// Save element to instance
var d = this.dom_set($('<div/>', {
'id': this.get_id(true),
'class': this.get_ns()
})).appendTo(this.dom_get_container()).hide();
// Add theme classes
var thm = this.get_theme();
d.addClass(thm.get_classes(' '));
// Add theme layout (basic)
var v = this;
if ( !this.get_status('render-init') ) {
this.set_status('render-init');
thm.on('render-init', function(ev) {
// Add rendered theme layout to viewer DOM
v.dom_put('layout', ev.data);
});
}
thm.render(true);
},
/**
* Prepare DOM for viewer
*/
dom_prep: function(mode) {
var m = ( this.util.is_bool(mode) && !mode ) ? 'removeClass' : 'addClass';
$('html')[m](this.util.add_prefix('overlay'));
},
/**
* Restore DOM
* Required after viewer is closed
*/
dom_restore: function() {
this.dom_prep(false);
},
/* Layout */
get_layout: function() {
var ret = this.dom_get('layout', {
'put': {
'success': function() {
$(this).hide();
}
}
});
return ret;
},
/* Animation */
animation_enabled: function() {
return this.get_attribute('animate', true);
},
/* Overlay */
/**
* Determine if overlay is enabled for viewer
* @return bool TRUE if overlay is enabled, FALSE otherwise
*/
overlay_enabled: function() {
var ov = this.get_attribute('overlay_enabled');
return ( this.util.is_bool(ov) ) ? ov : false;
},
/**
* Retrieve overlay DOM element
* @return jQuery Overlay element (NULL if no overlay set for viewer)
*/
get_overlay: function() {
var o = null;
var v = this;
if ( this.overlay_enabled() ) {
o = this.dom_get('overlay', {
'put': {
'success': function() {
$(this).hide().css('opacity', v.get_attribute('overlay_opacity'));
}
}
});
}
return $(o);
},
/**
* Unload viewer
*/
unload: function() {
var dfr = $.Deferred();
// Unload item data
this.get_theme().dom_get_tag('item').text('');
dfr.resolve();
return dfr.promise();
},
/**
* Reset viewer
*/
reset: function() {
// Hide viewer
this.dom_get().hide();
// Restore DOM
this.dom_restore();
// History
this.history_reset();
// Item
this.clear_item();
// Reset properties
this.set_active(false);
this.set_loading(false);
this.slideshow_stop();
this.keys_disable();
// Clear for next item
this.unlock();
},
/* Content */
get_labels: function() {
return this.get_attribute('labels', {});
},
get_label: function(name) {
var lbls = this.get_labels();
return ( name in lbls ) ? lbls[name] : '';
},
/* Interactivity */
/**
* Initialize event handlers upon opening lightbox
*/
events_open: function() {
// Keyboard bindings
this.keys_enable();
if ( this.open ) {
return false;
}
// Control event bubbling
var l = this.get_layout();
l.children().click(function(ev) {
ev.stopPropagation();
});
/* Close */
var v = this;
var close = function() {
v.close();
};
// Layout
l.click(close);
// Overlay
this.get_overlay().click(close);
// Fire event
this.trigger('events-open');
},
/**
* Initialize event handlers upon completing lightbox rendering
*/
events_complete: function() {
if ( this.init ) {
return false;
}
// Fire event
this.trigger('events-complete');
},
keys_enable: function(mode) {
if ( !this.util.is_bool(mode) ) {
mode = true;
}
var e = ['keyup', this.util.get_prefix()].join('.');
var v = this;
var h = function(ev) {
return v.keys_control(ev);
};
if ( mode ) {
$(document).on(e, h);
} else {
$(document).off(e);
}
},
keys_disable: function() {
this.keys_enable(false);
},
keys_control: function(ev) {
var handlers = {
27: this.close,
37: this.item_prev,
39: this.item_next
};
if ( ev.which in handlers ) {
handlers[ev.which].call(this);
return false;
}
},
/**
* Check if slideshow functionality is enabled
* @return bool TRUE if slideshow is enabled, FALSE otherwise
*/
slideshow_enabled: function() {
var o = this.get_attribute('slideshow_enabled');
return ( this.util.is_bool(o) && o && this.get_item() && !this.get_item().get_group().is_single() ) ? true : false;
},
/**
* Checks if slideshow is currently active
* @return bool TRUE if slideshow is active, FALSE otherwise
*/
slideshow_active: function() {
return ( this.slideshow_enabled() && ( this.get_attribute('slideshow_active') || ( !this.init && this.get_attribute('slideshow_autostart') ) ) ) ? true : false;
},
/**
* Clear slideshow timer
*/
slideshow_clear_timer: function() {
clearInterval(this.get_attribute('slideshow_timer'));
},
/**
* Start slideshow timer
* @param function callback Callback function
*/
slideshow_set_timer: function(callback) {
this.set_attribute('slideshow_timer', setInterval(callback, this.get_attribute('slideshow_duration') * 1000));
},
/**
* Start Slideshow
*/
slideshow_start: function() {
if ( !this.slideshow_enabled() ) {
return false;
}
this.set_attribute('slideshow_active', true);
this.dom_get().addClass('slideshow_active');
// Clear residual timers
this.slideshow_clear_timer();
// Start timer
var v = this;
this.slideshow_set_timer(function() {
// Pause slideshow until next item fully loaded
v.slideshow_pause();
// Show next item
v.item_next();
});
this.trigger('slideshow-start');
},
/**
* Stop Slideshow
* @param bool full (optional) Full stop (TRUE) or pause (FALSE) (Default: TRUE)
*/
slideshow_stop: function(full) {
if ( !this.util.is_bool(full) ) {
full = true;
}
if ( full ) {
this.set_attribute('slideshow_active', false);
this.dom_get().removeClass('slideshow_active');
}
// Kill timers
this.slideshow_clear_timer();
this.trigger('slideshow-stop');
},
slideshow_toggle: function() {
if ( !this.slideshow_enabled() ) {
return false;
}
if ( this.slideshow_active() ) {
this.slideshow_stop();
} else {
this.slideshow_start();
}
this.trigger('slideshow-toggle');
},
/**
* Pause Slideshow
* @param bool mode (optional) Pause (TRUE) or Resume (FALSE) slideshow (default: TRUE)
*/
slideshow_pause: function(mode) {
// Validate
if ( !this.util.is_bool(mode) ) {
mode = true;
}
// Set viewer slideshow properties
if ( this.slideshow_active() ) {
if ( !mode ) {
// Slideshow resumed
this.slideshow_start();
} else {
// Slideshow paused
this.slideshow_stop(false);
}
}
this.trigger('slideshow-pause');
},
/**
* Resume slideshow
*/
slideshow_resume: function() {
this.slideshow_pause(false);
},
/**
* Next item
*/
item_next: function() {
var g = this.get_item().get_group(true);
var v = this;
var ev = 'item-next';
var st = ['events', 'viewer', ev].join('_');
// Setup event handler
if ( !g.get_status(st) ) {
g.set_status(st);
g.on(ev, function(e) {
v.trigger(e.type);
});
}
g.show_next();
},
/**
* Previous item
*/
item_prev: function() {
var g = this.get_item().get_group(true);
var v = this;
var ev = 'item-prev';
var st = ['events', 'viewer', ev].join('_');
if ( !g.get_status(st) ) {
g.set_status(st);
g.on(ev, function() {
v.trigger(ev);
});
}
g.show_prev();
},
/**
* Close viewer
*/
close: function() {
// Deactivate
this.set_active(false);
var v = this;
var thm = this.get_theme();
thm.transition('unload')
.always(function() {
thm.transition('close', true).always(function() {
// End processes
v.reset();
v.trigger('close');
});
})
.fail(function() {
thm.dom_get_tag('item', 'content').attr('style', '');
});
return false;
}
};
View.Viewer = Component.extend(Viewer);
/**
* Content group
* @param obj options Init options
*/
var Group = {
/* Configuration */
_slug: 'group',
_reciprocal: true,
_refs: {
'current': 'Content_Item'
},
/* References */
current: null,
/* Properties */
/**
* Selector for getting group items
* @var string
*/
selector: null,
/* Methods */
/* Init */
_hooks: function() {
var t = this;
this.on(['item-prev', 'item-next'], function() {
t.trigger('item-change');
});
},
/* Properties */
/**
* Retrieve selector for group items
* @return string Group items selector
*/
get_selector: function() {
if ( this.util.is_empty(this.selector) ) {
// Build selector
this.selector = this.util.format('a[%s="%s"]', this.dom_get_attribute(), this.get_id());
}
return this.selector;
},
/**
* Retrieve group items
*/
get_items: function() {
var items = $(this.get_selector());
if ( 0 === items.length && this.has_current() ) {
items = this.get_current().dom_get();
}
return items;
},
/**
* Retrieve item at specified index
* If no index specified, first item is returned
* @param int idx Index of item to return
* @return Content_Item Item
*/
get_item: function(idx) {
// Validation
if ( !this.util.is_int(idx) ) {
idx = 0;
}
// Retrieve all items
var items = this.get_items();
// Validate index
var max = this.get_size() - 1;
if ( idx > max ) {
idx = max;
}
// Return specified item
return items.get(idx);
},
/**
* Retrieve (zero-based) position of specified item in group
* @param Content_Item item Item to locate in group
* @return int Index position of item in group (-1 if item not in group)
*/
get_pos: function(item) {
if ( this.util.is_empty(item) ) {
// Get current item
item = this.get_current();
}
return ( this.util.is_type(item, View.Content_Item) ) ? this.get_items().index(item.dom_get()) : -1;
},
/**
* Check if current item set in group
* @return bool TRUE if current item is set
*/
has_current: function() {
// Sanitize
return ( !this.util.is_empty( this.get_current() ) );
},
/**
* Retrieve current item
* @uses Group.current
* @return NULL|Content_Item Current item (NULL if current item not set or invalid)
*/
get_current: function() {
// Sanitize
if ( null !== this.current && !this.util.is_type(this.current, View.Content_Item) ) {
this.current = null;
}
return this.current;
},
/**
* Sets current group item
* @param Content_Item item Item to set as current
*/
set_current: function(item) {
// Validate
if ( this.util.is_type(item, View.Content_Item) ) {
// Set current item
this.current = item;
}
},
get_next: function(item) {
// Validate
if ( !this.util.is_type(item, View.Content_Item) ) {
item = this.get_current();
}
if ( this.get_size() === 1 ) {
return item;
}
var next = null;
var pos = this.get_pos(item);
if ( pos !== -1 ) {
pos = ( pos + 1 < this.get_size() ) ? pos + 1 : 0;
if ( 0 !== pos || item.get_viewer().get_attribute('loop') ) {
next = this.get_item(pos);
}
}
return next;
},
get_prev: function(item) {
// Validate
if ( !this.util.is_type(item, View.Content_Item) ) {
item = this.get_current();
}
if ( this.get_size() === 1 ) {
return item;
}
var prev = null;
var pos = this.get_pos(item);
if ( pos !== -1 && ( 0 !== pos || item.get_viewer().get_attribute('loop') ) ) {
if ( pos === 0 ) {
pos = this.get_size();
}
pos -= 1;
prev = this.get_item(pos);
}
return prev;
},
show_next: function(item) {
if ( this.get_size() > 1 ) {
// Retrieve item
var next = this.get_next(item);
if ( !next ) {
if ( !this.util.is_type(item, View.Content_Item) ) {
item = this.get_current();
}
item.get_viewer().close();
}
var i = this.get_controller().get_item(next);
// Update current item
this.set_current(i);
// Show item
i.show();
// Fire event
this.trigger('item-next');
}
},
show_prev: function(item) {
if ( this.get_size() > 1 ) {
// Retrieve item
var prev = this.get_prev(item);
if ( !prev ) {
if ( !this.util.is_type(item, View.Content_Item) ) {
item = this.get_current();
}
item.get_viewer().close();
}
var i = this.get_controller().get_item(prev);
// Update current item
this.set_current(i);
// Show item
i.show();
// Fire event
this.trigger('item-prev');
}
},
/**
* Retrieve total number of items in group
* @return int Number of items in group
*/
get_size: function() {
return this.get_items().length;
},
is_single: function() {
return ( this.get_size() === 1 );
}
};
View.Group = Component.extend(Group);
/**
* Content Handler
* @param obj options Init options
*/
var Content_Handler = {
/* Configuration */
_slug: 'content_handler',
_refs: {
'item': 'Content_Item'
},
/* References */
item: null,
/* Properties */
/**
* Raw layout template
* @var string
*/
template: '',
/* Methods */
/* Item */
/**
* Check if item instance set for type
* @uses get_item()
* @uses clear_item() to remove invalid item values
* @return bool TRUE if valid item set, FALSE otherwise
*/
has_item: function() {
return ( this.util.is_empty(this.get_item()) ) ? false : true;
},
/**
* Retrieve item instance set on type
* @uses get_component()
* @return mixed Content_Item if valid item set, NULL otherwise
*/
get_item: function() {
return this.get_component('item');
},
/**
* Set item instance for type
* Items are only meant to be set/used while item is being processed
* @uses set_component()
* @param Content_Item item Item instance
* @return obj|null Item instance if item successfully set, NULL otherwise
*/
set_item: function(item) {
// Set reference
var r = this.set_component('item', item);
return r;
},
/**
* Clear item instance from type
* Sets value to NULL
*/
clear_item: function() {
this.clear_component('item');
},
/* Evaluation */
/**
* Check if item matches content handler
* @param object item Content_Item instance to check for type match
* @return bool TRUE if type matches, FALSE otherwise
*/
match: function(item) {
// Validate
var attr = 'match';
var m = this.get_attribute(attr);
// Stop processing types with no matching algorithm
if ( !this.util.is_empty(m) ) {
// Process regex patterns
// String-based
if ( this.util.is_string(m) ) {
// Create new regexp object
m = new RegExp(m, "i");
this.set_attribute(attr, m);
}
// RegExp based
if ( this.util.is_type(m, RegExp) ) {
return m.test(item.get_uri());
}
// Process function
if ( this.util.is_func(m) ) {
return ( m.call(this, item) ) ? true : false;
}
}
// Default
return false;
},
/* Processing/Output */
/**
* Loads item data
* @param obj item Content item to load data for
* @return obj Promise that is resolved when item data is loaded
*/
load: function(item) {
var dfr = $.Deferred();
var ret = this.call_attribute('load', item, dfr);
// Handle missing load method
if ( null === ret ) {
dfr.resolve();
}
return dfr.promise();
},
/**
* Render output to display item
* @param Content_Item item Item to render output for
* @return obj jQuery.Promise that is resolved when item is rendered
*/
render: function(item) {
var dfr = $.Deferred();
// Validate
this.call_attribute('render', item, dfr);
return dfr.promise();
}
};
View.Content_Handler = Component.extend(Content_Handler);
/**
* Content Item
* @param obj options Init options
*/
var Content_Item = {
/* Configuration */
_slug: 'content_item',
_reciprocal: true,
_refs: {
'viewer': 'Viewer',
'group': 'Group',
'type': 'Content_Handler'
},
_attr_default: {
source: null,
permalink: null,
dimensions: null,
title: '',
group: null,
internal: false,
output: null
},
/* References */
group: null,
viewer: null,
type: null,
/* Properties */
data: null,
loaded: null,
/* Init */
_c: function(el) {
// Save element to instance
this.dom_set(el);
// Default initialization
this._super();
},
/* Methods */
/*-** Attributes **-*/
/**
* Build default attributes
* Populates attributes with asset properties (attachments)
* Overrides super class method
* @uses Component.init_default_attributes()
*/
init_default_attributes: function() {
this._super();
// Add asset properties
var d = this.dom_get();
var key = d.attr(this.util.get_attribute('asset')) || null;
var assets = this.get_controller().assets || null;
// Merge asset data with default attributes
if ( this.util.is_string(key) ) {
var attrs = [{}, this._attr_default, {'permalink': d.attr('href')}];
if ( this.util.is_obj(assets) ) {
var t = this;
/**
* Retrieve item assets
* Handles variant items as well (Retrieves parent item assets)
* @param string key Item URI
* @return obj Item assets (Empty if no match)
*/
var get_assets = function(key) {
var ret = {};
if ( key in assets && t.util.is_obj(assets[key]) ) {
ret = assets[key];
}
return ret;
};
// Save assets
attrs.push(get_assets(key));
}
this._attr_default = $.extend.apply(this, attrs);
}
return this._attr_default;
},
/*-** Properties **-*/
/**
* Retrieve item output
* Output generated based on content handler if not previously generated
* @uses get_attribute() to retrieve cached output
* @uses set_attribute() to cache generated output
* @uses get_type() to retrieve item type
* @uses Content_Handler.render() to generate item output
* @return obj jQuery.Promise that is resolved when output is retrieved
*/
get_output: function() {
var dfr = $.Deferred();
// Check for cached output
var ret = this.get_attribute('output');
if ( this.util.is_string(ret) ) {
dfr.resolve(ret);
} else if ( this.has_type() ) {
// Render output from scratch (if necessary)
// Get item type
var type = this.get_type();
// Render type-based output
var item = this;
type.render(this).done(function(output) {
// Cache output
item.set_output(output);
dfr.resolve(output);
});
} else {
dfr.resolve('');
}
return dfr.promise();
},
/**
* Cache output for future retrieval
* @uses set_attribute() to cache output
*/
set_output: function(out) {
if ( this.util.is_string(out, false) ) {
this.set_attribute('output', out);
}
},
/**
* Retrieve item output
* Alias for `get_output()`
* @return jQuery.Promise Deferred that is resolved when content is retrieved
*/
get_content: function() {
return this.get_output();
},
/**
* Retrieve item URI
* @param string mode (optional) Which URI should be retrieved
* > source: Media source
* > permalink: Item permalink
* @return string Item URI
*/
get_uri: function(mode) {
// Validate
if ( $.inArray(mode ,['source', 'permalink']) === -1 ) {
mode = 'source';
}
// Retrieve URI
var ret = this.get_attribute(mode);
if ( !this.util.is_string(ret) ) {
ret = ( 'source' === mode ) ? this.get_attribute('permalink') : '';
}
// Format
ret = ret.replace('&', '&');
return ret;
},
/**
* Retrieve item title
*/
get_title: function() {
var prop = 'title';
var prop_cached = prop + '_cached';
// Check for cached value
if ( this.has_attribute(prop_cached) ) {
return this.get_attribute(prop_cached, '');
}
var title = '';
var sel_cap = '.wp-caption-text';
// Generate title from DOM values
var dom = this.dom_get();
// Standalone link
if ( dom.length && !this.in_gallery() ) {
// Link title
title = dom.attr(prop);
// Caption
if ( !title ) {
title = dom.siblings(sel_cap).html();
}
}
// Saved attributes
if ( !title ) {
var props = ['caption', 'title'];
for ( var x = 0; x < props.length; x++ ) {
title = this.get_attribute(props[x], '');
if ( !this.util.is_empty(title) ) {
break;
}
}
}
// Fallbacks
if ( !title && dom.length ) {
// Alt attribute
title = dom.find('img').first().attr('alt');
// Element text
if ( !title ) {
title = dom.text();
}
}
// Validate
if ( !this.util.is_string(title, false) ) {
title = '';
}
// Strip default title
if ( !this.util.is_empty(title) && !this.get_viewer().get_attribute('title_default') ) {
var f = this.get_uri('source');
var i = f.lastIndexOf('/');
if ( -1 !== i ) {
f = f.substr(i + 1);
i = f.lastIndexOf('.');
if ( -1 !== i ) {
f = f.substr(0, i);
}
if ( title === f ) {
title = '';
}
}
}
// Cache retrieved value
this.set_attribute(prop_cached, title);
// Return value
return title;
},
/**
* Retrieve item dimensions
* @return obj Item `width` and `height` properties (px)
*/
get_dimensions: function() {
return $.extend({'width': 0, 'height': 0}, this.get_attribute('dimensions'), {});
},
/**
* Save item data to instance
* Item data is saved when rendered
* @param mixed data Item data (property cleared if NULL)
*/
set_data: function(data) {
this.data = data;
},
get_data: function() {
return this.data;
},
/**
* Determine gallery type
* @return string|null Gallery type ID (NULL if item not in gallery)
*/
gallery_type: function() {
var ret = null;
var types = {
'wp': '.gallery-icon',
'ngg': '.ngg-gallery-thumbnail'
};
var dom = this.dom_get();
for ( var type in types ) {
if ( dom.parent(types[type]).length > 0 ) {
ret = type;
break;
}
}
return ret;
},
/**
* Check if current link is part of a gallery
* @param string gType (optional) Gallery type to check for
* @return bool TRUE if link is part of (specified) gallery (FALSE otherwise)
*/
in_gallery: function(gType) {
var type = this.gallery_type();
// No gallery
if ( null === type ) {
return false;
}
// Boolean check
if ( !this.util.is_string(gType) ) {
return true;
}
// Check for specific gallery type
return ( gType === type ) ? true : false;
},
/*-** Component References **-*/
/* Viewer */
get_viewer: function() {
return this.get_component('viewer', {get_default: true});
},
/**
* Sets item's viewer property
* @uses View.get_viewer() to retrieve global viewer
* @uses this.viewer to save item's viewer
* @param string|View.Viewer v Viewer to set for item
* > Item's viewer is reset if invalid viewer provided
*/
set_viewer: function(v) {
return this.set_component('viewer', v);
},
/* Group */
/**
* Retrieve item's group
* @param bool set_current (optional) Sets item as current item in group (Default: FALSE)
* @return View.Group|bool Group reference item belongs to (FALSE if no group)
*/
get_group: function(set_current) {
var prop = 'group';
// Check if group reference already set
var g = this.get_component(prop);
if ( g ) {
} else {
// Set empty group if no group exists
g = this.set_component(prop, new View.Group());
set_current = true;
}
if ( !!set_current ) {
g.set_current(this);
}
return g;
},
/**
* Sets item's group property
* @uses View.get_group() to retrieve global group
* @uses this.group to set item's group
* @param string|View.Group g Group to set for item
* > Item's group is reset if invalid group provided
*/
set_group: function(g) {
// If group ID set, get object reference
if ( this.util.is_string(g) ) {
g = this.get_controller().get_group(g);
}
// Set (or clear) group property
this.group = ( this.util.is_type(g, View.Group) ) ? g : false;
},
/* Content Handler */
/**
* Retrieve item type
* @uses get_component() to retrieve saved reference to Content_Handler instance
* @uses View.get_content_handler() to determine item content handler (if necessary)
* @return Content_Handler|null Content Handler of item (NULL no valid type exists)
*/
get_type: function() {
var t = this.get_component('type', {check_attr: false});
if ( !t ) {
t = this.set_type(this.get_controller().get_content_handler(this));
}
return t;
},
/**
* Save content handler reference
* @uses set_component() to save type reference
* @return Content_Handler|null Saved content handler (NULL if invalid)
*/
set_type: function(type) {
return this.set_component('type', type);
},
/**
* Check if content handler exists for item
* @return bool TRUE if content handler exists, FALSE otherwise
*/
has_type: function() {
var ret = !this.util.is_empty(this.get_type());
return ret;
},
/* Actions */
/**
* Display item in viewer
* @uses get_viewer() to retrieve viewer instance for item
* @uses Viewer.show() to display item in viewer
* @param obj options (optional) Options
*/
show: function(options) {
// Validate content handler
if ( !this.has_type() ) {
return false;
}
// Set display options
this.set_attribute('options_show', options);
// Retrieve viewer
var v = this.get_viewer();
// Load item
this.load();
var ret = v.show(this);
return ret;
},
/**
* Load item data
*
* Retrieves item data from external sources (if necessary)
* @uses this.loaded to save loaded state
* @return obj Promise that is resolved when item data is loaded
*/
load: function() {
if ( !this.util.is_promise(this.loaded) ) {
// Load item data (via content handler)
this.loaded = this.get_type().load(this);
}
return this.loaded.promise();
},
reset: function() {
this.set_attribute('options_show', null);
}
};
View.Content_Item = Component.extend(Content_Item);
/**
* Modeled Component
*/
var Modeled_Component = {
_slug: 'modeled_component',
/* Methods */
/* Attributes */
/**
* Retrieve attribute
* Gives priority to model values
* @see Component.get_attribute()
* @param string key Attribute to retrieve
* @param mixed def (optional) Default value (Default: NULL)
* @param bool check_model (optional) Check model for value (Default: TRUE)
* @param bool enforce_type (optional) Return value data type should match default value data type (Default: TRUE)
* @return mixed Attribute value
*/
get_attribute: function(key, def, check_model, enforce_type) {
// Validate
if ( !this.util.is_string(key) ) {
// Invalid requests sent straight to super method
return this._super(key, def, enforce_type);
}
if ( !this.util.is_bool(check_model) ) {
check_model = true;
}
var ret = null;
// Check model for attribute
if ( check_model ) {
var m = this.get_ancestor(key, false);
if ( this.util.in_obj(m, key) ) {
ret = m[key];
}
}
// Check standard attributes as fallback
if ( null === ret ) {
ret = this._super(key, def, enforce_type);
}
return ret;
},
/**
* Get attribute recursively
* Merges objects from ancestors together
* @see Component.get_attribute() for more information
*/
get_attribute_recursive: function(key, def, enforce_type) {
var ret = this.get_attribute(key, def, true, enforce_type);
if ( this.util.is_obj(ret) ) {
// Merge ancestor objects
var models = this.get_ancestors(false);
ret = [ret];
var t = this;
$.each(models, function(idx, model) {
if ( key in model && t.util.is_obj(model[key]) ) {
ret.push(model[key]);
}
});
// Merge transition handlers into current theme
ret.push({});
ret = $.extend.apply($, ret.reverse());
}
return ret;
},
/**
* Set attribute value
* Gives priority to model values
* @see Component.set_attribute()
* @param string key Attribute to set
* @param mixed val Value to set for attribute
* @param bool|obj use_model (optional) Set the value on the model (Default: TRUE)
* > bool: Set attribute on current model (TRUE) or as standard attribute (FALSE)
* > obj: Model object to set attribute on
* @return mixed Attribute value
*/
set_attribute: function(key, val, use_model) {
// Validate
if ( ( !this.util.is_string(key) ) || !this.util.is_set(val) ) {
return false;
}
if ( !this.util.is_bool(use_model) && !this.util.is_obj(use_model) ) {
use_model = true;
}
// Determine where to set attribute
if ( !!use_model ) {
var model = this.util.is_obj(use_model) ? use_model : this.get_model();
// Set attribute in model
model[key] = val;
} else {
// Set as standard attribute
this._super(key, val);
}
return val;
},
/* Model */
/**
* Retrieve Template model
* @return obj Model (Default: Empty object)
*/
get_model: function() {
var m = this.get_attribute('model', null, false);
if ( !this.util.is_obj(m) ) {
// Set default value
m = {};
this.set_attribute('model', m, false);
}
return m;
},
/**
* Check if instance has model
* @return bool TRUE if model is set, FALSE otherwise
*/
has_model: function() {
return ( this.util.is_empty( this.get_model() ) ) ? false : true;
},
/**
* Check if specified attribute exists in model
* @param string key Attribute to check for
* @return bool TRUE if attribute exists, FALSE otherwise
*/
in_model: function(key) {
return ( this.util.in_obj(this.get_model(), key) ) ? true : false;
},
/**
* Retrieve all ancestor models
* @param bool inc_current (optional) Include current model in list (Default: FALSE)
* @return array Theme ancestor models (Closest parent first)
*/
get_ancestors: function(inc_current) {
var ret = [];
var m = this.get_model();
while ( this.util.is_obj(m) ) {
ret.push(m);
m = ( this.util.in_obj(m, 'parent') && this.util.is_obj(m.parent) ) ? m.parent : null;
}
// Remove current model from list
if ( !inc_current ) {
ret.shift();
}
return ret;
},
/**
* Retrieve first ancestor of current theme with specified attribute
* > Current model is also evaluated
* @param string attr Attribute to search ancestors for
* @param bool safe_mode (optional) Return current model if no matching ancestor found (Default: TRUE)
* @return obj Theme ancestor (Default: Current theme model)
*/
get_ancestor: function(attr, safe_mode) {
// Validate
if ( !this.util.is_string(attr) ) {
return false;
}
if ( !this.util.is_bool(safe_mode) ) {
safe_mode = true;
}
var mcurr = this.get_model();
var m = mcurr;
var found = false;
while ( this.util.is_obj(m) ) {
// Check if attribute exists in model
if ( this.util.in_obj(m, attr) && !this.util.is_empty(m[attr]) ) {
found = true;
break;
}
// Get next model
m = ( this.util.in_obj(m, 'parent') ) ? m['parent'] : null;
}
if ( !found ) {
if ( safe_mode ) {
// Use current model as fallback
if ( this.util.is_empty(m) ) {
m = mcurr;
}
// Add attribute to object
if ( !this.util.in_obj(m, attr) ) {
m[attr] = null;
}
} else {
m = null;
}
}
return m;
}
};
Modeled_Component = Component.extend(Modeled_Component);
/**
* Theme
*/
var Theme = {
/* Configuration */
_slug: 'theme',
_refs: {
'viewer': 'Viewer',
'template': 'Template'
},
_models: {},
_attr_default: {
template: null,
model: null
},
/* References */
viewer: null,
template: null,
/* Methods */
/**
* Custom constructor
* @see Component._c()
*/
_c: function(id, attributes, viewer) {
// Validate
if ( arguments.length === 1 && this.util.is_type(arguments[0], View.Viewer) ) {
viewer = arguments[0];
id = null;
}
// Pass parameters to parent constructor
this._super(id, attributes);
// Set viewer instance
this.set_viewer(viewer);
// Set theme model
this.set_model(id);
},
/* Viewer */
get_viewer: function() {
return this.get_component('viewer', {check_attr: false, get_default: true});
},
/**
* Sets theme's viewer property
* @uses View.get_viewer() to retrieve global viewer
* @uses this.viewer to save item's viewer
* @param string|View.Viewer v Viewer to set for item
* > Theme's viewer is reset if invalid viewer provided
*/
set_viewer: function(v) {
return this.set_component('viewer', v);
},
/* Template */
/**
* Retrieve template instance
* @return Template instance
*/
get_template: function() {
// Get saved template
var ret = this.get_component('template');
// Template needs to be initialized
if ( this.util.is_empty(ret) ) {
// Pass model to Template instance
var attr = { 'theme': this, 'model': this.get_model() };
ret = this.set_component('template', new View.Template(attr));
}
return ret;
},
/* Tags */
/**
* Retrieve tags from template
* All tags will be retrieved by default
* Specific tag/property instances can be retrieved as well
* @see Template.get_tags()
* @param string name (optional) Name of tags to retrieve
* @param string prop (optional) Specific tag property to retrieve
* @return array Tags in template
*/
get_tags: function(name, prop) {
return this.get_template().get_tags(name, prop);
},
/**
* Retrieve tag DOM elements
* @see Template.dom_get_tag()
*/
dom_get_tag: function(tag, prop) {
return $(this.get_template().dom_get_tag(tag, prop));
},
/**
* Retrieve template tag CSS selector
* @uses Template.get_tag_selector()
* @param name string Tag name
* @param prop string Tag Property
* @return string Template tag CSS selector
*/
get_tag_selector: function(name, prop) {
return this.get_template().get_tag_selector(name, prop);
},
/* Model */
/**
* Retrieve theme models
* @return obj Theme models
*/
get_models: function() {
return this._models;
},
/**
* Retrieve specified theme model
* @param string id (optional) Theme model to retrieve
* > Default model retrieved if ID is invalid/not set
* @return obj Specified theme model
*/
get_model: function(id) {
var ret = null;
// Pass request to superclass method
if ( !this.util.is_set(id) && this.util.is_obj( this.get_attribute('model', null, false) ) ) {
ret = this._super();
} else {
// Retrieve matching theme model
var models = this.get_models();
if ( !this.util.is_string(id) ) {
id = this.get_controller().get_option('theme_default');
}
// Select first theme model if specified model is invalid
if ( !this.util.in_obj(models, id) ) {
id = $.map(models, function(v, key) { return key; })[0];
}
ret = models[id];
}
return ret;
},
/**
* Set model for current theme instance
* @param string id (optional) Theme ID (Default theme retrieved if ID invalid)
*/
set_model: function(id) {
this.set_attribute('model', this.get_model(id), false);
/* @deprecated
// Set ID using model attributes (if necessary)
if ( !this._check_id(true) ) {
var m = this.get_model();
if ( 'id' in m ) {
this._set_id(m.id);
}
}
*/
},
/* Properties */
/**
* Generate class names for DOM node
* @param string rtype (optional) Return data type
* > Default: array
* > If string supplied: Joined classes delimited by parameter
* @uses get_class() to generate class names
* @uses Array.join() to convert class names array to string
* @return array Class names
*/
get_classes: function(rtype) {
// Build array of class names
var cls = [];
var thm = this;
// Include theme parent's class name
var models = this.get_ancestors(true);
$.each(models, function(idx, model) {
cls.push(thm.add_ns(model.id));
});
// Convert class names array to string
if ( this.util.is_string(rtype) ) {
cls = cls.join(rtype);
}
// Return class names
return cls;
},
/**
* Get custom measurement
* @param string attr Measurement to retrieve
* @param obj def (optional) Default value
* @return obj Attribute measurements
*/
get_measurement: function(attr, def) {
var meas = null;
// Validate
if ( !this.util.is_string(attr) ) {
return meas;
}
if ( !this.util.is_obj(def, false) ) {
def = {};
}
// Manage cache
var attr_cache = this.util.format('%s_cache', attr);
var cache = this.get_attribute(attr_cache, {}, false);
var status = '_status';
var item = this.get_viewer().get_item();
var w = $(window);
// Check cache freshness
if ( !( status in cache ) || !this.util.is_obj(cache[status]) || cache[status].width !== w.width() || cache[status].height !== w.height() ) {
cache = {};
}
if ( this.util.is_empty(cache) ) {
// Set status
cache[status] = {
'width': w.width(),
'height': w.height(),
'index': []
};
}
// Retrieve cached values
var pos = $.inArray(item, cache[status].index);
if ( pos !== -1 && pos in cache ) {
meas = cache[pos];
}
// Generate measurement
if ( !this.util.is_obj(meas) ) {
// Get custom theme measurement
meas = this.call_attribute(attr);
if ( !this.util.is_obj(meas) ) {
// Retrieve fallback value
meas = this.get_measurement_default(attr);
}
}
// Normalize measurement
meas = ( this.util.is_obj(meas) ) ? $.extend({}, def, meas) : def;
// Cache measurement
pos = cache[status].index.push(item) - 1;
cache[pos] = meas;
this.set_attribute(attr_cache, cache, false);
// Return measurement (copy)
return $.extend({}, meas);
},
/**
* Get default measurement using attribute's default handler
* @param string attr Measurement attribute
* @return obj Measurement values
*/
get_measurement_default: function(attr) {
// Validate
if ( !this.util.is_string(attr) ) {
return null;
}
// Find default handler
attr = this.util.format('get_%s_default', attr);
if ( this.util.in_obj(this, attr) ) {
attr = this[attr];
if ( this.util.is_func(attr) ) {
// Execute default handler
attr = attr.call(this);
}
} else {
attr = null;
}
return attr;
},
/**
* Retrieve theme offset
* @return obj Theme offset with `width` & `height` properties
*/
get_offset: function() {
return this.get_measurement('offset', { 'width': 0, 'height': 0});
},
/**
* Generate default offset
* @return obj Theme offsets with `width` & `height` properties
*/
get_offset_default: function() {
var offset = { 'width': 0, 'height': 0 };
var v = this.get_viewer();
var vn = v.dom_get();
// Clone viewer
var vc = vn
.clone()
.attr('id', '')
.css({'visibility': 'hidden', 'position': 'absolute', 'top': ''})
.removeClass('loading')
.appendTo(vn.parent());
// Get offset from layout node
var l = vc.find(v.dom_get_selector('layout'));
if ( l.length ) {
// Clear inline styles
l.find('*').css({
'width': '',
'height': '',
'display': ''
});
// Resize content nodes
var tags = this.get_tags('item', 'content');
if ( tags.length ) {
var offset_item = v.get_item().get_dimensions();
// Set content dimensions
tags = $(l.find(tags[0].get_selector('full')).get(0)).css({'width': offset_item.width, 'height': offset_item.height});
$.each(offset_item, function(key, val) {
offset[key] = -1 * val;
});
}
// Set offset
offset.width += l.width();
offset.height += l.height();
// Normalize
$.each(offset, function(key, val) {
if ( val < 0 ) {
offset[key] = 0;
}
});
}
vc.empty().remove();
return offset;
},
/**
* Retrieve theme margins
* @return obj Theme margin with `width` & `height` properties
*/
get_margin: function() {
return this.get_measurement('margin', {'width': 0, 'height': 0});
},
/**
* Retrieve item dimensions
* Dimensions are adjusted to fit window (if necessary)
* @return obj Item dimensions with `width` & `height` properties
*/
get_item_dimensions: function() {
var v = this.get_viewer();
var dims = v.get_item().get_dimensions();
if ( v.get_attribute('autofit', false) ) {
// Get maximum dimensions
var margin = this.get_margin();
var offset = this.get_offset();
offset.height += margin.height;
offset.width += margin.width;
var max = {'width': $(window).width(), 'height': $(window).height() };
if ( max.width > offset.width ) {
max.width -= offset.width;
}
if ( max.height > offset.height ) {
max.height -= offset.height;
}
// Get resize factor
var factor = Math.min(max.width / dims.width, max.height / dims.height);
// Resize dimensions
if ( factor < 1 ) {
$.each(dims, function(key) {
dims[key] = Math.round(dims[key] * factor);
});
}
}
return $.extend({}, dims);
},
/**
* Retrieve theme dimensions
* @return obj Theme dimensions with `width` & `height` properties
*/
get_dimensions: function() {
var dims = this.get_item_dimensions();
var offset = this.get_offset();
$.each(dims, function(key) {
dims[key] += offset[key];
});
return dims;
},
/**
* Retrieve all breakpoints
* @return object Breakpoints
*/
get_breakpoints: function() {
return this.get_attribute_recursive('breakpoints');
},
/**
* Get breakpoint value
* @param string target Breakpoint target
* @return int Breakpoint value (pixels)
*/
get_breakpoint: function(target) {
var ret = 0;
if ( this.util.is_string(target) ) {
var b = this.get_attribute_recursive('breakpoints');
if ( this.util.is_obj(b) && target in b ) {
ret = b[target];
}
}
return ret;
},
/* Output */
/**
* Render Theme output
* @param bool init (optional) Initialize theme (Default: FALSE)
* @see Template.render()
*/
render: function(init) {
var thm = this;
var tpl = this.get_template();
var st = 'events_render';
if ( !this.get_status(st) ) {
this.set_status(st);
// Register events
tpl.on([
'render-init',
'render-loading',
'render-complete'
],
function(ev) {
return thm.trigger(ev.type, ev.data);
});
}
// Render template
tpl.render(init);
},
transition: function(event, clear_queue) {
var dfr = null;
var attr = 'transition';
var v = this.get_viewer();
var fx_temp = null;
var anim_on = v.animation_enabled();
if ( v.get_attribute(attr, true) && this.util.is_string(event) ) {
var anim_stop = function() {
var l = v.get_layout();
l.find('*').each(function() {
var el = $(this);
while ( el.queue().length ) {
el.stop(false, true);
}
});
};
// Stop queued animations
if ( !!clear_queue ) {
anim_stop();
}
// Get transition handlers
var attr_set = [attr, 'set'].join('_');
var trns;
if ( !this.get_attribute(attr_set) ) {
var models = this.get_ancestors(true);
trns = [];
this.set_attribute(attr_set, true);
var thm = this;
$.each(models, function(idx, model) {
if ( attr in model && thm.util.is_obj(model[attr]) ) {
trns.push(model[attr]);
}
});
// Merge transition handlers into current theme
trns.push({});
trns = this.set_attribute(attr, $.extend.apply($, trns.reverse()));
} else {
trns = this.get_attribute(attr, {});
}
if ( this.util.is_method(trns, event) ) {
// Disable animations if necessary
if ( !anim_on ) {
fx_temp = $.fx.off;
$.fx.off = true;
}
// Pass control to transition event
dfr = trns[event].call(this, v, $.Deferred());
}
}
if ( !this.util.is_promise(dfr) ) {
dfr = $.Deferred();
dfr.reject();
}
dfr.always(function() {
// Restore animation state
if ( null !== fx_temp ) {
$.fx.off = fx_temp;
}
});
return dfr.promise();
}
};
View.Theme = Modeled_Component.extend(Theme);
/**
* Template handler
* Parses and Builds layout from raw template
*/
var Template = {
/* Configuration */
_slug: 'template',
_reciprocal: true,
_refs: {
'theme': 'Theme'
},
_attr_default: {
/**
* URI to layout (raw) file
* @var string
*/
layout_uri: '',
/**
* Raw layout template
* @var string
*/
layout_raw: '',
/**
* Parsed layout
* Placeholders processed
* @var string
*/
layout_parsed: '',
/**
* Tags in template
* Populated once template has been parsed
* @var array
*/
tags: null,
/**
* Model to use for properties
* Usually reference to an object in other component
* @var obj
*/
model: null
},
/* References */
theme: null,
/* Methods */
_c: function(attributes) {
this._super('', attributes);
},
_hooks: function() {
// TODO: Refactor to event that can save retrieved tags
// (`dom_init` event called during attribute initialization so tags are not saved)
this.on('dom_init', function(ev) {
// Init tag handlers
var tags = this.get_tags(null, null, true);
var names = [];
var t = this;
$.each(tags, function(idx, tag) {
var name = tag.get_name();
if ( -1 === $.inArray(name, names) ) {
names.push(name);
tag.get_handler().trigger(ev.type, {template: t});
}
});
});
},
get_theme: function() {
var ret = this.get_component('theme');
return ret;
},
/* Output */
/**
* Render output
* @param bool init (optional) Whether to initialize layout (TRUE) or render item (FALSE) (Default: FALSE)
* Events
* > render-init: Initialize template
* > render-loading: DOM elements created and item content about to be loaded
* > render-complete: Item content loaded, ready for display
*/
render: function(init) {
var v = this.get_theme().get_viewer();
if ( !this.util.is_bool(init) ) {
init = false;
}
// Populate layout
if ( !init ) {
if ( !v.is_active() ) {
return false;
}
var item = v.get_item();
if ( !this.util.is_type(item, View.Content_Item) ) {
v.close();
return false;
}
// Iterate through tags and populate layout
if ( v.is_active() && this.has_tags() ) {
var loading_promise = this.trigger('render-loading');
var tpl = this;
var tags = this.get_tags(),
tag_promises = [];
// Render Tag output
$.when(item.load(), loading_promise).done(function() {
if ( !v.is_active() ) {
return false;
}
$.each(tags, function(idx, tag) {
if ( !v.is_active() ) {
return false;
}
tag_promises.push(tag.render(item).done(function(r) {
if ( !v.is_active() ) {
return false;
}
r.tag.dom_get().html(r.output);
}));
});
// Fire event when all tags rendered
if ( !v.is_active() ) {
return false;
}
$.when.apply($, tag_promises).done(function() {
tpl.trigger('render-complete');
});
});
}
} else {
// Get Layout (basic)
this.trigger('render-init', this.dom_get());
}
},
/*-** Layout **-*/
/**
* Retrieve layout
* @param bool parsed (optional) TRUE retrieves parsed layout, FALSE retrieves raw layout (Default: TRUE)
* @return string Layout (HTML)
*/
get_layout: function(parsed) {
// Validate
if ( !this.util.is_bool(parsed) ) {
parsed = true;
}
// Determine which layout to retrieve (raw/parsed)
var l = ( parsed ) ? this.parse_layout() : this.get_attribute('layout_raw', '');
return l;
},
/**
* Parse layout
* Converts template tags to HTML elements
* > Template tag properties saved to HTML elements for future initialization
* Returns saved layout if previously parsed
* @return string Parsed layout
*/
parse_layout: function() {
// Check for previously-parsed layout
var a = 'layout_parsed';
var ret = this.get_attribute(a);
// Return cached layout immediately
if ( this.util.is_string(ret) ) {
return ret;
}
// Parse raw layout
ret = this.sanitize_layout( this.get_layout(false) );
ret = this.parse_tags(ret);
// Save parsed layout
this.set_attribute(a, ret);
// Return parsed layout
return ret;
},
/**
* Sanitize layout
* @param obj|string l Layout string or jQuery object
* @return obj|string Sanitized layout (Same data type that was passed to method)
*/
sanitize_layout: function(l) {
// Stop processing if invalid value
if ( this.util.is_empty(l) ) {
return l;
}
// Set return type
var rtype = ( this.util.is_string(l) ) ? 'string' : null;
/* Quarantine hard-coded tags */
// Create DOM structure from raw template
var dom = $(l);
// Find hard-coded tag nodes
var tag_temp = this.get_tag_temp();
var cls = tag_temp.get_class();
var cls_new = ['x', cls].join('_');
$(tag_temp.get_selector(), dom).each(function() {
// Replace matching class name with blocking class
$(this).removeClass(cls).addClass(cls_new);
});
// Format return value
switch ( rtype ) {
case 'string' :
dom = dom.wrap('<div />').parent().html();
l = dom;
break;
default :
l = dom;
}
return l;
},
/*-** Tags **-*/
/**
* Extract tags from template
* Tags are replaced with DOM element placeholders
* Extracted tags are saved as element attribute values (for future use)
* @param string l Raw layout to parse
* @return string Parsed layout
*/
parse_tags: function(l) {
// Validate
if ( !this.util.is_string(l) ) {
return '';
}
// Parse tags in layout
// Tag regex
var re = /\{{2}\s*(\w.*?)\s*\}{2}/gim;
// Tag match results
var match;
// Iterate through template and find tags
while ( match = re.exec(l) ) {
// Replace tag in layout with DOM container
l = l.substring(0, match.index) + this.get_tag_container(match[1]) + l.substring(match.index + match[0].length);
}
return l;
},
/**
* Create DOM element container for tag
* @param string Tag ID (will be prefixed)
* @return string DOM element
*/
get_tag_container: function(tag) {
// Build element
var attr = this.get_tag_attribute();
return this.util.format('<span %s="%s"></span>', attr, encodeURI(tag));
},
get_tag_attribute: function() {
return this.get_tag_temp().dom_get_attribute();
},
/**
* Retrieve Template_Tag instance at specified index
* @param int idx (optional) Index to retrieve tag from
* @return Template_Tag Tag instance
*/
get_tag: function(idx) {
var ret = null;
if ( this.has_tags() ) {
var tags = this.get_tags();
if ( !this.util.is_int(idx) || 0 > idx || idx >= tags.length ) {
idx = 0;
}
ret = tags[idx];
}
return ret;
},
/**
* Retrieve tags from template
* Subset of tags may be retrieved based on parameter values
* Template is parsed if tags not set
* @param string name (optional) Tag type to retrieve instances of
* @param string prop (optional) Tag property to retrieve instances of
* @param bool isolate (optional) Do not save retrieved tags, only fetch and return (Default: TRUE)
* @return array Template_Tag instances
*/
get_tags: function(name, prop, isolate) {
// Validate
if ( !this.util.is_bool(isolate) ) {
isolate = false;
}
// Setup
var a = 'tags';
var tags = this.get_attribute(a);
// Initialize tags
if ( !this.util.is_array(tags) ) {
tags = [];
// Retrieve layout DOM tree
var d = this.dom_get();
// Select tag nodes
var attr = this.get_tag_attribute();
var nodes = $(d).find('[' + attr + ']');
// Build tag instances from nodes
$(nodes).each(function() {
// Get tag placeholder
var el = $(this);
var tag = new View.Template_Tag(decodeURI(el.attr(attr)));
// Populate valid tags
if ( tag.has_handler() ) {
// Add tag to array
tags.push(tag);
if ( !isolate ) {
// Connect tag to DOM node
tag.dom_set(el);
// Set classes
el.addClass(tag.get_classes(' '));
}
}
// Clear data attribute
if ( !isolate ) {
el.removeAttr(attr);
}
});
if ( !isolate ) {
// Save tags
this.set_attribute(a, tags, false);
}
}
// Filter tags by parameters
if ( !this.util.is_empty(tags) && this.util.is_string(name) ) {
// Normalize
if ( !this.util.is_string(prop) ) {
prop = false;
}
var tags_filtered = [];
var tc = null;
for ( var x = 0; x < tags.length; x++ ) {
tc = tags[x];
if ( name === tc.get_name() ) {
// Check tag property
if ( !prop || prop === tc.get_prop() ) {
tags_filtered.push(tc);
}
}
}
tags = tags_filtered;
}
return ( this.util.is_array(tags, false) ) ? tags : [];
},
/**
* Check if template contains tags
* @return bool TRUE if tags exist, FALSE otherwise
*/
has_tags: function() {
return ( this.get_tags().length > 0 ) ? true : false;
},
/**
* Retrieve temporary tag instance
* @return Template_Tag Temporary tag
*/
get_tag_temp: function() {
return this.get_controller().get_component_temp(View.Template_Tag);
},
/**
* Retrieve Template tag CSS selector
* @uses Template.get_tag_temp() to retrieve temporary tag instance
* @uses Template_Tag.get_selector() to retrieve selector
* @param name string Tag name
* @param prop string Tag Property
* @return string Template Tag CSS selector
*/
get_tag_selector: function(name, prop) {
if ( !this.util.is_string(name) ) {
name = '';
}
if ( !this.util.is_string(prop) ) {
prop = '';
}
var tag = this.get_tag_temp();
tag.set_attribute('name', name);
tag.set_attribute('prop', prop);
return tag.get_selector('full');
},
/*-** DOM **-*/
/**
* Custom DOM initialization
*/
dom_init: function() {
// Create DOM object from parsed layout
this.dom_set(this.get_layout());
this.trigger('dom_init');
},
/**
* Retrieve DOM element(s) for specified tag
* @param string tag Name of tag to retrieve
* @param string prop (optional) Specific tag property to retrieve
* @return array DOM elements for tag
*/
dom_get_tag: function(tag, prop) {
var ret = $();
var tags = this.get_tags(tag, prop);
if ( tags.length ) {
// Build selector
var level = null;
if ( this.util.is_string(tag) ) {
level = ( this.util.is_string(prop) ) ? 'full' : 'tag';
}
var sel = '.' + tags[0].get_class(level);
ret = this.dom_get().find(sel);
}
return ret;
}
};
View.Template = Modeled_Component.extend(Template);
/**
* Template tag
*/
var Template_Tag = {
/* Configuration */
_slug: 'template_tag',
_reciprocal: true,
/* Properties */
_attr_default: {
name: null,
prop: null,
match: null
},
/**
* Tag Handlers
* Collection of Template_Tag_Handler instances
* @var obj
*/
handlers: {},
/* Methods */
/**
* Constructor
* @param
*/
_c: function(tag_match) {
this.parse(tag_match);
},
/**
* Set instance attributes using tag extracted from template
* @param string tag_match Extracted tag match
*/
parse: function(tag_match) {
// Return default value for invalid instances
if ( !this.util.is_string(tag_match) ) {
return false;
}
// Parse instance options
var parts = tag_match.split('|'),
part;
if ( !parts.length ) {
return null;
}
var attrs = {
name: null,
prop: null,
match: tag_match
};
// Get tag ID
attrs.name = parts[0];
// Get main property
if ( attrs.name.indexOf('.') !== -1 ) {
attrs.name = attrs.name.split('.', 2);
attrs.prop = attrs.name[1];
attrs.name = attrs.name[0];
}
// Get other attributes
for ( var x = 1; x < parts.length; x++ ) {
part = parts[x].split(':', 1);
if ( part.length > 1 && !( part[0] in attrs ) ) {
// Add key/value pair to attributes
attrs[part[0]] = part[1];
}
}
// Save to instance
this.set_attributes(attrs, true);
},
/**
* Render tag output
* @param Content_Item item
* @return obj jQuery.Promise object that is resolved when tag is rendered
* Parameters passed to callbacks
* > tag obj Current tag instance
* > output string Tag output
*/
render: function(item) {
var tag = this;
return tag.get_handler().render(item, tag).pipe(function(output) {
return {'tag': tag, 'output': output};
});
},
/**
* Retrieve tag name
* @return string Tag name (DEFAULT: NULL)
*/
get_name: function() {
return this.get_attribute('name');
},
/**
* Retrieve tag property
*/
get_prop: function() {
return this.get_attribute('prop');
},
/**
* Retrieve tag handler
* @return Template_Tag_Handler Handler instance (Empty instance if handler does not exist)
*/
get_handler: function() {
return ( this.has_handler() ) ? this.handlers[this.get_name()] : new View.Template_Tag_Handler('');
},
/**
* Check if handler exists for tag
* @return bool TRUE if handler exists, FALSE otherwise
*/
has_handler: function() {
return ( this.get_name() in this.handlers );
},
/**
* Generate class names for DOM node
* @param string rtype (optional) Return data type
* > Default: array
* > If string supplied: Joined classes delimited by parameter
* @uses get_class() to generate class names
* @uses Array.join() to convert class names array to string
* @return array Class names
*/
get_classes: function(rtype) {
// Build array of class names
var cls = [
// General tag class
this.get_class(),
// Tag name
this.get_class('tag'),
// Tag name + property
this.get_class('full')
];
// Convert class names array to string
if ( this.util.is_string(rtype) ) {
cls = cls.join(rtype);
}
// Return class names
return cls;
},
/**
* Generate DOM-compatible class name based with varied levels of specificity
* @param int level (optional) Class name specificity
* > Default: General tag class (common to all tag elements)
* > tag: Tag Name
* > full: Tag Name + Property
* @return string Class name
*/
get_class: function(level) {
var cls = '';
// Build base
switch ( level ) {
case 'tag' :
// Tag name
cls = this.get_name();
break;
case 'full' :
// Tag name + property
var parts = [this.get_name(), this.get_prop()];
var a = [];
var i;
for ( i = 0; i < parts.length; i++ ) {
if ( this.util.is_string(parts[i]) ) {
a.push(parts[i]);
}
}
cls = a.join('_');
break;
}
// Format & return
return ( !this.util.is_string(cls) ) ? this.get_ns() : this.add_ns(cls);
},
/**
* Generate tag selector based on specified class name level
* @param string level (optional) Class name specificity (@see get_class() for parameter values)
* @return string Tag selector
*/
get_selector: function(level) {
// Get base
var ret = this.get_class(level);
// Format
if ( this.util.is_string(ret) ) {
ret = '.' + ret;
} else {
ret = '';
}
return ret;
}
};
View.Template_Tag = Component.extend(Template_Tag);
/**
* Theme tag handler
*/
var Template_Tag_Handler = {
/* Configuration */
_slug: 'template_tag_handler',
/* Properties */
_attr_default: {
supports_modifiers: false,
dynamic: false,
props: {}
},
/* Methods */
/**
* Render tag output
* @param Content_Item item Item currently being displayed
* @param Template_Tag Tag instance (from template)
* @return obj jQuery.Promise linked to rendering process
*/
render: function(item, instance) {
var dfr = $.Deferred();
// Pass to attribute method
this.call_attribute('render', item, instance, dfr);
// Return promise
return dfr.promise();
},
add_prop: function(prop, fn) {
// Get attribute
var a = 'props';
var props = this.get_attribute(a);
// Validate
if ( !this.util.is_string(prop) || !this.util.is_func(fn) ) {
return false;
}
if ( !this.util.is_obj(props, false) ) {
props = {};
}
// Add property
props[prop] = fn;
// Save attribute
this.set_attribute(a, props);
},
handle_prop: function(prop, item, instance) {
// Locate property
var props = this.get_attribute('props');
var out = '';
if ( this.util.is_obj(props) && ( prop in props ) && this.util.is_func(props[prop]) ) {
out = props[prop].call(this, item, instance);
} else {
out = item.get_viewer().get_label(prop);
}
return out;
}
};
View.Template_Tag_Handler = Component.extend(Template_Tag_Handler);
/* Update References */
// Attach to global object
View = SLB.attach('View', View);
})(jQuery);}
ACC SHELL 2018