/** @jsx React.DOM */ var tablr = $('[data-json]').tablr(); var map, mapContent, primaryGeom, tabEls, type; //util fns var Utils = { getTabDisplayList: function(tables) { return _.map(tables, function(table) {return table.displayTableName}); }, getTabList: function(tables) { return _.map(tables, function(table) {return table.tableName}); } }; var Maplist = React.createClass({displayName: 'Maplist', getInitialState: function() { return { tables: tablr, selIdx: this.props.selIdx || 0, showList: false, showMobileMap: this.props.showMobileMap || false, showMapReturn: false, showMapLoad: false, showMapErr: false }; }, getTableLengths: function() { return _.map(this.state.tables, function(table) { return table.tableData.length; }); }, componentDidMount: function() { if ((maplistConfig.envelope === null && maplistConfig.primaryEntityGeom === null) || maplistConfig.hideMap === true) { this.setState({showList: true}); } else { this.changeContent(this.state.selIdx); } }, componentWillUnmount: function() { //clean up map so it will load again map = null; }, getGeoJson: function(type) { return data = $.get(maplistConfig.serviceUrl, { primaryId: maplistConfig.primaryEntityGuid, listType: type }); }, setupMap: function() { //can't render map until DOMNode is on page var map = L.mapbox.map('maplist', 'jerrycp.i29pfkkc', { scrollWheelZoom: false, keyboard: true, trackResize: true, tapTolerance: 0 }); map.zoomControl.setPosition('bottomright'); // turn on the scroll wheel when someones 'clicks' in the map... map.on('click dblclick dragstart', function () { map.scrollWheelZoom.enable(); }); // and disable it again when they leave map.on('mouseout', function () { map.scrollWheelZoom.disable(); }); map.on('dragend', function () { if (!mapContent.getBounds().intersects(map.getBounds())) { this.showReturnButton(); } }.bind(this)); if (maplistConfig.envelope) { map.fitBounds(maplistConfig.envelope); } else if (maplistConfig.primaryEntityGeom) { map.setView(maplistConfig.primaryEntityGeom.features[0].geometry.coordinates, 12); } else { console.log("No envelope or primaryEntityGeom"); } // put the primary entity outline on the map if (maplistConfig.primaryEntityGeom) { primaryGeom = L.geoJson(maplistConfig.primaryEntityGeom, { style: function () { return primaryOutlineStyle; }, pointToLayer: function (feature, latlng) { $('#maplistLegend').addClass('is-point'); return L.marker(latlng, { icon: L.icon({ iconUrl: '/_css/images/map/pin-blue.png', iconUrlRetina: '/_css/images/map/pin-blue-retina.png', shadowUrl: '/_css/images/map/pin-shadow.png', shadowUrlRetina: '/_css/images/map/pin-shadow-retina.png', iconSize: [32, 42], iconAnchor: [16, 42], popupAnchor: [0, -32], shadowSize: [33, 32], shadowAnchor: [7, 32], labelAnchor: [7, -20] }), riseOnHover: true }); }, onEachFeature: function (feature, layer) { if (!NicheGlobal.isTouchDevice()) { layer.bindLabel(maplistConfig.name); } } }).addTo(map); map.invalidateSize(); } return map; }, setupSpidering: function() { return oms = new OverlappingMarkerSpiderfier(map, { keepSpiderfied: true, circleSpiralSwitchover: 11 }); }, processGeoJson: function(geo) { if ((maplistConfig.envelope === null && maplistConfig.primaryEntityGeom === null) || maplistConfig.hideMap === true) { return false; } var geoJson = geo; //only build map on init if (typeof map === "undefined" || map === null) { map = this.setupMap(); oms = this.setupSpidering(); } else { //remove stuff that's already on map if (mapContent) map.removeLayer(mapContent); map.invalidateSize(); } mapContent = L.geoJson(geoJson, { style: function (feature) { return outlineDefaultStyle; }, pointToLayer: function (feature, latlng) { var marker = L.marker(latlng, { icon: pinSelector(feature), riseOnHover: true }); oms.addMarker(marker); //spiderify oms.addListener('spiderify', function (markers) { feature.closePopup(); }); return marker; }, onEachFeature: function (feature, layer) { if (feature.geometry.type == 'MultiPolygon' || feature.geometry.type == 'Polygon') { layer.on('mouseover', function () { layer.setStyle(outlineHoverStyle); }).on('mouseout', function () { layer.setStyle(outlineDefaultStyle); }); layer.on('dblclick', function (e) { map.setView(e.latlng, map.getZoom() + 1); }); } if (!NicheGlobal.isTouchDevice()) { layer.bindLabel(feature.properties.name); } layer.bindPopup('' + feature.properties.name + 'View profile'); } }).addTo(map); primaryGeom.bringToFront(); this.zoomToContent(this.state.tables[this.state.selIdx],geoJson); }, render: function() { if (NicheGlobal.isSmallLayout() === true) { if ((maplistConfig.envelope === null && maplistConfig.primaryEntityGeom === null) || maplistConfig.hideMap === true) { return ( React.DOM.div( {className:"maplist-wrap " + (this.state.showMobileMap === true ? "show" : "")}, MobileSelectList( {selIdx:this.state.selIdx, onSelectChange:this.changeContent, tabs:Utils.getTabDisplayList(this.state.tables), onCloseMobileMaplist:this.closeMobileMaplist} ), Toggle( {showList:this.state.showList, onToggle:this.showList} ), React.DOM.div( {className:"map-or-list"}, TableList( {showList:this.state.showList, tables:this.state.tables, selIdx:this.state.selIdx, onContentChange:this.changeContent} ) ) ) ); } else { return ( React.DOM.div( {className:"maplist-wrap " + (this.state.showMobileMap === true ? "show" : "")}, Toggle( {showList:this.state.showList, onToggle:this.showList} ), MobileSelectList( {selIdx:this.state.selIdx, onSelectChange:this.changeContent, tabs:Utils.getTabDisplayList(this.state.tables), onCloseMobileMaplist:this.closeMobileMaplist} ), React.DOM.div( {className:"map-or-list"}, Map( {returnHome:this.returnHome, showMapReturn:this.state.showMapReturn} ), TableList( {showList:this.state.showList, tables:this.state.tables, selIdx:this.state.selIdx, onContentChange:this.changeContent} ) ) ) ); } } else if ((maplistConfig.envelope === null && maplistConfig.primaryEntityGeom === null) || maplistConfig.hideMap === true) { return ( React.DOM.div( {className:"maplist-wrap"}, Toggle( {showList:this.state.showList, onToggle:this.showList} ), Tabber( {selIdx:this.state.selIdx, onContentChange:this.changeContent, tabs:Utils.getTabDisplayList(this.state.tables), tableLengths:this.getTableLengths()} ), React.DOM.div( {className:"map-or-list"}, TableList( {showList:this.state.showList, tables:this.state.tables, selIdx:this.state.selIdx, onContentChange:this.changeContent} ) ) ) ); } else { return ( React.DOM.div( {className:"maplist-wrap"}, Toggle( {showList:this.state.showList, onToggle:this.showList} ), Tabber( {selIdx:this.state.selIdx, onContentChange:this.changeContent, tabs:Utils.getTabDisplayList(this.state.tables), tableLengths:this.getTableLengths()} ), React.DOM.div( {className:"map-or-list"}, Map( {returnHome:this.returnHome, showMapReturn:this.state.showMapReturn} ), TableList( {showList:this.state.showList, tables:this.state.tables, selIdx:this.state.selIdx, onContentChange:this.changeContent} ) ) ) ); } }, changeContent: function(i) { var geo = this.getGeoJson(Utils.getTabList(this.state.tables)[i]); geo.success(function(data) { this.processGeoJson(data); }.bind(this)) .error(this.setState({showMapErr: true})); this.setState({ selIdx: i }); }, showList: function() { this.setState({showList: !this.state.showList}); }, zoomToContent: function(type, geoJson) { if (maplistConfig.envelope && (type.tableName != 'Nearby' && type.tableName != 'Comparable' && type.tableName != 'SchoolDistricts')) { map.fitBounds(maplistConfig.envelope); } else if (primaryGeom) { //map.fitBounds(maplistConfig.envelope); map.fitBounds(mapContent.getBounds().extend(primaryGeom.getBounds()), { padding: [-50, -50] // allow stuff to go slightly off-screen }); } }, closeMobileMaplist: function() { this.setState({showMobileMap: false}); React.unmountComponentAtNode(document.getElementById('new-maplist')); }, returnHome: function () { this.changeContent(this.state.selIdx); this.setState({showMapReturn: false}); }, showReturnButton: function() { if (!mapContent) return false; this.setState({showMapReturn: true}); } }); var MobileSelectList = React.createClass({displayName: 'MobileSelectList', render: function() { var tabs = this.props.tabs.map(function(tab, i) { return ( React.DOM.option( {'data-list-type':tab, className:'tab ' + (this.props.selIdx === i ? 'selected' : ''), value:i}, tab ) ); }.bind(this)); return ( React.DOM.div( {className:"tabs-wrap"}, React.DOM.div( {className:"close", onClick:this.closeMobileMaplist}), React.DOM.div( {className:"select-wrap maplist-mobile-tab-select"}, React.DOM.select( {ref:"sel", value:this.props.selIdx, onChange:this.selectTab}, tabs ) ) ) ); }, selectTab: function() { var refVal = parseInt(this.refs.sel.getDOMNode().value,10); this.props.onSelectChange(refVal); }, closeMobileMaplist: function() { this.props.onCloseMobileMaplist(); } }); var Toggle = React.createClass({displayName: 'Toggle', render: function() { if ((maplistConfig.envelope === null && maplistConfig.primaryEntityGeom === null) || maplistConfig.hideMap === true) { return ( React.DOM.span(null) ); } else { var showList = this.props.showList; return ( React.DOM.div( {className:"niche-ui toggle"}, React.DOM.span( {className:showList === false ? 'selected' : '', onClick:this.toggleList}, " Map" ), React.DOM.span( {className:showList === true ? 'selected' : '', onClick:this.toggleList}, " List" ) ) ); } }, toggleList: function() { this.props.onToggle(); } }); var Tabber = React.createClass({displayName: 'Tabber', render: function() { var sel = this.props.selIdx, tableLengths = this.props.tableLengths; var tabs = this.props.tabs.map(function(tab, i) { return ( React.DOM.li( {'data-list-type':tab, className:'tab ' + (sel === i ? 'selected' : ''), onClick:this.selectTab.bind(this, i)}, tab, " ", React.DOM.small(null, tableLengths[i]) ) ); }.bind(this)); return ( React.DOM.div( {className:"tabs-wrap"}, React.DOM.ul( {id:"maplistTabs"}, tabs ), React.DOM.div( {className:"maplist-zillow-link"}, React.DOM.a( {href:maplistConfig.zillowLink, target:"_blank", rel:"nofollow"}, "View Nearby Homes »")), React.DOM.p( {className:"mapsearch-callout " + (maplistConfig.mapsearchUrl === null ? "" : "visible")}, " For advanced filtering capabilities, please visit our ", React.DOM.a( {href:maplistConfig.mapsearchUrl}, "map search tool"),"." ) ) ); }, selectTab: function(i) { this.props.onContentChange(i); } }); var Map = React.createClass({displayName: 'Map', getInitialState: function() { return { scrollPosition: $(document).scrollTop() }; }, componentDidMount: function() { if (NicheGlobal.isSmallLayout() === true) { this.mobileDocumentResize(); } }, render: function() { return ( React.DOM.div( {className:"map-container", id:"mapContainer"}, React.DOM.div( {id:"maplist"}), React.DOM.div( {className:"map-loading"}, React.DOM.div( {id:"mapError", className:"map-error " + (this.props.showMapErr === true ? "visible" : "")}, "There was an error when loading the map. Please try again.") ), React.DOM.div( {className:this.props.showMapReturn === true ? "return-btn" : "", onClick:this.handleMapReturn}, React.DOM.span( {className:"ss-crosshair"})), React.DOM.div( {className:"legend", id:"maplistLegend", dangerouslySetInnerHTML:{__html: maplistConfig.name}}) ) ); }, componentWillUnmount: function() { if (NicheGlobal.isSmallLayout() === true) { this.mobileResetDocumentSize(); } }, mobileDocumentResize: function() { // This is a fix for iPhone to prevent chaotic map movements when // selecting from the category dropdown $('.master-row').css({height: '0px', overflow: 'hidden'}); }, mobileResetDocumentSize: function() { // This resets the fix above when closing MapList on iPhone $('.master-row').removeAttr('style'); $(document).scrollTop(this.state.scrollPosition); }, handleMapReturn: function() { this.props.returnHome(); } }); var TableList = React.createClass({displayName: 'TableList', getInitialState: function() { return { filterBy: false }; }, render: function() { var sel = this.props.selIdx, showList = this.props.showList; var tablrs = this.props.tables.map(function(table,i) { if (sel === i && table.isFilterable === true) { return ( React.DOM.div( {className:"table-container row"}, DropdownFilter( {changeFilter:this.changeFilter, filterBy:this.state.filterBy} ), Tablr( {table:table, filterBy:this.state.filterBy} ) ) ); } else if (sel === i) { return ( React.DOM.div( {className:"table-container row"}, Tablr( {table:table} ) ) ); } }.bind(this)); return ( React.DOM.div( {className:"list-container " + (showList === true || (maplistConfig.envelope === null && maplistConfig.primaryEntityGeom === null) || maplistConfig.hideMap === true ? 'selected' : '')}, tablrs ) ); }, changeFilter: function(cat) { if (cat === "All Grades") { cat = false; } this.setState({filterBy: cat}); } }); var tableSorterMixin = { getInitialState: function() { return { sortTable: this.props.table.tableData, columns: this.props.table.tableHeaders }; }, resetSorted: function() { _.each(this.state.columns, function(c) { c.currentSorted = false; }); }, sortColumn: function(col) { this.resetSorted(); col.currentSorted = true; //should state be sorted? this.state.sortTable.sort(function(a,b) { var startsWithDigits = /^\d+/, valA = a[col.name]['name'], valB = b[col.name]['name']; if (!col.sortDesc) { var t = valB; valB = valA; valA = t; } //eliminate empty strings valA = valA || "0"; //"0" must be typeof "string" so .replace does not fail valB = valB || "0"; if (startsWithDigits.test(valA.replace(/,/g, ''))) { return (parseFloat(valA.replace(/,/g, '')) > parseFloat(valB.replace(/,/g, '')) ? 1 : -1); } else { return (valA > valB ? 1 : -1); } }); col.sortDesc = !col.sortDesc; this.forceUpdate(); } }; var tableFilterMixin = { filterRows: function(row) { if (this.props.table.isFilterable === false) { return true; } var currentRow = row['Grade Level'].name.split(/,[\s]+/); if (currentRow.indexOf(this.props.filterBy) !== -1) { return true; } else { return false; } } }; var DropdownFilter = React.createClass({displayName: 'DropdownFilter', render: function() { var collection = [{ name: "All Grades" }, { name: "Pre-K", }, { name: "Elementary", }, { name: "Middle/Jr. High", }, { name: "High" }]; var opts = collection.map(function(opt, i) { var optVal = opt.name; if (opt.name === "Middle/Jr. High") { optVal = "Middle"; } return ( React.DOM.option( {value:optVal}, opt.name ) ); }.bind(this)); var sortBy = this.props.filterBy || collection[0]; return ( React.DOM.div( {className:"large-6 small-7 columns"}, React.DOM.div( {className:"select-wrap"}, React.DOM.select( {className:"mobile-select", ref:"sel", value:sortBy, onChange:this.filterList}, opts ) ) ) ); }, filterList: function() { var refVal = this.refs.sel.getDOMNode().value; this.props.changeFilter(refVal); } }); var Tablr = React.createClass({displayName: 'Tablr', mixins: [tableSorterMixin, tableFilterMixin], componentWillMount: function() { var col = _.find(this.state.columns, function (elem) { return elem.name === 'Name'; }); if (col) this.handleSort(col); }, render: function() { var table = this.props.table, tableHeaders = table.tableHeaders, tableData = table.tableData, tableName = table.tableName, secondaryCols = [], i = 0, j = 0; var headerRow = tableHeaders.map(function(head, i) { if (head.hdn !== true) { var classList = ""; if (head.unsorted === true) { classList = "unsortable"; } else if (head.currentSorted === true) { if (head.sortDesc === true) { classList = "sorted descending"; } else { classList = "sorted ascending"; } } if (NicheGlobal.isSmallLayout() && head.secondary === true) { secondaryCols.push(i); return false; } else { return ( React.DOM.th( {onClick:this.handleSort.bind(this, head), className:classList}, head.name ) ); } } }.bind(this)); i = 0; var rowData = tableData.map(function(row, i) { if (this.props.filterBy !== false) { var shouldShow = this.filterRows(row); if (shouldShow === false) { return false; } } var cellData = Object.keys(row).map(function(key, j) { if (secondaryCols.indexOf(j) !== -1) { return false; } var val = row[key]; if (val.hdn !== true) { if (typeof val['data-grade'] !== "undefined") { return ( React.DOM.td( {className:((val.class) ? (val.class + " ") : "")}, React.DOM.span( {className:"grade-wrap"}, React.DOM.span( {className:"small overall-grade " + val['data-grade']}) ) ) ); } else if (typeof val.link !== "undefined") { return ( React.DOM.td( {className:((val.class) ? (val.class + " ") : "")}, React.DOM.a( {href:val.link, className:val.link}, val.name ) ) ); } else if (typeof val.link === "undefined") { return ( React.DOM.td( {className:((val.class) ? (val.class + " ") : "")}, React.DOM.span(null, val.name ) ) ); } } }.bind(this)); return ( React.DOM.tr( {key:i}, cellData ) ); }.bind(this)); if (rowData.length > 0) { return ( React.DOM.table( {className:"sortable-table large-12 columns"}, React.DOM.thead(null, React.DOM.tr(null, headerRow ) ), React.DOM.tbody(null, rowData ) ) ); } else { return ( React.DOM.span( {className:"full-width"}, " Sorry, there are no items to display." ) ); } }, handleSort: function(col) { if (col.unsorted === true) { return false; } else { this.sortColumn(col); } } }); var MobileMaplistTrigger = React.createClass({displayName: 'MobileMaplistTrigger', getInitialState: function() { return { tables: tablr, selIdx: 0 }; }, render: function() { return ( React.DOM.div( {className:"mobile-show maplist-callout"}, React.DOM.div( {id:"showMapList"}, React.DOM.div( {className:"fake-map", onClick:this.triggerShowMapList.bind(null, 0)}), React.DOM.h3(null, "View on map"), MobileEntityList( {selIdx:this.state.selIdx, tabs:Utils.getTabDisplayList(this.state.tables), onEntitySelect:this.triggerShowMapList} ) ) ) ); }, triggerShowMapList: function(i) { React.renderComponent(Maplist( {selIdx:i, showMobileMap:true} ), document.getElementById('new-maplist')); } }); var MobileEntityList = React.createClass({displayName: 'MobileEntityList', render: function() { var sel = this.props.selIdx; var entities = this.props.tabs.map(function(entity, i) { return ( React.DOM.li( {'data-list-type':entity, className:'mobile-tab ' + (sel === i) ? 'selected' : '', onClick:this.selectEntity.bind(this, i)}, entity ) ); }.bind(this)); return ( React.DOM.ul(null, entities, React.DOM.li( {className:"maplist-zillow-link"}, React.DOM.a( {href:maplistConfig.zillowLink, target:"_blank", rel:"nofollow"}, "View Nearby Homes »")) ) ); }, selectEntity: function(i) { this.props.onEntitySelect(i); } }); var $window = $(window), $windowHeight = $window.height(); if (tablr.length) { //delay rendering until maplist is in viewport if (document.getElementById('new-maplist') !== null && NicheGlobal.isSmallLayout() === false) { if (isInViewport() === false) { $window.on('scroll.maplistScroll', _.throttle(function() { if (isInViewport() === true) { React.renderComponent(Maplist(null ), document.getElementById('new-maplist')); $(window).off('.maplistScroll'); } }, 33.33)); } else { React.renderComponent(Maplist(null ), document.getElementById('new-maplist')); } } else if (document.getElementById('new-maplist') !== null && NicheGlobal.isSmallLayout() === true) { React.renderComponent(MobileMaplistTrigger(null ), document.getElementById('new-maplist-trigger')); } } else { $('#maplist-disclaimer').remove(); } function isInViewport() { var $elem = $('#new-maplist'), $elemOffsetTop = $elem.offset().top, $viewportTop = $window.scrollTop() - parseInt($elem[0].clientHeight, 10), $viewportBottom = ($window.scrollTop() + $windowHeight); if ($elemOffsetTop > $viewportTop && $elemOffsetTop < $viewportBottom) { return true; } else if ($elemOffsetTop < $viewportTop || $elemOffsetTop > $viewportBottom) { return false; } } /** @jsx React.DOM */ var Transition = React.addons.CSSTransitionGroup; /** Author: Nate Bridi Description: Displays current page and allows user to go forward/backward or to a specific page. */ var Pagination = React.createClass({displayName: 'Pagination', propTypes: { totalPages: React.PropTypes.number.isRequired, // total number of pages currentPage: React.PropTypes.number.isRequired, // current page update: React.PropTypes.func.isRequired // called with (page) when page is changed }, updatePage: function (p) { var page = (p.target && p.target.value) || p; if (page > 0 && page <= this.props.totalPages) { this.props.update(page); } }, render: function () { var pages = []; for (i = 1; i <= this.props.totalPages; i++) { pages.push(React.DOM.option( {key:i, value:i}, i)); } var cx = React.addons.classSet; var prevClasses = cx({ 'disabled': this.props.currentPage == 1 }); var nextClasses = cx({ 'disabled': this.props.currentPage == this.props.totalPages }); if (this.props.totalPages > 1) { return React.DOM.div( {className:"pagination"}, React.DOM.a( {className:prevClasses, rel:"prev", onClick:this.updatePage.bind(null, this.props.currentPage-1)}, "Previous"), React.DOM.div( {className:"page-count"}, React.DOM.div( {className:"page-changer"}, React.DOM.div( {className:"current"}, this.props.currentPage), React.DOM.select( {value:this.props.currentPage, onChange:this.updatePage}, pages ) ), " of ", this.props.totalPages ), React.DOM.a( {className:nextClasses, rel:"next", onClick:this.updatePage.bind(null, +this.props.currentPage+1)}, "Next") ); } else { return React.DOM.div(null ); } } }); $(document).ready(function () { $(document).on("click", function (e) { $('[data-subscribe-global]').trigger('closemenus', e); }); }); /** Author: Nate Bridi Description: Open a modal window for content reporting and submits it. */ var Report = React.createClass({displayName: 'Report', propTypes: { guid: React.PropTypes.string.isRequired, // guid of the content being reported type: React.PropTypes.string.isRequired, // type of the content being reported url: React.PropTypes.string.isRequired // url to the flagging service }, getInitialState: function () { return { showForm: false, submitted: false, body: '' } }, toggleForm: function () { this.setState({showForm: !this.state.showForm}); }, handleChange: function (e) { this.setState({body: e.target.value}); }, submitForm: function () { var _this = this; $.ajax({ url: this.props.url, type: 'POST', data: { ContentType: this.props.type, Id: this.props.guid, Reason: this.state.body }, success: function () { _this.setState({submitted: true}, function () { setTimeout(function () { _this.setState({showForm: false}); }, 1000); }); } }); }, componentDidMount: function () { var _this = this; $(this.getDOMNode()).on('closemenus', function (e, originalEvent) { if ($(this).has(originalEvent.target).length == 0) { _this.setState({showForm: false}); } }); }, componentWillUnmount: function () { $(this.getDOMNode()).unbind('closemenus'); }, render: function () { var form, formContent, openReportClasses = 'open-report'; if (this.state.showForm) { openReportClasses += ' form-open'; if (this.state.submitted) { formContent = React.DOM.div( {className:"inner"}, " Thank you for submitting." ); } else { formContent = React.DOM.div( {className:"inner"}, React.DOM.textarea( {placeholder:"Please explain why you think this content should be removed", value:this.state.body, onChange:this.handleChange} ), React.DOM.div( {className:"red button", onClick:this.submitForm}, "Submit report") ); } form = React.DOM.div( {className:"report-form"}, formContent); } else { form = []; } return React.DOM.div( {className:"report-wrap", 'data-subscribe-global':true}, React.DOM.div( {className:openReportClasses, onClick:this.toggleForm}), Transition( {transitionName:"report"}, form ) ); } }); $('[data-report]').each(function () { var guid = $(this).attr('data-report'); var url = $(this).attr('data-report-url'); var type = $(this).attr('data-report-type'); React.renderComponent( Report( {guid:guid, url:url, type:type} ), this ); }); /** Author: Nate Bridi Description: Renders three buttons (Facebook, Twitter and G+) which open sharing windows */ var Share = React.createClass({displayName: 'Share', propTypes: { classes: React.PropTypes.string.isRequired, // classes applied to individual buttons url: React.PropTypes.string, // base url to share campaign: React.PropTypes.string.isRequired, term: React.PropTypes.string, text: React.PropTypes.string, title: React.PropTypes.string, rankingTitle: React.PropTypes.string, highlightUrl: React.PropTypes.string, hashtag: React.PropTypes.string, showLinkText: React.PropTypes.bool // show action text inside element }, handleClick: function (site, e) { e.preventDefault(); var params = { 'utm_medium': 'social', 'utm_source': site.name, 'utm_campaign': this.props.campaign }; if (typeof this.props.term !== "undefined" && this.props.term.length > 0) params['utm_term'] = this.props.term; if (typeof this.props.url !== "undefined") { var currentUrl = this.props.url + (this.props.url.indexOf('?') >= 0 ? '&' : '?'); // note: the parens are important } else { var currentBaseUrl = window.location.protocol + '//' + window.location.host, //straight-up url w/o query or hash currentUrl = currentBaseUrl + window.location.pathname + '?'; //todo: check if IE adds an extra slash } var initParams = (window.location.search.length > 0) ? window.location.search.split('?')[1] : ''; //rm highlight param to either avoid dups or use var paramList = initParams.split('&').join('=').split('='), highlightParamIndex = paramList.indexOf('highlight'); paramList.splice(highlightParamIndex, 2); //rm the param name and val //if it is shared url //strip out any param that starts with 'utm_' if (paramList.indexOf('utm_medium') !== -1) { var cleanedParams = _.map(paramList, function(param,i) { if (param.split('_')[0] === 'utm') { paramList[i+1] = ''; return ''; } else { return param; } }); } else { var cleanedParams = paramList; } //put humpty dumpty back together again var joinedParams = _.compact(_.map(cleanedParams, function(param,i) { if (param.length === 0) { return false; } if (i % 2 === 0) { return param + '='; } else { return param + '&'; } })).join(''); if (typeof this.props.highlightUrl !== "undefined") { var highlightUrl = 'highlight=' + this.props.highlightUrl, currentParams = highlightUrl + '&' + joinedParams; } else { var currentParams = joinedParams; } //text of the tweet for twitter if (site.name === 'twitter' && this.props.text) { var text = '&text=' + encodeURIComponent(this.props.text); if (typeof this.props.hashtag !== "undefined") { var hashtag = this.props.hashtag; if (hashtag.split('')[0] !== "#") { hashtag = "#" + hashtag; } text = text + encodeURIComponent(" " + hashtag); } var shareParams = _.pairs(params).join('&').split(',').join('='); var renderedUrl = site.url + encodeURIComponent(currentUrl + currentParams + shareParams) + text; } else { //this breaks the twitter text if (this.props.content) params['utm_content'] = this.props.content; var shareParams = _.pairs(params).join('&').split(',').join('='); var renderedUrl = site.url + encodeURIComponent(currentUrl + currentParams + shareParams); } window.open(renderedUrl, 'Sharer', 'toolbar=0, status=0, width=550, height=420'); }, render: function () { var sites = [{ name: 'facebook', url: 'http://www.facebook.com/sharer.php?u=', linkText: 'Share' },{ name: 'googleplus', url: 'https://plus.google.com/share?url=', linkText: 'Share' },{ name: 'twitter', url: 'https://twitter.com/intent/tweet?url=', linkText: 'Tweet' }], buttons = []; _.each(sites, function (site) { var classes = this.props.classes + ' ' + site.name; buttons.push(React.DOM.div( {onClick:this.handleClick.bind(null, site), className:classes}, this.props.showLinkText === "true" ? site.linkText : "")); }, this); return ( React.DOM.div(null, React.DOM.div( {className:"ranking-title-container"}, React.DOM.h3( {className:"ranking-title"}, this.props.rankingTitle)), React.DOM.h4( {className:this.props.title === "false" ? "hidden" : ""}, "Share"), React.DOM.div( {className:"rankings-share-button-container"}, buttons ) ) ); } }); $(function() { $('[data-share]').each(function () { var $this = $(this), classes = $this.attr('data-share-classes'), //required url = $this.attr('data-share-url'), //required campaign = $this.attr('data-share-campaign'), //required term = $this.attr('data-share-term'), content = $this.attr('data-share-content'), title = $this.attr('data-title'), //should the component have the word "Share" above it; defaults to showing rankingTitle = $this.attr('data-ranking-title'), text = $this.attr('data-text'), highlightUrl = $this.attr('data-highlight-url'), hashtag = $this.attr('data-hashtag'), showLinkText = $this.attr('data-show-link-text'); React.renderComponent( Share( {classes:classes, url:url, campaign:campaign, term:term, content:content, title:title, rankingTitle:rankingTitle, text:text, highlightUrl:highlightUrl, hashtag:hashtag, showLinkText:showLinkText} ), this ); }); }); /** @jsx React.DOM */ var BarChartRow = React.createClass({displayName: 'BarChartRow', render: function () { var columns = this.props.columns, primaryBarStyle = { width: columns[0].percent + '%' }, barClasses = "bar", comparison, comparisonBarStyle = {}; if (this.props.showComparison) { var comparisonColumn = columns[this.props.comparison+1]; comparisonBarStyle = { width: comparisonColumn.percent + '%' }; comparison = React.DOM.div( {className:"bar context-bar", style:comparisonBarStyle, 'data-value':comparisonColumn.display}); } if (columns[0].percent < 0) barClasses += ' negative'; return React.DOM.div( {className:"chart-row"}, React.DOM.div( {className:"chart-label"}, this.props.label), React.DOM.div( {className:barClasses, style:primaryBarStyle}), React.DOM.div( {className:"bar-data"}, columns[0].display), comparison ); } }); var Legend = React.createClass({displayName: 'Legend', getInitialState: function () { return { showDropdown: false }; }, componentDidMount: function () { var _this = this; $(this.getDOMNode()).on('closemenus', function (e, originalEvent) { if ($(this).has(originalEvent.target).length == 0) { _this.setState({showDropdown: false}); } }); }, changeComparison: function (i) { this.props.changeComparison(i); this.setState({showDropdown: false}); }, toggleMenu: function () { this.setState({showDropdown: !this.state.showDropdown}); }, render: function () { var cx = React.addons.classSet; var comparisonDropdown = [], menuClasses = cx({ 'clickable': true, 'comparison': true }); if (this.props.comparisons.length > 1) { if (this.state.showDropdown) { var comparisons = this.props.comparisons.map(function (label, i) { var comparisonClasses = cx({ 'selected': (this.state.comparison == i), 'nodata': !label.hasData }); return React.DOM.li( {className:comparisonClasses, onClick:this.changeComparison.bind(this, i)}, label.name, " ", React.DOM.small(null, "(No data available)")); }, this); comparisonDropdown = React.DOM.ul( {className:"chart-dropdown"}, React.DOM.li(null, "Select a comparison"), comparisons ); } } else { menuClasses = 'comparison'; // get rid of the 'clickable' class } return React.DOM.div( {className:"chart-legend", 'data-subscribe-global':true}, React.DOM.div(null, this.props.primary), React.DOM.div( {className:menuClasses, onClick:this.toggleMenu}, this.props.comparisons[this.props.comparison].name), comparisonDropdown ); } }); var BarChart = React.createClass({displayName: 'BarChart', getInitialState: function () { return { data: [], comparison: 0 }; }, componentWillMount: function () { // defined = explicitly set by user // natural = derived from the bounds of the data var scale = { definedMax: this.props.max || -1, definedMin: this.props.min || -1, naturalMax: findChartMax($(this.props.source)), naturalMin: 0 }; /* Chart object: { labels: [{ name: '...', hasData: true },{...}], rows: [{ label: '...', columns: [{ percent: '...', display: '...' }] },{...}], entityLabel: '...', showComparison: true, title: '...', unit: { prefix: '...', suffix: '...' }, min: '...', max: '...' } */ this.setState({data: reapChartData($(this.props.source), scale)}); }, changeComparison: function (i) { this.setState({comparison: i}); }, render: function () { var cx = React.addons.classSet; var chartClasses = cx({ 'has-context': this.state.data.showComparison, 'chart-wrap': true }); var rows = this.state.data.rows.map(function (row) { return BarChartRow( {label:row.label, showComparison:this.state.data.showComparison, columns:row.columns, comparison:this.state.comparison} ); }, this); var legend = [], comparisonDropdown = [], scale = []; if (this.state.data.showComparison) { legend = Legend( {primary:this.state.data.entityLabel, comparisons:this.state.data.labels, comparison:this.state.comparison, changeComparison:this.changeComparison} ); } if (!(this.state.data.unit.suffix == '%' && this.state.data.max == 100)) { var rScale = this.state.data.unit.prefix, lScale = this.state.data.unit.prefix; if (this.state.data.preserveScaleFormat) { rScale += this.state.data.max; lScale += this.state.data.min; } else { rScale += commaSeparateNumber(this.state.data.max); lScale += commaSeparateNumber(this.state.data.min); } rScale += this.state.data.unit.suffix; lScale += this.state.data.unit.suffix; scale = React.DOM.div( {className:"niche-chart-scale"}, React.DOM.div( {className:"right-scale"}, rScale), React.DOM.div( {className:"left-scale"}, lScale) ) } return React.DOM.div( {className:chartClasses}, React.DOM.div( {className:"bar-chart"}, legend, React.DOM.div( {className:"chart-row-wrapper"}, rows ), scale ) ); } }); $('[data-bar-chart]').each(function () { React.renderComponent(BarChart( {source:this, min:$(this).attr('data-chart-min'), max:$(this).attr('data-chart-max')} ), $('
').insertAfter(this)[0]); }); function findChartMax($table) { var $td = $table.find('td:not(:first-child)'); var max = _.max(_.map($td, function (t) { return parseFloat((t.innerHTML.replace('$', '').replace(':1', '').replace(/,/g, '')), 10) || 0; })); if ($td[0].innerHTML.indexOf('%') != -1 && max > 10) { return 100; // if we find a %, it's always 100 (except for situations where the percents are small) } if (max < 10) { max = ~~((max + 9) / 10) * 10; // round up to nearest 10 } else if (max < 1000) { max = ~~((max + 49) / 50) * 50; // round up to nearest 50 } else { max = ~~((max + 499) / 500) * 500; // round up to nearest 500 } return max; } function reapChartData($table, scale) { var chart = { labels: [], rows: [], showComparison: true, preserveScaleFormat: false, title: '', unit: { prefix: '', suffix: '' } }; chart.unit.suffix = ($table.find('td')[1].innerHTML.indexOf('%') !== -1) ? '%' : ''; chart.unit.prefix = ($table.find('td')[1].innerHTML.indexOf('$') !== -1) ? '$' : ''; if (scale.definedMin != -1) { chart.min = parseInt(scale.definedMin, 10); chart.preserveScaleFormat = true; } else { chart.min = scale.naturalMin; } if (scale.definedMax != -1) { chart.max = parseInt(scale.definedMax, 10); chart.preserveScaleFormat = true; } else { chart.max = scale.naturalMax; } // Adds each table heading to a list of labels and checks if they // have all rows of data. Any missing columns results in that // label being deactivated. $table.find('th').each(function (i) { chart.labels.push({ name: $(this).text(), hasData: findNAValues($table, i) }); }); chart.showComparison = chart.labels.length > 2; chart.scaleLabel = chart.labels[0]; // remove the first label, which labels the rows (i.e. 'Income') chart.labels = chart.labels.splice(1); chart.entityLabel = chart.labels[0].name; // again, remove the first label, which labels the main entity chart.labels = chart.labels.splice(1); $table.find('tbody tr').each(function () { var $row = $(this), label, columns = []; label = $row.find('td:first-child').text(); $row.find('td:not(:first-child)').each(function () { var column = {}, text = $(this).text(); // copy the value exactly as is for display purposes // we assume the text on the page is formatted properly column['display'] = text; // then calculate the width of the bar as a percent text = text.replace(/,/g, '').replace('$', '').replace(':1', '').replace('%', ''); text = (parseFloat(text) - chart.min) / (chart.max - chart.min) * 100; column['percent'] = text; columns.push(column); }); chart.rows.push({ label: label, columns: columns }); }); $table.hide(); return chart; } function commaSeparateNumber(val){ while (/(\d+)(\d{3})/.test(val.toString())){ val = val.toString().replace(/(\d+)(\d{3})/, '$1'+','+'$2'); } return val; } // Returns true if "NA" exists in any row in the table at the corresponding column (index) function findNAValues($table, index) { var hasNA = false; $table.find('tbody tr').each(function () { if ($(this).find('td:eq(' + index + ')').text() == 'NA') { hasNA = true; } }); return !hasNA; } /** @jsx React.DOM */ var ReviewFeed = React.createClass({displayName: 'ReviewFeed', getInitialState: function () { var totalWithRatings = _.reduce(ReviewModel.config.ratings, function (memo, num) { return memo + num; }); return { guid: ReviewModel.config.guid, type: ReviewModel.config.type, section: ReviewModel.config.section, url: ReviewModel.config.url, showEntityName: ReviewModel.config.showEntityName, linkToSection: ReviewModel.config.linkToSection, reportUrl: ReviewModel.config.reportUrl, reportType: ReviewModel.config.reportType, sort: ReviewModel.state.sort, dotFilter: ReviewModel.state.dot, page: ReviewModel.state.page, pageSize: ReviewModel.config.pageSize, total: ReviewModel.config.total, totalWithRatings: totalWithRatings, pageCount: Math.ceil(ReviewModel.config.total/ReviewModel.config.pageSize), ratingTotals: ReviewModel.config.ratings, reviews: [] }; }, componentWillMount: function () { var _this = this; $(document).on('updateDotFilter', function () { _this.updateDotFilter(ReviewModel.state.dot); }); $(document).on('updateSortFilter', function () { _this.updateSortFilter(ReviewModel.state.sort); }); }, getReviews: function () { var _this = this; var data = JSON.stringify({ EntityId: this.state.guid, Type: this.state.type, Order: this.state.sort, PageFilter: { Page: this.state.page, Size: this.state.pageSize }, Rating: this.state.dotFilter, Section: this.state.section }); $.ajax({ type: 'POST', contentType: 'application/json; charset=utf-8', dataType: 'json', url: this.state.url, data: data, success: function (d) { $('.initial.review').remove(); // toss the reviews loaded with the page $('.review-container').addClass('ajax-initiated'); // so we know when to show the "no reviews" message _this.updatePageCount(); _this.setState({reviews: d}); } }); }, updateDotFilter: function (dot) { this.setState({dotFilter: dot, page: 1}, function () { this.getReviews(); }); }, updateSortFilter: function (s) { this.setState({sort: s, page: 1}, function () { this.getReviews(); }); }, updatePageFilter: function (p) { this.setState({page: p}, function () { this.getReviews(); }); }, updatePageCount: function () { var ps = this.state.pageSize; if (ps > 0) { if (this.state.dotFilter) { this.setState({pageCount: Math.ceil(this.state.ratingTotals[5-this.state.dotFilter]/ps)}); } else if (this.state.sort == 'WorstRating' || this.state.sort == 'BestRating') { // 'N/A' reviews are not included this.setState({pageCount: Math.ceil(this.state.totalWithRatings/ps)}); } else { this.setState({pageCount: Math.ceil(this.state.total/ps)}) } } else { this.setState({pageCount: 0}); } }, render: function () { var filterAlert = []; if (this.state.dotFilter) { filterAlert = React.DOM.div( {className:"alert"}, "Showing reviews with a rating of ", this.state.dotFilter,"."); } return React.DOM.div( {className:"review-content"}, filterAlert, ReviewList( {reviews:this.state.reviews, showEntityName:this.state.showEntityName, entityType:this.state.type, linkToSection:this.state.linkToSection, reportUrl:this.state.reportUrl, reportType:this.state.reportType} ), Pagination( {totalPages:this.state.pageCount, currentPage:this.state.page, update:this.updatePageFilter} ) ) } }); var ReviewList = React.createClass({displayName: 'ReviewList', getDefaultProps: function () { return { reviews: [{}] }; }, render: function () { var noReviews = ''; reviews = this.props.reviews.map(function(review) { return Review( {content:review, showEntityName:this.props.showEntityName, linkToSection:this.props.linkToSection, entityType:this.props.entityType, reportUrl:this.props.reportUrl, reportType:this.props.reportType} ); }, this); if (this.props.reviews.length == 0) { noReviews = React.DOM.li( {className:"no-reviews"}, "There are no reviews to show you."); } return React.DOM.ul(null, noReviews, reviews ); } }); var ReviewSort = React.createClass({displayName: 'ReviewSort', changeSort: function (event) { this.props.handleSort(event.target.value); }, render: function () { return React.DOM.div( {className:"sort"}, React.DOM.h4(null, "Sort"), React.DOM.div( {className:"select-wrap"}, React.DOM.select( {value:this.props.currentSort, onChange:this.changeSort}, React.DOM.option( {value:"Oldest"}, "Oldest"), React.DOM.option( {value:"Newest"}, "Newest"), React.DOM.option( {value:"BestRating"}, "Best ratings"), React.DOM.option( {value:"WorstRating"}, "Worst ratings") ) ) ); } }); var Review = React.createClass({displayName: 'Review', render: function () { var c = this.props.content; var reviewBodyLead = ''; if (this.props.showEntityName) { if (this.props.linkToSection && c.EntityType.Value != this.props.entityType) { reviewBodyLead = React.DOM.a( {href:c.EntityUrl}, c.SectionLabel, " at ", c.EntityName); } else { reviewBodyLead = c.SectionLabel + " at " + c.EntityName; } } else { if (this.props.linkToSection) { reviewBodyLead = React.DOM.a( {href:c.EntityUrl + c.SectionUrlFragment}, c.SectionLabel); } else { reviewBodyLead = c.SectionLabel; } } return React.DOM.li( {className:"review"}, ReviewMeta( {rating:c.Rating.CssClass, author:c.UserDisplayString, date:c.ReadableDate}, Report( {guid:c.Guid, url:this.props.reportUrl, type:this.props.reportType} ) ), React.DOM.div( {className:"body"}, React.DOM.strong(null, reviewBodyLead,":"), " ", c.Body ) ); } }); var ReviewMeta = React.createClass({displayName: 'ReviewMeta', render: function () { var cx = React.addons.classSet; var dotClasses = 'small five-dots dot-' + this.props.rating; var author = (this.props.author) ? React.DOM.li(null, this.props.author) : []; return React.DOM.ul( {className:"meta"}, author, React.DOM.li(null, this.props.date), React.DOM.li( {className:"meta-rating"}, React.DOM.div( {className:dotClasses})), React.DOM.li(null, this.props.children) ) } }); var ReviewFilters = React.createClass({displayName: 'ReviewFilters', getInitialState: function () { return { currentDot: null, sort: 'Newest' }; }, updateDotFilter: function (dot) { this.setState({currentDot: dot}, function () { ReviewModel.state.dot = dot; $(document).triggerHandler('updateDotFilter'); }); }, updateSortFilter: function (sort) { this.setState({sort: sort}, function () { ReviewModel.state.sort = sort; $(document).triggerHandler('updateSortFilter'); }); }, render: function () { var dots = [], total; total = _.reduce(this.props.dots, function(memo, num){ return memo + num; }, 0); for (i = 0; i < 5; i++) { dots.push(ReviewAggDot( {count:this.props.dots[i], total:total, selected:(5-i) == this.state.currentDot, handleClick:this.updateDotFilter.bind(null, 5-i)} )); } var clearFilter = ''; if (this.state.currentDot != null) { clearFilter = React.DOM.span( {className:"clear-dot-filter", onClick:this.updateDotFilter.bind(null, null)}, "Clear"); } return React.DOM.div( {className:"review-filters"}, React.DOM.div( {className:"dot-breakdown"}, React.DOM.h4(null, "Ratings ", clearFilter ), React.DOM.ul(null, dots ) ), ReviewSort( {currentSort:this.state.sort, handleSort:this.updateSortFilter} ) ); } }); var ReviewAggDot = React.createClass({displayName: 'ReviewAggDot', render: function () { var cx = React.addons.classSet; var classes = cx({ 'selected': this.props.selected }); var style = { width: (this.props.count / this.props.total * 100) + '%' }; return React.DOM.li( {onClick:this.props.handleClick, className:classes}, React.DOM.span( {style:style})) } }); var ReviewModel = ReviewModel || {}; if (ReviewModel.config) { React.renderComponent( ReviewFilters( {dots:ReviewModel.config.ratings} ), document.getElementById('reviewFilters') ); React.renderComponent( ReviewFeed(null ), document.getElementById('reviewList') ); } /** @jsx React.DOM */ function grabRankingsData(listItems) { $('[data-rankings-component]').remove(); var rankings = [], keys = ['title','ordinal','ordinalSetTotal','factors'], factorsKeys = ['label','value','tooltip']; $(listItems).each(function(i, item) { var rankingsType = $(item).attr('data-list-items'); var cells = $(item).find('>td'), ranking = {}; _.each(cells, function(data, j) { if ($(data).find('a').length) { ranking.title = $(data).text().trim(); ranking.shortTitle = $(data).text().split('-')[0].trim(); ranking.url = $(data).find('a').attr('href'); } else if ($(data).find('table').length) { var factorData = $(data).find('tr'), factors = []; _.each(factorData, function(factor,k) { var factorDataItem = $(factor).find('td'), factor = {}; _.each(factorDataItem, function(factorItem,l) { factor[factorsKeys[l]] = $(factorItem).text().trim(); }); factors.push(factor); }); ranking['factors'] = factors; } else { ranking[keys[j]] = $(data).text().trim(); } }); ranking.rankingType = rankingsType; ranking.percentile = 100 - (((ranking.ordinal - 1)/(ranking.ordinalSetTotal - 1)) * 100); ranking.shortTitleWType = ranking.shortTitle; if (ranking.title.indexOf('- ') !== -1) { var titleType = ranking.title.split('- ')[1].replace('Public ','').replace('Private ','').replace(' School Districts',''); if (titleType !== 'High Schools' && titleType !== 'School Districts') { ranking.shortTitleWType = ranking.shortTitleWType + " (" + titleType + ")"; } } else { ranking.shortTitleWType = ranking.title; } switch(ranking.shortTitleWType) { case "2016 Best School Districts": ranking.shortTitleWType = "Best School Districts"; break; case "2016 Districts with the Best Academics": ranking.shortTitleWType = "Best Academics"; break; case "2016 Districts with the Best Administration": ranking.shortTitleWType = "Best Administration"; break; case "2016 College Readiness Ranking": ranking.shortTitleWType = "College Readiness"; break; case "2016 College Readiness Ranking for High Schools": ranking.shortTitleWType = "College Readiness"; break; case "2016 Districts with the Best Extracurriculars": ranking.shortTitleWType = "Best Extracurriculars"; break; case "2016 Districts with the Best Food": ranking.shortTitleWType = "Best Food"; break; case "2016 Safest School Districts": ranking.shortTitleWType = "Safest School Districts"; break; case "2016 Districts with the Best Facilities": ranking.shortTitleWType = "Best Faciliites"; break; case "2016 Districts with the Best Sports": ranking.shortTitleWType = "Best Sports"; break; case "2016 Most Diverse School Districts": ranking.shortTitleWType = "Most Diverse"; break; case "2016 Districts with the Best Teachers": ranking.shortTitleWType = "Best Teachers"; break; case "2016 Largest School Districts": ranking.shortTitleWType = "Largest School Districts"; break; case "2016 Best Public High Schools": ranking.shortTitleWType = "Best High Schools"; break; case "2016 High Schools with the Best Academics": ranking.shortTitleWType = "Best Academics"; break; case "2016 High Schools with the Best Administration": ranking.shortTitleWType = "Best Administration"; break; case "2016 College Readiness Ranking": ranking.shortTitleWType = "College Readiness"; break; case "2016 College Readiness Ranking for School Districts": ranking.shortTitleWType = "College Readiness"; break; case "2016 High Schools with the Best Extracurriculars": ranking.shortTitleWType = "Best Extracurriculars"; break; case "2016 High Schools with the Best Food": ranking.shortTitleWType = "Best Food"; break; case "2016 Safest Public High Schools": ranking.shortTitleWType = "Safest Public High Schools"; break; case "2016 High Schools with the Best Facilities": ranking.shortTitleWType = "Best Facilities"; break; case "2016 Best High School Sports": ranking.shortTitleWType = "Best High School Sports"; break; case "2016 Most Diverse Public High Schools": ranking.shortTitleWType = "Most Diverse"; break; case "2016 High Schools with the Best Teachers": ranking.shortTitleWType = "Best Teachers"; break; case "2016 Largest Public High Schools": ranking.shortTitleWType = "Largest Public High Schools"; break; case "2016 Best Charter High Schools": ranking.shortTitleWType = "Best Charter High Schools"; break; case "2016 Best Magnet High Schools": ranking.shortTitleWType = "Best Magnet High Schools"; break; case "2016 Best Public Middle Schools": ranking.shortTitleWType = "Best Public Middle Schools"; break; case "2016 Middle Schools with the Best Academics": ranking.shortTitleWType = "Best Academics"; break; case "2016 Most Diverse Middle Schools": ranking.shortTitleWType = "Most Diverse"; break; case "2016 Middle Schools with the Best Teachers": ranking.shortTitleWType = "Best Teachers"; break; case "2016 Best Public Elementary Schools": ranking.shortTitleWType = "Best Public Elementary Schools"; break; case "2016 Elementary Schools with the Best Academics": ranking.shortTitleWType = "Best Academics"; break; case "2016 Most Diverse Elementary Schools": ranking.shortTitleWType = "Most Diverse"; break; case "2016 Elementary Schools with the Best Teachers": ranking.shortTitleWType = "Best Teachers"; break; case "2016 Private High Schools with the Best Academics": ranking.shortTitleWType = "Best Academics"; break; case "2016 Best All-Boys High Schools": ranking.shortTitleWType = "Best All-Boys High Schools"; break; case "2016 Best All-Girls High Schools": ranking.shortTitleWType = "Best All-Girls High Schools"; break; case "2016 Best Catholic High Schools": ranking.shortTitleWType = "Best Catholic High Schools"; break; case "2016 Best Christian High Schools": ranking.shortTitleWType = "Best Christian High Schools"; break; case "2016 Private High School College Readiness": ranking.shortTitleWType = "College Readiness"; break; case "2016 Best Private High Schools": ranking.shortTitleWType = "Best High Schools"; break; case "2016 Best Private School Teachers": ranking.shortTitleWType = "Best Teachers"; break; case "2016 Most Diverse Private High Schools": ranking.shortTitleWType = "Most Diverse"; break; } var titleLevel = ranking.title.split(' '); if (titleLevel.indexOf('High') !== -1) { ranking.schoolLevel = "high"; } else if (titleLevel.indexOf('Middle') !== -1) { ranking.schoolLevel = "middle"; } else if (titleLevel.indexOf('Elementary') !== -1) { ranking.schoolLevel = "elementary"; } else { ranking.schoolLevel = ""; } if (ranking.ordinal !== "0") { rankings.push(ranking); } }); return rankings; } var EntityRankingsList = React.createClass({displayName: 'EntityRankingsList', getInitialState: function() { return { factorsOpen: this.props.factorsOpen, entityData: null }; }, componentWillReceiveProps: function(nextProps) { this.setState({factorsOpen: nextProps.factorsOpen}); }, render: function() { return ( React.DOM.ul( {className:"entity-rankings-list"}, React.DOM.p( {className:"entity-rankings-tap-msg"}, "Tap rows for more information"), ListItem( {onToggleFactors:this.toggleFactors, onCloseFactors:this.closeFactors, factorsOpen:this.state.factorsOpen, rankings:this.props.rankings, rankingType:this.props.type} ) ) ); }, toggleFactors: function(i) { this.setState({factorsOpen: i}); }, closeFactors: function() { this.setState({factorsOpen: null}); } }); var ListItem = React.createClass({displayName: 'ListItem', render: function() { var noBg = {'background-image': 'none'}; if (this.props.rankings.length === 0) return ( React.DOM.h3(null, "Sorry, this school does not have any eligible rankings to display for this category.") ); var rankings = this.props.rankings.map(function(ranking, i) { function numberWithCommas(x) { return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); } var rankingImg = {'background-image': "url(" + nicheCdnUrl + "/images" + ranking.url.replace("/rankings/","/rankings/k12/") + "/900px.jpg)"}; var width = {width: ranking.percentile + '%'}, rankingType = null; if (this.props.rankingType === "metroarea") { rankingType = "area"; } else { rankingType = this.props.rankingType; } var ordinalSuffix = null, ordinalEnd = ranking.ordinal.toString().split(''); switch(ordinalEnd[ordinalEnd.length - 1]) { case "1": (ordinalEnd[ordinalEnd.length - 2] === "1") ? ordinalSuffix = "th" : ordinalSuffix = "st"; break; case "2": (ordinalEnd[ordinalEnd.length - 2] === "1") ? ordinalSuffix = "th" : ordinalSuffix = "nd"; break; case "3": (ordinalEnd[ordinalEnd.length - 2] === "1") ? ordinalSuffix = "th" : ordinalSuffix = "rd"; break; default: ordinalSuffix = "th"; } var rankingLocale = null; var geoUrl = ranking.url; switch(rankingType) { case 'area': rankingLocale = "the " + metroName.replace('Metro ', ''); geoUrl += '/m/' + metroUrl; break; case 'state': rankingLocale = stateAbbr.toUpperCase(); geoUrl += '/s/' + stateUrl; break; default: rankingLocale = 'America'; } var ordinal = numberWithCommas(ranking.ordinal), ordinalSetTotal = numberWithCommas(ranking.ordinalSetTotal); var shareMsg = "Niche ranked " + entityName.replace('School District','SD') + " " + ordinal+ordinalSuffix + " in " + rankingLocale + " for " + ranking.shortTitle.replace('School Districts','Districts') + "!"; var shareInfo = { classes: "round size fifty social-media-icon facebook solid", url: ranking.url, campaign: (entityType == "school") ? "SchoolRanking" : "DistrictRanking", term: "ERP", content: shareMsg, title: "false", hashtag: "#BestHighSchools" }; if (entityType === 'district') { shareInfo.hashtag = "#BestPublicSchools"; } else { if (ranking.schoolLevel === 'middle' || ranking.schoolLevel === 'elementary') { shareInfo.hashtag = "#BestSchools"; } } return ( React.DOM.li( {className:'entity-rankings-list-item ' + (this.props.factorsOpen === i ? 'open' : ''), onClick:this.toggleFactors.bind(this,i)}, React.DOM.div( {className:"img-container", style:(this.props.factorsOpen === i ? rankingImg : noBg)}), React.DOM.div( {className:"ranking-label"}, React.DOM.div( {className:"ranking-title"}, React.DOM.span( {dangerouslySetInnerHTML:{__html: ranking.shortTitleWType}}), React.DOM.p( {className:'ranking-description ' + (this.props.factorsOpen === i ? '' : 'hidden')}, entityName, " is ranked ", ordinal,ordinalSuffix, " of ", ordinalSetTotal, " in the ", rankingType, " for ", schoolType, " ", ranking.schoolLevel, " ", entityType === 'district' ? 'school districts' : 'schools',".", React.DOM.a( {href:geoUrl}, "View the full list »") ) ) ), React.DOM.div( {className:"ranking-bar"}, React.DOM.div( {className:"ranking-inner-bar"}, React.DOM.span( {className:"bar", style:width}), React.DOM.div( {className:"bar-expansion"}, Factors( {factors:ranking.factors, url:ranking.url} ) ) ) ), React.DOM.div( {className:'close ' + (this.props.factorsOpen === i ? '' : 'hidden'), onClick:this.closeFactors}, React.DOM.span( {className:"ss-delete"})) ) ); }.bind(this)); return ( React.DOM.div( {className:"ranking-scale"}, rankings ) ); }, toggleFactors: function(i) { if (this.props.factorsOpen === i) { i = null; } else { this.props.onToggleFactors(i); } }, closeFactors: function() { this.props.onCloseFactors(); } }); var Factors = React.createClass({displayName: 'Factors', getInitialState: function() { return { factorOpen: null }; }, render: function() { var factors = this.props.factors.map(function(factor, i) { if (typeof factor.label !== "undefined") { return ( React.DOM.li( {className:"factors-list-item"}, React.DOM.div( {className:"factor-name"}, React.DOM.span( {className:"factor-label " + (!factor.tooltip ? 'expanded' : '')}, factor.label), React.DOM.a( {className:factor.tooltip.length === 0 ? 'hidden' : '', onClick:this.expandFactor.bind(this,i)}, React.DOM.span( {className:(this.state.factorOpen === i ? 'hidden' : '')}, "More " ), React.DOM.span( {className:(this.state.factorOpen !== i ? 'hidden' : '')}, "Hide " ), " details" ) ), React.DOM.div( {className:"factor-details " + (this.state.factorOpen === i ? '' : 'hidden')}, factor.tooltip ), React.DOM.div( {className:"factor-score", dangerouslySetInnerHTML:{__html: factor['value']}}) ) ); } }.bind(this)); return ( React.DOM.div( {className:"factors"}, React.DOM.p(null, "What went into this ranking?"), React.DOM.ul( {className:"factors-list"}, factors ), React.DOM.div( {className:"factors-fine-print"}, " The values for this school were current at the time this ranking"+' '+ "was calculated and may not represent the most recent available"+' '+ "data. For more details,",React.DOM.strong(null, React.DOM.a( {href:this.props.url + "/methodology/", target:"_blank"}, " see how these values were used " )), " to calculate this ranking." ) ) ); }, expandFactor: function(i) { if (i === this.state.factorOpen) { i = null; } this.setState({factorOpen: i}); } }); if ($('[data-entity-rankings]').length) { var entityName = $('.entity-rankings-container').attr('data-entity-name'), entityType = $('[data-entity-rankings]').attr('data-entity-rankings'), schoolType = ''; if ($('[data-school-type]').length) { schoolType = $('[data-school-type]').attr('data-school-type'); } //set the selected tab based on the hash, man //or set it to the first tab if (window.location.hash.length > 0) { var hashVal = window.location.hash.split('#'), tabTypes = _.map( $('.tab[data-type]'), function(tab) { return $(tab).attr('data-type'); }), hashTab = _.where( tabTypes, hashVal[1] ).toString(); $(".tab").eq( tabTypes.indexOf( hashTab ) ).addClass("selected"); } else { $(".tab").eq(0).addClass("selected"); } //todo: make sure this accounts for hashes if (window.location.search.length > 0) { var highlightUrl = window.location.search.split("=")[1]; if (highlightUrl.indexOf("&") !== -1) { highlightUrl = highlightUrl.split("&")[0]; } } var listTypes = _.uniq(_.map($("[data-list-items]"), function(item) { return $(item).attr("data-list-items"); })); var rankings = grabRankingsData($("[data-list-items]")); var metroName = $("[data-metro]").attr("data-metro"), stateAbbr = $("[data-state-abbr]").attr("data-state-abbr"); var metroUrl = $('[data-metro-url]').attr('data-metro-url'), stateUrl = $('[data-state-url]').attr('data-state-url'); var currentRankingType = function() { //do some magic to make sure selected tab has content if (listTypes.length < 3 && listTypes.indexOf('state') !== -1) { $('.tab[data-type]').removeClass('selected'); var newTab = _.compact(_.map($('.tab[data-type]'), function(tab) { return $(tab).attr('data-type') == 'state' ? tab : null; })); $(newTab).addClass('selected'); } else if (listTypes.length < 3 && listTypes.indexOf('country') !== -1) { $('.tab[data-type]').removeClass('selected'); var newTab = _.compact(_.map($('.tab[data-type]'), function(tab) { return $(tab).attr('data-type') == 'country' ? tab : null; })); $(newTab).addClass('selected'); } return $(".tab.selected").attr("data-type") }(), selectedRankings = _.where(rankings,{rankingType: currentRankingType}); selectedRankings.sort(function (a,b) { return a.percentile < b.percentile ? 1 : -1; }); _.each(selectedRankings, function(ranking,i) { if (highlightUrl && ranking.url === highlightUrl) { ranking.highlightIndex = i; } }); var highlightedRanking = _.find(selectedRankings, function(rank) { return (typeof rank.highlightIndex !== "undefined"); }); if (typeof highlightedRanking !== "undefined") { highlightedRanking = highlightedRanking.highlightIndex; } else { highlightedRanking = null; } $('.tab[data-type]').on('click', function() { $('.tab[data-type]').removeClass('selected'); $(this).addClass('selected'); var currentRankingType = $(".tab.selected").attr("data-type"), selectedRankings = _.where(rankings,{rankingType: currentRankingType}); var sortedSelRankings = selectedRankings.sort(function (a,b) { return a.percentile < b.percentile ? 1 : -1; }); _.each(sortedSelRankings, function(ranking,i) { if (highlightUrl && ranking.url === highlightUrl) { ranking.highlightIndex = i; } }); React.renderComponent(EntityRankingsList( {type:currentRankingType, rankings:sortedSelRankings, factorsOpen:null} ), $('[data-entity-rankings]')[0]); //todo: make sure this goes after the querystring window.location.href = "#" + currentRankingType; }); $('[data-ranking-state]').on('click', function() { $('.tab[data-type]').removeClass('selected'); $(".tab").eq( tabTypes.indexOf( hashTab ) ).addClass("selected"); var currentRankingType = 'state', selectedRankings = _.where(rankings,{rankingType: currentRankingType}); var sortedSelRankings = selectedRankings.sort(function (a,b) { return a.percentile < b.percentile ? 1 : -1; }); _.each(sortedSelRankings, function(ranking,i) { if (highlightUrl && ranking.url === highlightUrl) { ranking.highlightIndex = i; } }); React.renderComponent(EntityRankingsList( {type:currentRankingType, rankings:sortedSelRankings, factorsOpen:null} ), $('[data-entity-rankings]')[0]); //todo: make sure this goes after the querystring window.location.href = "#" + currentRankingType; }); React.renderComponent(EntityRankingsList( {type:currentRankingType, rankings:selectedRankings, factorsOpen:highlightedRanking} ), $('[data-entity-rankings]')[0]); }