Commit 899c2c75511d9167407ddd7bcecfb1604cf87bdb
- Diff rendering mode:
- inline
- side by side
README.md
(86 / 2)
  | |||
1 | mouchak | ||
1 | Mouchak | ||
2 | 2 | ======= | |
3 | 3 | ||
4 | A framework for building websites. | ||
4 | A Javascript framework for building single page websites or web apps. | ||
5 | |||
6 | It takes the content of the website as a JSON. The JSON file can contain | ||
7 | text or multimedia content(images, audio, video). | ||
8 | |||
9 | Mouchak can also load external JS/CSS files through the JSON. This gives the website to load | ||
10 | plugins to enhance/customize the website either through JS or CSS. | ||
11 | |||
12 | |||
13 | How to use it | ||
14 | ============= | ||
15 | |||
16 | Either download this codebase or git clone the repo. | ||
17 | |||
18 | Once you have downloaded or cloned the repo, load the index.html file in your browser. | ||
19 | This loads the example content from example.json. | ||
20 | |||
21 | Use the index.html file as the boilerplate file of your index.html file. | ||
22 | Modify the code in the script tag, which loads the example.json, and change | ||
23 | the URL to point to your JSON file. | ||
24 | Remember the JSON files is loaded in the client side. Hence your JSON file should | ||
25 | be from the same domain where this app is loaded. | ||
26 | See cross-domain policies for details. | ||
27 | |||
28 | The global object for this framework is exposed as the variable M. This can be | ||
29 | inspected in the console. | ||
30 | |||
31 | The M.filterTags() method takes an array of strings representing the tags to filter. | ||
32 | It returns an array of the content objects with the matched tags. | ||
33 | |||
34 | |||
35 | How it works | ||
36 | ============ | ||
37 | |||
38 | It takes the content of the site in the form of JSON. The JSON should describe the content | ||
39 | (in terms of type like text, audio or image) and also provide the content (in case of text | ||
40 | the content is as it is, in case images urls are given). The JSON should also describe the | ||
41 | content semantically by giving it tags. | ||
42 | More details about the JSON format in example.json file. | ||
43 | |||
44 | The framework provides an easy way to pull up related content by just specifying the tags. | ||
45 | |||
46 | Backbone models are used to model the content. Different content has different types. | ||
47 | Again all types are derived from one base type. | ||
48 | Each model has a corresponding view as well. The views manage how the content are rendered. | ||
49 | |||
50 | Each page is made of multiple content (content objects). | ||
51 | Pages are also modeled in Backbone models and their view is managed via Backbone views. | ||
52 | |||
53 | The framework also use Backbone router to handle client side routing. | ||
54 | |||
55 | |||
56 | What it uses | ||
57 | ============ | ||
58 | |||
59 | Mouchak uses HTML5 Boilerplate and Bootstrap project as a boilerplate code for the website. | ||
60 | Mouchak also leverages powerful libraries like Backbone.js and Underscore.js to manage and render | ||
61 | content. This gives more flexibility to the content of the website. | ||
62 | |||
63 | The main code resides in js/mouchak.js. The HTML markup it uses is in index.html. | ||
64 | |||
65 | Javascript libary files are in js/lib. We use backbone.js, underscore.js and jquery in this | ||
66 | framework. | ||
67 | |||
68 | Boilerplate code/files: | ||
69 | 404.html - error template | ||
70 | crossdomain.xml - cross-domain policies to be obeyed by the client | ||
71 | css/bootstrap.css - boilerplate css | ||
72 | css/normalize.css - boilerplate css | ||
73 | css/main.css - boilerplate css | ||
74 | humans.txt - write your own credits | ||
75 | img/ - directory for images | ||
76 | robots.txt - crawl spider rules | ||
77 | |||
78 | |||
79 | Support | ||
80 | ======= | ||
81 | |||
82 | Email to rayanon at janastu dot org for any kind of feedback. | ||
83 | |||
84 | |||
85 | Issues | ||
86 | ====== | ||
87 | |||
88 | Report issues at http://bugzilla.pantoto.org/bugzilla3/ |
example.json
(42 / 0)
  | |||
1 | [ | ||
2 | { | ||
3 | "name": "index", | ||
4 | "children": ["Motivation", "The JSON file"], | ||
5 | "title": "Mouchak", | ||
6 | "content": [ | ||
7 | { | ||
8 | "type": "image", | ||
9 | "src": "img/mouchak.jpg" | ||
10 | }, | ||
11 | { | ||
12 | "type": "text", | ||
13 | "title": "What's this?", | ||
14 | "data": "<p> Mouchak is a Javascript framework for building websites quickly.</p><p>It takes the components and the content of the website in a JSON format. The content in the JSON can be of type text or multimedia like images and videos.</p> It also has the capabilities to load external JS and CSS files to customize the website." | ||
15 | } | ||
16 | ] | ||
17 | }, | ||
18 | { | ||
19 | "name": "Motivation", | ||
20 | "title": "Motivation", | ||
21 | "children": [], | ||
22 | "content": [ | ||
23 | { | ||
24 | "type": "text", | ||
25 | "title": "Why Mouchak?", | ||
26 | "data": "Explanation of why we built this framework..." | ||
27 | } | ||
28 | ] | ||
29 | }, | ||
30 | { | ||
31 | "name": "The JSON file", | ||
32 | "title": "The JSON format", | ||
33 | "children": [], | ||
34 | "content": [ | ||
35 | { | ||
36 | "type": "text", | ||
37 | "title": "", | ||
38 | "data": "{ } Explain the JSON format here.." | ||
39 | } | ||
40 | ] | ||
41 | } | ||
42 | ] |
img/mouchak.jpg
(374 / 0)
Binary files differ
index.html
(9 / 6)
  | |||
24 | 24 | ||
25 | 25 | <div class="container" id="container"> | |
26 | 26 | <div id="header"></div> | |
27 | <div id="nav"> | ||
28 | <ul class="nav nav-pills"></ul> | ||
29 | </div> | ||
30 | <div class="clearfix"></div> | ||
31 | 27 | <div id="content-container"></div> | |
32 | 28 | <div id="footer"></div> | |
33 | 29 | </div> | |
… | … | ||
32 | 32 | //Code to initialize the framework | |
33 | 33 | window.M = window.M || {}; | |
34 | 34 | window.onload = function() { | |
35 | M.load("/maraa/data/maraa2.json"); | ||
35 | M.load('example.json'); | ||
36 | 36 | }; | |
37 | 37 | </script> | |
38 | 38 | <script src="//ajax.googleapis.com/ajax/libs/jquery/1.8.0/jquery.min.js"></script> | |
… | … | ||
41 | 41 | <script src="js/lib/backbone.js"></script> | |
42 | 42 | <script src="js/lib/bootstrap.js"></script> | |
43 | 43 | <script src="js/plugins.js"></script> | |
44 | <script src="js/framework.js"></script> | ||
44 | <script src="js/mouchak.js"></script> | ||
45 | 45 | ||
46 | 46 | <!-- Underscore templates --> | |
47 | 47 | <script type="text/template" id="news-template"> | |
… | … | ||
55 | 55 | <a href="<%= link %>">Read More</a> | |
56 | 56 | </div> | |
57 | 57 | </script> | |
58 | |||
59 | <script type="text/template" id="nav-template"> | ||
60 | <div class="navigation" id="nav-<%= page %>"> | ||
61 | <ul class="nav nav-pills"></ul> | ||
62 | </div> | ||
63 | </script> | ||
58 | 64 | <script type="text/template" id="nav-dropdown-template"> | |
59 | 65 | <li class="dropdown"> | |
60 | 66 | <a class="dropdown-toggle" data-toggle="dropdown" href="#/<%= M.sanitize(name) %>" | |
… | … | ||
78 | 78 | </ul> | |
79 | 79 | </li> | |
80 | 80 | </script> | |
81 | |||
81 | 82 | <script type="text/template" id="image-view-template"> | |
82 | 83 | <div class="img-wrapper"> | |
83 | 84 | <img src="<%= src %>"> |
js/framework.js
(0 / 493)
  | |||
1 | (function(M) { | ||
2 | /* Defining Backbone models, collections and views */ | ||
3 | |||
4 | var BaseType = Backbone.Model.extend({ | ||
5 | defaults: { | ||
6 | tags: [], | ||
7 | title: "", | ||
8 | attr: {} | ||
9 | }, | ||
10 | initialize: function() { | ||
11 | } | ||
12 | }); | ||
13 | |||
14 | var Text = BaseType.extend({ | ||
15 | defaults: { | ||
16 | data: "", | ||
17 | }, | ||
18 | initialize: function() { | ||
19 | } | ||
20 | }); | ||
21 | |||
22 | var TextView = Backbone.View.extend({ | ||
23 | tagName: 'div', | ||
24 | className: '', | ||
25 | initialize: function() { | ||
26 | _.bindAll(this); | ||
27 | _.bind(this.render, this); | ||
28 | }, | ||
29 | render: function(el) { | ||
30 | $(el).append(this.el); | ||
31 | var str = '<h4>'+ this.model.get('title') +'</h4> <p>' + | ||
32 | this.model.get('data') + '</p>'; | ||
33 | $(this.el).html(str); | ||
34 | M.appendAttrs(this.model, this.el); | ||
35 | } | ||
36 | }); | ||
37 | |||
38 | var Table = BaseType.extend({ | ||
39 | defaults: { | ||
40 | data : { | ||
41 | th: [], | ||
42 | tr:[] | ||
43 | } | ||
44 | }, | ||
45 | initialize: function() { | ||
46 | } | ||
47 | }); | ||
48 | |||
49 | var TableView = Backbone.View.extend({ | ||
50 | tagName: 'table', | ||
51 | className: 'table', | ||
52 | initialize: function() { | ||
53 | _.bindAll(this); | ||
54 | _.bind(this.render, this); | ||
55 | }, | ||
56 | render: function(el) { | ||
57 | var heading = this.model.get('data').th; | ||
58 | var str = '<tr>'; | ||
59 | |||
60 | for(var i = 0; i < heading.length; i++) { | ||
61 | str += '<th>' + heading[i] + '</th>'; | ||
62 | } | ||
63 | str += '</tr>'; | ||
64 | |||
65 | _.each(this.model.get('data').tr, function(row) { | ||
66 | str += '<tr>'; | ||
67 | for(var i = 0; i < row.length; i++) { | ||
68 | if(row[i].match(/http.?:/)) { | ||
69 | //console.log(row[i].match(/http:/)) | ||
70 | } | ||
71 | str += '<td>'+ row[i] + '</td>'; | ||
72 | } | ||
73 | str += '</tr>'; | ||
74 | }); | ||
75 | |||
76 | $(el).append(this.el); | ||
77 | $(this.el).html(str); | ||
78 | M.appendAttrs(this.model, this.el); | ||
79 | } | ||
80 | }); | ||
81 | |||
82 | var Image = BaseType.extend({ | ||
83 | defaults: { | ||
84 | src: "" | ||
85 | }, | ||
86 | initialize:function() { | ||
87 | } | ||
88 | }); | ||
89 | |||
90 | var ImageView = Backbone.View.extend({ | ||
91 | tagName: 'image', | ||
92 | className: '', | ||
93 | |||
94 | initialize: function() { | ||
95 | _.bindAll(this); | ||
96 | _.bind(this.render, this); | ||
97 | }, | ||
98 | render: function(el) { | ||
99 | $(el).append(this.el); | ||
100 | $(this.el).attr('src', this.model.get('src')); | ||
101 | M.appendAttrs(this.model, this.el); | ||
102 | } | ||
103 | }); | ||
104 | |||
105 | |||
106 | var Video = BaseType.extend({ | ||
107 | defaults: { | ||
108 | src: "" | ||
109 | }, | ||
110 | initialize:function() { | ||
111 | } | ||
112 | }); | ||
113 | |||
114 | var VideoView = Backbone.View.extend({ | ||
115 | initialize: function() { | ||
116 | _.bindAll(this); | ||
117 | _.bind(this.render, this); | ||
118 | // assuming cross-domain urls will have http in the src, | ||
119 | // so also assuming they are embedded flash urls, | ||
120 | // hence iframe | ||
121 | if(this.model.get('src').match('http')) { | ||
122 | this.tagName = 'iframe'; | ||
123 | } | ||
124 | // otherwise, use html5 video tag, if the video is served | ||
125 | // from the same domain | ||
126 | else { | ||
127 | this.tagName = 'video'; | ||
128 | } | ||
129 | }, | ||
130 | render: function(el) { | ||
131 | $(el).append(this.el); | ||
132 | $(this.el).attr('src', this.model.get('src')); | ||
133 | M.appendAttrs(this.model, this.el); | ||
134 | } | ||
135 | }); | ||
136 | |||
137 | var RSS = BaseType.extend({ | ||
138 | defaults: { | ||
139 | src: "" | ||
140 | }, | ||
141 | initialize:function() { | ||
142 | } | ||
143 | }); | ||
144 | |||
145 | var RSSView = Backbone.View.extend({ | ||
146 | el: '#feeds', | ||
147 | initialize: function() { | ||
148 | _.bindAll(this); | ||
149 | _.bind(this.render, this); | ||
150 | }, | ||
151 | render: function() { | ||
152 | M.populateFeeds(this.model.get('src')); | ||
153 | } | ||
154 | }); | ||
155 | |||
156 | // Plugin model can be used to load dynamic components | ||
157 | // to the website by loading external JS files. | ||
158 | // Also the website can be styled by using external CSS files, | ||
159 | // which can also be loaded via this plugin model. | ||
160 | var Plugin = BaseType.extend({ | ||
161 | defaults: { | ||
162 | src: "", | ||
163 | data: {}, | ||
164 | callback: "" | ||
165 | }, | ||
166 | initialize: function() { | ||
167 | if(this.get('src').match(/\.js/)) { | ||
168 | var script = document.createElement('script'); | ||
169 | var callback = this.get('callback'); | ||
170 | script.src = this.get('src'); | ||
171 | document.body.appendChild(script); | ||
172 | script.onload = function() { | ||
173 | eval(callback); | ||
174 | }; | ||
175 | } | ||
176 | else if(this.get('src').match(/\.css/)) { | ||
177 | var link = document.createElement('link'); | ||
178 | link.rel = 'stylesheet'; | ||
179 | link.href = this.get('src'); | ||
180 | link.type = 'text/css'; | ||
181 | document.body.appendChild(link); | ||
182 | } | ||
183 | } | ||
184 | }); | ||
185 | |||
186 | var PluginView = Backbone.View.extend({ | ||
187 | initialize: function() { | ||
188 | return; | ||
189 | }, | ||
190 | render: function(el) { | ||
191 | return; | ||
192 | } | ||
193 | }); | ||
194 | |||
195 | var type_map; | ||
196 | M.type_map = type_map = { | ||
197 | model : { | ||
198 | 'text': Text, | ||
199 | 'image': Image, | ||
200 | 'video': Video, | ||
201 | 'rss': RSS, | ||
202 | 'table': Table, | ||
203 | 'plugin': Plugin | ||
204 | }, | ||
205 | view: { | ||
206 | 'text': TextView, | ||
207 | 'image': ImageView, | ||
208 | 'video': VideoView, | ||
209 | 'rss': RSSView, | ||
210 | 'table': TableView, | ||
211 | 'plugin': PluginView | ||
212 | } | ||
213 | }; | ||
214 | |||
215 | // var Media = Backbone.Model.extend({ | ||
216 | // defaults:{ | ||
217 | // "audio": new Audio() , | ||
218 | // "video": new Video(), | ||
219 | // "image": new Image() | ||
220 | // } | ||
221 | |||
222 | // }); | ||
223 | |||
224 | var Texts = Backbone.Collection.extend({model: Text}); | ||
225 | var Images = Backbone.Collection.extend({model: Image}); | ||
226 | var Videos = Backbone.Collection.extend({model: Video}); | ||
227 | var RSSs = Backbone.Collection.extend({model: RSS}); | ||
228 | |||
229 | |||
230 | var Page = Backbone.Model.extend({ | ||
231 | defaults: { | ||
232 | name: "index", | ||
233 | title: "", | ||
234 | children: [], | ||
235 | content: [] | ||
236 | }, | ||
237 | initialize: function() { | ||
238 | // adding the name of the model as its id. | ||
239 | // look up of this model through the collection | ||
240 | // is faster this way. | ||
241 | this.set({id: M.sanitize(this.get('name'))}); | ||
242 | } | ||
243 | }); | ||
244 | |||
245 | |||
246 | var Pages = Backbone.Collection.extend({ | ||
247 | model: Page | ||
248 | }); | ||
249 | |||
250 | var PageView = Backbone.View.extend({ | ||
251 | tagName: 'div', | ||
252 | className: 'page', | ||
253 | initialize: function() { | ||
254 | _.bindAll(this); | ||
255 | _.bind(this.render, this); | ||
256 | this.render(); | ||
257 | $(this.el).hide(); | ||
258 | }, | ||
259 | render: function() { | ||
260 | $('#content-container').append(this.el); | ||
261 | $(this.el).append('<h3>'+this.model.get('title')+'</h3>'); | ||
262 | var self = this; | ||
263 | _.each(this.model.get('content'), function(item) { | ||
264 | var view = type_map.view[item.get('type')]; | ||
265 | if(!view) { | ||
266 | console.log('Error initing view', item); | ||
267 | return; | ||
268 | } | ||
269 | if(item.get('type') === 'rss') { | ||
270 | M.rss_view = new view({model: item}); | ||
271 | $(self.el).append(_.template($('#news-template').html())); | ||
272 | } | ||
273 | else { | ||
274 | var item_view = new view({model: item}); | ||
275 | item_view.render(self.el); | ||
276 | } | ||
277 | }); | ||
278 | } | ||
279 | }); | ||
280 | |||
281 | var AppView = Backbone.View.extend({ | ||
282 | el: 'body', | ||
283 | events: { | ||
284 | 'click .nav li a' : 'navClicked' | ||
285 | }, | ||
286 | initialize: function() { | ||
287 | _.bindAll(this); | ||
288 | }, | ||
289 | render: function() { | ||
290 | M.createNavigation(); | ||
291 | $('#index').show(); | ||
292 | }, | ||
293 | navClicked: function(event) { | ||
294 | $('.nav li').removeClass('active'); | ||
295 | $(event.currentTarget).parent().addClass('active'); | ||
296 | } | ||
297 | }); | ||
298 | |||
299 | var AppRouter = Backbone.Router.extend({ | ||
300 | routes : { | ||
301 | 'index' : 'index', | ||
302 | ':page' : 'showPage' | ||
303 | }, | ||
304 | index: function() { | ||
305 | $('.page').hide(); | ||
306 | $('#index').show(); | ||
307 | }, | ||
308 | showPage: function(page) { | ||
309 | $('.page').hide(); | ||
310 | //news pages are rendered on the fly, | ||
311 | //as feeds have to be fetched. | ||
312 | if(page === 'news') { | ||
313 | M.rss_view.render(); | ||
314 | } | ||
315 | $('#'+page).show(); | ||
316 | $('.'+page).show(); | ||
317 | } | ||
318 | }); | ||
319 | |||
320 | /* Defining other necessary functions */ | ||
321 | M.init = function() { | ||
322 | M.tags = {}; //global tag cache | ||
323 | M.pages = new Pages(); //global collection of all pages | ||
324 | |||
325 | // iterate through the JSON to intialize models and views | ||
326 | _.each(M.site_content, function(page) { | ||
327 | var new_page = new Page(page); | ||
328 | var contents = []; | ||
329 | _.each(page.content, function(content) { | ||
330 | var Item = type_map.model[content.type]; | ||
331 | if(!Item) { | ||
332 | console.log('Error initing item: ', content); | ||
333 | return; | ||
334 | } | ||
335 | var item = new Item(content); | ||
336 | contents.push(item); | ||
337 | }); | ||
338 | new_page.set({content: contents}); | ||
339 | var new_page_view = new PageView({model: new_page, | ||
340 | id: new_page.get('id')}); | ||
341 | M.pages.add(new_page); | ||
342 | }); | ||
343 | |||
344 | M.appView = new AppView(); | ||
345 | M.appView.render(); | ||
346 | var app_router = new AppRouter(); | ||
347 | Backbone.history.start(); | ||
348 | // start with index page | ||
349 | //app_router.index(); | ||
350 | M.simHeir(); | ||
351 | }; | ||
352 | |||
353 | // hack to simulate heirarchy among the page views | ||
354 | // basically add the parent id as class in all of its children | ||
355 | // elements. | ||
356 | M.simHeir = function() { | ||
357 | _.each(M.pages.models, function(page) { | ||
358 | if(page.id == 'index') return; | ||
359 | _.each(page.get('children'), function(child) { | ||
360 | child = M.sanitize(child); | ||
361 | $('#'+child).addClass(page.id); | ||
362 | }); | ||
363 | }); | ||
364 | }; | ||
365 | |||
366 | |||
367 | // append attributes to elements from the model | ||
368 | M.appendAttrs = function(model, el) { | ||
369 | _.each(model.get('attr'), function(val, key) { | ||
370 | $(el).attr(key, val); | ||
371 | }); | ||
372 | } | ||
373 | |||
374 | //Helper method for making a list of id associated to tag | ||
375 | M.createTagList = function(item,x) | ||
376 | { | ||
377 | |||
378 | for(var i in item['tags']) | ||
379 | { | ||
380 | if( M.tags[item['tags'][i]] === undefined) | ||
381 | { | ||
382 | M.tags[item['tags'][i]] = []; | ||
383 | M.tags[item['tags'][i]].push(x); | ||
384 | } | ||
385 | else | ||
386 | M.tags[item['tags'][i]].push(x); | ||
387 | } | ||
388 | }; | ||
389 | |||
390 | //create navigational links | ||
391 | M.createNavigation = function() { | ||
392 | var top_level = M.pages.get('index').get('children'); | ||
393 | $('<li class="active"><a href="#/index">Home</a></li>').appendTo('.nav');; | ||
394 | _.each(top_level, function(child) { | ||
395 | child = M.sanitize(child); | ||
396 | var children = M.pages.get(child).get('children'); | ||
397 | var page = M.pages.get(child); | ||
398 | var dropdown_template = _.template($('#nav-dropdown-template').html()); | ||
399 | if(_.isEmpty(children)) { | ||
400 | li = '<li><a href="#/' + child + '">'+ M.humanReadable(child) +'</a></li>'; | ||
401 | } | ||
402 | else { | ||
403 | li = dropdown_template({ | ||
404 | //no: page.cid, | ||
405 | name: M.humanReadable(page.get('name')), | ||
406 | list: _.map(children, M.humanReadable) | ||
407 | }); | ||
408 | } | ||
409 | $(li).appendTo('.nav'); | ||
410 | }); | ||
411 | }; | ||
412 | |||
413 | // populate with news feeds in the news section | ||
414 | // gets the feeds from server side script 'feed.py' | ||
415 | M.populateFeeds = function(rss_url) { | ||
416 | $('#feeds-loader').show(); | ||
417 | $('.news-item-wrapper').remove(); | ||
418 | jQuery.getFeed({ | ||
419 | url: 'feeds', | ||
420 | type: 'GET', | ||
421 | data: "rss_url="+encodeURIComponent(rss_url), | ||
422 | success: function(feed) { | ||
423 | $('#feeds-loader').hide(); | ||
424 | var template = _.template($('#news-item-template').html()); | ||
425 | _.each(feed.items, function(item) { | ||
426 | x = $('#feeds').append(template({ | ||
427 | title: item.title, | ||
428 | link: item.link | ||
429 | })); | ||
430 | }); | ||
431 | }, | ||
432 | error: function(err) { | ||
433 | $('#feeds-loader').hide(); | ||
434 | $('#feeds').append('Oops, something went wrong! <br/> Please try again.'); | ||
435 | } | ||
436 | }); | ||
437 | }; | ||
438 | |||
439 | /* Other helper functions */ | ||
440 | |||
441 | M.contentList = []; //A list to hold out filtered content objects. | ||
442 | |||
443 | //Check for the tags and return only those "content" objects which match a given tag. | ||
444 | M.checkTags = function(tags){ | ||
445 | if(_.isArray(tags)) | ||
446 | { | ||
447 | var list = []; | ||
448 | _.each(tags,function(item){ | ||
449 | list.push(M.tags[item]); | ||
450 | }); | ||
451 | return _.uniq(list); | ||
452 | } | ||
453 | else | ||
454 | return false; //Failure code, the function will only accept a list as input | ||
455 | }; | ||
456 | |||
457 | // change all '-' to spaces and capitalize first letter of | ||
458 | // every word | ||
459 | M.humanReadable = function(str) { | ||
460 | if(typeof str !== "string") { | ||
461 | str = ''; | ||
462 | } | ||
463 | return '' + str.replace(/[-]+/g, ' ').replace(/[^\s]+/g, function(str) { | ||
464 | //return '' + str.replace(/[-]+/g, ' ').replace(/([A-Z])/g, function(s) { return ' '+s;}).replace(/[^\s]+/g, function(str) { | ||
465 | return str.substr(0,1).toUpperCase() + str.substr(1).toLowerCase(); | ||
466 | }); | ||
467 | }; | ||
468 | |||
469 | // change all spaces to '-' and everything to lowercase | ||
470 | M.sanitize = function(str) { | ||
471 | if(typeof str !== "string") { | ||
472 | str = ''; | ||
473 | } | ||
474 | return '' + str.replace(/[\s]+/g,'-').replace(/[^\s]+/g, function(str) { | ||
475 | //TODO: handle special characters! | ||
476 | return str.replace('&', 'and').toLowerCase(); | ||
477 | }); | ||
478 | }; | ||
479 | |||
480 | |||
481 | // Loader | ||
482 | M.load = function(content_url) { | ||
483 | if(typeof content_url !== 'string') { | ||
484 | console.error('URL to load has to be of type string!!');//TODO: raise custom exception | ||
485 | return; | ||
486 | } | ||
487 | $.getJSON(content_url, function(data) { | ||
488 | M.site_content = data; | ||
489 | M.init(); | ||
490 | }); | ||
491 | }; | ||
492 | |||
493 | })(M); |
js/mouchak.js
(489 / 0)
  | |||
1 | (function(M) { | ||
2 | /* Defining Backbone models, collections and views */ | ||
3 | |||
4 | var BaseType = Backbone.Model.extend({ | ||
5 | defaults: { | ||
6 | tags: [], | ||
7 | title: "", | ||
8 | attr: {} | ||
9 | }, | ||
10 | initialize: function() { | ||
11 | } | ||
12 | }); | ||
13 | |||
14 | var Text = BaseType.extend({ | ||
15 | defaults: { | ||
16 | data: "", | ||
17 | }, | ||
18 | initialize: function() { | ||
19 | } | ||
20 | }); | ||
21 | |||
22 | var TextView = Backbone.View.extend({ | ||
23 | tagName: 'div', | ||
24 | className: '', | ||
25 | initialize: function() { | ||
26 | _.bindAll(this); | ||
27 | _.bind(this.render, this); | ||
28 | }, | ||
29 | render: function(el) { | ||
30 | $(el).append(this.el); | ||
31 | var str = '<h4>'+ this.model.get('title') +'</h4> <p>' + | ||
32 | this.model.get('data') + '</p>'; | ||
33 | $(this.el).html(str); | ||
34 | M.appendAttrs(this.model, this.el); | ||
35 | } | ||
36 | }); | ||
37 | |||
38 | var Table = BaseType.extend({ | ||
39 | defaults: { | ||
40 | data : { | ||
41 | th: [], | ||
42 | tr:[] | ||
43 | } | ||
44 | }, | ||
45 | initialize: function() { | ||
46 | } | ||
47 | }); | ||
48 | |||
49 | var TableView = Backbone.View.extend({ | ||
50 | tagName: 'table', | ||
51 | className: 'table', | ||
52 | initialize: function() { | ||
53 | _.bindAll(this); | ||
54 | _.bind(this.render, this); | ||
55 | }, | ||
56 | render: function(el) { | ||
57 | var heading = this.model.get('data').th; | ||
58 | var str = '<tr>'; | ||
59 | |||
60 | for(var i = 0; i < heading.length; i++) { | ||
61 | str += '<th>' + heading[i] + '</th>'; | ||
62 | } | ||
63 | str += '</tr>'; | ||
64 | |||
65 | _.each(this.model.get('data').tr, function(row) { | ||
66 | str += '<tr>'; | ||
67 | for(var i = 0; i < row.length; i++) { | ||
68 | if(row[i].match(/http.?:/)) { | ||
69 | //console.log(row[i].match(/http:/)) | ||
70 | } | ||
71 | str += '<td>'+ row[i] + '</td>'; | ||
72 | } | ||
73 | str += '</tr>'; | ||
74 | }); | ||
75 | |||
76 | $(el).append(this.el); | ||
77 | $(this.el).html(str); | ||
78 | M.appendAttrs(this.model, this.el); | ||
79 | } | ||
80 | }); | ||
81 | |||
82 | var Image = BaseType.extend({ | ||
83 | defaults: { | ||
84 | src: "" | ||
85 | }, | ||
86 | initialize:function() { | ||
87 | } | ||
88 | }); | ||
89 | |||
90 | var ImageView = Backbone.View.extend({ | ||
91 | tagName: 'image', | ||
92 | className: '', | ||
93 | |||
94 | initialize: function() { | ||
95 | _.bindAll(this); | ||
96 | _.bind(this.render, this); | ||
97 | }, | ||
98 | render: function(el) { | ||
99 | $(el).append(this.el); | ||
100 | $(this.el).attr('src', this.model.get('src')); | ||
101 | M.appendAttrs(this.model, this.el); | ||
102 | } | ||
103 | }); | ||
104 | |||
105 | |||
106 | var Video = BaseType.extend({ | ||
107 | defaults: { | ||
108 | src: "" | ||
109 | }, | ||
110 | initialize:function() { | ||
111 | } | ||
112 | }); | ||
113 | |||
114 | var VideoView = Backbone.View.extend({ | ||
115 | initialize: function() { | ||
116 | _.bindAll(this); | ||
117 | _.bind(this.render, this); | ||
118 | // assuming cross-domain urls will have http in the src, | ||
119 | // so also assuming they are embedded flash urls, | ||
120 | // hence iframe | ||
121 | if(this.model.get('src').match('http')) { | ||
122 | this.tagName = 'iframe'; | ||
123 | } | ||
124 | // otherwise, use html5 video tag, if the video is served | ||
125 | // from the same domain | ||
126 | else { | ||
127 | this.tagName = 'video'; | ||
128 | } | ||
129 | }, | ||
130 | render: function(el) { | ||
131 | $(el).append(this.el); | ||
132 | $(this.el).attr('src', this.model.get('src')); | ||
133 | M.appendAttrs(this.model, this.el); | ||
134 | } | ||
135 | }); | ||
136 | |||
137 | var RSS = BaseType.extend({ | ||
138 | defaults: { | ||
139 | src: "" | ||
140 | }, | ||
141 | initialize:function() { | ||
142 | } | ||
143 | }); | ||
144 | |||
145 | var RSSView = Backbone.View.extend({ | ||
146 | el: '#feeds', | ||
147 | initialize: function() { | ||
148 | _.bindAll(this); | ||
149 | _.bind(this.render, this); | ||
150 | }, | ||
151 | render: function() { | ||
152 | M.populateFeeds(this.model.get('src')); | ||
153 | } | ||
154 | }); | ||
155 | |||
156 | // Plugin model can be used to load dynamic components | ||
157 | // to the website by loading external JS files. | ||
158 | // Also the website can be styled by using external CSS files, | ||
159 | // which can also be loaded via this plugin model. | ||
160 | var Plugin = BaseType.extend({ | ||
161 | defaults: { | ||
162 | src: "", | ||
163 | data: {}, | ||
164 | callback: "" | ||
165 | }, | ||
166 | initialize: function() { | ||
167 | if(this.get('src').match(/\.js/)) { | ||
168 | var script = document.createElement('script'); | ||
169 | var callback = this.get('callback'); | ||
170 | script.src = this.get('src'); | ||
171 | document.body.appendChild(script); | ||
172 | script.onload = function() { | ||
173 | eval(callback); | ||
174 | }; | ||
175 | } | ||
176 | else if(this.get('src').match(/\.css/)) { | ||
177 | var link = document.createElement('link'); | ||
178 | link.rel = 'stylesheet'; | ||
179 | link.href = this.get('src'); | ||
180 | link.type = 'text/css'; | ||
181 | document.body.appendChild(link); | ||
182 | } | ||
183 | } | ||
184 | }); | ||
185 | |||
186 | var PluginView = Backbone.View.extend({ | ||
187 | initialize: function() { | ||
188 | return; | ||
189 | }, | ||
190 | render: function(el) { | ||
191 | return; | ||
192 | } | ||
193 | }); | ||
194 | |||
195 | var type_map; | ||
196 | M.type_map = type_map = { | ||
197 | model : { | ||
198 | 'text': Text, | ||
199 | 'image': Image, | ||
200 | 'video': Video, | ||
201 | 'rss': RSS, | ||
202 | 'table': Table, | ||
203 | 'plugin': Plugin | ||
204 | }, | ||
205 | view: { | ||
206 | 'text': TextView, | ||
207 | 'image': ImageView, | ||
208 | 'video': VideoView, | ||
209 | 'rss': RSSView, | ||
210 | 'table': TableView, | ||
211 | 'plugin': PluginView | ||
212 | } | ||
213 | }; | ||
214 | |||
215 | // model for each Page | ||
216 | var Page = Backbone.Model.extend({ | ||
217 | defaults: { | ||
218 | name: "index", | ||
219 | title: "", | ||
220 | children: [], | ||
221 | content: [] | ||
222 | }, | ||
223 | initialize: function() { | ||
224 | // adding the name of the model as its id. | ||
225 | // look up of this model through the collection | ||
226 | // is faster this way. | ||
227 | this.set({id: M.sanitize(this.get('name'))}); | ||
228 | } | ||
229 | }); | ||
230 | |||
231 | var Pages = Backbone.Collection.extend({ | ||
232 | model: Page | ||
233 | }); | ||
234 | |||
235 | var PageView = Backbone.View.extend({ | ||
236 | tagName: 'div', | ||
237 | className: 'page', | ||
238 | initialize: function() { | ||
239 | _.bindAll(this); | ||
240 | _.bind(this.render, this); | ||
241 | this.render(); | ||
242 | $(this.el).hide(); | ||
243 | }, | ||
244 | render: function() { | ||
245 | $('#content-container').append(this.el); | ||
246 | this.appendNavTemplate(); | ||
247 | $(this.el).append('<h3>'+this.model.get('title')+'</h3>'); | ||
248 | var self = this; | ||
249 | _.each(this.model.get('content'), function(item) { | ||
250 | var view = type_map.view[item.get('type')]; | ||
251 | if(!view) { | ||
252 | console.log('Error initing view', item); | ||
253 | return; | ||
254 | } | ||
255 | if(item.get('type') === 'rss') { | ||
256 | M.rss_view = new view({model: item}); | ||
257 | $(self.el).append(_.template($('#news-template').html())); | ||
258 | } | ||
259 | else { | ||
260 | var item_view = new view({model: item}); | ||
261 | item_view.render(self.el); | ||
262 | } | ||
263 | }); | ||
264 | }, | ||
265 | appendNavTemplate: function() { | ||
266 | var li; | ||
267 | var nav_template = _.template($('#nav-template').html()); | ||
268 | $(this.el).append(nav_template({ | ||
269 | page: this.model.id | ||
270 | })); | ||
271 | } | ||
272 | }); | ||
273 | |||
274 | var AppView = Backbone.View.extend({ | ||
275 | el: 'body', | ||
276 | events: { | ||
277 | 'click .nav li a' : 'navClicked' | ||
278 | }, | ||
279 | initialize: function() { | ||
280 | _.bindAll(this); | ||
281 | }, | ||
282 | render: function() { | ||
283 | $('#index').show(); | ||
284 | _.each(M.pages.models, function(page) { | ||
285 | this.createNavigation(page.id); | ||
286 | }, this); | ||
287 | }, | ||
288 | navClicked: function(event) { | ||
289 | $('.nav li').removeClass('active'); | ||
290 | $(event.currentTarget).parent().addClass('active'); | ||
291 | }, | ||
292 | createNavigation: function(page) { | ||
293 | var li; | ||
294 | if(page === 'index') { | ||
295 | li = '<li class="active"><a href="#/index"> Home </a></li>'; | ||
296 | $('#nav-index .nav').append(li); | ||
297 | } | ||
298 | var dropdown_template = _.template($('#nav-dropdown-template').html()); | ||
299 | var children = M.pages.get(page).get('children'); | ||
300 | _.each(children, function(child) { | ||
301 | child = M.sanitize(child); | ||
302 | var model = M.pages.get(child); | ||
303 | var children = model.get('children'); | ||
304 | if(_.isEmpty(children)) { | ||
305 | li = '<li><a href="#/' + child + '">' + M.humanReadable(child) + '</a></li>'; | ||
306 | } | ||
307 | else { | ||
308 | li = dropdown_template({ | ||
309 | name: M.humanReadable(model.get('name')), | ||
310 | list: _.map(children, M.humanReadable) | ||
311 | }); | ||
312 | } | ||
313 | $(li).appendTo('#nav-' + page + ' .nav'); | ||
314 | }); | ||
315 | }, | ||
316 | updateBreadcrumbs: function(event) { | ||
317 | //TODO: write code to use bootstrap's breadcrumbs to render a | ||
318 | // navigational breadcrumb | ||
319 | } | ||
320 | }); | ||
321 | |||
322 | var AppRouter = Backbone.Router.extend({ | ||
323 | routes : { | ||
324 | 'index' : 'index', | ||
325 | ':page' : 'showPage' | ||
326 | }, | ||
327 | index: function() { | ||
328 | $('.page').hide(); | ||
329 | $('#index').show(); | ||
330 | }, | ||
331 | showPage: function(page) { | ||
332 | $('.page').hide(); | ||
333 | //news pages are rendered on the fly, | ||
334 | //as feeds have to be fetched. | ||
335 | if(page === 'news') { | ||
336 | M.rss_view.render(); | ||
337 | } | ||
338 | $('#'+page).show(); | ||
339 | $('.'+page).show(); | ||
340 | } | ||
341 | }); | ||
342 | |||
343 | /* Defining other necessary functions */ | ||
344 | M.init = function() { | ||
345 | M.tags = {}; //global tag cache | ||
346 | M.pages = new Pages(); //global collection of all pages | ||
347 | |||
348 | // iterate through the JSON to intialize models and views | ||
349 | _.each(M.site_content, function(page) { | ||
350 | var new_page = new Page(page); | ||
351 | var contents = []; | ||
352 | _.each(page.content, function(content) { | ||
353 | var Item = type_map.model[content.type]; | ||
354 | if(!Item) { | ||
355 | console.log('Error initing item: ', content); | ||
356 | return; | ||
357 | } | ||
358 | var item = new Item(content); | ||
359 | contents.push(item); | ||
360 | M.createTagList(content, item); | ||
361 | }); | ||
362 | new_page.set({content: contents}); | ||
363 | var new_page_view = new PageView({model: new_page, | ||
364 | id: new_page.get('id')}); | ||
365 | M.pages.add(new_page); | ||
366 | }); | ||
367 | |||
368 | M.appView = new AppView(); | ||
369 | M.appView.render(); | ||
370 | var app_router = new AppRouter(); | ||
371 | Backbone.history.start(); | ||
372 | // start with index page | ||
373 | var location = window.location; | ||
374 | location.href = location.origin + location.pathname + '#/index'; | ||
375 | M.simHeir(); | ||
376 | }; | ||
377 | |||
378 | // hack to simulate heirarchy among the page views | ||
379 | // basically add the parent id as class in all of its children | ||
380 | // elements. | ||
381 | M.simHeir = function() { | ||
382 | _.each(M.pages.models, function(page) { | ||
383 | if(page.id == 'index') return; | ||
384 | _.each(page.get('children'), function(child) { | ||
385 | child = M.sanitize(child); | ||
386 | $('#'+child).addClass(page.id); | ||
387 | }); | ||
388 | }); | ||
389 | }; | ||
390 | |||
391 | |||
392 | // append attributes to elements from the model | ||
393 | M.appendAttrs = function(model, el) { | ||
394 | _.each(model.get('attr'), function(val, key) { | ||
395 | $(el).attr(key, val); | ||
396 | }); | ||
397 | } | ||
398 | |||
399 | // create the list of tags and associate the objects with related tags | ||
400 | M.createTagList = function(content, model) { | ||
401 | for(var i in content.tags) { | ||
402 | if(!M.tags[content.tags[i]]) { | ||
403 | M.tags[content.tags[i]] = []; | ||
404 | } | ||
405 | M.tags[content.tags[i]].push(model); | ||
406 | } | ||
407 | }; | ||
408 | |||
409 | // Filter the tags and return only those "content" objects which match a given tag. | ||
410 | // @tags should be an array | ||
411 | M.filterTags = function(tags) { | ||
412 | if(!_.isArray(tags)) { | ||
413 | console.log('You have to pass an array'); //TODO: raise an exception | ||
414 | return false; | ||
415 | } | ||
416 | var list = []; | ||
417 | _.each(tags, function(item) { | ||
418 | if(M.tags[item]) { | ||
419 | list.push(M.tags[item]); | ||
420 | } | ||
421 | }); | ||
422 | return _.uniq(_.flatten(list)); | ||
423 | }; | ||
424 | |||
425 | // populate with news feeds in the news section | ||
426 | // gets the feeds from server side script 'feed.py' | ||
427 | M.populateFeeds = function(rss_url) { | ||
428 | $('#feeds-loader').show(); | ||
429 | $('.news-item-wrapper').remove(); | ||
430 | jQuery.getFeed({ | ||
431 | url: 'feeds', | ||
432 | type: 'GET', | ||
433 | data: "rss_url="+encodeURIComponent(rss_url), | ||
434 | success: function(feed) { | ||
435 | $('#feeds-loader').hide(); | ||
436 | var template = _.template($('#news-item-template').html()); | ||
437 | _.each(feed.items, function(item) { | ||
438 | x = $('#feeds').append(template({ | ||
439 | title: item.title, | ||
440 | link: item.link | ||
441 | })); | ||
442 | }); | ||
443 | }, | ||
444 | error: function(err) { | ||
445 | $('#feeds-loader').hide(); | ||
446 | $('#feeds').append('Oops, something went wrong! <br/> Please try again.'); | ||
447 | } | ||
448 | }); | ||
449 | }; | ||
450 | |||
451 | |||
452 | /* Other helper functions */ | ||
453 | |||
454 | // change all '-' to spaces and capitalize first letter of | ||
455 | // every word | ||
456 | M.humanReadable = function(str) { | ||
457 | if(typeof str !== "string") { | ||
458 | str = ''; | ||
459 | } | ||
460 | return '' + str.replace(/[-]+/g, ' ').replace(/[^\s]+/g, function(str) { | ||
461 | return str.substr(0,1).toUpperCase() + str.substr(1).toLowerCase(); | ||
462 | }); | ||
463 | }; | ||
464 | |||
465 | // change all spaces to '-' and everything to lowercase | ||
466 | M.sanitize = function(str) { | ||
467 | if(typeof str !== "string") { | ||
468 | str = ''; | ||
469 | } | ||
470 | return '' + str.replace(/[\s]+/g,'-').replace(/[^\s]+/g, function(str) { | ||
471 | //TODO: handle special characters! | ||
472 | return str.replace('&', 'and').toLowerCase(); | ||
473 | }); | ||
474 | }; | ||
475 | |||
476 | |||
477 | // Loader | ||
478 | M.load = function(content_url) { | ||
479 | if(typeof content_url !== 'string') { | ||
480 | console.error('URL to load has to be of type string!!');//TODO: raise custom exception | ||
481 | return; | ||
482 | } | ||
483 | $.getJSON(content_url, function(data) { | ||
484 | M.site_content = data; | ||
485 | M.init(); | ||
486 | }); | ||
487 | }; | ||
488 | |||
489 | })(M); |