Last active
April 26, 2024 08:52
-
-
Save alejandrolechuga/5127505 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/**** | |
* Inmediate Function scopes the Application to prevent variable pollution | |
* @param: {Object} global , Reference of the global object | |
* @param: {Obejct} FWK , Custom framework | |
*/ | |
;(function (global, FWK, Node) { | |
'use strict'; | |
// Defining global constants for the app | |
FWK.const('RANGE_MINUTES' , 720); // Range minutes from 0 to 720 | |
FWK.const('START_HOUR' , {hour : 9, minutes : 0, period : "AM"}); // Start time point | |
FWK.const('GRID_HEIGHT' , 720); // Grid height that match with the Rage of minutes | |
FWK.const('GRID_WIDTH' , 600); // Grid width | |
FWK.const('MAXIMUM_EVENTS' , 100); // Maximum allowed events defined for rendering concerns | |
// Calendar View Module | |
var CalendarView = function () { | |
var | |
TIME_UNITS = [], | |
TIME_GRID_RENDERED = false, | |
// DOM ELEMENTS | |
TIME_GRID = FWK.$("#time-grid"), | |
EVENTS_GRID = FWK.$("#events-grid"), | |
// TEMPLATES | |
EVENTS_GRID_TMPL = FWK.$("#events_grid_tmpl").innerHTML, | |
TIME_GRID_TMPL = FWK.$("#time_grid_tmpl").innerHTML, | |
// Collection of Event items | |
EVENTS = {}; | |
return { | |
/** | |
* @method: build | |
* @description: This method builds the UI | |
*/ | |
build: function () { | |
var end_time = FWK.const('RANGE_MINUTES'), | |
start_time = FWK.const('START_HOUR'), | |
i; | |
for (i = 0; i <= end_time; i+=30) { | |
TIME_UNITS.push(FWK.minutesTo12Hours(i, start_time)); | |
} | |
this.render(); | |
}, | |
/** | |
* @method : render | |
* @description : This method renders templates and appends to the defined container | |
*/ | |
render : function () { | |
var temporalContainer = document.createElement("div"), | |
node, | |
eventElement, | |
elements = [], length, i, | |
events_grid_parent = EVENTS_GRID.parentNode; | |
// Render time grid if is not defined | |
if (!TIME_GRID_RENDERED) { | |
TIME_GRID.innerHTML = FWK.tmpl(TIME_GRID_TMPL, { hours: TIME_UNITS }); | |
TIME_GRID_RENDERED = true; | |
} | |
// Clears the container contents | |
EVENTS_GRID.innerHTML = ""; | |
// Traverses the EVENTS collection and sets the CSS position | |
if (EVENTS) { | |
for (var id in EVENTS) { | |
node = EVENTS[id]; | |
temporalContainer.innerHTML = FWK.tmpl(EVENTS_GRID_TMPL, {id: node.item.id}); | |
eventElement = FWK.$("#" + node.item.id, temporalContainer); | |
temporalContainer.removeChild(eventElement); | |
this.setElementPosition(eventElement , { | |
start: node.attribute("start"), | |
end: node.attribute("end"), | |
width: node.attribute("width"), | |
left: node.attribute("left") | |
}); | |
elements.push(eventElement); | |
} | |
} | |
length = elements.length; | |
// Removes the container from its Parent DOM Element | |
EVENTS_GRID.parentNode.removeChild(EVENTS_GRID); | |
// Appends each Event DOM element | |
for (i = 0; i < length;i++) { | |
EVENTS_GRID.appendChild(elements[i]); | |
} | |
events_grid_parent.appendChild(EVENTS_GRID); | |
}, | |
setElementPosition : function (element, obj) { | |
var top = obj.start, | |
height = obj.end - top, | |
width = obj.width || FWK.const('GRID_WIDTH'), | |
element_style = element.style; | |
//Setting styles | |
element_style.top = top + "px"; | |
element_style.height = height + "px"; | |
element_style.width = width + "px"; | |
element_style.marginLeft = obj.left + "px"; | |
}, | |
// Sets the events | |
setEvents: function (nodes) { | |
EVENTS = nodes; | |
} | |
}; | |
}; | |
// Calendar App Mudule | |
var CalendarApp = function () { | |
var _pub = {}, // Public object to return | |
VIEW = CalendarView(), // View for rendering the events | |
EVENTS = [], // Events Array | |
EVENTS_COUNTER = 0, // Events counter | |
ROOT; // ROOT Node container | |
/** | |
* @method: _pub.init | |
* @description: this method launchs the calendar app | |
*/ | |
_pub.init = function () { | |
// Build Calendar Interface | |
VIEW.build(); | |
}; | |
/** | |
* @method: _pub.layOutDay accesable from global object as layOutDay | |
* @param: {Array} events , Array of events with this structure {start: {Integer}, end: {Integer}} | |
* @description: this method would be part of the global object, | |
* it can be invoked for adding new event. | |
*/ | |
_pub.layOutDay = function (events) { | |
var length = events.length, | |
i, event , | |
failed = true; | |
// Traverse array of events | |
for (i = 0; i < length; i++) { | |
event = events[i]; | |
// validate time range | |
if (event.start < FWK.const('RANGE_MINUTES') && | |
event.end <= FWK.const('RANGE_MINUTES') && | |
event.end > 0 && | |
event.start >= 0 && | |
event.start < event.end) { | |
if (EVENTS_COUNTER <= FWK.const('MAXIMUM_EVENTS')) { | |
// Generating an ID | |
event.id = "event-" + EVENTS_COUNTER; | |
// Setting Default Properties | |
event.collided = false; | |
// Adding item to the EVENTS Array | |
EVENTS.push(event); | |
// Total Events counters | |
EVENTS_COUNTER ++; | |
failed = false; | |
} else { | |
FWK.debug("Maximum 100"); | |
} | |
} | |
} | |
// Just in case no event was added | |
if (failed) return; | |
// Detect which Events Collide with the ROOT_EVENTS | |
_pub.findCollidingEvents(); | |
// Arrange Events Node Size and Position | |
var nodes = _pub.arrangeNodes(); | |
// Set Events data | |
VIEW.setEvents(nodes); | |
// Render View | |
VIEW.render(); | |
}; | |
/** | |
* @method: _pub.findCollidingEvents | |
* @description: | |
*/ | |
_pub.findCollidingEvents = function () { | |
var length = EVENTS.length, | |
event, | |
collides = false, // Flag to determine if doesn't collide with no event | |
notRootChild = []; // Array for storing temporally the Nodes that collide! | |
ROOT = ROOT || Node(); // If root Node doesn't exist creates new one | |
// Traverse the added Events Array | |
// Has dynamic length did not cached it | |
while (EVENTS.length > 0) { | |
collides = false; | |
event = EVENTS.shift(); //Do not remove this or will loop infinetlly | |
event.collideCounter = 0; | |
if (ROOT.hasNodes()) { | |
ROOT.iterateH(function (node, index, level, parent, stop, topNode) { | |
node.clearAttributes(); | |
if (event.id != node.item.id) { | |
if (_pub.collides(node.item, event)) { | |
event.collideCounter++; | |
Node(event, node); | |
collides = true; | |
notRootChild.push(event); | |
} | |
} else return true; | |
}); | |
if (!collides) { | |
Node(event, ROOT); | |
if (notRootChild.length > 0) { | |
for (var x = 0; x < notRootChild.length; x++) { | |
EVENTS.push(notRootChild[x]); | |
} | |
} | |
} else { | |
} | |
} else { | |
Node(event, ROOT); | |
} | |
} | |
}; | |
/** | |
* @method: _pub.arrangeNodes . | |
* @description: Organizes the size and position from each Node | |
*/ | |
_pub.arrangeNodes = function () { | |
if (ROOT.hasNodes()) { | |
var countNodes = 0, | |
nodes = {}; | |
// Debug | |
/*ROOT.iterate(function (node, index, level, parent) { | |
console.log(new Array(level).join("|_"), node.item.id); | |
});*/ | |
ROOT.iterate(function (node, index, level, parent) { | |
// Add each one to the stack | |
if (!nodes[node.item.id]) { | |
if (node.level > 1) | |
if (nodes[parent.item.id] && !parent.hasAttribute("width")) { | |
return; | |
} | |
// Determine width | |
if (node.nodes.length > 0) { | |
if (!parent.attribute("width")) { | |
node.attribute("width", (FWK.const('GRID_WIDTH')/(node.nodes.length + 1))); | |
var nextNode = node.nodes[0]; | |
if (nextNode) { | |
nextNode = nodes[nextNode.item.id]; | |
if (nextNode) | |
if (nextNode.hasAttribute("width")) { | |
node.attribute("width", nextNode.attribute("width")); | |
} | |
} | |
} else { | |
node.attribute("width", parent.attribute("width")); | |
} | |
} else { | |
if (parent.hasAttribute("width")) { | |
node.attribute("width", parent.attribute("width")); | |
} else { | |
node.attribute("width", FWK.const('GRID_WIDTH')); | |
} | |
} | |
// Start position | |
if (parent.hasAttribute("left")) { | |
node.attribute("left", (parent.attribute("width") + parent.attribute("left"))); | |
} else { | |
node.attribute("left", 0); | |
} | |
node.attribute("start", node.item.start); | |
node.attribute("end", node.item.end); | |
var difference = (FWK.const('GRID_WIDTH')-node.attribute("left")); | |
if (difference > 0) { | |
nodes[node.item.id] = node; | |
// | |
} else node.clearAttributes(); | |
} | |
}); | |
return nodes; | |
} | |
return false; | |
}; | |
/** | |
* @method: _pub.collides | |
* @description: Determines if the events ranges collide | |
* @param: {Object} eventA | |
* @param: {Object} eventB | |
*/ | |
_pub.collides = function (event_a, event_b) { | |
var start_a = event_a.start, | |
end_a = event_a.end, | |
start_b = event_b.start, | |
end_b = event_b.end; | |
if (start_a == start_b && end_a == end_b) { | |
return true; | |
} | |
if (start_a <= end_b && start_b <= end_a) { | |
return true; | |
} | |
return false; | |
}; | |
return _pub; | |
}; | |
/************************************************ | |
* RUNS HERE * RUNS HERE * RUNS HERE * RUNS HERE * | |
*************************************************/ | |
// Checking if the global method layOutDay exists | |
if (!("layOutDay" in global)) { | |
var calendar = CalendarApp(); | |
// Attaching the layOutDay as global method | |
global["layOutDay"] = calendar.layOutDay; | |
// Starting the CalendarApp upon the DOM is ready | |
FWK.ready(function () { | |
// Initializing the CalendarApp | |
calendar.init(); | |
// Adding Events | |
// Starts with this Events loaded | |
global.layOutDay([ | |
{start: 30, end: 150}, | |
{start: 540, end: 600}, | |
{start: 560, end: 620}, | |
{start: 610, end: 670} | |
]); | |
}); | |
} | |
}(this, | |
/** | |
* CUSTOM FRAMEWORK | |
* This framework comes just with a few functions | |
*/ | |
(function () { | |
var CONSTANTS = {}; | |
return { | |
/** | |
* constant is a function to define constant variables or getting its value | |
* * set mode | |
* @param {String} | |
* @param {Mixed} | |
* * get mode | |
* @param {String} arguments[0] | |
*/ | |
const : function () { | |
var value = arguments[1], | |
key = arguments[0]; | |
if (typeof value === 'undefined') { | |
return CONSTANTS[key]; | |
} else { | |
if (!(key in CONSTANTS)) { | |
CONSTANTS[key] = value; | |
return true; | |
} | |
} | |
return false; | |
}, | |
/** | |
* $ is just a shortcut for querySelector a built-in method | |
* @param {String} selector | |
* @param {Object} el | |
*/ | |
$ : function (selector, el) { | |
if (!el) { | |
el = document; | |
} | |
return el.querySelector(selector); | |
}, | |
/** | |
* ready is a function that callsback a function when the DOM is ready | |
* @param {Function} fn | |
*/ | |
ready : function (fn) { | |
var done = false, top = true, win = window, | |
doc = win.document, root = doc.documentElement, | |
add = doc.addEventListener ? 'addEventListener' : 'attachEvent', | |
rem = doc.addEventListener ? 'removeEventListener' : 'detachEvent', | |
pre = doc.addEventListener ? '' : 'on', | |
init = function(e) { | |
if (e.type == 'readystatechange' && doc.readyState != 'complete') return; | |
(e.type == 'load' ? win : doc)[rem](pre + e.type, init, false); | |
if (!done && (done = true)) fn.call(win, e.type || e); | |
}, | |
poll = function() { | |
try { root.doScroll('left'); } catch(e) { setTimeout(poll, 50); return; } | |
init('poll'); | |
}; | |
if (doc.readyState == 'complete') fn.call(win, 'lazy'); | |
else { | |
if (doc.createEventObject && root.doScroll) { | |
try { top = !win.frameElement; } catch(e) { } | |
if (top) poll(); | |
} | |
doc[add](pre + 'DOMContentLoaded', init, false); | |
doc[add](pre + 'readystatechange', init, false); | |
win[add](pre + 'load', init, false); | |
} | |
}, | |
//Proxying the console.log in case of disabling all debugs | |
debug : function () { | |
var disable = false; | |
try { | |
if (!disable) { | |
console.log.apply(console, arguments); | |
} | |
} catch(e) {} | |
}, | |
minutesTo12Hours : function (minutes, start_time) { | |
var initial_hour = start_time.hour, | |
initial_minutes = start_time.minutes, | |
initial_perdiod = start_time.perdiod; //Default | |
//Minutes to hours | |
var hours_left = 0, | |
minutes_left = 0, | |
format = {}, | |
period = "AM"; | |
hours_left = Math.floor(minutes / 60) | |
minutes_left = minutes - (hours_left * 60); | |
hours_left += initial_hour; | |
if (hours_left > 12) { | |
hours_left = hours_left - 12 | |
period = "PM"; | |
} | |
//format hours | |
format.hours = hours_left; | |
//format minutes | |
if (minutes_left < 10) { | |
format.minutes = "0" + minutes_left; | |
} else { | |
format.minutes = minutes_left; | |
} | |
format.period = period; | |
return format; | |
}, | |
/** | |
* Micro-Templating from John Resig | |
* @param {Object} str | |
* @param {Object} data | |
*/ | |
tmpl : function (str, data) { | |
// Figure out if we're getting a template, or if we need to | |
// load the template - and be sure to cache the result. | |
var fn = !/\W/.test(str) ? | |
cache[str] = cache[str] || | |
tmpl(document.getElementById(str).innerHTML) : | |
// Generate a reusable function that will serve as a template | |
// generator (and which will be cached). | |
new Function("obj", | |
"var p=[],print=function(){p.push.apply(p,arguments);};" + | |
// Introduce the data as local variables using with(){} | |
"with(obj){p.push('" + | |
// Convert the template into pure JavaScript | |
str | |
.replace(/[\r\t\n]/g, " ") | |
.split("<%").join("\t") | |
.replace(/((^|%>)[^\t]*)'/g, "$1\r") | |
.replace(/\t=(.*?)%>/g, "',$1,'") | |
.split("\t").join("');") | |
.split("%>").join("p.push('") | |
.split("\r").join("\\'") | |
+ "');}return p.join('');"); | |
// Provide some basic currying to the user | |
return data ? fn( data ) : fn; | |
} | |
}; | |
}()) | |
,(function () { | |
/*** | |
* Node contstructor for parent-child mapping | |
*/ | |
function Node (object, parent) { | |
if (!(this instanceof Node)) { | |
return new Node(object, parent); | |
} | |
if (!object&&!parent) this.root= true; | |
this.item = object; | |
this.nodes = []; | |
this.parent = parent || null; | |
this.level = 0; | |
this.attributes = {}; | |
this.lastIndex = 0; | |
this.topNode = null; | |
if (this.parent) { | |
parent.add.apply(parent,[this]); | |
} | |
}; | |
Node.prototype.attribute = function () { | |
var value = arguments[1], | |
key = arguments[0]; | |
if (typeof value === 'undefined') { | |
return this.attributes[key]; | |
} else { | |
this.attributes[key] = value; | |
return true; | |
} | |
return false; | |
}; | |
Node.prototype.clearAttributes = function () { | |
this.attributes = {}; | |
}; | |
Node.prototype.hasAttribute = function (key) { | |
return (key in this.attributes); | |
}; | |
Node.prototype.add = function (node) { | |
if (!node.parent.hasNode(node)) { | |
node.parent = this; | |
node.level = this.level + 1; | |
if (!this.root && this.level == 1) { | |
this.topNode = this; | |
} | |
this.nodes.push(node); | |
} | |
return node; | |
}; | |
Node.prototype.hasNode = function (node) { | |
var i = this.nodes.length; | |
while (i--) { | |
if (this.nodes[i].item == node.item) { | |
return true; | |
} | |
} | |
return false; | |
}; | |
/** | |
* Iterates every node child | |
*/ | |
Node.prototype.iterate = function (fn) { | |
var length = this.nodes.length, | |
i; | |
if (length < 1) { | |
return false; | |
} | |
for (i = 0; i < length; i++) { | |
// Callback | |
fn (this.nodes[i], // Node | |
i, // Nodes index | |
this.nodes[i].level, // Node Level | |
this.nodes[i].parent); // Parent Node | |
if (this.nodes[i].hasNodes()) { | |
this.nodes[i].iterate(fn); | |
} | |
} | |
return true; | |
}; | |
/** | |
* Iterates the same level node then every child node | |
*/ | |
Node.prototype.iterateH = function (fn, topNode) { | |
var length = this.nodes.length, | |
i = 0, | |
that = this, | |
nodes = this.nodes, | |
cached = [], | |
callReturn, | |
topNode, | |
node; | |
//This time I didn't cached the length since the this.nodes.length can change | |
for (i; i < this.nodes.length; i++) { | |
// Callback | |
node = this.nodes[i]; | |
if (this.topNode) { | |
topNode = this.topNode; | |
//this.topNode = node; | |
} | |
//console.log(i); | |
//console.log(node); | |
fn (node, // Node | |
i, // Nodes index | |
node.level, // Node Level | |
node.parent, | |
function () { | |
i = that.nodes.length-1; | |
}, topNode); // Parent Node | |
if (this.nodes[i].hasNodes()) { | |
cached.push(function(){ | |
that.nodes[i].iterateH(fn, topNode); | |
}); | |
} | |
length = nodes.length; | |
} | |
i = 0;length = cached.length; | |
for (; i < length; i++) { | |
cached[i](); | |
} | |
}; | |
/** | |
* Iterate Backwards | |
*/ | |
Node.prototype.iterateHB = function (fn, topNode) { | |
var length = this.lastIndex || this.nodes.length, | |
i = 0, | |
that = this, | |
nodes = this.nodes, | |
cached = [], | |
callReturn, | |
topNode, | |
node, | |
stop = false; | |
var i = that.lastIndex || this.nodes.length; | |
while (i--) { | |
// Callback | |
node = this.nodes[i]; | |
if (this.topNode) { | |
topNode = this.topNode; | |
} | |
if (fn (node, // Node | |
i, // Nodes index | |
node.level, // Node Level | |
node.parent, | |
function () { | |
that.lastIndex = i; | |
}, topNode)) { | |
stop = true; | |
} // Parent Node | |
if (this.nodes[i].hasNodes()) { | |
cached.push(function(){ | |
that.nodes[i].iterateH(fn, topNode); | |
}); | |
} | |
if (nodes.length !== length) { | |
i = nodes.length-i; | |
} | |
if (stop) { | |
stop = false; | |
return; | |
} | |
} | |
i = 0;length = cached.length; | |
for (; i < length; i++) { | |
cached[i](); | |
} | |
}; | |
Node.prototype.getNodes = function () { | |
return this.nodes; | |
}; | |
Node.prototype.hasNodes = function () { | |
return this.nodes.length > 0; | |
}; | |
return Node; | |
}()))); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment