%PDF- %PDF-
Direktori : /home2/vacivi36/www2]/old/wp-content/plugins/simple-lightbox/client/js/dev/ |
Current File : //home2/vacivi36/www2]/old/wp-content/plugins/simple-lightbox/client/js/dev/lib.view.js |
/** * 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(/&(#38|amp);/, '&'); 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.get(0).innerText.trim(); } } // 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);}