Skip to content

Instantly share code, notes, and snippets.

@garrus
Created July 30, 2013 08:44
Show Gist options
  • Save garrus/6111327 to your computer and use it in GitHub Desktop.
Save garrus/6111327 to your computer and use it in GitHub Desktop.
pagemanager, eventmanage, datapersister
/**
* A Renderer is responsible for rendering specified container on the page, maybe
* with given jquery template. It should also delegate UI events on the container.
* On destroyed, it will clear the container and remove all delegates.
*
* @param id
* @param container
* @param jQTemplateId
* @param defaultRenderMode
* @constructor
*/
function Renderer(id, container, jQTemplateId, defaultRenderMode) {
this.id = id;
this.container = $(container); //TODO validation
this.jqTemplateId = jQTemplateId; //TODO validation
this.defaultRenderMode = /^redraw|append|prepend$/.test(defaultRenderMode) ? defaultRenderMode : 'redraw';
this._eventHandlers = [];
this._destroyCallback = undefined;
this._behaviors = {};
}
$.extend(Renderer.prototype, {
reset: function () {
var c = this.container;
c.empty();
this._eventHandlers.map(function (index, args) {
c.undelegate.apply(c, args);
c.delegate.apply(c, args);
});
},
toString: function () {
return '[Renderer "' + this.id + '"]';
},
render: function (data, renderMode) {
var content = '';
if (typeof data == 'object') {
if (data instanceof jQuery || data instanceof Element) {
content = data;
} else {
if (this.jqTemplateId) {
content = $(this.jqTemplateId).tmp(data);
} else {
log(this + ' Rendering data requires jqTemplate.', 'warning');
return;
}
}
} else {
content = data.toString();
}
this._renderInternal(content, renderMode);
},
_renderInternal: function (content, renderMode) {
renderMode = /^redraw|append|prepend$/.test(renderMode) ? renderMode : this.defaultRenderMode;
switch (renderMode) {
case 'redraw':
this.container.empty().append(content);
break;
case 'append':
this.container.append(content);
break;
case 'prepend':
this.container.prepend(content);
break;
default:
log(this + ' Unexpected executing point: _renderInternal', 'warning');
break;
}
},
execute: function (behaviorName, data) {
var handler = this._behaviors[behaviorName];
if (handler) {
log(this + ' Behaving "' + behaviorName + '".', 'debug');
handler.call(this.container, data ? data : undefined);
} else {
log(this + ' No behavior defined with name "' + behaviorName + '".', 'warning');
}
},
/*
update: function(selector, callback){
var $target = this.container.find(selector);
if ($target.length) {
try {
callback($target);
} catch (e) {
dumpError(e, '[Renderer "' + this.id + '"] Error occurred updating "' + $target.selector + '"');
}
} else {
log('[Renderer "' + this.id + '"] No dom selected with relative selector "' + selector + '".', 'warning');
}
},
*/
setBehavior: function (behaviorName, handler) {
if (typeof handler == 'function') {
log(this + ' Defining behavior with name "' + behaviorName + '".', 'debug');
if (this._behaviors[behaviorName]) {
log(this + ' Overriding behavior of name "' + behaviorName + '".', 'warning');
}
this._behaviors[behaviorName] = handler;
}
return this;
},
on: function (selector, eventType, callback) {
this._eventHandlers.push([selector, eventType, callback]);
this.container.delegate(selector, eventType, callback);
log(this + ' delegate event "' + eventType + '" for "' + selector + '".', 'debug');
return this;
},
destroy: function (callback) {
if (callback) {
this._destroyCallback = callback;
log(this + ' Set destroy callback.', 'debug');
} else {
log(this + ' Destroying.', 'debug');
var c = this.container;
this._eventHandlers.map(function (args) {
c.undelegate.apply(c, args);
});
c.empty();
if (typeof this._destroyCallback == 'function') {
this._destroyCallback();
}
}
}
});
// this component manages the internal events. other components may register some event listener, or
// trigger a certain event here. Once an event is triggered, it will be broadcast to all listeners that
// have registered to this kind of event.
var EventManager = {
eventQueue: [],
handlers: {},
timeo: 0,
init: function () {
this.eventQueue = [];
this.handlers = {};
this.timeo = 0;
this.run();
},
run: function () {
clearTimeout(this.timeo);
var ths = this;
setTimeout(function () {
ths.runInternal();
}, 1);
this.timeo = setTimeout(function () {
ths.run();
}, 1000);
},
runInternal: function () {
var queue = this.eventQueue;
if (queue.length) {
var e = queue[0]; // the first event is the oldest
this.eventQueue = queue.slice(1);
// trigger the handlers
var handlers = this.handlers[e.name];
if (handlers && handlers.length) {
handlers.map(function (callback) {
try {
callback.apply(null, e.data);
} catch (error) {
dumpError(error);
}
});
}
// if there remains tasks, schedule next running
if (this.eventQueue.length) {
var ths = this;
setTimeout(function () {
ths.runInternal();
}, 1);
}
}
},
/**
*
* @param {string|object} name can be a string or an object map {eventName: callback}
* @param {function} [callback]
* @returns {EventManager}
*/
bind: function (name, callback) {
function addCallback(eventType, handler) {
var handlers = EventManager.handlers[eventType];
if (!handlers) {
handlers = [handler];
} else {
handlers.push(handler);
}
EventManager.handlers[eventType] = handlers;
}
if (typeof name == 'string' && typeof callback == 'function') {
addCallback(name, callback);
} else if (typeof name == 'object') {
for (var n in name) {
if (name.hasOwnProperty(n) && typeof name[n] == 'function') {
addCallback(n, name[n]);
}
}
}
return this;
},
trigger: function (name, data) {
log('Event "' + name + '" triggered.', 'debug');
this.eventQueue.push({
name: name,
data: data
});
this.run();
}
};
// this component manages page rendering. other components may create a renderer here, and afterward
// request to render some data. PageManager will manage all renderers
var PageManager = {
_renderers: {},
_groups: {},
init: function () {
var renderers = this._renderers;
for (var id in renderers) {
if (renderers.hasOwnProperty(id)) {
renderers[id].destroy();
}
}
this._renderers = {};
this._groups = {};
},
render: function (id, data, renderMode) {
var renderer = this._renderers[id];
if (renderer) {
renderer.render(data, renderMode);
} else {
log('Unknown renderer id "' + id + '". No data is rendered.', 'warning');
}
},
execute: function (id, behaviorName, data) {
var renderer = this._renderers[id];
if (renderer) {
renderer.execute(behaviorName, data);
} else {
log('Unknown renderer id "' + id + '". No behavior is executed.', 'warning');
}
},
createRenderer: function (id, containerSelector, jqTemplateId, defaultRenderMode) {
var renderer = new Renderer(id, containerSelector, jqTemplateId, defaultRenderMode);
log(renderer + ' created.', 'debug');
if (this._renderers[id]) {
log('An old renderer with the same id "' + id + '" already exists, which will be destroyed.', 'warning');
this._renderers[id].destroy();
}
var groupInfo = this._parseGroupInfo(id);
if (groupInfo) {
this._addGroupContainer(groupInfo.name, groupInfo.id, containerSelector);
}
this._renderers[id] = renderer;
return renderer;
},
createPopup: function () {
//TODO
},
createSubContainer: function (parentSelector, tagName, attributes) {
var id = 'sc' + Math.random().toString().slice(2, 18);
if (typeof(tagName) !== 'string') {
tagName = 'div';
}
var subContainer = $('<' + tagName + ' id="' + id + '"></' + tagName + '>');
if (typeof attributes == 'object') {
for (var attrName in attributes) {
if (attributes.hasOwnProperty(attrName)) {
subContainer.attr(attrName, attributes[attrName]);
}
}
}
subContainer.hide();
$(parentSelector).append(subContainer);
return id;
},
addDelegate: function (id, selector, eventName, callback) {
if (this._renderers[id]) {
this._renderers[id].on(selector, eventName, callback);
} else {
log('Unknown renderer id "' + id + '". No event is delegated.', 'warning');
}
},
addBehavior: function (id, behaviorName, handler) {
if (this._renderers[id]) {
this._renderers[id].setBehavior(behaviorName, handler);
} else {
log('Unknown renderer id "' + id + '". No behavior is added.', 'warning');
}
},
show: function (id) {
var groupInfo = this._parseGroupInfo(id);
if (groupInfo) {
this._groupToggle(groupInfo.name, groupInfo.id, true);
} else {
this._toggleRenderer(id, true);
}
},
hide: function (id) {
this._toggleRenderer(id, false);
},
removeRenderer: function (id) {
var renderer = this._renderers[id];
if (renderer) {
renderer.destroy();
delete this._renderers[id];
var groupInfo = this._parseGroupInfo(id);
if (groupInfo) {
this._removeGroupContainer(groupInfo.name, groupInfo.id);
}
} else {
log('Unknown renderer id "' + id + '". No renderer is destroyed.', 'warning');
}
},
_parseGroupInfo: function (id) {
var offset = id.indexOf('.');
if (offset != -1) {
return {
name: id.slice(0, offset),
id: id.slice(offset + 1)
};
} else {
return null;
}
},
_addGroupContainer: function (groupName, idInGroup, containerSelector) {
var group = this._groups[groupName];
if (!group) {
this._groups[groupName] = group = {};
}
group[idInGroup] = containerSelector;
log('Container ' + containerSelector + ' is added to group ' + groupName + ' as ' + idInGroup, 'debug');
},
_removeGroupContainer: function (groupName, idInGroup) {
var group = this._groups[groupName];
if (group) {
delete group[idInGroup];
}
},
_groupToggle: function (groupName, idInGroup) {
var group = this._groups[groupName];
if (group) {
for (var id in group) {
if (group.hasOwnProperty(id)) {
this._toggleRenderer(groupName + '.' + id, false);
}
}
this._toggleRenderer(groupName + '.' + idInGroup, true);
} else {
log('Unknown group name "' + groupName + '". No group is toggled.', 'warning');
}
},
_toggleRenderer: function (id, show) {
var renderer = this._renderers[id];
if (renderer) {
renderer.container.toggle(show);
} else {
log('Unknown renderer id "' + id + '". No renderer is toggled.', 'warning');
}
}
};
var DataPersister = function () {
this._prefix = null;
this._lastRead = {};
};
$.extend(DataPersister, {
_instances: {},
load: function (user) {
if (this._instances[user]) {
return this._instances[user];
} else {
return this._instances[user] = new DataPersister(user);
}
}
});
$.extend(DataPersister.prototype, {
load: function (user) {
if (this._prefix !== user) {
this._prefix = user;
this._lastRead = {};
}
return this;
},
_readFromCache: function (key) {
return this._lastRead.key == key ? this._lastRead.value : null;
},
read: function (key) {
var r = this._readFromCache(key);
if (r === null) {
r = R(this._prefix + key);
this._lastRead = {key: key, value: r};
}
return r;
},
write: function (key, value) {
if (this._lastRead.key == key) {
this._lastRead.value = value;
}
S(this._prefix + key, value);
},
remove: function (key) {
if (this._lastRead.key == key) {
this._lastRead = {};
}
D(this._prefix + key);
},
contains: function (key) {
return this.read(key) !== undefined;
},
include: function (key, value) {
if (this.contains(key)) {
var vals = this.read(key);
if (typeof vals == 'object' && vals instanceof Array) {
if (vals.indexOf(value) == -1) {
vals.push(value);
this.write(key, vals);
}
} else {
console.error('Persisted data at offset "' + key + '" does not support multiple values.');
}
} else {
this.write(key, [value]);
}
},
exclude: function (key, value) {
if (this.contains(key)) {
var vals = this.read(key);
if (typeof vals == 'object' && vals instanceof Array) {
var pos = vals.indexOf(value);
if (pos !== -1) {
vals = vals.slice(0, pos).concat(vals.slice(pos + 1));
this.write(key, vals);
}
} else {
console.error('Persisted data at offset "' + key + '" does not support multiple values.');
}
}
}
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment