<commit_before>import offsetCalculator from 'discourse/lib/offset-calculator';
import LockOn from 'discourse/lib/lock-on';
import { defaultHomepage } from 'discourse/lib/utilities';

const rewrites = [];
const TOPIC_REGEXP = /\/t\/([^\/]+)\/(\d+)\/?(\d+)?/;

// We can add links here that have server side responses but not client side.
const SERVER_SIDE_ONLY = [
  /^\/posts\/\d+\/raw/,
  /^\/posts\.rss/
];

let _jumpScheduled = false;
export function jumpToElement(elementId) {
  if (_jumpScheduled || Ember.isEmpty(elementId)) { return; }

  const selector = `#${elementId}, a[name=${elementId}]`;
  _jumpScheduled = true;
  Ember.run.schedule('afterRender', function() {
    const lockon = new LockOn(selector, {
      finished() {
        _jumpScheduled = false;
      }
    });
    lockon.lock();
  });
}

const DiscourseURL = Ember.Object.extend({

  isJumpScheduled() {
    return _jumpScheduled;
  },

  // Jumps to a particular post in the stream
  jumpToPost(postNumber, opts) {
    opts = opts || {};
    const holderId = `#post_${postNumber}`;

    Em.run.schedule('afterRender', () => {
      let elementId;
      let holder;

      if (postNumber === 1 && !opts.anchor) {
        $(window).scrollTop(0);
        return;
      }

      if (opts.anchor) {
        elementId = opts.anchor;
        holder = $(elementId);
      }

      if (!holder || holder.length === 0) {
        elementId = holderId;
        holder = $(elementId);
      }

      const lockon = new LockOn(elementId);

      if (holder.length > 0 && opts && opts.skipIfOnScreen){
        const elementTop = lockon.elementTop();
        const scrollTop = $(window).scrollTop();
        const windowHeight = $(window).height() - offsetCalculator();
        const height = holder.height();

        if (elementTop > scrollTop && (elementTop + height) < (scrollTop + windowHeight)) {
          return;
        }
      }

      lockon.lock();
    });
  },

  // Browser aware replaceState. Will only be invoked if the browser supports it.
  replaceState(path) {
    if (window.history &&
        window.history.pushState &&
        window.history.replaceState &&
        !navigator.userAgent.match(/((iPod|iPhone|iPad).+\bOS\s+[1-4]|WebApps\/.+CFNetwork)/) &&
        (window.location.pathname !== path)) {

        // Always use replaceState in the next runloop to prevent weird routes changing
        // while URLs are loading. For example, while a topic loads it sets `currentPost`
        // which triggers a replaceState even though the topic hasn't fully loaded yet!
        Ember.run.next(() => {
          const location = DiscourseURL.get('router.location');
          if (location && location.replaceURL) {
            location.replaceURL(path);
          }
        });
    }
  },

  routeToTag(a) {
    if (a && a.host !== document.location.host) {
      document.location = a.href;
      return false;
    }

    return this.routeTo(a.href);
  },

  /**
    Our custom routeTo method is used to intelligently overwrite default routing
    behavior.

    It contains the logic necessary to route within a topic using replaceState to
    keep the history intact.
  **/
  routeTo(path, opts) {
    opts = opts || {};

    if (Em.isEmpty(path)) { return; }

    if (Discourse.get('requiresRefresh')) {
      document.location.href = Discourse.getURL(path);
      return;
    }

    const serverSide = SERVER_SIDE_ONLY.some(r => {
      if (path.match(r)) {
        document.location = path;
        return true;
      }
    });

    if (serverSide) { return; }

    // Protocol relative URLs
    if (path.indexOf('//') === 0) {
      document.location = path;
      return;
    }

    // Scroll to the same page, different anchor
    const m = /^#(.+)$/.exec(path);
    if (m) {
      jumpToElement(m[1]);
      return this.replaceState(path);
    }

    const oldPath = window.location.pathname;
    path = path.replace(/(https?\:)?\/\/[^\/]+/, '');

    // handle prefixes
    if (path.match(/^\//)) {
      let rootURL = (Discourse.BaseUri === undefined ? "/" : Discourse.BaseUri);
      rootURL = rootURL.replace(/\/$/, '');
      path = path.replace(rootURL, '');
    }

    // Rewrite /my/* urls
    if (path.indexOf('/my/') === 0) {
      const currentUser = Discourse.User.current();
      if (currentUser) {
        path = path.replace('/my/', '/users/' + currentUser.get('username_lower') + "/");
      } else {
        document.location.href = "/404";
        return;
      }
    }

    rewrites.forEach(rw => path = path.replace(rw.regexp, rw.replacement));

    if (this.navigatedToPost(oldPath, path, opts)) { return; }

    // Schedule a DOM cleanup event
    Em.run.scheduleOnce('afterRender', Discourse.Route, 'cleanDOM');

    if (oldPath === path) {
      // If navigating to the same path send an app event. Views can watch it
      // and tell their controllers to refresh
      this.appEvents.trigger('url:refresh');
    }

    // TODO: Extract into rules we can inject into the URL handler
    if (this.navigatedToHome(oldPath, path, opts)) { return; }

    return this.handleURL(path, opts);
  },

  rewrite(regexp, replacement) {
    rewrites.push({ regexp, replacement });
  },

  redirectTo(url) {
    window.location = Discourse.getURL(url);
  },

  /**
   * Determines whether a URL is internal or not
   *
   * @method isInternal
   * @param {String} url
  **/
  isInternal(url) {
    if (url && url.length) {
      if (url.indexOf('//') === 0) { url = "http:" + url; }
      if (url.indexOf('#') === 0) { return true; }
      if (url.indexOf('/') === 0) { return true; }
      if (url.indexOf(this.origin()) === 0) { return true; }
      if (url.replace(/^http/, 'https').indexOf(this.origin()) === 0) { return true; }
      if (url.replace(/^https/, 'http').indexOf(this.origin()) === 0) { return true; }
    }
    return false;
  },

  /**
    If the URL is in the topic form, /t/something/:topic_id/:post_number
    then we want to apply some special logic. If the post_number changes within the
    same topic, use replaceState and instruct our controller to load more posts.
  **/
  navigatedToPost(oldPath, path, routeOpts) {
    const newMatches = TOPIC_REGEXP.exec(path);
    const newTopicId = newMatches ? newMatches[2] : null;

    if (newTopicId) {
      const oldMatches = TOPIC_REGEXP.exec(oldPath);
      const oldTopicId = oldMatches ? oldMatches[2] : null;

      // If the topic_id is the same
      if (oldTopicId === newTopicId) {
        DiscourseURL.replaceState(path);

        const container = Discourse.__container__;
        const topicController = container.lookup('controller:topic');
        const opts = {};
        const postStream = topicController.get('model.postStream');

        if (newMatches[3]) { opts.nearPost = newMatches[3]; }
        if (path.match(/last$/)) { opts.nearPost = topicController.get('model.highest_post_number'); }
        const closest = opts.nearPost || 1;

        opts.cancelSummary = true;

        postStream.refresh(opts).then(() => {
          topicController.setProperties({
            'model.currentPost': closest,
            enteredAt: new Date().getTime().toString()
          });

          this.appEvents.trigger('post:highlight', closest);
        }).then(() => {
          const jumpOpts = {
            skipIfOnScreen: routeOpts.skipIfOnScreen
          };

          const m = /#.+$/.exec(path);
          if (m) {
            jumpOpts.anchor = m[0];
          }

          this.jumpToPost(closest, jumpOpts);
        });

        // Abort routing, we have replaced our state.
        return true;
      }
    }

    return false;
  },

  /**
    @private

    Handle the custom case of routing to the root path from itself.

    @param {String} oldPath the previous path we were on
    @param {String} path the path we're navigating to
  **/
  navigatedToHome(oldPath, path) {
    const homepage = defaultHomepage();

    if (window.history &&
        window.history.pushState &&
        (path === "/" || path === "/" + homepage) &&
        (oldPath === "/" || oldPath === "/" + homepage)) {
      this.appEvents.trigger('url:refresh');
      return true;
    }

    return false;
  },

  // This has been extracted so it can be tested.
  origin() {
    return window.location.origin + (Discourse.BaseUri === "/" ? '' : Discourse.BaseUri);
  },

  /**
    @private

    Get a handle on the application's router. Note that currently it uses `__container__` which is not
    advised but there is no other way to access the router.

    @property router
  **/
  router: function() {
    return Discourse.__container__.lookup('router:main');
  }.property().volatile(),

  // Get a controller. Note that currently it uses `__container__` which is not
  // advised but there is no other way to access the router.
  controllerFor(name) {
    return Discourse.__container__.lookup('controller:' + name);
  },

  /**
    Be wary of looking up the router. In this case, we have links in our
    HTML, say form compiled markdown posts, that need to be routed.
  **/
  handleURL(path, opts) {
    opts = opts || {};

    const router = this.get('router');

    if (opts.replaceURL) {
      this.replaceState(path);
    } else {
      router.router.updateURL(path);
    }

    const split = path.split('#');
    let elementId;

    if (split.length === 2) {
      path = split[0];
      elementId = split[1];
    }

    const transition = router.handleURL(path);
    transition._discourse_intercepted = true;
    transition.promise.then(() => jumpToElement(elementId));
  }
}).create();

export default DiscourseURL;
<commit_msg>FIX: Route to `.rss` or `.json` as a server side URL<commit_after>import offsetCalculator from 'discourse/lib/offset-calculator';
import LockOn from 'discourse/lib/lock-on';
import { defaultHomepage } from 'discourse/lib/utilities';

const rewrites = [];
const TOPIC_REGEXP = /\/t\/([^\/]+)\/(\d+)\/?(\d+)?/;

// We can add links here that have server side responses but not client side.
const SERVER_SIDE_ONLY = [
  /^\/posts\/\d+\/raw/,
  /\.rss$/,
  /\.json/,
];

let _jumpScheduled = false;
export function jumpToElement(elementId) {
  if (_jumpScheduled || Ember.isEmpty(elementId)) { return; }

  const selector = `#${elementId}, a[name=${elementId}]`;
  _jumpScheduled = true;
  Ember.run.schedule('afterRender', function() {
    const lockon = new LockOn(selector, {
      finished() {
        _jumpScheduled = false;
      }
    });
    lockon.lock();
  });
}

const DiscourseURL = Ember.Object.extend({

  isJumpScheduled() {
    return _jumpScheduled;
  },

  // Jumps to a particular post in the stream
  jumpToPost(postNumber, opts) {
    opts = opts || {};
    const holderId = `#post_${postNumber}`;

    Em.run.schedule('afterRender', () => {
      let elementId;
      let holder;

      if (postNumber === 1 && !opts.anchor) {
        $(window).scrollTop(0);
        return;
      }

      if (opts.anchor) {
        elementId = opts.anchor;
        holder = $(elementId);
      }

      if (!holder || holder.length === 0) {
        elementId = holderId;
        holder = $(elementId);
      }

      const lockon = new LockOn(elementId);

      if (holder.length > 0 && opts && opts.skipIfOnScreen){
        const elementTop = lockon.elementTop();
        const scrollTop = $(window).scrollTop();
        const windowHeight = $(window).height() - offsetCalculator();
        const height = holder.height();

        if (elementTop > scrollTop && (elementTop + height) < (scrollTop + windowHeight)) {
          return;
        }
      }

      lockon.lock();
    });
  },

  // Browser aware replaceState. Will only be invoked if the browser supports it.
  replaceState(path) {
    if (window.history &&
        window.history.pushState &&
        window.history.replaceState &&
        !navigator.userAgent.match(/((iPod|iPhone|iPad).+\bOS\s+[1-4]|WebApps\/.+CFNetwork)/) &&
        (window.location.pathname !== path)) {

        // Always use replaceState in the next runloop to prevent weird routes changing
        // while URLs are loading. For example, while a topic loads it sets `currentPost`
        // which triggers a replaceState even though the topic hasn't fully loaded yet!
        Ember.run.next(() => {
          const location = DiscourseURL.get('router.location');
          if (location && location.replaceURL) {
            location.replaceURL(path);
          }
        });
    }
  },

  routeToTag(a) {
    if (a && a.host !== document.location.host) {
      document.location = a.href;
      return false;
    }

    return this.routeTo(a.href);
  },

  /**
    Our custom routeTo method is used to intelligently overwrite default routing
    behavior.

    It contains the logic necessary to route within a topic using replaceState to
    keep the history intact.
  **/
  routeTo(path, opts) {
    opts = opts || {};

    if (Em.isEmpty(path)) { return; }

    if (Discourse.get('requiresRefresh')) {
      document.location.href = Discourse.getURL(path);
      return;
    }

    const serverSide = SERVER_SIDE_ONLY.some(r => {
      if (path.match(r)) {
        document.location = path;
        return true;
      }
    });

    if (serverSide) { return; }

    // Protocol relative URLs
    if (path.indexOf('//') === 0) {
      document.location = path;
      return;
    }

    // Scroll to the same page, different anchor
    const m = /^#(.+)$/.exec(path);
    if (m) {
      jumpToElement(m[1]);
      return this.replaceState(path);
    }

    const oldPath = window.location.pathname;
    path = path.replace(/(https?\:)?\/\/[^\/]+/, '');

    // handle prefixes
    if (path.match(/^\//)) {
      let rootURL = (Discourse.BaseUri === undefined ? "/" : Discourse.BaseUri);
      rootURL = rootURL.replace(/\/$/, '');
      path = path.replace(rootURL, '');
    }

    // Rewrite /my/* urls
    if (path.indexOf('/my/') === 0) {
      const currentUser = Discourse.User.current();
      if (currentUser) {
        path = path.replace('/my/', '/users/' + currentUser.get('username_lower') + "/");
      } else {
        document.location.href = "/404";
        return;
      }
    }

    rewrites.forEach(rw => path = path.replace(rw.regexp, rw.replacement));

    if (this.navigatedToPost(oldPath, path, opts)) { return; }

    // Schedule a DOM cleanup event
    Em.run.scheduleOnce('afterRender', Discourse.Route, 'cleanDOM');

    if (oldPath === path) {
      // If navigating to the same path send an app event. Views can watch it
      // and tell their controllers to refresh
      this.appEvents.trigger('url:refresh');
    }

    // TODO: Extract into rules we can inject into the URL handler
    if (this.navigatedToHome(oldPath, path, opts)) { return; }

    return this.handleURL(path, opts);
  },

  rewrite(regexp, replacement) {
    rewrites.push({ regexp, replacement });
  },

  redirectTo(url) {
    window.location = Discourse.getURL(url);
  },

  /**
   * Determines whether a URL is internal or not
   *
   * @method isInternal
   * @param {String} url
  **/
  isInternal(url) {
    if (url && url.length) {
      if (url.indexOf('//') === 0) { url = "http:" + url; }
      if (url.indexOf('#') === 0) { return true; }
      if (url.indexOf('/') === 0) { return true; }
      if (url.indexOf(this.origin()) === 0) { return true; }
      if (url.replace(/^http/, 'https').indexOf(this.origin()) === 0) { return true; }
      if (url.replace(/^https/, 'http').indexOf(this.origin()) === 0) { return true; }
    }
    return false;
  },

  /**
    If the URL is in the topic form, /t/something/:topic_id/:post_number
    then we want to apply some special logic. If the post_number changes within the
    same topic, use replaceState and instruct our controller to load more posts.
  **/
  navigatedToPost(oldPath, path, routeOpts) {
    const newMatches = TOPIC_REGEXP.exec(path);
    const newTopicId = newMatches ? newMatches[2] : null;

    if (newTopicId) {
      const oldMatches = TOPIC_REGEXP.exec(oldPath);
      const oldTopicId = oldMatches ? oldMatches[2] : null;

      // If the topic_id is the same
      if (oldTopicId === newTopicId) {
        DiscourseURL.replaceState(path);

        const container = Discourse.__container__;
        const topicController = container.lookup('controller:topic');
        const opts = {};
        const postStream = topicController.get('model.postStream');

        if (newMatches[3]) { opts.nearPost = newMatches[3]; }
        if (path.match(/last$/)) { opts.nearPost = topicController.get('model.highest_post_number'); }
        const closest = opts.nearPost || 1;

        opts.cancelSummary = true;

        postStream.refresh(opts).then(() => {
          topicController.setProperties({
            'model.currentPost': closest,
            enteredAt: new Date().getTime().toString()
          });

          this.appEvents.trigger('post:highlight', closest);
        }).then(() => {
          const jumpOpts = {
            skipIfOnScreen: routeOpts.skipIfOnScreen
          };

          const m = /#.+$/.exec(path);
          if (m) {
            jumpOpts.anchor = m[0];
          }

          this.jumpToPost(closest, jumpOpts);
        });

        // Abort routing, we have replaced our state.
        return true;
      }
    }

    return false;
  },

  /**
    @private

    Handle the custom case of routing to the root path from itself.

    @param {String} oldPath the previous path we were on
    @param {String} path the path we're navigating to
  **/
  navigatedToHome(oldPath, path) {
    const homepage = defaultHomepage();

    if (window.history &&
        window.history.pushState &&
        (path === "/" || path === "/" + homepage) &&
        (oldPath === "/" || oldPath === "/" + homepage)) {
      this.appEvents.trigger('url:refresh');
      return true;
    }

    return false;
  },

  // This has been extracted so it can be tested.
  origin() {
    return window.location.origin + (Discourse.BaseUri === "/" ? '' : Discourse.BaseUri);
  },

  /**
    @private

    Get a handle on the application's router. Note that currently it uses `__container__` which is not
    advised but there is no other way to access the router.

    @property router
  **/
  router: function() {
    return Discourse.__container__.lookup('router:main');
  }.property().volatile(),

  // Get a controller. Note that currently it uses `__container__` which is not
  // advised but there is no other way to access the router.
  controllerFor(name) {
    return Discourse.__container__.lookup('controller:' + name);
  },

  /**
    Be wary of looking up the router. In this case, we have links in our
    HTML, say form compiled markdown posts, that need to be routed.
  **/
  handleURL(path, opts) {
    opts = opts || {};

    const router = this.get('router');

    if (opts.replaceURL) {
      this.replaceState(path);
    } else {
      router.router.updateURL(path);
    }

    const split = path.split('#');
    let elementId;

    if (split.length === 2) {
      path = split[0];
      elementId = split[1];
    }

    const transition = router.handleURL(path);
    transition._discourse_intercepted = true;
    transition.promise.then(() => jumpToElement(elementId));
  }
}).create();

export default DiscourseURL;
<|endoftext|>


<commit_before>import React from 'react';
import _ from 'lodash';
import Stack from '../components/Stack';
import BoxAndWhisker from '../components/BoxAndWhisker';
import DibelsBreakdownBar from '../components/DibelsBreakdownBar';
import {studentsInRoom} from './studentIdsByRoomFunctions';

// This component is written particularly for Somerville and it's likely this would require factoring out
// into `PerDistrict` to respect the way this data is stored across districts.
export default class ClassroomStats extends React.Component {
  constructor(props) {
    super(props);
    this.renderLabelFn = this.renderLabelFn.bind(this);
  }

  studentsInRoom(room) {
    const {students, studentIdsByRoom} = this.props;
    return studentsInRoom(students, studentIdsByRoom, room.roomKey);
  }

  render() {
    const {rooms, gradeLevelNextYear} = this.props;

    // Show different academic indicators by grade level.  STAR starts in 2nd grade.
    const showStar = (['1', '2'].indexOf(gradeLevelNextYear) === -1);
    const showDibels = !showStar;
    return (

              IEP or 504
              Limited or FLEP

                Gender, male

                Low income

                Discipline, 3+

              {showDibels &&

                  Dibels CORE
                	}
              {showStar &&

                  STAR Math
                	}
              {showStar &&

                  STAR Reading
                	}

            {rooms.map(room => {
              const studentsInRoom = this.studentsInRoom(room);
              return (

                  {room.roomName}

                  {this.renderIepOr504(studentsInRoom)}
                  {this.renderEnglishLearners(studentsInRoom)}

                  {this.renderGender(studentsInRoom)}
                  {this.renderLowIncome(studentsInRoom)}

                  {this.renderDiscipline(studentsInRoom)}
                  {showDibels && {this.renderDibelsBreakdown(studentsInRoom)}	}
                  {showStar && {this.renderMath(studentsInRoom)}	}
                  {showStar && {this.renderReading(studentsInRoom)}	}

              );
            })}

    );
  }

  renderIepOr504(studentsInRoom) {
    const count = studentsInRoom.filter(student => {
      return (student.disability !== null || student.plan_504 !== 'Not 504');
    }).length;
    return this.renderStackSimple(count);
  }

  renderEnglishLearners(studentsInRoom) {
    const count = studentsInRoom.filter(student => {
      return ['Limited', 'FLEP'].indexOf(student.limited_english_proficiency !== -1);
    }).length;
    return this.renderStackSimple(count);
  }

  renderGender(studentsInRoom) {
    const count = studentsInRoom.filter(student => student.gender === 'M').length;
    return this.renderStackSimple(count);
  }

  renderLowIncome(studentsInRoom) {
    const count = studentsInRoom.filter(student => {
      return ['Free Lunch', 'Reduced Lunch'].indexOf(student.free_reduced_lunch) !== -1;
    }).length;
    return this.renderStackSimple(count);
  }

  renderDiscipline(studentsInRoom) {
    const count = studentsInRoom.filter(student => {
      return (student.most_recent_school_year_discipline_incidents_count >= 3);
    }).length;
    return this.renderStackSimple(count);
  }

  renderDibelsBreakdown(studentsInRoom) {
    const students = studentsInRoom;
    const dibelsCounts = {
      strategic: 0,
      intensive: 0,
      core: 0
    };
    students.forEach(student => {
      if (!student.latest_dibels) return;
      const level = dibelsLevel(student.latest_dibels);
      dibelsCounts[level] = dibelsCounts[level] + 1;
    });

    return (
      <DibelsBreakdownBar
        coreCount={dibelsCounts.core}
        intensiveCount={dibelsCounts.intensive}
        strategicCount={dibelsCounts.strategic}
        height={5}
        labelTop={5} />
    );
  }

  renderMath(studentsInRoom) {
    return this.renderStar(studentsInRoom, student => student.most_recent_star_math_percentile);
  }

  renderReading(studentsInRoom) {
    return this.renderStar(studentsInRoom, student => student.most_recent_star_reading_percentile);
  }

  renderStar(studentsInRoom, accessor) {
    const values = _.compact(studentsInRoom.map(accessor));
    return (

        {(values.length === 0)
          ? null
          : <BoxAndWhisker values={values} style={{width: 100, marginLeft: 'auto', marginRight: 'auto'}} />}

    );
  }

  // This uses <Stack /> in a way different than intended, where it only
  // shows one bar, and positions the label to the right versus underneath.
  renderStackSimple(count) {
    const {students, rooms} = this.props;

    // tunable, this is a multiplier above the space an even split would take
    const scaleTuningFactor = (students.length / rooms.length) * 1.5;
    const stacks = [{ count, color: 'rgb(137, 175, 202)' }];
    return (
      <Stack
        stacks={stacks}
        style={styles.stackStyle}
        barStyle={styles.stackBarStyle}
        labelStyle={styles.stackLabelStyle}
        scaleFn={count => count / scaleTuningFactor}
        labelFn={this.renderLabelFn} />
    );
  }

  renderLabelFn(count, stack, index) {
    if (count === 0) return '\u00A0';
    return {count};
  }
}
ClassroomStats.propTypes = {
  students: React.PropTypes.array.isRequired,
  gradeLevelNextYear: React.PropTypes.string.isRequired,
  rooms: React.PropTypes.array.isRequired,
  studentIdsByRoom: React.PropTypes.object.isRequired
};

const styles = {
  root: {
    padding: 20,
    paddingBottom: 15,
    borderBottom: '1px solid #eee'
  },
  table: {
    width: '100%',
    textAlign: 'left',
    fontSize: 12,
    tableLayout: 'fixed',
    borderCollapse: 'collapse'
  },
  cell: { /* overridding some global CSS */
    textAlign: 'left',
    fontWeight: 'normal',
    fontSize: 12,
    verticalAlign: 'top',
    overflow: 'hidden'
  },
  heading: {
    textDecoration: 'dashed #ccc underline',
    cursor: 'help',
    paddingBottom: 5
  },
  spacer: {
    width: 1
  },
  stackStyle: {
    paddingTop: 5,
    height: 20,
    marginBottom: 1
  },
  stackBarStyle: {
    height: 3
  },
  // Positions label to the right of bar
  stackLabelStyle: {
    fontSize: 10,
    display: 'flex',
    alignItems: 'flex-end',
    justifyContent: 'flex-end',
    position: 'relative',
    top: -5,
    left: 12
  }
};

function dibelsLevel(dibels) {
  const performanceLevel = dibels.performance_level.toLowerCase();
  if (performanceLevel.indexOf('core') !== -1) return 'core';
  if (performanceLevel.indexOf('strategic') !== -1) return 'strategic';
  if (performanceLevel.indexOf('intensive') !== -1) return 'intensive';
  return null;
}<commit_msg>Tweaking bars<commit_after>import React from 'react';
import _ from 'lodash';
import Stack from '../components/Stack';
import BoxAndWhisker from '../components/BoxAndWhisker';
import DibelsBreakdownBar from '../components/DibelsBreakdownBar';
import {studentsInRoom} from './studentIdsByRoomFunctions';

// This component is written particularly for Somerville and it's likely this would require factoring out
// into `PerDistrict` to respect the way this data is stored across districts.
export default class ClassroomStats extends React.Component {
  constructor(props) {
    super(props);
    this.renderLabelFn = this.renderLabelFn.bind(this);
  }

  studentsInRoom(room) {
    const {students, studentIdsByRoom} = this.props;
    return studentsInRoom(students, studentIdsByRoom, room.roomKey);
  }

  render() {
    const {rooms, gradeLevelNextYear} = this.props;

    // Show different academic indicators by grade level.  STAR starts in 2nd grade.
    const showStar = (['1', '2'].indexOf(gradeLevelNextYear) === -1);
    const showDibels = !showStar;
    return (

              IEP or 504
              Limited or FLEP

                Gender, male

                Low income

                Discipline, 3+

              {showDibels &&

                  Dibels CORE
                	}
              {showStar &&

                  STAR Math
                	}
              {showStar &&

                  STAR Reading
                	}

            {rooms.map(room => {
              const studentsInRoom = this.studentsInRoom(room);
              return (

                  {room.roomName}

                  {this.renderIepOr504(studentsInRoom)}
                  {this.renderEnglishLearners(studentsInRoom)}

                  {this.renderGender(studentsInRoom)}
                  {this.renderLowIncome(studentsInRoom)}

                  {this.renderDiscipline(studentsInRoom)}
                  {showDibels && {this.renderDibelsBreakdown(studentsInRoom)}	}
                  {showStar && {this.renderMath(studentsInRoom)}	}
                  {showStar && {this.renderReading(studentsInRoom)}	}

              );
            })}

    );
  }

  renderIepOr504(studentsInRoom) {
    const count = studentsInRoom.filter(student => {
      return (student.disability !== null || student.plan_504 !== 'Not 504');
    }).length;
    return this.renderStackSimple(count);
  }

  renderEnglishLearners(studentsInRoom) {
    const count = studentsInRoom.filter(student => {
      return ['Limited', 'FLEP'].indexOf(student.limited_english_proficiency !== -1);
    }).length;
    return this.renderStackSimple(count);
  }

  renderGender(studentsInRoom) {
    const count = studentsInRoom.filter(student => student.gender === 'M').length;
    return this.renderStackSimple(count);
  }

  renderLowIncome(studentsInRoom) {
    const count = studentsInRoom.filter(student => {
      return ['Free Lunch', 'Reduced Lunch'].indexOf(student.free_reduced_lunch) !== -1;
    }).length;
    return this.renderStackSimple(count);
  }

  renderDiscipline(studentsInRoom) {
    const count = studentsInRoom.filter(student => {
      return (student.most_recent_school_year_discipline_incidents_count >= 3);
    }).length;
    return this.renderStackSimple(count);
  }

  renderDibelsBreakdown(studentsInRoom) {
    const students = studentsInRoom;
    const dibelsCounts = {
      strategic: 0,
      intensive: 0,
      core: 0
    };
    students.forEach(student => {
      if (!student.latest_dibels) return;
      const level = dibelsLevel(student.latest_dibels);
      dibelsCounts[level] = dibelsCounts[level] + 1;
    });

    return (
      <DibelsBreakdownBar
        coreCount={dibelsCounts.core}
        intensiveCount={dibelsCounts.intensive}
        strategicCount={dibelsCounts.strategic}
        style={{paddingTop: 2}}
        height={5}
        labelTop={5} />
    );
  }

  renderMath(studentsInRoom) {
    return this.renderStar(studentsInRoom, student => student.most_recent_star_math_percentile);
  }

  renderReading(studentsInRoom) {
    return this.renderStar(studentsInRoom, student => student.most_recent_star_reading_percentile);
  }

  renderStar(studentsInRoom, accessor) {
    const values = _.compact(studentsInRoom.map(accessor));
    return (

        {(values.length === 0)
          ? null
          : <BoxAndWhisker values={values} style={{width: 100, marginLeft: 'auto', marginRight: 'auto'}} />}

    );
  }

  // This uses <Stack /> in a way different than intended, where it only
  // shows one bar, and positions the label to the right versus underneath.
  renderStackSimple(count) {
    const {students, rooms} = this.props;

    // tunable, this is a multiplier above the space an even split would take
    const scaleTuningFactor = (students.length / rooms.length) * 1.5;
    const stacks = [{ count, color: 'rgb(137, 175, 202)' }];
    return (
      <Stack
        stacks={stacks}
        style={styles.stackStyle}
        barStyle={styles.stackBarStyle}
        labelStyle={styles.stackLabelStyle}
        scaleFn={count => count / scaleTuningFactor}
        labelFn={this.renderLabelFn} />
    );
  }

  renderLabelFn(count, stack, index) {
    if (count === 0) return '\u00A0';
    return {count};
  }
}
ClassroomStats.propTypes = {
  students: React.PropTypes.array.isRequired,
  gradeLevelNextYear: React.PropTypes.string.isRequired,
  rooms: React.PropTypes.array.isRequired,
  studentIdsByRoom: React.PropTypes.object.isRequired
};

const styles = {
  root: {
    padding: 20,
    paddingBottom: 15,
    borderBottom: '1px solid #eee'
  },
  table: {
    width: '100%',
    textAlign: 'left',
    fontSize: 12,
    tableLayout: 'fixed',
    borderCollapse: 'collapse'
  },
  cell: { /* overridding some global CSS */
    textAlign: 'left',
    fontWeight: 'normal',
    fontSize: 12,
    verticalAlign: 'top',
    overflow: 'hidden'
  },
  heading: {
    textDecoration: 'dashed #ccc underline',
    cursor: 'help',
    paddingBottom: 5
  },
  spacer: {
    width: 1
  },
  stackStyle: {
    paddingTop: 5,
    height: 20,
    marginBottom: 1
  },
  stackBarStyle: {
    height: 3
  },
  // Positions label to the right of bar
  stackLabelStyle: {
    fontSize: 10,
    display: 'flex',
    alignItems: 'flex-end',
    justifyContent: 'flex-end',
    position: 'relative',
    top: -5,
    left: 12
  }
};

function dibelsLevel(dibels) {
  const performanceLevel = dibels.performance_level.toLowerCase();
  if (performanceLevel.indexOf('core') !== -1) return 'core';
  if (performanceLevel.indexOf('strategic') !== -1) return 'strategic';
  if (performanceLevel.indexOf('intensive') !== -1) return 'intensive';
  return null;
}<|endoftext|>


<commit_before>Ext.define('FastUI.view.VForm', {
    extend:'Ext.form.Panel',
    title:'Simple Form',
    bodyPadding:5,
    url:'save-form.php',
    layout:'anchor',
    defaults:{
        anchor:'100%'
    },
    defaultType:'textfield',

    initComponent:function () {
//        this.title = this.vfactory.getVData().title;
        this.url = '/fastui/m_windows.json';
        this.items = this.vfactory.getFormFields();
        this.reader = this.getReader();
        this.callParent();

    },
    getReader: function(){
        var data = this.vfactory.getVData();
        alert(data.title);

    },

    // Reset and Submit buttons
    buttons:[
        {
            text:'Reset',
            handler:function () {
                this.up('form').getForm().reset();
            }
        },
        {
            text:'Submit',
            formBind:true, //only enabled once the form is valid
            disabled:true,
            handler:function () {
                var form = this.up('form').getForm();
//                if (form.isValid()) {
                    form.submit({
                        success:function (form, action) {
                            Ext.Msg.alert('Success', action.result.msg);
                        },
                        failure:function (form, action) {
                            Ext.Msg.alert('Failed', action.result.msg);
                        }
                    });
//                }
            }
        }
    ]
//    submitForm: function(){
//        Ext.MessageBox.show({
//            title: '\u8bf7\u7a0d\u7b49',//请稍等
//            msg: '\u6b63\u5728\u63d0\u4ea4...',//正在提交...
//            progressText: '',
//            width: 300,
//            progress: true,
//            closable: false,
//            animEl: 'loding'
//        });
//        //set the speed of the submit action
//        var f = function(v){
//            return function(){
//                var i = v/11;
//                Ext.MessageBox.updateProgress(i, '');
//            };
//        }
//        for(var i = 1; i < 13; i++){
//            setTimeout(f(i), i*150);
//        }
//        this.form.submit({
//            url     : this.url,
//            params  : this.params,
//            success : this.onSuccess,
//            failure : this.onFailure,
//            scope   : this});
//    },
//
//    onSuccess: function(){
//        Ext.MessageBox.alert('\u63d0\u793a', '\u64cd\u4f5c\u6210\u529f\uff01');//'提示', '操作成功！'
////        this.findParentBy(function(c){return Rib.vp.VTab.prototype.isPrototypeOf(c)}).load_grid();
////       this.findParentByType(Rib.vp.VTab).loadGrid();
//    },
//
//    onFailure: function(){
//        Ext.MessageBox.alert('\u63d0\u793a', '\u64cd\u4f5c\u5931\u8d25\uff01');//'提示', '操作失败！'
//    }
});<commit_msg>added VGrid and VForm<commit_after>Ext.define('FastUI.view.VForm', {
    extend:'Ext.form.Panel',
    title:'Simple Form',
    bodyPadding:5,
    url:'save-form.php',
    layout:'anchor',
    defaults:{
        anchor:'100%'
    },
    defaultType:'textfield',

    initComponent:function () {
//        this.title = this.vfactory.getVData().title;
        this.url = '/fastui/m_windows.json';
        this.items = this.vfactory.getFormFields();
        this.reader = this.getReader();
        this.callParent();

    },
    getReader: function(){
        var data = this.vfactory.getVData();
        alert(data.title);
        return new Ext.data.reader.Json({
            fields: [
                        { "name": "userId", "type": "int" },
                        { "name": "name", "type": "string" },
                        { "name": "birthday", "type": "date", "dateFormat": "Y-j-m" }
                    ],
            columns: [
                        { "text": "User ID", "dataIndex": "userId", "width": 40 },
                        { "text": "User Name", "dataIndex": "name", "flex": 1 },
                        { "text": "Birthday", "dataIndex": "birthday", "flex": 1, "format": 'Y-j-m', "xtype": "datecolumn" }
                    ]

        });

    },

    // Reset and Submit buttons
    buttons:[
        {
            text:'Reset',
            handler:function () {
                this.up('form').getForm().reset();
            }
        },
        {
            text:'Submit',
            formBind:true, //only enabled once the form is valid
            disabled:true,
            handler:function () {
                var form = this.up('form').getForm();
//                if (form.isValid()) {
                    form.submit({
                        success:function (form, action) {
                            Ext.Msg.alert('Success', action.result.msg);
                        },
                        failure:function (form, action) {
                            Ext.Msg.alert('Failed', action.result.msg);
                        }
                    });
//                }
            }
        }
    ]
//    submitForm: function(){
//        Ext.MessageBox.show({
//            title: '\u8bf7\u7a0d\u7b49',//请稍等
//            msg: '\u6b63\u5728\u63d0\u4ea4...',//正在提交...
//            progressText: '',
//            width: 300,
//            progress: true,
//            closable: false,
//            animEl: 'loding'
//        });
//        //set the speed of the submit action
//        var f = function(v){
//            return function(){
//                var i = v/11;
//                Ext.MessageBox.updateProgress(i, '');
//            };
//        }
//        for(var i = 1; i < 13; i++){
//            setTimeout(f(i), i*150);
//        }
//        this.form.submit({
//            url     : this.url,
//            params  : this.params,
//            success : this.onSuccess,
//            failure : this.onFailure,
//            scope   : this});
//    },
//
//    onSuccess: function(){
//        Ext.MessageBox.alert('\u63d0\u793a', '\u64cd\u4f5c\u6210\u529f\uff01');//'提示', '操作成功！'
////        this.findParentBy(function(c){return Rib.vp.VTab.prototype.isPrototypeOf(c)}).load_grid();
////       this.findParentByType(Rib.vp.VTab).loadGrid();
//    },
//
//    onFailure: function(){
//        Ext.MessageBox.alert('\u63d0\u793a', '\u64cd\u4f5c\u5931\u8d25\uff01');//'提示', '操作失败！'
//    }
});<|endoftext|>


<commit_before>//= require fe/fe.common.js
//= require fe/jquery.scrollTo-min.js
//= require fe/jquery.validate.pack.js

// used by answer sheets

(function($) {
  //debugger;
  $(function() {

    $(document).on('click', '.save_button', function() {
      $.fe.pageHandler.savePage($(this).closest('.answer-page'), true);
    });

    $(document).on('click', '.reference_send_invite', function() {
      var el = this;
      var data = $(el).closest('form').serializeArray();

      data.push({name: 'answer_sheet_type', value: answer_sheet_type});
      $.ajax({url: $(el).attr('href'), data: data, dataType: 'script',  type: 'POST',
             beforeSend: function (xhr) {
               $('body').trigger('ajax:loading', xhr);
             },
             complete: function (xhr) {
               $('body').trigger('ajax:complete', xhr);
             },
             error: function (xhr, status, error) {
               $('body').trigger('ajax:failure', [xhr, status, error]);
             }
      });
      return false;
    });
    $(document).on('focus', 'textarea[maxlength]', function() {
      var max = parseInt($(this).attr('maxlength'));
      $(this).parent().find('.charsRemaining').html('You have ' + (max - $(this).val().length) + ' characters remaining');
    }).on('keyup', 'textarea[maxlength]', function(){
      var max = parseInt($(this).attr('maxlength'));
      if($(this).val().length > max){
        $(this).val($(this).val().substr(0, $(this).attr('maxlength')));
      }
      $(this).parent().find('.charsRemaining').html('You have ' + (max - $(this).val().length) + ' characters remaining');
    }).on('blur', 'textarea[maxlength]', function() {
      $(this).parent().find('.charsRemaining').html('');
    });
  });

  $.fe = {};
  $.fe.pageHandler = {
    initialize : function(page) {
      this.auto_save_frequency = 30;  // seconds
      this.timer_id = null;
      this.suspendLoad = false;

      this.current_page = page;
      $('#' + page).data('form_data', this.captureForm($('#' + page)));
      this.registerAutoSave();

      this.page_validation = {};  // validation objects for each page
      this.enableValidation(page);

      // this.background_load = false;
      // this.final_submission = false;
    },

    // swap to a different page
    showPage : function(page) {
      // hide the old
      $('#' + this.current_page + '-li').removeClass('active');
      $('#' + this.current_page).hide();

      // HACK: Need to clear the main error message when returning to the submit page
      //       It is very confusing to users to be there when they revisit the page
      if ((page=='submit_page') && ($('#submit_message')[0] != null)) $('#submit_message').hide();
      if ((page=='submit_page') && ($('#application_errors')[0] != null)) $('#application_errors').html('');

      // show the new
      // $('#' + page + '-li').removeClass('incomplete');
      // $('#' + page + '-li').removeClass('valid');
      $('#' + page + '-li').addClass('active');
      $('#' + page).show();
      this.current_page = page;
      this.registerAutoSave(page);
      this.suspendLoad = false;
      fixGridColumnWidths();
      $(document).trigger('feShowPage'); // allow other code to handle show page event by using $(document).on('feShowPage', function() { ... });
    },

    // callback onSuccess
    pageLoadedBackground : function(response, textStatus, jqXHR) {
      //this.pageLoaded(response, textStatus, jqXHR, true)
      $.fe.pageHandler.pageLoaded(response, textStatus, jqXHR, true)
    },

    // callback onSuccess
    pageLoaded : function(response, textStatus, jqXHR, background_load) {
      background_load = typeof background_load !== 'undefined' ? background_load : false;

      // var response = new String(transport.responseText);
      var match = response.match(/<div id=\"(.*?)\"/i); // what did I just load? parse out the first div id
      if( match != null )
        {
          var page = match[1];
          if ($('#'+page).length > 0) {
            $('#'+page).replaceWith(response);
          } else {
            $('#preview').append(response);
          }

          background_load ? this.suspendLoad = false : $.fe.pageHandler.showPage(page);  // show after load, unless loading in background
          setUpJsHelpers();
          $.fe.pageHandler.enableValidation(page);
          if (background_load) { $.fe.pageHandler.validatePage(page, true); }
          $('#' + page).data('form_data', $.fe.pageHandler.captureForm($('#' + page)));
        }
        $('#page_ajax_spinner').hide();
        $('.reference_send_invite').button();
        updateTotals();
        $(document).trigger('fePageLoaded'); // allow other code to handle page load event by using $(document).on('fePageLoaded', function() { ... });
    },

    loadPage : function(page, url, background_load) {
      background_load = typeof background_load !== 'undefined' ? background_load : false;

      if (!this.suspendLoad) {
        this.suspendLoad = true; // don't load a page if one is currently loading (prevent double-click behavior where two pages end up visible!)

        this.unregisterAutoSave();  // don't auto-save while loading/saving
        // will register auto-save on new page once loaded/shown

        if (!background_load) { $.scrollTo('#main'); }

        this.validatePage(this.current_page);   // mark current page as valid (or not) as we're leaving

        this.savePage();

        if( $.fe.pageHandler.isPageLoaded(page) && page.match('no_cache') == null )   // if already loaded (element exists) excluding pages that need reloading
          {
            if (!background_load) { $.fe.pageHandler.showPage(page); }
            $('#page_ajax_spinner').hide();
          }
          else
            {
              $.ajax({
                url: url,
                type: 'GET',
                data: {'answer_sheet_type':answer_sheet_type},
                success: background_load ? $.fe.pageHandler.pageLoadedBackground : $.fe.pageHandler.pageLoaded,
                error: function (xhr, status, error) {
                  alert("There was a problem loading that page. We've been notified and will fix it as soon as possible. To work on other pages, please refresh the website.");
                  document.location = document.location;
                },
                beforeSend: function (xhr) {
                  $('body').trigger('ajax:loading', xhr);
                },
                complete: function (xhr) {
                  $('body').trigger('ajax:complete', xhr);
                }
              });
              // new Ajax.Request(url, {asynchronous:true, evalScripts:false, method:'get',
              //     onSuccess:this.pageLoaded.bind(this)});
            }
      }
    },

    // save form if any changes were made
    savePage : function(page, force) {
      if (page == null) page = $('#' + this.current_page);
      // don't save more than once per second
      timeNow = new Date();
      if (typeof(lastSave) != "undefined" && lastSave && !force && (timeNow - lastSave < 1000)) {
        return true;
      }
      lastSave = timeNow;
      form_data = this.captureForm(page);
      if( form_data ) {
        if( page.data('form_data') == null || page.data('form_data').data !== form_data.data || force === true) {  // if any changes
          page.data('form_data', form_data);
          $.ajax({url: form_data.url, type: 'put', data: form_data.data,
                 beforeSend: function (xhr) {
                   $('#spinner_' + page.attr('id')).show();
                 },
                 complete: function (xhr) {
                   $('#spinner_' + page.attr('id')).hide();
                 },
                 error: function() {
                   page.data('form_data', null);    // on error, force save for next call to save
                   // WARNING: race conditions with load & show?
                   // sort of a mess if save fails while another page is already loading!!
                 }});
        }
      }
      // Update last saved stamp
    },

    savePages : function(force) {
      $('.answer-page').each(function() {$.fe.pageHandler.savePage($(this), force)})
    },

    // setup a timer to auto-save (only one timer, for the page being viewed)
    registerAutoSave: function(page) {
      this.timer_id = setInterval(this.savePages, this.auto_save_frequency * 1000);
    },

    unregisterAutoSave: function() {
      if( this.timer_id != null )
        {
          clearInterval(this.timer_id);
          this.timer_id = null;
        }
    },

    // serialize form data and extract url to post to
    captureForm : function(page) {
      form_el = $('#' + page.attr('id') + '-form');
      if( form_el[0] == null ) return null;
      return {url: form_el.attr('action'), data: form_el.serialize() + '&answer_sheet_type=' + answer_sheet_type};
    },

    // enable form validation (when form is loaded)
    enableValidation : function(page) {
      $('#' + page + '-form').validate({onsubmit:false, focusInvalid:true, onfocusout: function(element) { this.element(element);}});
      // $('#' + page + '-form').valid();
    },

    validatePage : function(page, page_classes_only) {
      page_classes_only = typeof page_classes_only !== 'undefined' ? page_classes_only : false;

      try {
        var li = $('#' + page + '-li');
        var form = $('#' + page + '-form');

        valid = form.valid();

        if (!page_classes_only) {
          // Move radio button errors up
          $('input[type=radio].error').closest('tr').addClass('error');
          $('.choice_field input[type=radio].error').removeClass('error')
          .closest('.choice_field')
          .addClass('error');
          $('div.yesno label.error').hide();
        }

        if (valid)  {
          li.removeClass('incomplete');
          li.addClass('complete');
          $(document).trigger('fePageValid', page); // allow other code to handle show page event by using $(document).on('fePageValid', function() { ... });
        } else {
          li.removeClass('complete');
          li.addClass('incomplete');
          $(document).trigger('fePageInvalid', page); // allow other code to handle show page event by using $(document).on('fePageInvalid', function() { ... });
        }
        return valid;
      }
      catch(err) {

        // If the user clicks too quickly, sometimes the page element isn't properly defined yet.
        // If we don't catch the error, js stops execution. If we catch it, the user just has to click again.
      }
      $('page_ajax_spinner').hide();
    },

    // callback when falls to 0 active Ajax requests
    completeAll : function()
    {
      $('#submit_button').attr('disabled', true)
      $('#submit_message').html('');
      $('#submit_message').hide();
      // validate all the pages
      $('.page_link').each(function(index, page) {
        $.fe.pageHandler.validatePage($(page).attr('data-page-id'));
      });
      var all_valid = ($('#list-pages li.incomplete').length == 0);

      // Make sure any necessary payments are made
      var payments_made = $('.payment_question.required').length <= $('.payment').length

      if(  payments_made)
        {
          this.savePage($('#' + $.fe.pageHandler.current_page));  // in case any input fields on submit_page

          // submit the application
          if($('#submit_to')[0] != null)
            {
              url = $('#submit_to').val();
              // clear out pages array to force reload.  This enables "frozen" apps
              //       immediately after submission - :onSuccess (for USCM which stays in the application vs. redirecting to the dashboard)
              var curr = $.fe.pageHandler.current_page;
              $.ajax({url: url, dataType:'script',
                     data: {answer_sheet_type: answer_sheet_type},
                     type:'post',
                     beforeSend: function(xhr) {
                       $('body').trigger('ajax:loading', xhr);
                     },
                     success: function(xhr) {
                       $('#list-pages a').each(function() {
                         if ($(this).attr('data-page-id') != curr) $('#' + $(this).attr('data-page-id')).remove();
                       })
                     },
                     complete: function(xhr) {
                       $('body').trigger('ajax:complete', xhr);
                       var btn = $('#submit_button');
                       if (btn) { btn.attr('disabled', false); }
                     }
              });
            }
        }
        else
          {
            // some pages aren't valid
            $('#submit_message').html("Please make a payment");
            $('#submit_message').show();

            var btn = $('#submit_button'); if (btn) { btn.attr('disabled', false); }
          }
    },

    // is page loaded? (useful for toggling enabled state of questions)
    isPageLoaded : function(page)
    {
      return $('#' + page)[0] != null
    },

    checkConditional : function($element_li) {
      matchable_answers = String($element_li.data('conditional_answer')).split(',').map(function(s) { return s.trim(); })
      if ($element_li.hasClass('fe_choicefield') && $element_li.hasClass('style_yes-no')) {
        if ($(matchable_answers).filter([1, '1', true, 'true', 'yes', 'Yes']).length > 0) {
          matchable_answers = [1, '1', true, 'true', 'yes', 'Yes'];
        }
        if ($(matchable_answers).filter([0, '0', false, 'false', 'no', 'No']).length > 0) {
          matchable_answers = [0, '0', false, 'false', 'no', 'No'];
        }
        vals = $([$element_li.find("input[type=radio]:checked").val()]);
      } else if ($element_li.hasClass('fe_choicefield') && $element_li.hasClass('checkbox')) {
        vals = $element_li.find("input[type=checkbox]:checked").map(function(i, el) { return $(el).val(); });
      } else {
        vals = $([$element_li.find("input:visible, select:visible").val()]);
      }
      match = $(matchable_answers).filter(vals).length > 0;

      switch ($element_li.data('conditional_type')) {
        case 'Fe::Element':
          if (match) {
            $("#element_" + $element_li.data('conditional_id')).show();
          } else {
            $("#element_" + $element_li.data('conditional_id')).hide();
          }
        case 'Fe::Page':
          prefix = $element_li.data('answer_sheet_id_prefix');
          pg = prefix + '_' + $element_li.data('application_id') + '-fe_page_' + $element_li.data('conditional_id');
          li_id = 'li#'+pg+'-li';

          if (match) {
            $(li_id).show();
            // load the page (in the background) to determine validity
            this.loadPage(pg, $(li_id).find('a').attr('href'), true);
          } else {
            $(li_id).hide();
          }
      }
    }

  };

  $(document).on('click', "li.conditional input, li.conditional select", function() {
    $.fe.pageHandler.checkConditional($(this).closest('li'));
  });
  $(document).on('keyup', "li.conditional input, li.conditional select", function() { $(this).click(); });
  $(document).on('blug', "li.conditional input, li.conditional select", function() { $(this).click(); });
  $(document).on('change', "li.conditional select", function() { $(this).click(); });

})(jQuery);

$(function() {
  fixGridColumnWidths();
  setUpCalendars();
});

function updateTotal(id) {
  try {
    total = 0;
    $(".col_" + id).each(function(index, el) {
      total += Number($(el).val());
    });
    $('#total_' + id).val(total).trigger('totalChanged');
  } catch(e) {
  }
}

function submitToFrame(id, url) {
  form = $('<form method="post" action="'+url+'.js" endtype="multipart/form-data"></form>')
  var csrf_token = $('meta[name=csrf-token]').attr('content'),
  csrf_param = $('meta[name=csrf-param]').attr('content'),
  dom_id = '#attachment_field_' + id,
  metadata_input = '<input name="'+csrf_param+'" value="'+csrf_token+'" type="hidden" />',
  file_field = '<input type="file" name="answers['+ id + ']>';
  if ($(dom_id).val() == '')	return;
  form.hide()
  .append(metadata_input)
  .append(file_field)
  .appendTo('body');
  $(dom_id + "-spinner").show();
  form.submit();
  return false
}
<commit_msg>trigger a feShowPage on initial page load<commit_after>//= require fe/fe.common.js
//= require fe/jquery.scrollTo-min.js
//= require fe/jquery.validate.pack.js

// used by answer sheets

(function($) {
  //debugger;
  $(function() {

    $(document).on('click', '.save_button', function() {
      $.fe.pageHandler.savePage($(this).closest('.answer-page'), true);
    });

    $(document).on('click', '.reference_send_invite', function() {
      var el = this;
      var data = $(el).closest('form').serializeArray();

      data.push({name: 'answer_sheet_type', value: answer_sheet_type});
      $.ajax({url: $(el).attr('href'), data: data, dataType: 'script',  type: 'POST',
             beforeSend: function (xhr) {
               $('body').trigger('ajax:loading', xhr);
             },
             complete: function (xhr) {
               $('body').trigger('ajax:complete', xhr);
             },
             error: function (xhr, status, error) {
               $('body').trigger('ajax:failure', [xhr, status, error]);
             }
      });
      return false;
    });
    $(document).on('focus', 'textarea[maxlength]', function() {
      var max = parseInt($(this).attr('maxlength'));
      $(this).parent().find('.charsRemaining').html('You have ' + (max - $(this).val().length) + ' characters remaining');
    }).on('keyup', 'textarea[maxlength]', function(){
      var max = parseInt($(this).attr('maxlength'));
      if($(this).val().length > max){
        $(this).val($(this).val().substr(0, $(this).attr('maxlength')));
      }
      $(this).parent().find('.charsRemaining').html('You have ' + (max - $(this).val().length) + ' characters remaining');
    }).on('blur', 'textarea[maxlength]', function() {
      $(this).parent().find('.charsRemaining').html('');
    });
  });

  $.fe = {};
  $.fe.pageHandler = {
    initialize : function(page) {
      this.auto_save_frequency = 30;  // seconds
      this.timer_id = null;
      this.suspendLoad = false;

      this.current_page = page;
      $('#' + page).data('form_data', this.captureForm($('#' + page)));
      this.registerAutoSave();

      this.page_validation = {};  // validation objects for each page
      this.enableValidation(page);

      $(document).trigger('feShowPage'); // allow other code to handle show page event by using $(document).on('feShowPage', function() { ... });

      // this.background_load = false;
      // this.final_submission = false;
    },

    // swap to a different page
    showPage : function(page) {
      // hide the old
      $('#' + this.current_page + '-li').removeClass('active');
      $('#' + this.current_page).hide();

      // HACK: Need to clear the main error message when returning to the submit page
      //       It is very confusing to users to be there when they revisit the page
      if ((page=='submit_page') && ($('#submit_message')[0] != null)) $('#submit_message').hide();
      if ((page=='submit_page') && ($('#application_errors')[0] != null)) $('#application_errors').html('');

      // show the new
      // $('#' + page + '-li').removeClass('incomplete');
      // $('#' + page + '-li').removeClass('valid');
      $('#' + page + '-li').addClass('active');
      $('#' + page).show();
      this.current_page = page;
      this.registerAutoSave(page);
      this.suspendLoad = false;
      fixGridColumnWidths();
      $(document).trigger('feShowPage'); // allow other code to handle show page event by using $(document).on('feShowPage', function() { ... });
    },

    // callback onSuccess
    pageLoadedBackground : function(response, textStatus, jqXHR) {
      //this.pageLoaded(response, textStatus, jqXHR, true)
      $.fe.pageHandler.pageLoaded(response, textStatus, jqXHR, true)
    },

    // callback onSuccess
    pageLoaded : function(response, textStatus, jqXHR, background_load) {
      background_load = typeof background_load !== 'undefined' ? background_load : false;

      // var response = new String(transport.responseText);
      var match = response.match(/<div id=\"(.*?)\"/i); // what did I just load? parse out the first div id
      if( match != null )
        {
          var page = match[1];
          if ($('#'+page).length > 0) {
            $('#'+page).replaceWith(response);
          } else {
            $('#preview').append(response);
          }

          background_load ? this.suspendLoad = false : $.fe.pageHandler.showPage(page);  // show after load, unless loading in background
          setUpJsHelpers();
          $.fe.pageHandler.enableValidation(page);
          if (background_load) { $.fe.pageHandler.validatePage(page, true); }
          $('#' + page).data('form_data', $.fe.pageHandler.captureForm($('#' + page)));
        }
        $('#page_ajax_spinner').hide();
        $('.reference_send_invite').button();
        updateTotals();
        $(document).trigger('fePageLoaded'); // allow other code to handle page load event by using $(document).on('fePageLoaded', function() { ... });
    },

    loadPage : function(page, url, background_load) {
      background_load = typeof background_load !== 'undefined' ? background_load : false;

      if (!this.suspendLoad) {
        this.suspendLoad = true; // don't load a page if one is currently loading (prevent double-click behavior where two pages end up visible!)

        this.unregisterAutoSave();  // don't auto-save while loading/saving
        // will register auto-save on new page once loaded/shown

        if (!background_load) { $.scrollTo('#main'); }

        this.validatePage(this.current_page);   // mark current page as valid (or not) as we're leaving

        this.savePage();

        if( $.fe.pageHandler.isPageLoaded(page) && page.match('no_cache') == null )   // if already loaded (element exists) excluding pages that need reloading
          {
            if (!background_load) { $.fe.pageHandler.showPage(page); }
            $('#page_ajax_spinner').hide();
          }
          else
            {
              $.ajax({
                url: url,
                type: 'GET',
                data: {'answer_sheet_type':answer_sheet_type},
                success: background_load ? $.fe.pageHandler.pageLoadedBackground : $.fe.pageHandler.pageLoaded,
                error: function (xhr, status, error) {
                  alert("There was a problem loading that page. We've been notified and will fix it as soon as possible. To work on other pages, please refresh the website.");
                  document.location = document.location;
                },
                beforeSend: function (xhr) {
                  $('body').trigger('ajax:loading', xhr);
                },
                complete: function (xhr) {
                  $('body').trigger('ajax:complete', xhr);
                }
              });
              // new Ajax.Request(url, {asynchronous:true, evalScripts:false, method:'get',
              //     onSuccess:this.pageLoaded.bind(this)});
            }
      }
    },

    // save form if any changes were made
    savePage : function(page, force) {
      if (page == null) page = $('#' + this.current_page);
      // don't save more than once per second
      timeNow = new Date();
      if (typeof(lastSave) != "undefined" && lastSave && !force && (timeNow - lastSave < 1000)) {
        return true;
      }
      lastSave = timeNow;
      form_data = this.captureForm(page);
      if( form_data ) {
        if( page.data('form_data') == null || page.data('form_data').data !== form_data.data || force === true) {  // if any changes
          page.data('form_data', form_data);
          $.ajax({url: form_data.url, type: 'put', data: form_data.data,
                 beforeSend: function (xhr) {
                   $('#spinner_' + page.attr('id')).show();
                 },
                 complete: function (xhr) {
                   $('#spinner_' + page.attr('id')).hide();
                 },
                 error: function() {
                   page.data('form_data', null);    // on error, force save for next call to save
                   // WARNING: race conditions with load & show?
                   // sort of a mess if save fails while another page is already loading!!
                 }});
        }
      }
      // Update last saved stamp
    },

    savePages : function(force) {
      $('.answer-page').each(function() {$.fe.pageHandler.savePage($(this), force)})
    },

    // setup a timer to auto-save (only one timer, for the page being viewed)
    registerAutoSave: function(page) {
      this.timer_id = setInterval(this.savePages, this.auto_save_frequency * 1000);
    },

    unregisterAutoSave: function() {
      if( this.timer_id != null )
        {
          clearInterval(this.timer_id);
          this.timer_id = null;
        }
    },

    // serialize form data and extract url to post to
    captureForm : function(page) {
      form_el = $('#' + page.attr('id') + '-form');
      if( form_el[0] == null ) return null;
      return {url: form_el.attr('action'), data: form_el.serialize() + '&answer_sheet_type=' + answer_sheet_type};
    },

    // enable form validation (when form is loaded)
    enableValidation : function(page) {
      $('#' + page + '-form').validate({onsubmit:false, focusInvalid:true, onfocusout: function(element) { this.element(element);}});
      // $('#' + page + '-form').valid();
    },

    validatePage : function(page, page_classes_only) {
      page_classes_only = typeof page_classes_only !== 'undefined' ? page_classes_only : false;

      try {
        var li = $('#' + page + '-li');
        var form = $('#' + page + '-form');

        valid = form.valid();

        if (!page_classes_only) {
          // Move radio button errors up
          $('input[type=radio].error').closest('tr').addClass('error');
          $('.choice_field input[type=radio].error').removeClass('error')
          .closest('.choice_field')
          .addClass('error');
          $('div.yesno label.error').hide();
        }

        if (valid)  {
          li.removeClass('incomplete');
          li.addClass('complete');
          $(document).trigger('fePageValid', page); // allow other code to handle show page event by using $(document).on('fePageValid', function() { ... });
        } else {
          li.removeClass('complete');
          li.addClass('incomplete');
          $(document).trigger('fePageInvalid', page); // allow other code to handle show page event by using $(document).on('fePageInvalid', function() { ... });
        }
        return valid;
      }
      catch(err) {

        // If the user clicks too quickly, sometimes the page element isn't properly defined yet.
        // If we don't catch the error, js stops execution. If we catch it, the user just has to click again.
      }
      $('page_ajax_spinner').hide();
    },

    // callback when falls to 0 active Ajax requests
    completeAll : function()
    {
      $('#submit_button').attr('disabled', true)
      $('#submit_message').html('');
      $('#submit_message').hide();
      // validate all the pages
      $('.page_link').each(function(index, page) {
        $.fe.pageHandler.validatePage($(page).attr('data-page-id'));
      });
      var all_valid = ($('#list-pages li.incomplete').length == 0);

      // Make sure any necessary payments are made
      var payments_made = $('.payment_question.required').length <= $('.payment').length

      if(  payments_made)
        {
          this.savePage($('#' + $.fe.pageHandler.current_page));  // in case any input fields on submit_page

          // submit the application
          if($('#submit_to')[0] != null)
            {
              url = $('#submit_to').val();
              // clear out pages array to force reload.  This enables "frozen" apps
              //       immediately after submission - :onSuccess (for USCM which stays in the application vs. redirecting to the dashboard)
              var curr = $.fe.pageHandler.current_page;
              $.ajax({url: url, dataType:'script',
                     data: {answer_sheet_type: answer_sheet_type},
                     type:'post',
                     beforeSend: function(xhr) {
                       $('body').trigger('ajax:loading', xhr);
                     },
                     success: function(xhr) {
                       $('#list-pages a').each(function() {
                         if ($(this).attr('data-page-id') != curr) $('#' + $(this).attr('data-page-id')).remove();
                       })
                     },
                     complete: function(xhr) {
                       $('body').trigger('ajax:complete', xhr);
                       var btn = $('#submit_button');
                       if (btn) { btn.attr('disabled', false); }
                     }
              });
            }
        }
        else
          {
            // some pages aren't valid
            $('#submit_message').html("Please make a payment");
            $('#submit_message').show();

            var btn = $('#submit_button'); if (btn) { btn.attr('disabled', false); }
          }
    },

    // is page loaded? (useful for toggling enabled state of questions)
    isPageLoaded : function(page)
    {
      return $('#' + page)[0] != null
    },

    checkConditional : function($element_li) {
      matchable_answers = String($element_li.data('conditional_answer')).split(',').map(function(s) { return s.trim(); })
      if ($element_li.hasClass('fe_choicefield') && $element_li.hasClass('style_yes-no')) {
        if ($(matchable_answers).filter([1, '1', true, 'true', 'yes', 'Yes']).length > 0) {
          matchable_answers = [1, '1', true, 'true', 'yes', 'Yes'];
        }
        if ($(matchable_answers).filter([0, '0', false, 'false', 'no', 'No']).length > 0) {
          matchable_answers = [0, '0', false, 'false', 'no', 'No'];
        }
        vals = $([$element_li.find("input[type=radio]:checked").val()]);
      } else if ($element_li.hasClass('fe_choicefield') && $element_li.hasClass('checkbox')) {
        vals = $element_li.find("input[type=checkbox]:checked").map(function(i, el) { return $(el).val(); });
      } else {
        vals = $([$element_li.find("input:visible, select:visible").val()]);
      }
      match = $(matchable_answers).filter(vals).length > 0;

      switch ($element_li.data('conditional_type')) {
        case 'Fe::Element':
          if (match) {
            $("#element_" + $element_li.data('conditional_id')).show();
          } else {
            $("#element_" + $element_li.data('conditional_id')).hide();
          }
        case 'Fe::Page':
          prefix = $element_li.data('answer_sheet_id_prefix');
          pg = prefix + '_' + $element_li.data('application_id') + '-fe_page_' + $element_li.data('conditional_id');
          li_id = 'li#'+pg+'-li';

          if (match) {
            $(li_id).show();
            // load the page (in the background) to determine validity
            this.loadPage(pg, $(li_id).find('a').attr('href'), true);
          } else {
            $(li_id).hide();
          }
      }
    }

  };

  $(document).on('click', "li.conditional input, li.conditional select", function() {
    $.fe.pageHandler.checkConditional($(this).closest('li'));
  });
  $(document).on('keyup', "li.conditional input, li.conditional select", function() { $(this).click(); });
  $(document).on('blug', "li.conditional input, li.conditional select", function() { $(this).click(); });
  $(document).on('change', "li.conditional select", function() { $(this).click(); });

})(jQuery);

$(function() {
  fixGridColumnWidths();
  setUpCalendars();
});

function updateTotal(id) {
  try {
    total = 0;
    $(".col_" + id).each(function(index, el) {
      total += Number($(el).val());
    });
    $('#total_' + id).val(total).trigger('totalChanged');
  } catch(e) {
  }
}

function submitToFrame(id, url) {
  form = $('<form method="post" action="'+url+'.js" endtype="multipart/form-data"></form>')
  var csrf_token = $('meta[name=csrf-token]').attr('content'),
  csrf_param = $('meta[name=csrf-param]').attr('content'),
  dom_id = '#attachment_field_' + id,
  metadata_input = '<input name="'+csrf_param+'" value="'+csrf_token+'" type="hidden" />',
  file_field = '<input type="file" name="answers['+ id + ']>';
  if ($(dom_id).val() == '')	return;
  form.hide()
  .append(metadata_input)
  .append(file_field)
  .appendTo('body');
  $(dom_id + "-spinner").show();
  form.submit();
  return false
}
<|endoftext|>


<commit_before>$.extend({
  keys: function(obj){
    var a = [];
    $.each(obj, function(k){ a.push(k) });
    return a;
  }
});

if (!Array.indexOf) Array.prototype.indexOf = function(obj) {
  for(var i = 0; i < this.length; i++){
    if(this[i] == obj) {
      return i;
    }
  }
  return -1;
}

if (!Array.find_matches) Array.find_matches = function(a) {
  var i, m = [];
  a = a.sort();
  i = a.length
  while(i--) {
    if (a[i - 1] == a[i]) {
      m.push(a[i]);
    }
  }
  if (m.length == 0) {
    return false;
  }
  return m;
}

function VariantOptions(params) {

  var options = params['options'];
  var i18n = params['i18n'];
  var allow_backorders = !params['track_inventory_levels'];
  var allow_select_outofstock = params['allow_select_outofstock'];
  var default_instock = params['default_instock'];

  var variant, divs, parent, index = 0;
  var selection = [];
  var buttons;

  function init() {
    divs = $('#product-variants .variant-options');
    disable(divs.find('a.option-value').addClass('locked'));
    update();
    enable(parent.find('a.option-value'));
    toggle();
    $('div.variant-options a.clear-button').hide().click(handle_clear);

    if (default_instock) {
      divs.each(function(){
        $(this).find("ul.variant-option-values li a.in-stock:first").click();
      });
    }
  }

  function get_index(parent) {
    return parseInt($(parent).attr('class').replace(/[^\d]/g, ''));
  }

  function update(i) {
    index = isNaN(i) ? index : i;
    parent = $(divs.get(index));
    buttons = parent.find('a.option-value');
    parent.find('a.clear-button').hide();
  }

  function disable(btns) {
    return btns.removeClass('selected');
  }

  function enable(btns) {
    bt = btns.not('.unavailable').removeClass('locked').unbind('click')
    if (!allow_select_outofstock && !allow_backorders)
      bt = bt.filter('.in-stock')
    return bt.click(handle_click).filter('.auto-click').removeClass('auto-click').click();
  }

  function advance() {
    index++
    update();
    inventory(buttons.removeClass('locked'));
    enable(buttons);
  }

  function inventory(btns) {
    var keys, variants, selected = {};
    var sels = $.map(divs.find('a.selected'), function(i) { return i.rel });
    $.each(sels, function(key, value) {
      key = value.split('-');
      var v = options[key[0]][key[1]];
      keys = $.keys(v);
      var m = Array.find_matches(selection.concat(keys));
      if (selection.length == 0) {
        selection = keys;
      } else if (m) {
        selection = m;
      }
    });
    btns.removeClass('in-stock out-of-stock unavailable').each(function(i, element) {
      var variants = get_variant_objects(element.rel);
      var keys = $.keys(variants);
      if (keys.length == 0) {
        disable($(element).addClass('unavailable locked').unbind('click'));
      } else if (keys.length == 1) {
        _var = variants[keys[0]];
        $(element).addClass(_var.in_stock ? selection.length == 1 ? 'in-stock auto-click' : 'in-stock' : 'out-of-stock');
      } else if (allow_backorders) {
        $(element).addClass('in-stock');
      } else {
        var count = 0;
        $.each(variants, function(key, value) {
          count += value.in_stock ? 1 : 0
        });
        $(element).addClass(count > 0 ? 'in-stock' : 'out-of-stock');
      }
    });
  }

  function get_variant_objects(rels) {
    var i, ids, obj, variants = {};
    if (typeof(rels) == 'string') { rels = [rels]; }
    var otid, ovid, opt, opv;
    i = rels.length;
    try {
      while (i--) {
        ids = rels[i].split('-');
        otid = ids[0];
        ovid = ids[1];
        opt = options[otid];
        if (opt) {
          opv = opt[ovid];
          ids = $.keys(opv);
          if (opv && ids.length) {
            var j = ids.length;
            while (j--) {
              obj = opv[ids[j]];
              if (obj && $.keys(obj).length && 0 <= selection.indexOf(obj.id.toString())) {
                variants[obj.id] = obj;
              }
            }
          }
        }
      }
    } catch(error) {
      //console.log(error);
    }
    return variants;
  }

  function to_f(string) {
    return string ? parseFloat(string.replace(/[^\d\.]/g, '')) : 0;
  }

  // Find matching variants for selected option value
  // Set price or price range if matching variants have different prices.
  function find_variant() {
    var selected = divs.find('a.selected');
    var variants = get_variant_objects(selected.get(0).rel);
    var quant = $("#variant_quantity");
    if (selected.length == divs.length) {
      var variant = variants[selection[0]];
      if(quant && variant.quantity){
        quant.html("Quantity: "+variant.quantity);
      }
      return variant;
    } else {
      var prices = [];
      $.each(variants, function(key, value) {
        prices.push({price: value.price, quantity: value.quantity});
      });
      prices = $.unique(prices).sort(function(a, b) {
        return to_f(a.price) < to_f(b.price) ? -1 : 1;
      });
      if (prices.length == 1) {
        $('#product-price .price').html('' + prices[0].price + '');
        if(quant){
          quant.html("Quantity: "+ price.quantity);
        }
      } else {
        $('#product-price .price').html('' + prices[0].price + ' - ' + prices[prices.length - 1].price + '');
        if(quant){
          console.log(prices);
          var quantity = 0;
          $.each(prices,function(k,v){quantity += v.quantity || 0;});
          quant.html("Quantity: "+quantity);
        }
      }
      return variants;
    }
  }

  function toggle(variants) {
    if (variant) {
      $('#variant_id, form[data-form-type="variant"] input[name$="[variant_id]"]').val(variant.id);
      $('#product-price .price').removeClass('unselected').text(variant.price);
      if (variant.in_stock)
        $('#cart-form button[type=submit]').attr('disabled', false).fadeTo(100, 1);
      $('form[data-form-type="variant"] button[type=submit]').attr('disabled', false).fadeTo(100, 1);
      try {
        show_variant_images(variant.id);
      } catch(error) {
        // depends on modified version of product.js
      }
    } else {

      if (variants) {
        variants_ids = $.keys(variants);
        show_variant_images(variants_ids);
      }

      $('#variant_id, form[data-form-type="variant"] input[name$="[variant_id]"]').val('');
      $('#cart-form button[type=submit], form[data-form-type="variant"] button[type=submit]').attr('disabled', true).fadeTo(0, 0.5);
      price = $('#product-price .price').addClass('unselected')
      // Replace product price by "(select)" only when there are at least 1 variant not out-of-stock
      variants = $("div.variant-options.index-0")
      if (variants.find("a.option-value.out-of-stock").length != variants.find("a.option-value").length)
        price.text(i18n.variant_options_select);

    }
  }

  function clear(i) {
    variant = null;
    update(i);
    enable(buttons.removeClass('selected'));
    toggle();
    parent.nextAll().each(function(index, element) {
      disable($(element).find('a.option-value').show().removeClass('in-stock out-of-stock').addClass('locked').unbind('click'));
      $(element).find('a.clear-button').hide();
      $(element).find('h6 strong.selection').html('').removeClass('out-of-stock');
    });
    parent.find('strong.selection').html('').removeClass('out-of-stock');
    show_all_variant_images();
  }

  function handle_clear(evt) {
    evt.preventDefault();
    clear(get_index(this));
  }

  function handle_click(evt) {
    evt.preventDefault();
    variant = null;
    selection = [];
    var a = $(this);
    if (!parent.has(a).length) {
      clear(divs.index(a.parents('.variant-options:first')));
    }
    disable(buttons);
    var a = enable(a.addClass('selected'));
    parent.find('a.clear-button').css('display', 'inline-block');
    advance();
    handle_selected();

    variants = find_variant();
    toggle(variants);
  }

  function handle_selected() {
    var selected = divs.find('a.selected');
    selected.each(function(){
      $this = $(this)
      var selection = $this.parents('.variant-options').find('h6 strong.selection')
      selection.html($this.attr('title'));

      if ($this.hasClass('out-of-stock'))
        selection.addClass('out-of-stock').attr('title', i18n.out_of_stock);
    });
  };
  $(document).ready(init);

};
<commit_msg>Reverted a change that was messing with price display<commit_after>$.extend({
  keys: function(obj){
    var a = [];
    $.each(obj, function(k){ a.push(k) });
    return a;
  }
});

if (!Array.indexOf) Array.prototype.indexOf = function(obj) {
  for(var i = 0; i < this.length; i++){
    if(this[i] == obj) {
      return i;
    }
  }
  return -1;
}

if (!Array.find_matches) Array.find_matches = function(a) {
  var i, m = [];
  a = a.sort();
  i = a.length
  while(i--) {
    if (a[i - 1] == a[i]) {
      m.push(a[i]);
    }
  }
  if (m.length == 0) {
    return false;
  }
  return m;
}

function VariantOptions(params) {

  var options = params['options'];
  var i18n = params['i18n'];
  var allow_backorders = !params['track_inventory_levels'];
  var allow_select_outofstock = params['allow_select_outofstock'];
  var default_instock = params['default_instock'];

  var variant, divs, parent, index = 0;
  var selection = [];
  var buttons;

  function init() {
    divs = $('#product-variants .variant-options');
    disable(divs.find('a.option-value').addClass('locked'));
    update();
    enable(parent.find('a.option-value'));
    toggle();
    $('div.variant-options a.clear-button').hide().click(handle_clear);

    if (default_instock) {
      divs.each(function(){
        $(this).find("ul.variant-option-values li a.in-stock:first").click();
      });
    }
  }

  function get_index(parent) {
    return parseInt($(parent).attr('class').replace(/[^\d]/g, ''));
  }

  function update(i) {
    index = isNaN(i) ? index : i;
    parent = $(divs.get(index));
    buttons = parent.find('a.option-value');
    parent.find('a.clear-button').hide();
  }

  function disable(btns) {
    return btns.removeClass('selected');
  }

  function enable(btns) {
    bt = btns.not('.unavailable').removeClass('locked').unbind('click')
    if (!allow_select_outofstock && !allow_backorders)
      bt = bt.filter('.in-stock')
    return bt.click(handle_click).filter('.auto-click').removeClass('auto-click').click();
  }

  function advance() {
    index++
    update();
    inventory(buttons.removeClass('locked'));
    enable(buttons);
  }

  function inventory(btns) {
    var keys, variants, selected = {};
    var sels = $.map(divs.find('a.selected'), function(i) { return i.rel });
    $.each(sels, function(key, value) {
      key = value.split('-');
      var v = options[key[0]][key[1]];
      keys = $.keys(v);
      var m = Array.find_matches(selection.concat(keys));
      if (selection.length == 0) {
        selection = keys;
      } else if (m) {
        selection = m;
      }
    });
    btns.removeClass('in-stock out-of-stock unavailable').each(function(i, element) {
      var variants = get_variant_objects(element.rel);
      var keys = $.keys(variants);
      if (keys.length == 0) {
        disable($(element).addClass('unavailable locked').unbind('click'));
      } else if (keys.length == 1) {
        _var = variants[keys[0]];
        $(element).addClass(_var.in_stock ? selection.length == 1 ? 'in-stock auto-click' : 'in-stock' : 'out-of-stock');
      } else if (allow_backorders) {
        $(element).addClass('in-stock');
      } else {
        var count = 0;
        $.each(variants, function(key, value) {
          count += value.in_stock ? 1 : 0
        });
        $(element).addClass(count > 0 ? 'in-stock' : 'out-of-stock');
      }
    });
  }

  function get_variant_objects(rels) {
    var i, ids, obj, variants = {};
    if (typeof(rels) == 'string') { rels = [rels]; }
    var otid, ovid, opt, opv;
    i = rels.length;
    try {
      while (i--) {
        ids = rels[i].split('-');
        otid = ids[0];
        ovid = ids[1];
        opt = options[otid];
        if (opt) {
          opv = opt[ovid];
          ids = $.keys(opv);
          if (opv && ids.length) {
            var j = ids.length;
            while (j--) {
              obj = opv[ids[j]];
              if (obj && $.keys(obj).length && 0 <= selection.indexOf(obj.id.toString())) {
                variants[obj.id] = obj;
              }
            }
          }
        }
      }
    } catch(error) {
      //console.log(error);
    }
    return variants;
  }

  function to_f(string) {
    return string ? parseFloat(string.replace(/[^\d\.]/g, '')) : 0;
  }

  // Find matching variants for selected option value
  // Set price or price range if matching variants have different prices.
  function find_variant() {
    var selected = divs.find('a.selected');
    var variants = get_variant_objects(selected.get(0).rel);
    var quant = $("#variant_quantity");
    if (selected.length == divs.length) {
      if(quant && variants[selection[0]].quantity){
        quant.html("Quantity: "+variants[selection[0]].quantity);
      }
      return variant = variants[selection[0]];
    } else {
      var prices = [];
      $.each(variants, function(key, value) {
        prices.push({price: value.price, quantity: value.quantity});
      });
      prices = $.unique(prices).sort(function(a, b) {
        return to_f(a.price) < to_f(b.price) ? -1 : 1;
      });
      if (prices.length == 1) {
        $('#product-price .price').html('' + prices[0].price + '');
        if(quant){
          quant.html("Quantity: "+ price.quantity);
        }
      } else {
        $('#product-price .price').html('' + prices[0].price + ' - ' + prices[prices.length - 1].price + '');
        if(quant){
          var quantity = 0;
          $.each(prices,function(k,v){quantity += parseFloat(v.quantity) || 0;});
          quant.html("Quantity: "+quantity);
        }
      }
      return variants;
    }
  }

  function toggle(variants) {
    if (variant) {
      $('#variant_id, form[data-form-type="variant"] input[name$="[variant_id]"]').val(variant.id);
      $('#product-price .price').removeClass('unselected').text(variant.price);
      if (variant.in_stock)
        $('#cart-form button[type=submit]').attr('disabled', false).fadeTo(100, 1);
      $('form[data-form-type="variant"] button[type=submit]').attr('disabled', false).fadeTo(100, 1);
      try {
        show_variant_images(variant.id);
      } catch(error) {
        // depends on modified version of product.js
      }
    } else {

      if (variants) {
        variants_ids = $.keys(variants);
        show_variant_images(variants_ids);
      }

      $('#variant_id, form[data-form-type="variant"] input[name$="[variant_id]"]').val('');
      $('#cart-form button[type=submit], form[data-form-type="variant"] button[type=submit]').attr('disabled', true).fadeTo(0, 0.5);
      price = $('#product-price .price').addClass('unselected')
      // Replace product price by "(select)" only when there are at least 1 variant not out-of-stock
      variants = $("div.variant-options.index-0")
      if (variants.find("a.option-value.out-of-stock").length != variants.find("a.option-value").length)
        price.text(i18n.variant_options_select);

    }
  }

  function clear(i) {
    variant = null;
    update(i);
    enable(buttons.removeClass('selected'));
    toggle();
    parent.nextAll().each(function(index, element) {
      disable($(element).find('a.option-value').show().removeClass('in-stock out-of-stock').addClass('locked').unbind('click'));
      $(element).find('a.clear-button').hide();
      $(element).find('h6 strong.selection').html('').removeClass('out-of-stock');
    });
    parent.find('strong.selection').html('').removeClass('out-of-stock');
    $("#variant_quantity").html("");
    show_all_variant_images();
  }

  function handle_clear(evt) {
    evt.preventDefault();
    clear(get_index(this));
  }

  function handle_click(evt) {
    evt.preventDefault();
    variant = null;
    selection = [];
    var a = $(this);
    if (!parent.has(a).length) {
      clear(divs.index(a.parents('.variant-options:first')));
    }
    disable(buttons);
    var a = enable(a.addClass('selected'));
    parent.find('a.clear-button').css('display', 'inline-block');
    advance();
    handle_selected();

    variants = find_variant();
    toggle(variants);
  }

  function handle_selected() {
    var selected = divs.find('a.selected');
    selected.each(function(){
      $this = $(this)
      var selection = $this.parents('.variant-options').find('h6 strong.selection')
      selection.html($this.attr('title'));

      if ($this.hasClass('out-of-stock'))
        selection.addClass('out-of-stock').attr('title', i18n.out_of_stock);
    });
  };
  $(document).ready(init);

};
<|endoftext|>


<commit_before>'use strict';

const {isRenderDelayingExtension, isCustomElement} = require('../Extensions.js');
const {applyLayout} = require('./ApplyLayout.js');

class ServerSideRendering {
  // Determines whether the node |n| has an enclosing ancestor tag
  // identified as |tagid|.
  _hasAncestorWithTag(n, tagname) {
    for (let p = n.parent(); p !== null; p = p.parent()) {
      if (p.tagName === tagname) {
        return true;
      }
    }
    return false;
  }

  transform(tree) {
    const html = tree.root.firstChildByTag('html');
    const body = html.firstChildByTag('body');
    const head = html.firstChildByTag('head');

    // A simple check ensuring that the Server-side rendering is only applied once.
    if (typeof (html.attribs['i-amphtml-layout']) !== 'undefined' &&
      html.attribs['i-amphtml-layout'] !== null) {
      return;
    }
    html.attribs['i-amphtml-layout'] = '';

    // Within the loop we apply the layout to the custom tags (amp-foo...)
    // where possible, but while we're at this we also look for reasons
    // not to remove the boilerplate.
    let canRemoveBoilerplate = true;
    for (let node = body; node !== null; node = node.nextNode()) {
      // Skip tags that are not AMP custom elements.
      if (!isCustomElement(node)) {
        continue;
      }

      // Skip tags inside a template tag.
      if (this._hasAncestorWithTag(node, 'template')) {
        continue;
      }

      // If these attributes are used on any AMP custom element tags within
      // the document, we can't remove the boilerplate - they require the
      // boilerplate.
      if (node.attribs.heights || node.attribs.media || node.attribs.sizes) {
        canRemoveBoilerplate = false;
      }

      // amp-experiment is a render delaying extension iff the tag is used in
      // the doc. We check for that here rather than checking for the existence
      // of the amp-experiment script in IsRenderDelayingExtension below.
      if (node.tagName === 'amp-experiment') {
        canRemoveBoilerplate = false;
      }

      // amp-audio requires knowing the dimensions of the browser. Do not
      // remove the boilerplate or apply layout if amp-audio is present in the
      // document.
      if (node.tagName === 'amp-audio') {
        canRemoveBoilerplate = false;
        continue;
      }

      // Now apply the layout to the custom elements. If we encounter
      // any unsupported layout, the applyLayout function returns
      // false and we can't remove the boilerplate.
      if (!applyLayout(node, tree)) {
        canRemoveBoilerplate = false;
        continue;
      }
    }

    // Emit the amp-runtime marker to indicate that we're applying
    // server side rendering in the document.
    const ampRuntimeMarker = tree.createElement('style');
    ampRuntimeMarker.attribs['amp-runtime'] = '';

    const referenceNode = head.children && head.children.length ? head.children[0] : null;
    head.insertBefore(ampRuntimeMarker, referenceNode);

    // Below, we're only concerned about removing the boilerplate.
    // If we've already determined that we can't, we're done here.
    if (!canRemoveBoilerplate) {
      return;
    }

    let node = head.firstChild;
    while (node) {
      // amp-experiment is a render delaying extension iff the tag is used in
      // the doc, which we checked for above.
      if (node.tagName === 'script' && node.hasAttribute('custom-element') &&
          node.attribs['custom-element'] === 'amp-experiment') {
        continue;
      }
      if (isRenderDelayingExtension(node)) {
        return;  // We can't remove the boilerplate.
      }
      node = node.nextSibling;
    }

    // The boilerplate can be removed, note it on the <html> tag.
    html.attribs['i-amphtml-no-boilerplate'] = '';

    // Find the boilerplate and remove it.
    // The following code assumes that the <noscript>
    // tag in the head is only ever used for boilerplate; the test
    // AllNoScriptTagsInHeadEncloseBoilerplate covers this assumption.
    const toRemove = [];
    node = head.firstChild;
    while (node) {
      if (node.tagName === 'noscript' ||
          (node.tagName === 'style' && node.hasAttribute('amp-boilerplate'))) {
        toRemove.push(node);
      }
      node = node.nextSibling;
    }

    for (let n of toRemove) {
      n.remove();
    }
  }
}

module.exports = new ServerSideRendering();
<commit_msg>Fixes an API bug and an endless loop bug<commit_after>'use strict';

const {isRenderDelayingExtension, isCustomElement} = require('../Extensions.js');
const {applyLayout} = require('./ApplyLayout.js');

class ServerSideRendering {
  // Determines whether the node |n| has an enclosing ancestor tag
  // identified as |tagid|.
  _hasAncestorWithTag(n, tagname) {
    for (let p = n.parent; p !== null; p = p.parent) {
      if (p.tagName === tagname) {
        return true;
      }
    }
    return false;
  }

  transform(tree) {
    const html = tree.root.firstChildByTag('html');
    const body = html.firstChildByTag('body');
    const head = html.firstChildByTag('head');

    // A simple check ensuring that the Server-side rendering is only applied once.
    if (typeof (html.attribs['i-amphtml-layout']) !== 'undefined' &&
      html.attribs['i-amphtml-layout'] !== null) {
      return;
    }
    html.attribs['i-amphtml-layout'] = '';

    // Within the loop we apply the layout to the custom tags (amp-foo...)
    // where possible, but while we're at this we also look for reasons
    // not to remove the boilerplate.
    let canRemoveBoilerplate = true;
    for (let node = body; node !== null; node = node.nextNode()) {
      // Skip tags that are not AMP custom elements.
      if (!isCustomElement(node)) {
        continue;
      }

      // Skip tags inside a template tag.
      if (this._hasAncestorWithTag(node, 'template')) {
        continue;
      }

      // If these attributes are used on any AMP custom element tags within
      // the document, we can't remove the boilerplate - they require the
      // boilerplate.
      if (node.attribs.heights || node.attribs.media || node.attribs.sizes) {
        canRemoveBoilerplate = false;
      }

      // amp-experiment is a render delaying extension iff the tag is used in
      // the doc. We check for that here rather than checking for the existence
      // of the amp-experiment script in IsRenderDelayingExtension below.
      if (node.tagName === 'amp-experiment') {
        canRemoveBoilerplate = false;
      }

      // amp-audio requires knowing the dimensions of the browser. Do not
      // remove the boilerplate or apply layout if amp-audio is present in the
      // document.
      if (node.tagName === 'amp-audio') {
        canRemoveBoilerplate = false;
        continue;
      }

      // Now apply the layout to the custom elements. If we encounter
      // any unsupported layout, the applyLayout function returns
      // false and we can't remove the boilerplate.
      if (!applyLayout(node, tree)) {
        canRemoveBoilerplate = false;
        continue;
      }
    }

    // Emit the amp-runtime marker to indicate that we're applying
    // server side rendering in the document.
    const ampRuntimeMarker = tree.createElement('style');
    ampRuntimeMarker.attribs['amp-runtime'] = '';

    const referenceNode = head.children && head.children.length ? head.children[0] : null;
    head.insertBefore(ampRuntimeMarker, referenceNode);

    // Below, we're only concerned about removing the boilerplate.
    // If we've already determined that we can't, we're done here.
    if (!canRemoveBoilerplate) {
      return;
    }

    for (let node = head.firstChild; node; node = node.nextSibling) {
      // amp-experiment is a render delaying extension iff the tag is used in
      // the doc, which we checked for above.
      if (node.tagName === 'script' && node.hasAttribute('custom-element') &&
          node.attribs['custom-element'] === 'amp-experiment') {
        continue;
      }
      if (isRenderDelayingExtension(node)) {
        return;  // We can't remove the boilerplate.
      }
    }

    // The boilerplate can be removed, note it on the <html> tag.
    html.attribs['i-amphtml-no-boilerplate'] = '';

    // Find the boilerplate and remove it.
    // The following code assumes that the <noscript>
    // tag in the head is only ever used for boilerplate; the test
    // AllNoScriptTagsInHeadEncloseBoilerplate covers this assumption.
    const toRemove = [];
    for (let node = head.firstChild; node; node = node.nextSibling) {
      if (node.tagName === 'noscript' ||
          (node.tagName === 'style' && node.hasAttribute('amp-boilerplate'))) {
        toRemove.push(node);
      }
    }

    for (let n of toRemove) {
      n.remove();
    }
  }
}

module.exports = new ServerSideRendering();
<|endoftext|>