cedar 0.2.72.pre → 0.2.73.pre
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/assets/javascripts/cedar.js +2 -489
- data/lib/assets/javascripts/cedar_config.js +3 -0
- data/lib/assets/javascripts/{cedar.handlebars.js → cedar_handlebars.js} +1 -1
- data/lib/assets/javascripts/cedar_source.js +514 -0
- data/lib/cedar.rb +13 -0
- data/lib/cedar/config.rb +14 -0
- data/lib/cedar/version.rb +1 -1
- metadata +6 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d2a2deab61df20598991c211842cbf55d39233e4
|
4
|
+
data.tar.gz: e3af2747ae9c487b0103d0ab4eca240131f1e6c2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: bf3e413b4ad6165dc0e3c5844606df7bf5d92b4482dcc52f63725a12f68ddb498a604f6874382455cc6d151eebcd9d7c239a8b643b517d772001f82b80c82674
|
7
|
+
data.tar.gz: fb265f0cf9af94c415c72be0998f13e632ba6619e1c4deee81797d30ae29bb5e1ef278302fd398a5eb912783c4810116cdbf64660581552e5641a9e82a633a3c
|
@@ -1,489 +1,2 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
*/
|
4
|
-
window.Cedar = {
|
5
|
-
initialized: false,
|
6
|
-
store: null,
|
7
|
-
auth: null,
|
8
|
-
config: {}
|
9
|
-
};
|
10
|
-
|
11
|
-
|
12
|
-
/**
|
13
|
-
* Cedar.Init
|
14
|
-
*
|
15
|
-
* Initialize Application object
|
16
|
-
*
|
17
|
-
*/
|
18
|
-
Cedar.Init = function(options) {
|
19
|
-
return new Cedar.Application(options);
|
20
|
-
};
|
21
|
-
|
22
|
-
/**
|
23
|
-
* Global function to display message to console if Cedar.debug = true
|
24
|
-
*
|
25
|
-
* @param <string> message
|
26
|
-
*/
|
27
|
-
Cedar.debug = function(msg) {
|
28
|
-
if (Cedar.config.debug) {
|
29
|
-
window.console.log(msg);
|
30
|
-
}
|
31
|
-
};
|
32
|
-
|
33
|
-
/**
|
34
|
-
* Cedar.Application
|
35
|
-
*
|
36
|
-
* Application object for initializing and setting global values
|
37
|
-
*
|
38
|
-
* @param <Array> options
|
39
|
-
* - server : 'test.cdr.plyinc.com' - *required
|
40
|
-
* - debug : true | false
|
41
|
-
* - fetch : true | false
|
42
|
-
* - forceHttps : true | false
|
43
|
-
*/
|
44
|
-
Cedar.Application = function(options) {
|
45
|
-
if ( Cedar.initialized ) {
|
46
|
-
return;
|
47
|
-
}
|
48
|
-
|
49
|
-
var defaults = {
|
50
|
-
debug: false,
|
51
|
-
fetch: true,
|
52
|
-
wait: false,
|
53
|
-
forceHttps: false
|
54
|
-
};
|
55
|
-
|
56
|
-
this.options = $.extend({}, defaults, options);
|
57
|
-
|
58
|
-
if (this.options.server === undefined) {
|
59
|
-
throw 'Cedar Error: must provide "server" value on Init()';
|
60
|
-
}
|
61
|
-
|
62
|
-
Cedar.config.server = this.getProtocol() + this.options.server;
|
63
|
-
Cedar.config.api = this.getProtocol() + this.options.server + '/api';
|
64
|
-
Cedar.config.debug = this.options.debug;
|
65
|
-
Cedar.config.wait = this.options.wait;
|
66
|
-
Cedar.config.fetch = this.options.fetch;
|
67
|
-
|
68
|
-
if (Cedar.events === undefined) {
|
69
|
-
Cedar.events = jQuery({});
|
70
|
-
}
|
71
|
-
|
72
|
-
if ( Cedar.store === null ) {
|
73
|
-
Cedar.store = new Cedar.Store();
|
74
|
-
}
|
75
|
-
|
76
|
-
if ( Cedar.auth === null ) {
|
77
|
-
Cedar.auth = new Cedar.Auth();
|
78
|
-
}
|
79
|
-
|
80
|
-
if (Cedar.auth.isEditMode()) {
|
81
|
-
this.showGlobalActions();
|
82
|
-
}
|
83
|
-
|
84
|
-
Cedar.initialized = true;
|
85
|
-
|
86
|
-
this.initializeHTML();
|
87
|
-
};
|
88
|
-
|
89
|
-
Cedar.Application.prototype.initializeHTML = function() {
|
90
|
-
$('[data-cedar-id]').each(function(){
|
91
|
-
var $this = $(this);
|
92
|
-
$this.data("cedarObject", new Cedar.ContentEntry({
|
93
|
-
el: this,
|
94
|
-
cedarId: $this.data("cedarId")
|
95
|
-
}));
|
96
|
-
|
97
|
-
Cedar.events.on("content:loaded", function() {
|
98
|
-
$this.data("cedarObject").render();
|
99
|
-
}.bind(this));
|
100
|
-
});
|
101
|
-
Cedar.events.trigger("content:loaded");
|
102
|
-
};
|
103
|
-
|
104
|
-
Cedar.Application.prototype.getProtocol = function() {
|
105
|
-
if (this.options.forceHttps || window.location.protocol === 'https:') {
|
106
|
-
return 'https://';
|
107
|
-
} else {
|
108
|
-
return 'http://';
|
109
|
-
}
|
110
|
-
};
|
111
|
-
|
112
|
-
Cedar.Application.prototype.showGlobalActions = function() {
|
113
|
-
$(document).ready(function() {
|
114
|
-
var $body = $('body');
|
115
|
-
var globalActions = '<div class="cedar-cms-global-actions">' +
|
116
|
-
'<a href="#" class="cedar-cms-global-action" onclick="window.location.reload();">' +
|
117
|
-
'<span class="cedar-cms-icon cedar-cms-icon-edit"></span> ' +
|
118
|
-
'<span class="cedar-cms-global-action-label">Refresh</span>' +
|
119
|
-
'</a><br>' +
|
120
|
-
'<a class="cedar-cms-global-action" href="' + Cedar.auth.getLogOffURL() + '">' +
|
121
|
-
'<span class="cedar-cms-icon cedar-cms-icon-edit"></span> ' +
|
122
|
-
'<span class="cedar-cms-global-action-label">Log Off Cedar</span>' +
|
123
|
-
'</a>' +
|
124
|
-
'</div>';
|
125
|
-
$body.append(globalActions);
|
126
|
-
});
|
127
|
-
};
|
128
|
-
|
129
|
-
|
130
|
-
/**
|
131
|
-
* Cedar.Auth
|
132
|
-
*
|
133
|
-
* responsible for determining if we're in edit mode
|
134
|
-
*/
|
135
|
-
Cedar.Auth = function() {
|
136
|
-
return;
|
137
|
-
};
|
138
|
-
|
139
|
-
Cedar.Auth.prototype.isEditMode = function() {
|
140
|
-
return this.isEditUrl();
|
141
|
-
};
|
142
|
-
|
143
|
-
Cedar.Auth.prototype.isEditUrl = function() {
|
144
|
-
var sPageURL = window.location.search.substring(1);
|
145
|
-
var sURLVariables = sPageURL.split('&');
|
146
|
-
var i = 0;
|
147
|
-
while (i < sURLVariables.length) {
|
148
|
-
if (sURLVariables[i] === 'cdrlogin') {
|
149
|
-
return true;
|
150
|
-
}
|
151
|
-
i++;
|
152
|
-
}
|
153
|
-
return false;
|
154
|
-
};
|
155
|
-
|
156
|
-
Cedar.Auth.prototype.getLogOffURL = function() {
|
157
|
-
return this.removeURLParameter(window.location.href, 'cdrlogin');
|
158
|
-
};
|
159
|
-
|
160
|
-
// adapted from stackoverflow:
|
161
|
-
// http://stackoverflow.com/questions/1634748/how-can-i-delete-a-query-string-parameter-in-javascript
|
162
|
-
Cedar.Auth.prototype.removeURLParameter = function(url, parameter) {
|
163
|
-
var splitUrl = url.split('#');
|
164
|
-
var serverUrl = splitUrl[0];
|
165
|
-
var clientUrl = splitUrl[1] || '';
|
166
|
-
if (clientUrl) {
|
167
|
-
clientUrl = '#' + clientUrl;
|
168
|
-
}
|
169
|
-
// prefer to use l.search if you have a location/link object
|
170
|
-
var splitServerUrl= serverUrl.split('?');
|
171
|
-
if (splitServerUrl.length>=2) {
|
172
|
-
|
173
|
-
var prefix = encodeURIComponent(parameter); //+'=';
|
174
|
-
var pars = splitServerUrl[1].split(/[&;]/g);
|
175
|
-
|
176
|
-
//reverse iteration as may be destructive
|
177
|
-
var i = pars.length - 1;
|
178
|
-
while (i >= 0) {
|
179
|
-
// idiom for string.startsWith
|
180
|
-
if (pars[i].lastIndexOf(prefix, 0) !== -1) {
|
181
|
-
pars.splice(i, 1);
|
182
|
-
}
|
183
|
-
i--;
|
184
|
-
}
|
185
|
-
|
186
|
-
var updatedServerUrl= splitServerUrl[0];
|
187
|
-
if (pars.length > 0) {
|
188
|
-
updatedServerUrl += '?'+pars.join('&');
|
189
|
-
}
|
190
|
-
return updatedServerUrl + clientUrl;
|
191
|
-
} else {
|
192
|
-
return url;
|
193
|
-
}
|
194
|
-
};
|
195
|
-
|
196
|
-
|
197
|
-
/**
|
198
|
-
* Cedar.Store
|
199
|
-
*
|
200
|
-
* responsible for retrieving Cedar elements from server or local cache.
|
201
|
-
*
|
202
|
-
* different cedar types may use different api paths, therefore the paths
|
203
|
-
* are passed into some functions
|
204
|
-
*/
|
205
|
-
Cedar.Store = function() {
|
206
|
-
this.loaded = false;
|
207
|
-
|
208
|
-
var fail;
|
209
|
-
var uid;
|
210
|
-
try {
|
211
|
-
uid = new Date;
|
212
|
-
(this.cache = window.localStorage).setItem(uid, uid);
|
213
|
-
fail = this.cache.getItem(uid) != uid;
|
214
|
-
this.cache.removeItem(uid);
|
215
|
-
fail && (this.cache = false);
|
216
|
-
} catch (exception) {
|
217
|
-
this.cache = {};
|
218
|
-
}
|
219
|
-
|
220
|
-
if (Cedar.config.fetch) {
|
221
|
-
this.refresh();
|
222
|
-
}
|
223
|
-
};
|
224
|
-
|
225
|
-
/**
|
226
|
-
* store a single json item by key
|
227
|
-
*
|
228
|
-
* @param <string> 'key'
|
229
|
-
* @param <json> 'item'
|
230
|
-
*/
|
231
|
-
Cedar.Store.prototype.put = function ( key, item ) {
|
232
|
-
this.cache[key] = typeof item === "string" ? item : JSON.stringify(item);
|
233
|
-
};
|
234
|
-
|
235
|
-
// Return promise of parsed content from local or remote storage
|
236
|
-
Cedar.Store.prototype.get = function(key) {
|
237
|
-
return this.getDeferred(key).then(function(data) {
|
238
|
-
try {
|
239
|
-
return JSON.parse(data);
|
240
|
-
} catch (e) {
|
241
|
-
return data;
|
242
|
-
}
|
243
|
-
});
|
244
|
-
};
|
245
|
-
|
246
|
-
// Return local content immediately if possible. Otherwise return deferred remote content
|
247
|
-
Cedar.Store.prototype.getDeferred = function(key) {
|
248
|
-
var cachedDeferred = this.cachedDeferred(key);
|
249
|
-
var remoteDeferred = this.remoteDeferred(key);
|
250
|
-
|
251
|
-
if (Cedar.config.wait || !this.cache[key]) {
|
252
|
-
Cedar.debug('checking remote: ' + key);
|
253
|
-
return remoteDeferred;
|
254
|
-
} else {
|
255
|
-
Cedar.debug('get from cache: ' + key);
|
256
|
-
return cachedDeferred;
|
257
|
-
}
|
258
|
-
};
|
259
|
-
|
260
|
-
// Deferred object containing local content
|
261
|
-
Cedar.Store.prototype.cachedDeferred = function(key) {
|
262
|
-
return $.Deferred().resolve(this.cache[key]);
|
263
|
-
};
|
264
|
-
|
265
|
-
// Refresh local storage if needed and then return content
|
266
|
-
Cedar.Store.prototype.remoteDeferred = function(key) {
|
267
|
-
return this.refresh().then(function() {
|
268
|
-
return this.cachedDeferred(key);
|
269
|
-
}.bind(this));
|
270
|
-
};
|
271
|
-
|
272
|
-
// Check content version and update if needed
|
273
|
-
Cedar.Store.prototype.refresh = function() {
|
274
|
-
return this.checkVersion().then(function() {
|
275
|
-
if (this.loaded) {
|
276
|
-
return $.Deferred().resolve();
|
277
|
-
} else {
|
278
|
-
return this.getRemote();
|
279
|
-
}
|
280
|
-
}.bind(this));
|
281
|
-
};
|
282
|
-
|
283
|
-
// Get content objects from server and save to local storage
|
284
|
-
Cedar.Store.prototype.getRemote = function(options) {
|
285
|
-
var defaultOptions = {
|
286
|
-
path: '/queries/contententries/'
|
287
|
-
};
|
288
|
-
options = $.extend({}, defaultOptions, options);
|
289
|
-
var defaultParams = {};
|
290
|
-
var params = $.extend({}, defaultParams, {
|
291
|
-
guidfilter: options.filter
|
292
|
-
});
|
293
|
-
|
294
|
-
return this.lockedRequest({
|
295
|
-
path: options.path,
|
296
|
-
params: params,
|
297
|
-
success: function(response) {
|
298
|
-
$.each(response, function (index, value) {
|
299
|
-
if (value.hasOwnProperty('id') && value.settings.hasOwnProperty('content')) {
|
300
|
-
this.put(value.id, value);
|
301
|
-
Cedar.debug("storing: " + value.id);
|
302
|
-
}
|
303
|
-
}.bind(this));
|
304
|
-
Cedar.debug("local storage was updated");
|
305
|
-
this.loaded = true;
|
306
|
-
Cedar.events.trigger("content:loaded");
|
307
|
-
}.bind(this)
|
308
|
-
});
|
309
|
-
};
|
310
|
-
|
311
|
-
/**
|
312
|
-
* clear the local storage or remove a single locally store item by key
|
313
|
-
*
|
314
|
-
* @param {ID} key
|
315
|
-
*/
|
316
|
-
Cedar.Store.prototype.clear = function(key) {
|
317
|
-
if (key === undefined) {
|
318
|
-
if (window.hasOwnProperty('localStorage')) {
|
319
|
-
this.cache.clear();
|
320
|
-
} else {
|
321
|
-
this.cache = {};
|
322
|
-
}
|
323
|
-
}
|
324
|
-
else {
|
325
|
-
delete this.cache[key];
|
326
|
-
}
|
327
|
-
};
|
328
|
-
|
329
|
-
/**
|
330
|
-
* set the locally stored data version number
|
331
|
-
*
|
332
|
-
* @return <string> data version number
|
333
|
-
*/
|
334
|
-
Cedar.Store.prototype.setVersion = function(id) {
|
335
|
-
Cedar.debug("updating to version #" + id);
|
336
|
-
this.cache["___CEDAR__DATA__FINGERPRINT___"] = id;
|
337
|
-
};
|
338
|
-
|
339
|
-
/**
|
340
|
-
* return the currently stored data version number
|
341
|
-
*
|
342
|
-
* @return <string> data version number
|
343
|
-
*/
|
344
|
-
Cedar.Store.prototype.getVersion = function() {
|
345
|
-
return this.cache["___CEDAR__DATA__FINGERPRINT___"];
|
346
|
-
};
|
347
|
-
|
348
|
-
/**
|
349
|
-
* Query the server for the latest data version number
|
350
|
-
*
|
351
|
-
* @return <Deferred>
|
352
|
-
*/
|
353
|
-
Cedar.Store.prototype.checkVersion = function() {
|
354
|
-
return this.lockedRequest({
|
355
|
-
path: '/queries/status',
|
356
|
-
success: function(response) {
|
357
|
-
Cedar.debug("checking version #" + this.getVersion());
|
358
|
-
|
359
|
-
// response.settings.version is being returned as an array and must be converted
|
360
|
-
if ( this.getVersion() !== response.settings.version.toString() ) {
|
361
|
-
Cedar.debug('setting version: ' + response.settings.version);
|
362
|
-
this.loaded = false;
|
363
|
-
this.setVersion(response.settings.version);
|
364
|
-
} else {
|
365
|
-
Cedar.debug("version is up to date");
|
366
|
-
this.loaded = true;
|
367
|
-
Cedar.events.trigger("content:loaded");
|
368
|
-
}
|
369
|
-
}.bind(this)
|
370
|
-
});
|
371
|
-
};
|
372
|
-
|
373
|
-
// Returns an already resolving getJSON request if it matches
|
374
|
-
Cedar.Store.prototype.lockedRequest = function(options) {
|
375
|
-
options = options || {};
|
376
|
-
this.requestCache || (this.requestCache = {});
|
377
|
-
|
378
|
-
var requestKey = JSON.stringify({path: options.path, params: options.params});
|
379
|
-
|
380
|
-
return this.requestCache[requestKey] || (this.requestCache[requestKey] = $
|
381
|
-
.getJSON(Cedar.config.api + options.path, options.params).then(function(response){
|
382
|
-
options.success(response);
|
383
|
-
}.bind(this)));
|
384
|
-
};
|
385
|
-
|
386
|
-
|
387
|
-
/*
|
388
|
-
Cedar.ContentObject
|
389
|
-
Parent class for all Cedar content object types
|
390
|
-
*/
|
391
|
-
Cedar.ContentObject = function(options) {
|
392
|
-
var defaults = {
|
393
|
-
el: '<div />'
|
394
|
-
};
|
395
|
-
this.options = $.extend({}, defaults, options);
|
396
|
-
this.$el = $(this.options.el);
|
397
|
-
};
|
398
|
-
|
399
|
-
Cedar.ContentObject.prototype = {
|
400
|
-
render: function() {
|
401
|
-
this.load().then(function() {
|
402
|
-
this.$el.html(this.toString());
|
403
|
-
}.bind(this));
|
404
|
-
},
|
405
|
-
|
406
|
-
load: function() {
|
407
|
-
return Cedar.store.get(this.options.cedarId).then(function(data) {
|
408
|
-
this.setContent(data);
|
409
|
-
return this;
|
410
|
-
}.bind(this));
|
411
|
-
},
|
412
|
-
|
413
|
-
getContent: function() {
|
414
|
-
return this.content;
|
415
|
-
},
|
416
|
-
|
417
|
-
setContent: function(data) {
|
418
|
-
this.content = data;
|
419
|
-
},
|
420
|
-
|
421
|
-
getContentWithEditTools: function() {
|
422
|
-
return this.getEditOpen() + this.getContent() + this.getEditClose();
|
423
|
-
},
|
424
|
-
|
425
|
-
toString: function() {
|
426
|
-
return Cedar.auth.isEditMode() ? this.getContentWithEditTools() : this.getContent();
|
427
|
-
},
|
428
|
-
|
429
|
-
toJSON: function() {
|
430
|
-
return {
|
431
|
-
content: this.getContent()
|
432
|
-
};
|
433
|
-
},
|
434
|
-
|
435
|
-
getEditOpen: function() {
|
436
|
-
var jsString = "if(event.stopPropagation){event.stopPropagation();}" +
|
437
|
-
"event.cancelBubble=true;" +
|
438
|
-
"window.location.href=this.attributes.href.value + \'&referer=' + encodeURIComponent(window.location.href) + '\';" +
|
439
|
-
"return false;";
|
440
|
-
|
441
|
-
var block = '<span class="cedar-cms-editable clearfix">';
|
442
|
-
block += '<span class="cedar-cms-edit-tools">';
|
443
|
-
block += '<a onclick="' + jsString + '" href="' + Cedar.config.server +
|
444
|
-
'/cmsadmin/EditData?cdr=1&t=ContentEntry&o=' +
|
445
|
-
encodeURIComponent(this.options.cedarId) +
|
446
|
-
'" class="cedar-cms-edit-icon cedar-js-edit" >';
|
447
|
-
block += '<i class="cedar-cms-icon cedar-cms-icon-right cedar-cms-icon-edit"></i></a>';
|
448
|
-
block += '</span>';
|
449
|
-
return block;
|
450
|
-
},
|
451
|
-
|
452
|
-
getEditClose: function() {
|
453
|
-
return '</span>';
|
454
|
-
}
|
455
|
-
};
|
456
|
-
|
457
|
-
Cedar.ContentObject.prototype.constructor = Cedar.ContentObject;
|
458
|
-
|
459
|
-
/*
|
460
|
-
Cedar.ContentEntry
|
461
|
-
basic content block class
|
462
|
-
*/
|
463
|
-
Cedar.ContentEntry = function(options) {
|
464
|
-
Cedar.ContentObject.call(this, options);
|
465
|
-
};
|
466
|
-
Cedar.ContentEntry.prototype = Object.create(Cedar.ContentObject.prototype);
|
467
|
-
Cedar.ContentEntry.prototype.constructor = Cedar.ContentEntry;
|
468
|
-
|
469
|
-
Cedar.ContentEntry.prototype.setContent = function(data) {
|
470
|
-
this.content = (data && (data.settings && data.settings.content)) || '';
|
471
|
-
};
|
472
|
-
|
473
|
-
/*
|
474
|
-
Cedar.Program
|
475
|
-
program object class
|
476
|
-
*/
|
477
|
-
Cedar.Program = function(options) {
|
478
|
-
Cedar.ContentObject.call(this, options);
|
479
|
-
};
|
480
|
-
Cedar.Program.prototype = Object.create(Cedar.ContentObject.prototype);
|
481
|
-
Cedar.Program.prototype.constructor = Cedar.Program;
|
482
|
-
|
483
|
-
Cedar.Program.prototype.setContent = function(data) {
|
484
|
-
this.content = (data && (data.settings && JSON.parse(data.settings.content))) || '';
|
485
|
-
};
|
486
|
-
|
487
|
-
Cedar.Program.prototype.toJSON = function(data) {
|
488
|
-
return this.content;
|
489
|
-
};
|
1
|
+
//= require cedar_config
|
2
|
+
//= require cedar_source
|
@@ -65,7 +65,7 @@ Handlebars.registerHelper('cedar', function(options) {
|
|
65
65
|
|
66
66
|
var type = options.hash.type || 'ContentEntry';
|
67
67
|
|
68
|
-
new window.Cedar[type]({ cedarId: options.hash.id }).load().then(function(contentEntry){
|
68
|
+
new window.Cedar[type]({ cedarId: options.hash.id, defaultContent: options.hash.default }).load().then(function(contentEntry){
|
69
69
|
if (blockHelperStyle()) {
|
70
70
|
if (Cedar.auth.isEditMode()) {
|
71
71
|
output += contentEntry.getEditOpen();
|
@@ -0,0 +1,514 @@
|
|
1
|
+
/**
|
2
|
+
* Global object for settings and storage
|
3
|
+
*/
|
4
|
+
window.Cedar = window.Cedar || {};
|
5
|
+
window.Cedar = $.extend({}, window.Cedar, {
|
6
|
+
initialized: false,
|
7
|
+
store: null,
|
8
|
+
auth: null
|
9
|
+
});
|
10
|
+
window.Cedar.config = window.Cedar.config || {
|
11
|
+
liveMode: true
|
12
|
+
};
|
13
|
+
|
14
|
+
/**
|
15
|
+
* Cedar.Init
|
16
|
+
*
|
17
|
+
* Initialize Application object
|
18
|
+
*
|
19
|
+
*/
|
20
|
+
Cedar.Init = function(options) {
|
21
|
+
return new Cedar.Application(options);
|
22
|
+
};
|
23
|
+
|
24
|
+
/**
|
25
|
+
* Global function to display message to console if Cedar.debug = true
|
26
|
+
*
|
27
|
+
* @param <string> message
|
28
|
+
*/
|
29
|
+
Cedar.debug = function(msg) {
|
30
|
+
if (Cedar.config.debug) {
|
31
|
+
window.console.log(msg);
|
32
|
+
}
|
33
|
+
};
|
34
|
+
|
35
|
+
/**
|
36
|
+
* Cedar.Application
|
37
|
+
*
|
38
|
+
* Application object for initializing and setting global values
|
39
|
+
*
|
40
|
+
* @param <Array> options
|
41
|
+
* - server : 'test.cdr.plyinc.com' - *required
|
42
|
+
* - debug : true | false
|
43
|
+
* - fetch : true | false
|
44
|
+
* - forceHttps : true | false
|
45
|
+
*/
|
46
|
+
Cedar.Application = function(options) {
|
47
|
+
if ( Cedar.initialized ) {
|
48
|
+
return;
|
49
|
+
}
|
50
|
+
|
51
|
+
var defaults = {
|
52
|
+
debug: false,
|
53
|
+
fetch: true,
|
54
|
+
wait: false,
|
55
|
+
forceHttps: false
|
56
|
+
};
|
57
|
+
|
58
|
+
this.options = $.extend({}, defaults, options);
|
59
|
+
|
60
|
+
if (this.options.server === undefined) {
|
61
|
+
throw 'Cedar Error: must provide "server" value on Init()';
|
62
|
+
}
|
63
|
+
|
64
|
+
Cedar.config.server = this.getProtocol() + this.options.server;
|
65
|
+
Cedar.config.api = this.getProtocol() + this.options.server + '/api';
|
66
|
+
Cedar.config.debug = this.options.debug;
|
67
|
+
Cedar.config.wait = this.options.wait;
|
68
|
+
Cedar.config.fetch = this.options.fetch;
|
69
|
+
|
70
|
+
if (Cedar.events === undefined) {
|
71
|
+
Cedar.events = new Cedar.Events();
|
72
|
+
}
|
73
|
+
|
74
|
+
if ( Cedar.store === null ) {
|
75
|
+
Cedar.store = new Cedar.Store();
|
76
|
+
}
|
77
|
+
|
78
|
+
if ( Cedar.auth === null ) {
|
79
|
+
Cedar.auth = new Cedar.Auth();
|
80
|
+
}
|
81
|
+
|
82
|
+
if (Cedar.auth.isEditMode()) {
|
83
|
+
this.showGlobalActions();
|
84
|
+
}
|
85
|
+
|
86
|
+
Cedar.initialized = true;
|
87
|
+
|
88
|
+
this.initializeHTML();
|
89
|
+
};
|
90
|
+
|
91
|
+
Cedar.Application.prototype.initializeHTML = function() {
|
92
|
+
$('[data-cedar-id]').each(function(){
|
93
|
+
var $this = $(this);
|
94
|
+
$this.data("cedarObject", new Cedar.ContentEntry({
|
95
|
+
el: this,
|
96
|
+
cedarId: $this.data("cedarId")
|
97
|
+
}));
|
98
|
+
|
99
|
+
Cedar.events.on("content:loaded", function() {
|
100
|
+
$this.data("cedarObject").render();
|
101
|
+
}.bind(this));
|
102
|
+
});
|
103
|
+
Cedar.events.trigger("content:loaded");
|
104
|
+
};
|
105
|
+
|
106
|
+
Cedar.Application.prototype.getProtocol = function() {
|
107
|
+
if (this.options.forceHttps || window.location.protocol === 'https:') {
|
108
|
+
return 'https://';
|
109
|
+
} else {
|
110
|
+
return 'http://';
|
111
|
+
}
|
112
|
+
};
|
113
|
+
|
114
|
+
Cedar.Application.prototype.showGlobalActions = function() {
|
115
|
+
$(document).ready(function() {
|
116
|
+
var $body = $('body');
|
117
|
+
var globalActions = '<div class="cedar-cms-global-actions">' +
|
118
|
+
'<a href="#" class="cedar-cms-global-action" onclick="window.location.reload();">' +
|
119
|
+
'<span class="cedar-cms-icon cedar-cms-icon-edit"></span> ' +
|
120
|
+
'<span class="cedar-cms-global-action-label">Refresh</span>' +
|
121
|
+
'</a><br>' +
|
122
|
+
'<a class="cedar-cms-global-action" href="' + Cedar.auth.getLogOffURL() + '">' +
|
123
|
+
'<span class="cedar-cms-icon cedar-cms-icon-edit"></span> ' +
|
124
|
+
'<span class="cedar-cms-global-action-label">Log Off Cedar</span>' +
|
125
|
+
'</a>' +
|
126
|
+
'</div>';
|
127
|
+
$body.append(globalActions);
|
128
|
+
});
|
129
|
+
};
|
130
|
+
|
131
|
+
/**
|
132
|
+
* Cedar.Events
|
133
|
+
*/
|
134
|
+
Cedar.Events = function() {
|
135
|
+
this.eventCollection = {};
|
136
|
+
};
|
137
|
+
|
138
|
+
Cedar.Events.prototype.trigger = function(eventName) {
|
139
|
+
if (!this.eventCollection[eventName]) {
|
140
|
+
this.eventCollection[eventName] = document.createEvent('Event');
|
141
|
+
this.eventCollection[eventName].initEvent(eventName, true, true);
|
142
|
+
}
|
143
|
+
document.dispatchEvent(this.eventCollection[eventName]);
|
144
|
+
};
|
145
|
+
|
146
|
+
Cedar.Events.prototype.on = function(eventName, eventCallback) {
|
147
|
+
document.addEventListener(eventName, eventCallback);
|
148
|
+
};
|
149
|
+
|
150
|
+
/**
|
151
|
+
* Cedar.Auth
|
152
|
+
*
|
153
|
+
* responsible for determining if we're in edit mode
|
154
|
+
*/
|
155
|
+
Cedar.Auth = function() {
|
156
|
+
return;
|
157
|
+
};
|
158
|
+
|
159
|
+
Cedar.Auth.prototype.isEditMode = function() {
|
160
|
+
return this.isEditUrl();
|
161
|
+
};
|
162
|
+
|
163
|
+
Cedar.Auth.prototype.isEditUrl = function() {
|
164
|
+
var sPageURL = window.location.search.substring(1);
|
165
|
+
var sURLVariables = sPageURL.split('&');
|
166
|
+
var i = 0;
|
167
|
+
while (i < sURLVariables.length) {
|
168
|
+
if (sURLVariables[i] === 'cdrlogin') {
|
169
|
+
return true;
|
170
|
+
}
|
171
|
+
i++;
|
172
|
+
}
|
173
|
+
return false;
|
174
|
+
};
|
175
|
+
|
176
|
+
Cedar.Auth.prototype.getLogOffURL = function() {
|
177
|
+
return this.removeURLParameter(window.location.href, 'cdrlogin');
|
178
|
+
};
|
179
|
+
|
180
|
+
// adapted from stackoverflow:
|
181
|
+
// http://stackoverflow.com/questions/1634748/how-can-i-delete-a-query-string-parameter-in-javascript
|
182
|
+
Cedar.Auth.prototype.removeURLParameter = function(url, parameter) {
|
183
|
+
var splitUrl = url.split('#');
|
184
|
+
var serverUrl = splitUrl[0];
|
185
|
+
var clientUrl = splitUrl[1] || '';
|
186
|
+
if (clientUrl) {
|
187
|
+
clientUrl = '#' + clientUrl;
|
188
|
+
}
|
189
|
+
// prefer to use l.search if you have a location/link object
|
190
|
+
var splitServerUrl= serverUrl.split('?');
|
191
|
+
if (splitServerUrl.length>=2) {
|
192
|
+
|
193
|
+
var prefix = encodeURIComponent(parameter); //+'=';
|
194
|
+
var pars = splitServerUrl[1].split(/[&;]/g);
|
195
|
+
|
196
|
+
//reverse iteration as may be destructive
|
197
|
+
var i = pars.length - 1;
|
198
|
+
while (i >= 0) {
|
199
|
+
// idiom for string.startsWith
|
200
|
+
if (pars[i].lastIndexOf(prefix, 0) !== -1) {
|
201
|
+
pars.splice(i, 1);
|
202
|
+
}
|
203
|
+
i--;
|
204
|
+
}
|
205
|
+
|
206
|
+
var updatedServerUrl= splitServerUrl[0];
|
207
|
+
if (pars.length > 0) {
|
208
|
+
updatedServerUrl += '?'+pars.join('&');
|
209
|
+
}
|
210
|
+
return updatedServerUrl + clientUrl;
|
211
|
+
} else {
|
212
|
+
return url;
|
213
|
+
}
|
214
|
+
};
|
215
|
+
|
216
|
+
|
217
|
+
/**
|
218
|
+
* Cedar.Store
|
219
|
+
*
|
220
|
+
* responsible for retrieving Cedar elements from server or local cache.
|
221
|
+
*
|
222
|
+
* different cedar types may use different api paths, therefore the paths
|
223
|
+
* are passed into some functions
|
224
|
+
*/
|
225
|
+
Cedar.Store = function() {
|
226
|
+
this.loaded = false;
|
227
|
+
|
228
|
+
var fail;
|
229
|
+
var uid;
|
230
|
+
try {
|
231
|
+
uid = new Date;
|
232
|
+
(this.cache = window.localStorage).setItem(uid, uid);
|
233
|
+
fail = this.cache.getItem(uid) != uid;
|
234
|
+
this.cache.removeItem(uid);
|
235
|
+
fail && (this.cache = false);
|
236
|
+
} catch (exception) {
|
237
|
+
this.cache = {};
|
238
|
+
}
|
239
|
+
|
240
|
+
if (Cedar.config.fetch) {
|
241
|
+
this.refresh();
|
242
|
+
}
|
243
|
+
};
|
244
|
+
|
245
|
+
/**
|
246
|
+
* store a single json item by key
|
247
|
+
*
|
248
|
+
* @param <string> 'key'
|
249
|
+
* @param <json> 'item'
|
250
|
+
*/
|
251
|
+
Cedar.Store.prototype.put = function ( key, item ) {
|
252
|
+
this.cache[key] = typeof item === "string" ? item : JSON.stringify(item);
|
253
|
+
};
|
254
|
+
|
255
|
+
// Return promise of parsed content from local or remote storage
|
256
|
+
Cedar.Store.prototype.get = function(key) {
|
257
|
+
return this.getDeferred(key).then(function(data) {
|
258
|
+
try {
|
259
|
+
return JSON.parse(data);
|
260
|
+
} catch (e) {
|
261
|
+
return data;
|
262
|
+
}
|
263
|
+
});
|
264
|
+
};
|
265
|
+
|
266
|
+
// Return local content immediately if possible. Otherwise return deferred remote content
|
267
|
+
Cedar.Store.prototype.getDeferred = function(key) {
|
268
|
+
var cachedDeferred = this.cachedDeferred(key);
|
269
|
+
var remoteDeferred = this.remoteDeferred(key);
|
270
|
+
|
271
|
+
if (Cedar.config.wait || !this.cache[key]) {
|
272
|
+
Cedar.debug('checking remote: ' + key);
|
273
|
+
return remoteDeferred;
|
274
|
+
} else {
|
275
|
+
Cedar.debug('get from cache: ' + key);
|
276
|
+
return cachedDeferred;
|
277
|
+
}
|
278
|
+
};
|
279
|
+
|
280
|
+
// Deferred object containing local content
|
281
|
+
Cedar.Store.prototype.cachedDeferred = function(key) {
|
282
|
+
return $.Deferred().resolve(this.cache[key]);
|
283
|
+
};
|
284
|
+
|
285
|
+
// Refresh local storage if needed and then return content
|
286
|
+
Cedar.Store.prototype.remoteDeferred = function(key) {
|
287
|
+
return this.refresh().then(function() {
|
288
|
+
return this.cachedDeferred(key);
|
289
|
+
}.bind(this));
|
290
|
+
};
|
291
|
+
|
292
|
+
// Check content version and update if needed
|
293
|
+
Cedar.Store.prototype.refresh = function() {
|
294
|
+
return this.checkVersion().then(function() {
|
295
|
+
if (this.loaded) {
|
296
|
+
return $.Deferred().resolve();
|
297
|
+
} else {
|
298
|
+
return this.getRemote();
|
299
|
+
}
|
300
|
+
}.bind(this));
|
301
|
+
};
|
302
|
+
|
303
|
+
// Get content objects from server and save to local storage
|
304
|
+
Cedar.Store.prototype.getRemote = function(options) {
|
305
|
+
var defaultOptions = {
|
306
|
+
path: '/queries/contententries/'
|
307
|
+
};
|
308
|
+
options = $.extend({}, defaultOptions, options);
|
309
|
+
var defaultParams = {};
|
310
|
+
var params = $.extend({}, defaultParams, {
|
311
|
+
guidfilter: options.filter
|
312
|
+
});
|
313
|
+
|
314
|
+
return this.lockedRequest({
|
315
|
+
path: options.path,
|
316
|
+
params: params,
|
317
|
+
success: function(response) {
|
318
|
+
$.each(response, function (index, value) {
|
319
|
+
if (value.hasOwnProperty('id') && value.settings.hasOwnProperty('content')) {
|
320
|
+
this.put(value.id, value);
|
321
|
+
Cedar.debug("storing: " + value.id);
|
322
|
+
}
|
323
|
+
}.bind(this));
|
324
|
+
Cedar.debug("local storage was updated");
|
325
|
+
this.loaded = true;
|
326
|
+
Cedar.events.trigger("content:loaded");
|
327
|
+
}.bind(this)
|
328
|
+
});
|
329
|
+
};
|
330
|
+
|
331
|
+
/**
|
332
|
+
* clear the local storage or remove a single locally store item by key
|
333
|
+
*
|
334
|
+
* @param {ID} key
|
335
|
+
*/
|
336
|
+
Cedar.Store.prototype.clear = function(key) {
|
337
|
+
if (key === undefined) {
|
338
|
+
if (window.hasOwnProperty('localStorage')) {
|
339
|
+
this.cache.clear();
|
340
|
+
} else {
|
341
|
+
this.cache = {};
|
342
|
+
}
|
343
|
+
}
|
344
|
+
else {
|
345
|
+
delete this.cache[key];
|
346
|
+
}
|
347
|
+
};
|
348
|
+
|
349
|
+
/**
|
350
|
+
* set the locally stored data version number
|
351
|
+
*
|
352
|
+
* @return <string> data version number
|
353
|
+
*/
|
354
|
+
Cedar.Store.prototype.setVersion = function(id) {
|
355
|
+
Cedar.debug("updating to version #" + id);
|
356
|
+
this.cache["___CEDAR__DATA__FINGERPRINT___"] = id;
|
357
|
+
};
|
358
|
+
|
359
|
+
/**
|
360
|
+
* return the currently stored data version number
|
361
|
+
*
|
362
|
+
* @return <string> data version number
|
363
|
+
*/
|
364
|
+
Cedar.Store.prototype.getVersion = function() {
|
365
|
+
return this.cache["___CEDAR__DATA__FINGERPRINT___"];
|
366
|
+
};
|
367
|
+
|
368
|
+
/**
|
369
|
+
* Query the server for the latest data version number
|
370
|
+
*
|
371
|
+
* @return <Deferred>
|
372
|
+
*/
|
373
|
+
Cedar.Store.prototype.checkVersion = function() {
|
374
|
+
return this.lockedRequest({
|
375
|
+
path: '/queries/status',
|
376
|
+
success: function(response) {
|
377
|
+
Cedar.debug("checking version #" + this.getVersion());
|
378
|
+
|
379
|
+
// response.settings.version is being returned as an array and must be converted
|
380
|
+
if ( this.getVersion() !== response.settings.version.toString() ) {
|
381
|
+
Cedar.debug('setting version: ' + response.settings.version);
|
382
|
+
this.loaded = false;
|
383
|
+
this.setVersion(response.settings.version);
|
384
|
+
} else {
|
385
|
+
Cedar.debug("version is up to date");
|
386
|
+
this.loaded = true;
|
387
|
+
Cedar.events.trigger("content:loaded");
|
388
|
+
}
|
389
|
+
}.bind(this)
|
390
|
+
});
|
391
|
+
};
|
392
|
+
|
393
|
+
// Returns an already resolving getJSON request if it matches
|
394
|
+
Cedar.Store.prototype.lockedRequest = function(options) {
|
395
|
+
options = options || {};
|
396
|
+
this.requestCache || (this.requestCache = {});
|
397
|
+
|
398
|
+
var requestKey = JSON.stringify({path: options.path, params: options.params});
|
399
|
+
var request = $.Deferred().resolve();
|
400
|
+
|
401
|
+
if (Cedar.config.liveMode) {
|
402
|
+
request = $.getJSON(Cedar.config.api + options.path, options.params)
|
403
|
+
.then(function(response) {
|
404
|
+
options.success(response);
|
405
|
+
}.bind(this));
|
406
|
+
}
|
407
|
+
|
408
|
+
return this.requestCache[requestKey] || (this.requestCache[requestKey] = request);
|
409
|
+
};
|
410
|
+
|
411
|
+
|
412
|
+
/*
|
413
|
+
Cedar.ContentObject
|
414
|
+
Parent class for all Cedar content object types
|
415
|
+
*/
|
416
|
+
Cedar.ContentObject = function(options) {
|
417
|
+
var defaults = {
|
418
|
+
el: '<div />'
|
419
|
+
};
|
420
|
+
this.options = $.extend({}, defaults, options);
|
421
|
+
this.$el = $(this.options.el);
|
422
|
+
};
|
423
|
+
|
424
|
+
Cedar.ContentObject.prototype = {
|
425
|
+
render: function() {
|
426
|
+
this.load().then(function() {
|
427
|
+
this.$el.html(this.toString());
|
428
|
+
}.bind(this));
|
429
|
+
},
|
430
|
+
|
431
|
+
load: function() {
|
432
|
+
return Cedar.store.get(this.options.cedarId).then(function(data) {
|
433
|
+
this.setContent(data);
|
434
|
+
return this;
|
435
|
+
}.bind(this));
|
436
|
+
},
|
437
|
+
|
438
|
+
getContent: function() {
|
439
|
+
return this.content || this.options.defaultContent;
|
440
|
+
},
|
441
|
+
|
442
|
+
setContent: function(data) {
|
443
|
+
this.content = data;
|
444
|
+
},
|
445
|
+
|
446
|
+
getContentWithEditTools: function() {
|
447
|
+
return this.getEditOpen() + this.getContent() + this.getEditClose();
|
448
|
+
},
|
449
|
+
|
450
|
+
toString: function() {
|
451
|
+
return Cedar.auth.isEditMode() ? this.getContentWithEditTools() : this.getContent();
|
452
|
+
},
|
453
|
+
|
454
|
+
toJSON: function() {
|
455
|
+
return {
|
456
|
+
content: this.getContent()
|
457
|
+
};
|
458
|
+
},
|
459
|
+
|
460
|
+
getEditOpen: function() {
|
461
|
+
var jsString = "if(event.stopPropagation){event.stopPropagation();}" +
|
462
|
+
"event.cancelBubble=true;" +
|
463
|
+
"window.location.href=this.attributes.href.value + \'&referer=' + encodeURIComponent(window.location.href) + '\';" +
|
464
|
+
"return false;";
|
465
|
+
|
466
|
+
var block = '<span class="cedar-cms-editable clearfix">';
|
467
|
+
block += '<span class="cedar-cms-edit-tools">';
|
468
|
+
block += '<a onclick="' + jsString + '" href="' + Cedar.config.server +
|
469
|
+
'/cmsadmin/EditData?cdr=1&t=ContentEntry&o=' +
|
470
|
+
encodeURIComponent(this.options.cedarId) +
|
471
|
+
'" class="cedar-cms-edit-icon cedar-js-edit" >';
|
472
|
+
block += '<i class="cedar-cms-icon cedar-cms-icon-right cedar-cms-icon-edit"></i></a>';
|
473
|
+
block += '</span>';
|
474
|
+
return block;
|
475
|
+
},
|
476
|
+
|
477
|
+
getEditClose: function() {
|
478
|
+
return '</span>';
|
479
|
+
}
|
480
|
+
};
|
481
|
+
|
482
|
+
Cedar.ContentObject.prototype.constructor = Cedar.ContentObject;
|
483
|
+
|
484
|
+
/*
|
485
|
+
Cedar.ContentEntry
|
486
|
+
basic content block class
|
487
|
+
*/
|
488
|
+
Cedar.ContentEntry = function(options) {
|
489
|
+
Cedar.ContentObject.call(this, options);
|
490
|
+
};
|
491
|
+
Cedar.ContentEntry.prototype = Object.create(Cedar.ContentObject.prototype);
|
492
|
+
Cedar.ContentEntry.prototype.constructor = Cedar.ContentEntry;
|
493
|
+
|
494
|
+
Cedar.ContentEntry.prototype.setContent = function(data) {
|
495
|
+
this.content = (data && (data.settings && data.settings.content)) || '';
|
496
|
+
};
|
497
|
+
|
498
|
+
/*
|
499
|
+
Cedar.Program
|
500
|
+
program object class
|
501
|
+
*/
|
502
|
+
Cedar.Program = function(options) {
|
503
|
+
Cedar.ContentObject.call(this, options);
|
504
|
+
};
|
505
|
+
Cedar.Program.prototype = Object.create(Cedar.ContentObject.prototype);
|
506
|
+
Cedar.Program.prototype.constructor = Cedar.Program;
|
507
|
+
|
508
|
+
Cedar.Program.prototype.setContent = function(data) {
|
509
|
+
this.content = (data && (data.settings && JSON.parse(data.settings.content))) || '';
|
510
|
+
};
|
511
|
+
|
512
|
+
Cedar.Program.prototype.toJSON = function(data) {
|
513
|
+
return this.content;
|
514
|
+
};
|
data/lib/cedar.rb
CHANGED
@@ -1,6 +1,19 @@
|
|
1
1
|
require "cedar/version"
|
2
|
+
require "cedar/config"
|
2
3
|
|
3
4
|
module Cedar
|
5
|
+
class << self
|
6
|
+
attr_writer :config
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.config
|
10
|
+
@config ||= Config.new
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.configure
|
14
|
+
yield(config)
|
15
|
+
end
|
16
|
+
|
4
17
|
module Rails
|
5
18
|
class Engine < ::Rails::Engine
|
6
19
|
end
|
data/lib/cedar/config.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
# Usage example: initializers/cedar.rb
|
2
|
+
# Cedar.configure do |config|
|
3
|
+
# config.live_mode = (Rails.env.production? || Rails.env.staging? || Rails.env.development?)
|
4
|
+
# end
|
5
|
+
|
6
|
+
module Cedar
|
7
|
+
class Config
|
8
|
+
attr_accessor :live_mode
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
@live_mode = true
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
data/lib/cedar/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: cedar
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.2.
|
4
|
+
version: 0.2.73.pre
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jed Murdock
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-06-
|
11
|
+
date: 2015-06-22 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -48,11 +48,14 @@ files:
|
|
48
48
|
- LICENSE.txt
|
49
49
|
- README.md
|
50
50
|
- lib/assets/images/cedar-display-tools-sprite.png
|
51
|
-
- lib/assets/javascripts/cedar.handlebars.js
|
52
51
|
- lib/assets/javascripts/cedar.js
|
52
|
+
- lib/assets/javascripts/cedar_config.js
|
53
|
+
- lib/assets/javascripts/cedar_handlebars.js
|
54
|
+
- lib/assets/javascripts/cedar_source.js
|
53
55
|
- lib/assets/stylesheets/cedar.scss
|
54
56
|
- lib/assets/stylesheets/cedar_source.scss
|
55
57
|
- lib/cedar.rb
|
58
|
+
- lib/cedar/config.rb
|
56
59
|
- lib/cedar/version.rb
|
57
60
|
homepage: http://plyinteractive.com
|
58
61
|
licenses:
|