/* Story Streams - Vue.js app Summary: Uses monthly json files to populate story stream output Also filters item type by button Dependencies: vue.min.js 2.1.9 (standalone version) master.js (for "send_ga_event" function) Browser Requirements: IE9+ Table of Contents: UTILITY FUNCTIONS TITLE WRITE META WRITE SOCIAL STORY STREAM FILTERS CONSTANTS VUE COMPONENTS */ /////////////////////// // UTILITY FUNCTIONS // /////////////////////// function getAjax(url, finished) { var xhr; if(window.XMLHttpRequest) { xhr = new XMLHttpRequest(); } else { xhr = new ActiveXObject("Microsoft.XMLHTTP"); } xhr.open('GET', url); xhr.onreadystatechange = function() { if(xhr.readyState>= 4) { if(xhr.status==200) { finished(xhr.responseText); } else { finished(false); } } }; xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); xhr.send(); return xhr; } function num_with_ordinal_suffix(i) { var j = i % 10, k = i % 100; if (j == 1 && k != 11) { return i + "st"; } if (j == 2 && k != 12) { return i + "nd"; } if (j == 3 && k != 13) { return i + "rd"; } return i + "th"; } function pad_digits(num, size) { var s = "000000000000" + num; return s.substr(s.length-size); } String.prototype.stripSlashes = function(){ return this.replace(/\\(.)/mg, "$1"); } function isVisible(el) { return !!( el.offsetWidth || el.offsetHeight || el.getClientRects().length ); } function hasClass(el, className) { return el.classList ? el.classList.contains(className) : new RegExp('\\b'+ className+'\\b').test(el.className); } function addClass(el, className) { if (el.classList) el.classList.add(className); else if (!hasClass(el, className)) el.className += ' ' + className; } function removeClass(el, className) { if (el.classList) el.classList.remove(className); else if (hasClass(el, className)) el.className = el.className.replace(new RegExp('\\b'+ className+'\\b', 'g'), ''); } function addEvent(el, type, handler) { if (el.attachEvent) el.attachEvent('on'+type, handler); else el.addEventListener(type, handler); } function removeEvent(el, type, handler) { if (el.detachEvent) el.detachEvent('on'+type, handler); else el.removeEventListener(type, handler); } function removeClassAll(element_list, class_name) { for(var i = 0; i < element_list.length; i++) { removeClass(element_list[i], class_name); } } /////////////// // CONSTANTS // /////////////// // Grab the arguments from the URL var url = decodeURIComponent(window.location.href).toLowerCase(); var topic_match = url.match(/topic=([\w+.\-]*)/); // console.log(topic_match); var this_topic = topic_match.length>1 ? topic_match[1] : "election_2016"; var filter_match = url.match(/filter=([a-z._\|]+)/) || []; //var $FILTER = filter_match.length>1 ? filter_match[1] : 'everything'; var this_letter = this_topic.charAt(0); var JSON_BASE_URL = '/topic/'+this_letter+'/'+this_topic+'/'; var THIS_DATE = new Date(); var ITEMS_PER_PAGE = 25; var EARLIEST_YEAR = 2009; var EARLIEST_MONTH = 1; var MONTH_NAMES = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" ]; var MAX_LOOKBACK = 10; // Pages to look back when filter does not have enough content // console.log( Utils.get_query_param('topic') ); ///////////////// // TITLE WRITE // ///////////////// var stream_title = document.querySelector('.stream-header .title h1'); var current_filter = ''; //Process the topic to build an H1 title var topic_words = this_topic.split("_"); for(var i=0;i'; social_html += ''; social_html += ''; document.querySelector('.stream-details .title-wrapper').innerHTML += social_html; ////////////////////////// // STORY STREAM FILTERS // ////////////////////////// function stream_filter_button(e) { removeClassAll(filter_buttons, 'active'); addClass(this, 'active'); filter_story_stream(this.getAttribute('data-filter')); } function stream_filter_option(e) { removeClassAll(filter_buttons, 'active'); var filter_type = this.value; addClass(document.querySelectorAll('.stream-type a.filter[data-filter="'+filter_type+'"]'), 'active'); filter_story_stream(filter_type); } function create_filter_style(filter_type) { // Get the head element var head = document.head || document.getElementsByTagName('head')[0]; // Create a new style node var style = document.createElement('style'); // Set type attribute for the style node style.type = 'text/css'; style.id = 'stream_filter_style'; // Set the css rules var css = '#stream_content_app .result { display: none; } '; if(filter_type.indexOf('|') > -1) { filter_types = filter_type.split('|'); for(var i = 0; i < filter_types.length; i++) { css += filter_css(filter_types[i]); } } else { css += filter_css(filter_type); } // Append the css rules to the style node style.appendChild(document.createTextNode(css)); // Append the style node to the head of the page head.appendChild(style); } function filter_css(filter_type) { return '#stream_content_app .result.'+filter_type+' { display: block; }'; } function remove_filter_style() { var el = document.querySelector('#stream_filter_style'); if(el) { el.parentNode.removeChild(el); } } // Buttons var filter_buttons = document.querySelectorAll('.stream-type a.filter'); for(var i = 0; i < filter_buttons.length; i++) { addEvent(filter_buttons[i], 'click', stream_filter_button); } // Dropdown var filter_dropdown = document.querySelector('div.dropdown-container form select'); addEvent(filter_dropdown, 'change', stream_filter_option); // Filter function filter_story_stream(filter_type) { remove_filter_style(); create_filter_style(filter_type); current_filter = filter_type; var lm = document.querySelector('#stream_content_app .story_stream_load_more'); if(get_visible_results() < ITEMS_PER_PAGE && lm) { lm.click(); } } function get_visible_results() { var loaded_results = document.querySelectorAll('#stream_content_app .result'); var total_visible = 0; for(var i = 0; i < loaded_results.length; i++) { if(isVisible(loaded_results[i])) { total_visible++; } } return total_visible; } // Initial filter trigger filter_buttons[0].click(); //////////////////// // VUE COMPONENTS // //////////////////// /* Top-level app container Holds the main stream area and the load-more button */ Vue.component('rc-container', { template: '\
\
\
\
\

\ \ {{decodeHtml(item.heading1)}}\ \ {{ item.month }} {{ item.day }}, {{ item.year }}\

{{ (item.body.length>0?(decodeHtml(item.body).substring(0, 250)+"..."):"") }}

\

\

\

\

\
\
\
\ \
\
\
\ ', data : function() { return { end_reached : false, limit_reached : true, pages_loaded : 0, concurrent_files : 5, files_loaded : 0, pages_lookback : 0, items : [], buffer : [], concurrent_buffer : [], current_year : THIS_DATE.getFullYear(), current_month : THIS_DATE.getMonth()+1 } }, created : function() { this.load_more(); }, watch : { items : function() { this.$nextTick(function() { if(this.items.length >= 2 && $('.alpha_related_tags').length == 0 && typeof story_stream_related_tags_html !== 'undefined') { //console.log($('.story_stream_items .result:nth-child(0)').html()); $('.story_stream_items .result:visible').eq(2).after(''); } }); } }, methods : { load_more : function() { if(this.pages_loaded > 0) { var event_data = { 'ge_action' : 'click', 'ge_category' : 'Stream Pages', 'ge_label' : 'toggle / '+this_topic+' / '+current_filter }; send_ga_event(event_data); } this.pages_loaded++; this.load_content(); }, // load_content : function() { this.end_reached = false; if( this.items.length < this.pages_loaded * ITEMS_PER_PAGE && ( this.current_year > EARLIEST_YEAR || ( this.current_year == EARLIEST_YEAR && this.current_month >= EARLIEST_MONTH ) ) ) { // First try loading from buffer. If not enough, do ajax. this.limit_reached = false; var loaded_count = 0; for(var i = 0; i < this.buffer.length; i++) { this.items.push(this.buffer[i]); loaded_count++; if( this.items.length >= this.pages_loaded * ITEMS_PER_PAGE ) { this.limit_reached = true; break; } } // remove loaded items from buffer for(var i = 0; i < loaded_count; i++) { this.buffer.shift(); } if(!this.limit_reached) { for(var i = 0; i < this.concurrent_files; i++) { getAjax( JSON_BASE_URL+this.current_year+'/'+pad_digits(this.current_month,2)+'.json', this.content_loaded ); // Back one month, whether successful ajax or not this.current_month--; if(this.current_month <= 0) { this.current_month = 12; this.current_year--; } } } } else { this.end_reached = true; // Load any remaining buffer items for(var i = 0; i < this.buffer.length; i++) { //this.items.push(this.buffer[i]); this.items.push(this.buffer.shift()); } } if(this.limit_reached) { // Using callback for nextTick, so it checks DOM visibility AFTER view renders content // Loading page at a time too slow... actually loading to DOM everything... // Maybe instead of max months, max pages to look back this.$nextTick(function() { // Make sure visible elements add up to ITEMS_PER_PAGE (because of filters) if(get_visible_results() < ITEMS_PER_PAGE && this.pages_lookback < MAX_LOOKBACK) { this.limit_reached = false; this.pages_loaded++; this.pages_lookback++; // Must use setTimeout to avoid going over stack limit for recursion in IE //this.load_content(); setTimeout(this.load_content, 0); } else { this.pages_lookback = 0; } }); } }, // Callback function for getting json of list items content_loaded : function(data) { this.files_loaded++; if(data !== false) { var json = JSON.parse(data); var json_unique = this.unique_items(json); for(var i = 0; i < json_unique.length; i++) { // Check if article (not entry, video, etc) /*if(json[i]['type'] == 'article') { this.concurrent_buffer.push(json[i]); }*/ this.concurrent_buffer.push(json_unique[i]); } } if(this.files_loaded >= this.concurrent_files) { this.files_loaded = 0; var items = this.concurrent_buffer; items.sort(function(a,b) { if (a.date > b.date) return -1; if (a.date < b.date) return 1; return 0; }); this.concurrent_buffer = []; for(var i = 0; i < items.length; i++) { // create stream_url for each item if(typeof(items[i]['tags']) !== 'undefined' && items[i]['tags'].length > 0) { var temp_tags = []; for(var j = 0; j < items[i]['tags'].length; j++) { var tag_name = items[i]['tags'][j]; temp_tags.push({ 'name' : tag_name, 'stream_url' : '/stream/?topic='+ tag_name.replace(" ", "_") }); } items[i]['tags'] = temp_tags.slice(0); // .slice to make copy, not reference } // Create date var dateString = items[i]['date'].split("T"); var dateStr = dateString[0].split("-"); var year = dateStr[0]; var month = dateStr[1]; var day = dateStr[2]; items[i]['day'] = day; items[i]['month'] = MONTH_NAMES[parseInt(month)-1]; items[i]['year'] = year; //get post type and set icon accordingly var type = items[i]['type']; items[i]['type_icon'] = ''; if( type == "video" ) { items[i]['type_icon'] = '/asset/img/camera-red.png'; }else if( type == "entry" || type == "article" || type == "poll" || type == "cartoon" || type == "slide_show" ) { items[i]['type_icon'] = '/asset/img/red-stream-text-icon.png'; }else if( type == "tweet" ) { items[i]['type_icon'] = '/asset/img/red-stream-tweet-icon.png'; }else if( type == "undefined" || type == undefined || type == "" ) { items[i]['type_icon'] = ''; } this.buffer.push(items[i]); } } else { return; } // Call this again to make sure enough items loaded // Must use setTimeout to avoid going over stack limit for recursion in IE //this.load_content(); setTimeout(this.load_content, 0); }, decodeHtml : function(html) { var txt = document.createElement("textarea"); txt.innerHTML = html; return txt.value.stripSlashes(); }, image_exists(item) { return typeof item.img_url !== 'undefined' && item.img_url.trim().length > 0; }, unique_items : function(items) { // Give it an array of items, returns an array of unique items in that array0 var len = items.length; var seen = items.slice(0); // .slice to make copy, not reference var out = []; for(var i = 0; i < len; i++) { var use_seen = true; for(var j = 0; j < len; j++) { if(seen[i].content_url == items[j].content_url || seen[i].outbound_url == items[j].content_url) { var st = seen[i]['type']; var it = items[j]['type']; if( ( (st == 'entry' || st == 'photo') && it == 'article') || ( st == 'photo' && it == 'entry') ) { //seen[i] = JSON.parse(JSON.stringify(items[j])); // JSON stuff to do deep copy, not reference seen[i]['drop'] = true; } } } if(typeof seen[i]['drop'] === 'undefined' || !seen[i]['drop']) { out.push(JSON.parse(JSON.stringify(seen[i]))); // JSON stuff to do deep copy, not reference } } return out; } } }); // Start App var stream_content_app = new Vue({ el: '#stream_content_app' }); //sets the story stream widht on the fly by calculateing the amount of items inside of parent $(document).ready(function(){ var size = $('.stream-type-wrapper > div ').length; console.log(size); var item_width = 99.5/size; console.log(item_width); $('.stream-type').css({ width: item_width + '%', }); });