API Docs for: 0.8.0
Show:

File: include/service/entities/content/content_view_loader.js

  1. /*
  2. Copyright (C) 2016 PencilBlue, LLC
  3.  
  4. This program is free software: you can redistribute it and/or modify
  5. it under the terms of the GNU General Public License as published by
  6. the Free Software Foundation, either version 3 of the License, or
  7. (at your option) any later version.
  8.  
  9. This program is distributed in the hope that it will be useful,
  10. but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. GNU General Public License for more details.
  13.  
  14. You should have received a copy of the GNU General Public License
  15. along with this program. If not, see <http://www.gnu.org/licenses/>.
  16. */
  17.  
  18. //dependencies
  19. var util = require('../../../util.js');
  20. var async = require('async');
  21. var HtmlEncoder = require('htmlencode');
  22.  
  23. module.exports = function(pb) {
  24.  
  25. //pb dependencies
  26. var DAO = pb.DAO;
  27. var Localization = pb.Localization;
  28. var ClientJs = pb.ClientJs;
  29.  
  30. /**
  31. * Renders a 1 or more pieces of content such as articles or pages
  32. * @class ContentViewLoader
  33. * @constructor
  34. * @param {Object} context
  35. * @param {TemplateService} context.ts
  36. * @param {Localization} context.ls
  37. * @param {Object} [context.contentSettings]
  38. * @param {Object} context.session
  39. * @param {ContentObjectService} context.service
  40. * @param {String} context.activeTheme
  41. * @param {CommentService} [context.commentService]
  42. * @param {object} context.siteObj
  43. */
  44. function ContentViewLoader(context) {
  45. this.ts = context.ts;
  46. this.ls = context.ls;
  47. this.req = context.req;
  48. this.contentSettings = context.contentSettings;
  49. this.session = context.session;
  50. this.service = context.service;
  51. this.site = context.site;
  52. this.siteObj = context.siteObj;
  53. this.hostname = context.hostname;
  54. this.onlyThisSite = context.onlyThisSite;
  55. this.activeTheme = context.activeTheme;
  56.  
  57. /**
  58. * @property commentService
  59. * @type {CommentService}
  60. */
  61. this.commentService = context.commentService || new pb.CommentService(context);
  62. }
  63.  
  64. /**
  65. *
  66. * @private
  67. * @static
  68. * @property DISPLAY_NONE_STYLE_ATTR
  69. * @type {String}
  70. */
  71. var DISPLAY_NONE_STYLE_ATTR = 'display:none;';
  72.  
  73. /**
  74. *
  75. * @method renderSingle
  76. * @param {Object} content
  77. * @param {Object} options
  78. * @param {Function} cb
  79. */
  80. ContentViewLoader.prototype.renderSingle = function(content, options, cb) {
  81. this.render([content], options, cb);
  82. };
  83.  
  84. /**
  85. *
  86. * @method render
  87. * @param {Array} contentArray
  88. * @param {Object} options
  89. * @param {Boolean} [options.useDefaultTemplate] Forces the default theme template to be selected
  90. * @param {Object} [options.topic] The topic represented by the collection of content to be rendered
  91. * @param {Object} [options.section] The section represented by the collection of content to be rendered
  92. * @param {Function} cb
  93. */
  94. ContentViewLoader.prototype.render = function(contentArray, options, cb) {
  95. var self = this;
  96.  
  97. this.gatherData(contentArray, options, function(err, data) {
  98. if (util.isError(err)) {
  99. return cb(err);
  100. }
  101.  
  102. self.setMetaInfo(data.meta, options);
  103. self.ts.registerLocal('current_url', self.req.url);
  104. self.ts.registerLocal('navigation', new pb.TemplateValue(data.nav.navigation, false));
  105. self.ts.registerLocal('account_buttons', new pb.TemplateValue(data.nav.accountButtons, false));
  106. self.ts.registerLocal('infinite_scroll', function(flag, cb) {
  107. self.onInfiniteScroll(contentArray, options, cb);
  108. });
  109. self.ts.registerLocal('page_name', function(flag, cb) {
  110. self.onPageName(contentArray, options, cb);
  111. });
  112. self.ts.registerLocal('angular', function(flag, cb) {
  113. self.onAngular(contentArray, options, cb);
  114. });
  115. self.ts.registerLocal('articles', function(flag, cb) {
  116. self.onContent(contentArray, options, cb);
  117. });
  118.  
  119. self.getTemplate(contentArray, options, function(err, template) {
  120. if (util.isError(err)) {
  121. return cb(err);
  122. }
  123.  
  124. self.ts.load(template, cb);
  125. });
  126. });
  127. };
  128.  
  129. /**
  130. *
  131. * @method getTemplate
  132. * @param {Array|Object} content
  133. * @param {Object} options
  134. * @param {Boolean} [options.useDefaultTemplate] Forces the default theme template to be selected
  135. * @param {Object} [options.topic] The topic represented by the collection of content to be rendered
  136. * @param {Object} [options.section] The section represented by the collection of content to be rendered
  137. * @param {Function} cb
  138. */
  139. ContentViewLoader.prototype.getTemplate = function(content, options, cb) {
  140.  
  141. //check if we should just use whatever default there is.
  142. //this could fall back to an active theme or the default pencilblue theme.
  143. if (options.useDefaultTemplate || util.isObject(options.topic) || util.isObject(options.section)) {
  144. return cb(null, this.getDefaultTemplatePath());
  145. }
  146.  
  147. //now we are dealing with a single page or article. the template will be
  148. //judged based off the article's preference.
  149. if (util.isArray(content) && content.length > 0) {
  150. content = content[0];
  151. }
  152. var uidAndTemplate = content.template;
  153.  
  154. //when no template is specified or is empty we no that the article has no
  155. //preference and we can fall back on the default (index). We depend on the
  156. //template service to determine who has priority based on the active theme
  157. //then defaulting back to pencilblue.
  158. if (!pb.validation.isNonEmptyStr(uidAndTemplate, true)) {
  159. var defautTemplatePath = this.getDefaultTemplatePath();
  160. pb.log.silly("ContentController: No template specified, defaulting to %s.", defautTemplatePath);
  161. return cb(null, defautTemplatePath);
  162. }
  163.  
  164. //we now know that the template was specified. We have to split the value
  165. //to extract the intended theme and the template path
  166. var pieces = uidAndTemplate.split('|');
  167.  
  168. //for backward compatibility we let the template service determine where to
  169. //find the template when no template is specified. This mostly catches the
  170. //default case of "index"
  171. if (pieces.length === 1) {
  172.  
  173. pb.log.silly("ContentController: No theme specified, Template Service will delegate [%s]", pieces[0]);
  174. return cb(null, pieces[0]);
  175. }
  176. else if (pieces.length <= 0) {
  177.  
  178. //shit's broke. This should never be the case but better safe than sorry
  179. return cb(new Error("The content's template property provided an invalid value of ["+content.template+']'), null);
  180. }
  181.  
  182. //the theme is specified, we ensure that the theme is installed and
  183. //initialized otherwise we let the template service figure out how to
  184. //delegate.
  185. if (!pb.PluginService.isActivePlugin(pieces[0])) {
  186. pb.log.silly("ContentController: Theme [%s] is not active, Template Service will delegate [%s]", pieces[0], pieces[1]);
  187. return cb(null, pieces[1]);
  188. }
  189.  
  190. //the theme is OK. We don't gaurantee that the template is on the disk but we can testify that it SHOULD. We set the
  191. //prioritized theme for the template service.
  192. pb.log.silly("ContentController: Prioritizing Theme [%s] for template [%s]", pieces[0], pieces[1]);
  193. this.ts.setTheme(pieces[0]);
  194. cb(null, pieces[1]);
  195. };
  196.  
  197. /**
  198. *
  199. * @method getDefaultTemplatePath
  200. * @return {String}
  201. */
  202. ContentViewLoader.prototype.getDefaultTemplatePath = function() {
  203. return 'index';
  204. };
  205.  
  206. /**
  207. *
  208. * @method onContent
  209. * @param {Array} contentArray
  210. * @param {Object} options
  211. * @param {Function} cb
  212. */
  213. ContentViewLoader.prototype.onContent = function(contentArray, options, cb) {
  214. var self = this;
  215. var limit = Math.min(this.contentSettings.articles_per_page, contentArray.length);
  216.  
  217. var tasks = util.getTasks(contentArray, function(contentArray, i) {
  218. return function(callback) {
  219. if (i >= limit) {
  220. return callback(null, '');
  221. }
  222. self.renderContent(contentArray[i], options, callback);
  223. };
  224. });
  225. async.series(tasks, function(err, content) {
  226. cb(err, new pb.TemplateValue(content.join(''), false));
  227. });
  228. };
  229.  
  230. /**
  231. *
  232. * @method gatherData
  233. * @param {Array} contentArray
  234. * @param {Object} options
  235. * @param {Function} cb
  236. */
  237. ContentViewLoader.prototype.gatherData = function(contentArray, options, cb) {
  238. var self = this;
  239. var tasks = {
  240.  
  241. //navigation
  242. nav: function(callback) {
  243.  
  244. var opts = {
  245. currUrl: self.req.url,
  246. session: self.session,
  247. ls: self.ls,
  248. site: self.site,
  249. activeTheme: self.activeTheme
  250. };
  251. var topMenuService = new pb.TopMenuService();
  252. topMenuService.getNavItems(opts, callback);
  253. },
  254.  
  255. meta: function(callback) {
  256. self.getMetaInfo(contentArray, options, callback);
  257. },
  258.  
  259. contentSettings: function(callback) {
  260. if (util.isObject(self.contentSettings)) {
  261. return callback(null, self.contentSettings);
  262. }
  263.  
  264. var contentService = new pb.ContentService({site: self.site, onlyThisSite: self.onlyThisSite});
  265. contentService.getSettings(function(err, contentSettings) {
  266. self.contentSettings = contentSettings;
  267. callback(err, contentSettings);
  268. });
  269. }
  270. };
  271. async.parallel(tasks, cb);
  272. };
  273.  
  274. /**
  275. *
  276. * @method onAngular
  277. * @param {Array} contentArray
  278. * @param {Object} options
  279. * @param {Function} cb
  280. */
  281. ContentViewLoader.prototype.onAngular = function(contentArray, options, cb) {
  282. var objects = {
  283. trustHTML: 'function(string){return $sce.trustAsHtml(string);}'
  284. };
  285. var angularData = pb.ClientJs.getAngularController(objects, ['ngSanitize']);
  286. cb(null, angularData);
  287. };
  288.  
  289. /**
  290. *
  291. * @method onPageName
  292. * @param {Array} contentArray
  293. * @param {Object} options
  294. * @param {Function} cb
  295. */
  296. ContentViewLoader.prototype.onPageName = function(contentArray, options, cb) {
  297. var content = contentArray[0];
  298. if (!util.isObject(content)) {
  299. return cb(null, options.metaTitle || this.siteObj.displayName);
  300. }
  301.  
  302. var name = '';
  303. if(util.isObject(options.section)) {
  304. name = options.section.name;
  305. }
  306. else if (util.isObject(options.topic)) {
  307. name = options.topic.name;
  308. }
  309. else if (contentArray.length === 1) {
  310. name = content.headline;
  311. }
  312. else {
  313. name = options.metaTitle || '';
  314. }
  315.  
  316. cb(null, name ? name + ' | ' + this.siteObj.displayName : this.siteObj.displayName);
  317. };
  318.  
  319. /**
  320. *
  321. * @method onInfiniteScroll
  322. * @param {Array} contentArray
  323. * @param {Object} options
  324. * @param {Function} cb
  325. */
  326. ContentViewLoader.prototype.onInfiniteScroll = function(contentArray, options, cb) {
  327. if(contentArray.length <= 1) {
  328. return cb(null, '');
  329. }
  330.  
  331. var infiniteScrollScript = pb.ClientJs.includeJS('/js/infinite_article_scroll.js');
  332. if(util.isObject(options.section)) {
  333. infiniteScrollScript += pb.ClientJs.getJSTag('var infiniteScrollSection = "' + options.section[pb.DAO.getIdField()] + '";');
  334. }
  335. else if(util.isObject(options.topic)) {
  336. infiniteScrollScript += pb.ClientJs.getJSTag('var infiniteScrollTopic = "' + options.topic.topic[pb.DAO.getIdField()] + '";');
  337. }
  338.  
  339. var val = new pb.TemplateValue(infiniteScrollScript, false);
  340. cb(null, val);
  341. };
  342.  
  343. /**
  344. *
  345. * @method setMetaInfo
  346. * @param {Object} options
  347. */
  348. ContentViewLoader.prototype.setMetaInfo = function(meta, options) {
  349. this.ts.registerLocal('meta_keywords', meta.keywords);
  350. this.ts.registerLocal('meta_desc', options.metaDescription || meta.description);
  351. this.ts.registerLocal('meta_title', options.metaTitle || meta.title);
  352. this.ts.registerLocal('meta_thumbnail', meta.thumbnail || '');
  353. this.ts.registerLocal('meta_lang', options.metaLang || this.ls.language);
  354. };
  355.  
  356. /**
  357. *
  358. * @method getMetaInfo
  359. * @param {Array} contentArray
  360. * @param {Object} options
  361. * @param {Function} cb
  362. */
  363. ContentViewLoader.prototype.getMetaInfo = function(contentArray, options, cb) {
  364. if (contentArray.length === 0) {
  365. return cb(null, {});
  366. }
  367. this.service.getMetaInfo(contentArray[0], cb);
  368. };
  369.  
  370. /**
  371. *
  372. * @method renderContent
  373. * @param {Object} content
  374. * @param {Object} options
  375. * @param {Function} cb
  376. */
  377. ContentViewLoader.prototype.renderContent = function(content, options, cb) {
  378. var self = this;
  379.  
  380. //set recurring params
  381. if (util.isNullOrUndefined(options.contentIndex)) {
  382. options.contentIndex = 0;
  383. }
  384.  
  385. var isPage = this.service.getType() === 'page';
  386. var showByLine = this.contentSettings.display_bylines && !isPage;
  387. var showTimestamp = this.contentSettings.display_timestamp && !isPage;
  388. var ats = self.ts.getChildInstance();
  389. var contentUrlPrefix = '/' + this.service.getType() + '/';
  390. self.ts.reprocess = false;
  391. ats.registerLocal('article_permalink', function(flag, cb) {
  392. self.onContentPermalink(content, options, cb);
  393. });
  394. ats.registerLocal('article_headline', function(flag, cb) {
  395. self.onContentHeadline(content, options, cb);
  396. });
  397. ats.registerLocal('article_headline_nolink', content.headline);
  398. ats.registerLocal('article_subheading', ContentViewLoader.valOrEmpty(content.subheading));
  399. ats.registerLocal('article_subheading_display', ContentViewLoader.getDisplayAttr(content.subheading));
  400. ats.registerLocal('article_id', content[pb.DAO.getIdField()] + '');
  401. ats.registerLocal('article_index', options.contentIndex);
  402. ats.registerLocal('article_timestamp', showTimestamp && content.timestamp ? content.timestamp : '');
  403. ats.registerLocal('article_timestamp_display', ContentViewLoader.getDisplayAttr(showTimestamp));
  404. ats.registerLocal('article_layout', new pb.TemplateValue(content.layout, false));
  405. ats.registerLocal('article_url', content.url);
  406. ats.registerLocal('display_byline', ContentViewLoader.getDisplayAttr(showByLine));
  407. ats.registerLocal('author_photo', ContentViewLoader.valOrEmpty(content.author_photo));
  408. ats.registerLocal('author_photo_display', ContentViewLoader.getDisplayAttr(content.author_photo));
  409. ats.registerLocal('author_name', ContentViewLoader.valOrEmpty(content.author_name));
  410. ats.registerLocal('author_position', ContentViewLoader.valOrEmpty(content.author_position));
  411. ats.registerLocal('media_body_style', ContentViewLoader.valOrEmpty(content.media_body_style));
  412. ats.registerLocal('comments', function(flag, cb) {
  413. if (isPage || !pb.ArticleService.allowComments(self.contentSettings, content)) {
  414. return cb(null, '');
  415. }
  416.  
  417. var ts = ats.getChildInstance();
  418. self.renderComments(content, ts, function(err, comments) {
  419. cb(err, new pb.TemplateValue(comments, false));
  420. });
  421. });
  422. ats.load(self.getDefaultContentTemplatePath(), cb);
  423.  
  424. options.contentIndex++;
  425. };
  426.  
  427. /**
  428. *
  429. * @method getDefaultContentTemplatePath
  430. * @return {String}
  431. */
  432. ContentViewLoader.prototype.getDefaultContentTemplatePath = function() {
  433. return 'elements/article';
  434. };
  435.  
  436. /**
  437. *
  438. * @method renderComments
  439. * @param {Object} content
  440. * @param {TemplateService} ts
  441. * @param {Function} cb
  442. */
  443. ContentViewLoader.prototype.renderComments = function(content, ts, cb) {
  444. var self = this;
  445. var commentingUser = null;
  446. if(pb.security.isAuthenticated(this.session)) {
  447. commentingUser = this.commentService.getCommentingUser(this.session.authentication.user);
  448. }
  449.  
  450. ts.registerLocal('user_photo', function(flag, cb) {
  451. self.onCommentingUserPhoto(content, commentingUser, cb);
  452. });
  453. ts.registerLocal('user_position', function(flag, cb) {
  454. self.onCommentingUserPosition(content, commentingUser, cb);
  455. });
  456. ts.registerLocal('user_name', commentingUser ? commentingUser.name : '');
  457. ts.registerLocal('display_submit', commentingUser ? 'block' : 'none');
  458. ts.registerLocal('display_login', commentingUser ? 'none' : 'block');
  459. ts.registerLocal('comments_length', util.isArray(content.comments) ? content.comments.length : 0);
  460. ts.registerLocal('individual_comments', function(flag, cb) {
  461. if (!util.isArray(content.comments) || content.comments.length === 0) {
  462. return cb(null, '');
  463. }
  464.  
  465. var tasks = util.getTasks(content.comments, function(comments, i) {
  466. return function(callback) {
  467.  
  468. var cts = ts.getChildInstance();
  469. self.renderComment(comments[i], cts, callback);
  470. };
  471. });
  472. async.parallel(tasks, function(err, results) {
  473. cb(err, new pb.TemplateValue(results.join(''), false));
  474. });
  475. });
  476. ts.load(self.getDefaultCommentsTemplatePath(), cb);
  477. };
  478.  
  479. /**
  480. *
  481. * @method getDefaultCommentsTemplatePath
  482. * @return {String}
  483. */
  484. ContentViewLoader.prototype.getDefaultCommentsTemplatePath = function() {
  485. return 'elements/comments';
  486. };
  487.  
  488. /**
  489. *
  490. * @method renderComment
  491. * @param {Object} comment
  492. * @param {TemplateService} cts
  493. * @param {Function} cb
  494. */
  495. ContentViewLoader.prototype.renderComment = function(comment, cts, cb) {
  496.  
  497. cts.reprocess = false;
  498. cts.registerLocal('commenter_photo', comment.commenter_photo ? comment.commenter_photo : '');
  499. cts.registerLocal('display_photo', comment.commenter_photo ? 'block' : 'none');
  500. cts.registerLocal('commenter_name', comment.commenter_name);
  501. cts.registerLocal('commenter_position', comment.commenter_position ? ', ' + comment.commenter_position : '');
  502. cts.registerLocal('content', comment.content);
  503. cts.registerLocal('timestamp', comment.timestamp);
  504. cts.load(this.getDefaultCommentTemplatePath(), cb);
  505. };
  506.  
  507. /**
  508. *
  509. * @method getDefaultCommentTemplatePath
  510. * @return {String}
  511. */
  512. ContentViewLoader.prototype.getDefaultCommentTemplatePath = function() {
  513. return 'elements/comments/comment';
  514. };
  515.  
  516. /**
  517. *
  518. * @method onCommentingUserPhoto
  519. * @param {Object} content
  520. * @param {Object} commentingUser
  521. * @param {Function} cb
  522. */
  523. ContentViewLoader.prototype.onCommentingUserPhoto = function(content, commentingUser, cb) {
  524. var val = '';
  525. if (commentingUser) {
  526. val = commentingUser.photo || '';
  527. }
  528. cb(null, val);
  529. };
  530.  
  531. /**
  532. *
  533. * @method onCommentingUserPosition
  534. * @param {Object} content
  535. * @param {Object} options
  536. * @param {Function} cb
  537. */
  538. ContentViewLoader.prototype.onCommentingUserPosition = function(content, commentingUser, cb) {
  539. var val = '';
  540. if (commentingUser && util.isArray(commentingUser.position) && commentingUser.position.length > 0) {
  541. val = ', ' + commentingUser.position;
  542. }
  543. cb(null, val);
  544. };
  545.  
  546. /**
  547. *
  548. * @method onContentPermalink
  549. * @param {Object} content
  550. * @param {Object} options
  551. * @param {Function} cb
  552. */
  553. ContentViewLoader.prototype.onContentPermalink = function(content, options, cb) {
  554. cb(null, this.createContentPermalink(content));
  555. };
  556.  
  557. /**
  558. *
  559. * @method onContentHeadline
  560. * @param {Object} content
  561. * @param {Object} options
  562. * @param {Function} cb
  563. */
  564. ContentViewLoader.prototype.onContentHeadline = function(content, options, cb) {
  565. var url = this.createContentPermalink(content);
  566. var val = new pb.TemplateValue('<a href="' + url + '">' + HtmlEncoder.htmlEncode(content.headline) + '</a>', false);
  567. cb(null, val);
  568. };
  569.  
  570. /**
  571. *
  572. * @method createContentPermalink
  573. * @param {Object} content
  574. * @return {String}
  575. */
  576. ContentViewLoader.prototype.createContentPermalink = function(content) {
  577. var prefix = '/' + this.service.getType();
  578. return pb.UrlService.createSystemUrl(pb.UrlService.urlJoin(prefix, content.url), { hostname: this.hostname });
  579. };
  580.  
  581. /**
  582. *
  583. * @static
  584. * @method getDisplayAttr
  585. * @param {*} val
  586. * @return {String}
  587. */
  588. ContentViewLoader.getDisplayAttr = function(val) {
  589. return val ? '' : DISPLAY_NONE_STYLE_ATTR;
  590. };
  591.  
  592. /**
  593. * When passed a value it is evaluated as a boolean. If evaluated to TRUE
  594. * the value is returned, if FALSE empty string is returned
  595. * @static
  596. * @method valOrEmpty
  597. * @param {*} val
  598. * @return {*}
  599. */
  600. ContentViewLoader.valOrEmpty = function(val) {
  601. return val ? val : '';
  602. };
  603.  
  604. return ContentViewLoader;
  605. };
  606.