<commit_before>import _requiresPropertyElements from './_requiresPropertyElements'

export default function createValidator (rootType, definition) {
  const nodeChecks = new Map()
  for (const nodeSpec of definition.nodes.values()) {
    const type = nodeSpec.type
    nodeChecks.set(type, _createChecks(nodeSpec))
  }
  return new Validator(rootType, nodeChecks)
}

class Validator {
  constructor (rootType, nodeChecks) {
    this.rootType = rootType
    this.nodeChecks = nodeChecks
  }

  validate (xmlDom) {
    const state = new ValidatorState()
    const rootEls = xmlDom.children.filter(c => c.tagName === this.rootType)
    if (rootEls === 0) {
      state.error({ message: `Root element ${this.rootType} not found` })
    } else if (rootEls.length > 1) {
      state.error({ message: `Only one root element ${this.rootType} is allowed` })
    }
    xmlDom.findAll(['id']).reduce((s, el) => {
      const id = el.id
      if (s.has(id)) {
        state.error({ message: `Another element with the same id exists: ${id}` })
      } else {
        s.add(id)
      }
      return s
    }, new Set())

    state.requestChecks(rootEls)
    while (!state.hasFinished()) {
      const el = state.next()
      if (!this.nodeChecks.has(el.tagName)) {
        state.error({ message: `Unknown element type ${el.tagName}` })
      } else {
        const { checks, nodeSpec } = this.nodeChecks.get(el.tagName)
        for (const check of checks) {
          check(state, el, { nodeSpec })
        }
        // check for unused property elements, which is only necessary where property elements are used
        if (_requiresPropertyElements(nodeSpec)) {
          for (const c of state._currentPropertyElements) {
            state.error({ message: `element ${c.tagName} is not allowed in ${el.tagName}.` })
          }
        }
        for (const attr of state._currentAttributes) {
          state.error({ message: `attribute ${attr} is not allowed in ${el.tagName}.` })
        }
      }
    }
    return state
  }
}

class ValidatorState {
  constructor () {
    this.queue = []
    this.errors = []
  }

  get ok () {
    return this.errors.length === 0
  }

  error (err) {
    this.errors.push(err)
  }

  requestChecks (els) {
    this.queue = this.queue.concat(els)
  }

  hasFinished () {
    return this.queue.length === 0
  }

  next () {
    const el = this.queue.shift()
    this._currentAttributes = new Set(Array.from(el.getAttributes().keys()))
    this._currentPropertyElements = new Set(el.children)
    return el
  }

  elementChecked (el) {
    this._currentPropertyElements.delete(el)
  }

  attributeChecked (attr) {
    this._currentAttributes.delete(attr)
  }
}

function _createChecks (nodeSpec) {
  // validation checks
  const checks = []
  const nodeType = nodeSpec.type

  // add checkers for built-in properties (only id at the moment)
  checks.push(_attributeChecker(nodeType, 'id', str => {
    if (!/^[_@a-zA-Z][_@a-zA-Z0-9-]+$/.exec(str)) {
      return `Invalid id: ${str}`
    }
  }))

  for (const [propName, propSpec] of nodeSpec.properties.entries()) {
    checks.push(_createPropertyChecker(nodeSpec, propName, propSpec))
  }

  return {
    checks,
    nodeSpec
  }
}

function _createPropertyChecker (nodeSpec, propName, propSpec) {
  const nodeType = nodeSpec.type
  function _checkChildType (child, errors) {
    if (!targetTypes.has(child.tagName)) {
      errors.push(`Element of type ${child.tagName} not allowed in ${nodeType} > ${propName}. Expected one of ${Array.from(targetTypes).join(',')}`)
    }
  }
  function _checkTargetType (el, id, errors) {
    const target = el.getOwnerDocument().getElementById(id)
    if (!target) {
      errors.push(`Target element ${id} does not exist for ${nodeType}@${propName}`)
    } else if (!targetTypes.has(target.tagName)) {
      errors.push(`Target type ${target.tagName} is not allowed for ${nodeType}@${propName}`)
    }
  }

  const type = propSpec.type
  const options = propSpec.options || {}
  const targetTypes = new Set(options.childTypes || options.targetTypes || [])

  switch (type) {
    case 'integer': {
      return _attributeChecker(nodeType, propName, str => {
        if (isNaN(str) || str !== parseInt(str)) {
          return `Expected integer. Actual value: ${str}`
        }
      })
    }
    case 'number': {
      return _attributeChecker(nodeType, propName, str => {
        if (isNaN(str)) {
          return `Expected number. Actual value: ${str}`
        }
      })
    }
    case 'boolean': {
      return _attributeChecker(nodeType, propName, str => {
        if (str !== 'true' && str !== 'false') {
          return `Expected boolean (true|false). Actual value: ${str}`
        }
      })
    }
    case 'string': {
      return _attributeChecker(nodeType, propName, str => {
        // nothing special here
      })
    }
    case 'enum': {
      return _attributeChecker(nodeType, propName, str => {
        if (!options.values.has(str)) {
          return `Unsupported enum value. Expected one of ${Array.from(options.values).join(',')}, but was ${str}`
        }
      })
    }
    case 'one': {
      return _attributeChecker(nodeType, propName, (str, el) => {
        const errors = []
        this._checkTargetType(el, str, errors)
        return errors
      })
    }
    case 'many': {
      return _attributeChecker(nodeType, propName, (str, el) => {
        const errors = []
        const ids = str.split(',').map(id => id.trim())
        for (const id of ids) {
          _checkTargetType(el, id, errors)
        }
        return errors
      })
    }
    case 'child': {
      return _elementChecker(nodeSpec, propName, (state, el) => {
        const children = el.getChildren()
        const errors = []
        if (children.length !== 1) {
          errors.push(`Expected exactly one child. Found ${children.length}.`)
        }
        // Note even if mutliple children are given, we check every one of them
        // to receive more verbose information
        for (const child of children) {
          _checkChildType(child, errors)
        }
        return errors
      })
    }
    case 'children':
    case 'container':
    case 'text': {
      return _elementChecker(nodeSpec, propName, (state, el) => {
        const children = el.getChildren()
        const errors = []
        for (const child of children) {
          _checkChildType(child, errors)
        }
        return errors
      })
    }
    default:
      throw new Error('Invalid type: ' + type)
  }
}

function _attributeChecker (type, propertyName, check) {
  return (state, el) => {
    const str = el.getAttribute(propertyName)
    if (str) {
      // checker returns error message
      const error = check(str, el)
      if (error) {
        state.error({ type, propertyName, error })
      }
    }
    state.attributeChecked(propertyName)
  }
}

function _elementChecker (nodeSpec, propertyName, check) {
  return (state, el, { nodeSpec }) => {
    const nodeType = nodeSpec.type
    // For now, we force property elements for all 'structured' nodes
    if (_requiresPropertyElements(nodeSpec)) {
      const propertyEl = el.children.find(c => c.tagName === propertyName)
      if (!propertyEl) {
        state.error({ type: nodeType, propertyName, message: `Child element ${propertyName} is missing` })
      } else {
        const errors = check(state, propertyEl, { nodeSpec })
        if (errors && errors.length > 0) {
          errors.forEach(error => state.error({ type: nodeType, propertyName, error }))
        }
        state.requestChecks(propertyEl.children)
        state.elementChecked(propertyEl)
      }
    // no property elements: this is the case for text nodes
    // TODO: in the future we might want to add a flag to the node spec to allow
    // this for 'structured' nodes, too (e.g. a list could spare an extra 'items' element)
    } else {
      const errors = check(state, el, { nodeSpec })
      if (errors && errors.length > 0) {
        errors.forEach(error => state.error({ type: nodeType, propertyName, error }))
      }
      state.requestChecks(el.children)
    }
  }
}
<commit_msg>Fix child checker.<commit_after>import _requiresPropertyElements from './_requiresPropertyElements'

export default function createValidator (rootType, definition) {
  const nodeChecks = new Map()
  for (const nodeSpec of definition.nodes.values()) {
    const type = nodeSpec.type
    nodeChecks.set(type, _createChecks(nodeSpec))
  }
  return new Validator(rootType, nodeChecks)
}

class Validator {
  constructor (rootType, nodeChecks) {
    this.rootType = rootType
    this.nodeChecks = nodeChecks
  }

  validate (xmlDom) {
    const state = new ValidatorState()
    const rootEls = xmlDom.children.filter(c => c.tagName === this.rootType)
    if (rootEls === 0) {
      state.error({ message: `Root element ${this.rootType} not found` })
    } else if (rootEls.length > 1) {
      state.error({ message: `Only one root element ${this.rootType} is allowed` })
    }
    xmlDom.findAll(['id']).reduce((s, el) => {
      const id = el.id
      if (s.has(id)) {
        state.error({ message: `Another element with the same id exists: ${id}` })
      } else {
        s.add(id)
      }
      return s
    }, new Set())

    state.requestChecks(rootEls)
    while (!state.hasFinished()) {
      const el = state.next()
      if (!this.nodeChecks.has(el.tagName)) {
        state.error({ message: `Unknown element type ${el.tagName}` })
      } else {
        const { checks, nodeSpec } = this.nodeChecks.get(el.tagName)
        for (const check of checks) {
          check(state, el, { nodeSpec })
        }
        // check for unused property elements, which is only necessary where property elements are used
        if (_requiresPropertyElements(nodeSpec)) {
          for (const c of state._currentPropertyElements) {
            state.error({ message: `element ${c.tagName} is not allowed in ${el.tagName}.` })
          }
        }
        for (const attr of state._currentAttributes) {
          state.error({ message: `attribute ${attr} is not allowed in ${el.tagName}.` })
        }
      }
    }
    return state
  }
}

class ValidatorState {
  constructor () {
    this.queue = []
    this.errors = []
  }

  get ok () {
    return this.errors.length === 0
  }

  error (err) {
    this.errors.push(err)
  }

  requestChecks (els) {
    this.queue = this.queue.concat(els)
  }

  hasFinished () {
    return this.queue.length === 0
  }

  next () {
    const el = this.queue.shift()
    this._currentAttributes = new Set(Array.from(el.getAttributes().keys()))
    this._currentPropertyElements = new Set(el.children)
    return el
  }

  elementChecked (el) {
    this._currentPropertyElements.delete(el)
  }

  attributeChecked (attr) {
    this._currentAttributes.delete(attr)
  }
}

function _createChecks (nodeSpec) {
  // validation checks
  const checks = []
  const nodeType = nodeSpec.type

  // add checkers for built-in properties (only id at the moment)
  checks.push(_attributeChecker(nodeType, 'id', str => {
    if (!/^[_@a-zA-Z][_@a-zA-Z0-9-]+$/.exec(str)) {
      return `Invalid id: ${str}`
    }
  }))

  for (const [propName, propSpec] of nodeSpec.properties.entries()) {
    checks.push(_createPropertyChecker(nodeSpec, propName, propSpec))
  }

  return {
    checks,
    nodeSpec
  }
}

function _createPropertyChecker (nodeSpec, propName, propSpec) {
  const nodeType = nodeSpec.type
  function _checkChildType (child, errors) {
    if (!targetTypes.has(child.tagName)) {
      errors.push(`Element of type ${child.tagName} not allowed in ${nodeType} > ${propName}. Expected one of ${Array.from(targetTypes).join(',')}`)
    }
  }
  function _checkTargetType (el, id, errors) {
    const target = el.getOwnerDocument().getElementById(id)
    if (!target) {
      errors.push(`Target element ${id} does not exist for ${nodeType}@${propName}`)
    } else if (!targetTypes.has(target.tagName)) {
      errors.push(`Target type ${target.tagName} is not allowed for ${nodeType}@${propName}`)
    }
  }

  const type = propSpec.type
  const options = propSpec.options || {}
  const targetTypes = new Set(options.childTypes || options.targetTypes || [])

  switch (type) {
    case 'integer': {
      return _attributeChecker(nodeType, propName, str => {
        if (isNaN(str) || str !== parseInt(str)) {
          return `Expected integer. Actual value: ${str}`
        }
      })
    }
    case 'number': {
      return _attributeChecker(nodeType, propName, str => {
        if (isNaN(str)) {
          return `Expected number. Actual value: ${str}`
        }
      })
    }
    case 'boolean': {
      return _attributeChecker(nodeType, propName, str => {
        if (str !== 'true' && str !== 'false') {
          return `Expected boolean (true|false). Actual value: ${str}`
        }
      })
    }
    case 'string': {
      return _attributeChecker(nodeType, propName, str => {
        // nothing special here
      })
    }
    case 'enum': {
      return _attributeChecker(nodeType, propName, str => {
        if (!options.values.has(str)) {
          return `Unsupported enum value. Expected one of ${Array.from(options.values).join(',')}, but was ${str}`
        }
      })
    }
    case 'one': {
      return _attributeChecker(nodeType, propName, (str, el) => {
        const errors = []
        this._checkTargetType(el, str, errors)
        return errors
      })
    }
    case 'many': {
      return _attributeChecker(nodeType, propName, (str, el) => {
        const errors = []
        const ids = str.split(',').map(id => id.trim())
        for (const id of ids) {
          _checkTargetType(el, id, errors)
        }
        return errors
      })
    }
    case 'child': {
      return _elementChecker(nodeSpec, propName, (state, el) => {
        const errors = []
        _checkChildType(el, errors)
        state.requestChecks(el)
        return errors
      })
    }
    case 'children':
    case 'container':
    case 'text': {
      return _elementChecker(nodeSpec, propName, (state, el) => {
        const children = el.getChildren()
        const errors = []
        for (const child of children) {
          _checkChildType(child, errors)
        }
        return errors
      })
    }
    default:
      throw new Error('Invalid type: ' + type)
  }
}

function _attributeChecker (type, propertyName, check) {
  return (state, el) => {
    const str = el.getAttribute(propertyName)
    if (str) {
      // checker returns error message
      const error = check(str, el)
      if (error) {
        state.error({ type, propertyName, error })
      }
    }
    state.attributeChecked(propertyName)
  }
}

function _elementChecker (nodeSpec, propertyName, check) {
  return (state, el, { nodeSpec }) => {
    const nodeType = nodeSpec.type
    // For now, we force property elements for all 'structured' nodes
    if (_requiresPropertyElements(nodeSpec)) {
      const propertyEl = el.children.find(c => c.tagName === propertyName)
      if (!propertyEl) {
        state.error({ type: nodeType, propertyName, message: `Child element ${propertyName} is missing` })
      } else {
        const errors = check(state, propertyEl, { nodeSpec })
        if (errors && errors.length > 0) {
          errors.forEach(error => state.error({ type: nodeType, propertyName, error }))
        }
        state.requestChecks(propertyEl.children)
        state.elementChecked(propertyEl)
      }
    // no property elements: this is the case for text nodes
    // TODO: in the future we might want to add a flag to the node spec to allow
    // this for 'structured' nodes, too (e.g. a list could spare an extra 'items' element)
    } else {
      const errors = check(state, el, { nodeSpec })
      if (errors && errors.length > 0) {
        errors.forEach(error => state.error({ type: nodeType, propertyName, error }))
      }
      state.requestChecks(el.children)
    }
  }
}
<|endoftext|>


<commit_before>var play_flag = 0;
var dl_flag   = 0;

$(function(){
    /* ダウンロード数カウント */
    $(".downloads a").on("click",function(){
        if( dl_flag === 0 ){
            dl_flag = 1;
            $.get('dl_count',{name:$(this).data("name")},function(data){
                console.log("ダウンロードカウントアップ");
            });
        }
    });

    /* 再生数カウント */
    $(".audio").on("play",function(){
        if( play_flag === 0 ){
            play_flag = 1;
            $.get('play_count',{name:$(this).data("name")},function(data){
               console.log(data);
            });
        }
    });

    /* コメント送信 */
    // TOOD:要リファクタ
    $(".comment_submit").on("click",function(){
        var $comment_group = $(this).parent();
        console.log($comment_group);
        var $text_area     = $comment_group.children("div").children();
        console.log($text_area);

        var comment = $text_area.val();
        var name    = $(this).closest('.comment').data('name');

        // コメント表示
        var $parent = $(this).closest(".sound");
        $comment_list = $parent.children('.comment_list');
        $comment_list.append("
</div");
        var text = '
' + comment + '
';
        var $child  = $comment_list.children("div:last-child");
        $child.attr({class: 'message'}).append(text);

        // フォームリセット
        $(".comment_form").val("");

        $.get('send_comment',{comment:comment,sound_name:name},function(data){
            console.log("成功");
        })
    })

});
<commit_msg>JS をオブジェクト指向で書き換え (未完了)<commit_after>$(function(){
    var play_flag = 0;
    var dl_flag   = 0;
    /* クリックされた回数を DB に記録 */
    var ClickCounter = (function(){
        function ClickCounter(action){
            this.done_count  = false;
            this.action_name = action + "_count";
            this.listener();
        }
        ClickCounter.prototype.count = function(){
            if( !this.done_count ){
                $.get(this.action_name, {name:$(this).data("name")}, function(data){this.done_count = true;});
            }
        }
        ClickCounter.prototype.listener = function(){
            $('.downloads a').on("click", this.count);
        }
    })();

    /* コメント送信 */
    // TOOD:要リファクタ
    $(".comment_submit").on("click",function(){
        // 理想は、入力したコメント取得し、DBにわたし、画面に表示
        var comment   = $(".comment_form").val();
        var data_name = $(".comment").data("name");
        var add_text  = '
' + comment + '
';

        // TDOO: 曲ごとの場所を特定しなきゃダメだからここは違う
        // TODO: this からたどる
        $("#comments").append(add_text);

        //$.get('send_comment', {comment: comment,sound_name: data_name}, function(data){
        //     console.log("成功");
        // })

        $(".comment_form").val("");

    })

    // TODO:ボタンごとにnewすべきだと思う
    dl_count   = new ClickCounter("dl");
    play_count = new ClickCounter("play");

    // 初期化

});
<|endoftext|>


<commit_before>import Tweezer from 'tweezer.js';

import {
  getScrollingElement,
  getScrollEventContainer,
  getTargetScrollTop,
  getElementsInContainerViewport,
  elementFillsContainer,
  passiveIsSupported,
} from './utilities';

let INSTANCE_COUNTER = 0;

const defaultOptions = {
  container: document.body,
  panelSelector: '> section',
  offset: 0,
  directionThreshold: 50,
  delay: 0,
  slideSpeed: 200,
  easing: t => t,
};

export default class PanelSnap {
  constructor(options) {
    this.options = {
      ...defaultOptions,
      ...options,
    };

    if (this.options.container.dataset.panelsnapId) {
      throw new Error('PanelSnap is already initialised on this container, aborting.');
    }

    this.container = this.options.container;
    this.scrollContainer = getScrollingElement(this.container);
    this.scrollEventContainer = getScrollEventContainer(this.container);

    INSTANCE_COUNTER += 1;
    this.instanceIndex = INSTANCE_COUNTER;
    this.container.dataset.panelsnapId = this.instanceIndex;

    const panelQuery = `[data-panelsnap-id="${this.instanceIndex}"] ${this.options.panelSelector}`;
    this.panelList = Array.from(document.querySelectorAll(panelQuery));

    this.events = [];
    this.isEnabled = true;
    this.isInteracting = false;
    this.animation = null;
    this.currentScrollOffset = this.scrollContainer.scrollTop;
    this.targetScrollOffset = this.currentScrollOffset;

    this.scrollEventContainer.addEventListener('keydown', this.onInteractStart.bind(this), passiveIsSupported && { passive: true });
    this.scrollEventContainer.addEventListener('keyup', this.onInteractStop.bind(this), passiveIsSupported && { passive: true });
    this.scrollEventContainer.addEventListener('mousedown', this.onInteractStart.bind(this), passiveIsSupported && { passive: true });
    this.scrollEventContainer.addEventListener('mouseup', this.onInteractStop.bind(this), passiveIsSupported && { passive: true });
    this.scrollEventContainer.addEventListener('touchstart', this.onInteractStart.bind(this), passiveIsSupported && { passive: true });
    this.scrollEventContainer.addEventListener('touchend', this.onInteractStop.bind(this), passiveIsSupported && { passive: true });
    this.scrollEventContainer.addEventListener('scroll', this.onScroll.bind(this), passiveIsSupported && { passive: true });
  }

  on(name, handler) {
    const currentHandlers = this.events[name] || [];
    this.events[name] = [...currentHandlers, handler];
  }

  off(name, handler) {
    const currentHandlers = this.events[name] || [];
    this.events[name] = currentHandlers.filter(h => h !== handler);
  }

  emit(name, value) {
    const currentHandlers = this.events[name] || [];
    currentHandlers.forEach(h => h.call(this, value));
  }

  onInteractStart() {
    this.stopAnimation();
    this.isInteracting = true;
  }

  onInteractStop() {
    this.isInteracting = false;
    this.onScroll();
  }

  onInteract() {
    clearTimeout(this.scrollTimeout);
    this.stopAnimation();
  }

  onScroll() {
    clearTimeout(this.scrollTimeout);

    if (this.isInteracting || this.animation) {
      return;
    }

    if (this.currentScrollOffset === this.scrollContainer.scrollTop) {
      return;
    }

    this.stopAnimation();

    this.scrollTimeout = setTimeout(this.findSnapTarget.bind(this), 50);
  }

  findSnapTarget() {
    const delta = this.scrollContainer.scrollTop - this.currentScrollOffset;
    this.currentScrollOffset = this.scrollContainer.scrollTop;

    if (Math.abs(delta) < this.options.directionThreshold && this.activePanel) {
      this.snapToPanel(this.activePanel);
      return;
    }

    const panelsInViewport = getElementsInContainerViewport(this.container, this.panelList);
    if (panelsInViewport.length === 0) {
      throw new Error('PanelSnap could not find a snappable panel, aborting.');
    }

    if (panelsInViewport.length > 1) {
      const targetIndex = delta > 0 ? 1 : panelsInViewport.length - 2;
      this.snapToPanel(panelsInViewport[targetIndex], delta < 0);
      return;
    }

    const visiblePanel = panelsInViewport[0];
    if (elementFillsContainer(this.container, visiblePanel)) {
      return;
    }

    // TODO: Only one partial panel in viewport, add support for space between panels?
    // eslint-disable-next-line no-console
    console.error('PanelSnap does not support space between panels, snapping back.');
    this.snapToPanel(visiblePanel, delta > 0);
  }

  snapToPanel(panel, toBottom = false) {
    if (!this.isEnabled) {
      return;
    }

    this.activatePanel(panel);

    if (this.animation) {
      this.animation.stop();
    }

    const start = this.scrollContainer.scrollTop;
    const end = getTargetScrollTop(this.container, panel, toBottom);
    const duration = 300;

    this.animation = new Tweezer({ start, end, duration });

    this.animation.on('tick', (value) => {
      this.scrollContainer.scrollTop = value;
    });
    this.animation.on('done', () => {
      this.emit('snapStop', panel);
      this.clearAnimation();
    });

    this.emit('snapStart', panel);
    this.animation.begin();
  }

  stopAnimation() {
    if (!this.animation) {
      return;
    }

    this.animation.stop();
    this.clearAnimation();
  }

  clearAnimation() {
    this.currentScrollOffset = this.scrollContainer.scrollTop;
    this.animation = null;
  }

  activatePanel(panel) {
    this.emit('activatePanel', panel);
    this.activePanel = panel;
  }
}
<commit_msg>fix: only take directionThreshold into account when actually snapping<commit_after>import Tweezer from 'tweezer.js';

import {
  getScrollingElement,
  getScrollEventContainer,
  getTargetScrollTop,
  getElementsInContainerViewport,
  elementFillsContainer,
  passiveIsSupported,
} from './utilities';

let INSTANCE_COUNTER = 0;

const defaultOptions = {
  container: document.body,
  panelSelector: '> section',
  offset: 0,
  directionThreshold: 50,
  delay: 0,
  slideSpeed: 200,
  easing: t => t,
};

export default class PanelSnap {
  constructor(options) {
    this.options = {
      ...defaultOptions,
      ...options,
    };

    if (this.options.container.dataset.panelsnapId) {
      throw new Error('PanelSnap is already initialised on this container, aborting.');
    }

    this.container = this.options.container;
    this.scrollContainer = getScrollingElement(this.container);
    this.scrollEventContainer = getScrollEventContainer(this.container);

    INSTANCE_COUNTER += 1;
    this.instanceIndex = INSTANCE_COUNTER;
    this.container.dataset.panelsnapId = this.instanceIndex;

    const panelQuery = `[data-panelsnap-id="${this.instanceIndex}"] ${this.options.panelSelector}`;
    this.panelList = Array.from(document.querySelectorAll(panelQuery));

    this.events = [];
    this.isEnabled = true;
    this.isInteracting = false;
    this.animation = null;
    this.currentScrollOffset = this.scrollContainer.scrollTop;
    this.targetScrollOffset = this.currentScrollOffset;

    this.scrollEventContainer.addEventListener('keydown', this.onInteractStart.bind(this), passiveIsSupported && { passive: true });
    this.scrollEventContainer.addEventListener('keyup', this.onInteractStop.bind(this), passiveIsSupported && { passive: true });
    this.scrollEventContainer.addEventListener('mousedown', this.onInteractStart.bind(this), passiveIsSupported && { passive: true });
    this.scrollEventContainer.addEventListener('mouseup', this.onInteractStop.bind(this), passiveIsSupported && { passive: true });
    this.scrollEventContainer.addEventListener('touchstart', this.onInteractStart.bind(this), passiveIsSupported && { passive: true });
    this.scrollEventContainer.addEventListener('touchend', this.onInteractStop.bind(this), passiveIsSupported && { passive: true });
    this.scrollEventContainer.addEventListener('scroll', this.onScroll.bind(this), passiveIsSupported && { passive: true });
  }

  on(name, handler) {
    const currentHandlers = this.events[name] || [];
    this.events[name] = [...currentHandlers, handler];
  }

  off(name, handler) {
    const currentHandlers = this.events[name] || [];
    this.events[name] = currentHandlers.filter(h => h !== handler);
  }

  emit(name, value) {
    const currentHandlers = this.events[name] || [];
    currentHandlers.forEach(h => h.call(this, value));
  }

  onInteractStart() {
    this.stopAnimation();
    this.isInteracting = true;
  }

  onInteractStop() {
    this.isInteracting = false;
    this.onScroll();
  }

  onInteract() {
    clearTimeout(this.scrollTimeout);
    this.stopAnimation();
  }

  onScroll() {
    clearTimeout(this.scrollTimeout);

    if (this.isInteracting || this.animation) {
      return;
    }

    if (this.currentScrollOffset === this.scrollContainer.scrollTop) {
      return;
    }

    this.stopAnimation();

    this.scrollTimeout = setTimeout(this.findSnapTarget.bind(this), 50);
  }

  findSnapTarget() {
    const delta = this.scrollContainer.scrollTop - this.currentScrollOffset;
    this.currentScrollOffset = this.scrollContainer.scrollTop;

    const panelsInViewport = getElementsInContainerViewport(this.container, this.panelList);
    if (panelsInViewport.length === 0) {
      throw new Error('PanelSnap could not find a snappable panel, aborting.');
    }

    if (panelsInViewport.length > 1) {
      if (Math.abs(delta) < this.options.directionThreshold && this.activePanel) {
        this.snapToPanel(this.activePanel);
        return;
      }

      const targetIndex = delta > 0 ? 1 : panelsInViewport.length - 2;
      this.snapToPanel(panelsInViewport[targetIndex], delta < 0);
      return;
    }

    const visiblePanel = panelsInViewport[0];
    if (elementFillsContainer(this.container, visiblePanel)) {
      return;
    }

    // TODO: Only one partial panel in viewport, add support for space between panels?
    // eslint-disable-next-line no-console
    console.error('PanelSnap does not support space between panels, snapping back.');
    this.snapToPanel(visiblePanel, delta > 0);
  }

  snapToPanel(panel, toBottom = false) {
    if (!this.isEnabled) {
      return;
    }

    this.activatePanel(panel);

    if (this.animation) {
      this.animation.stop();
    }

    const start = this.scrollContainer.scrollTop;
    const end = getTargetScrollTop(this.container, panel, toBottom);
    const duration = 300;

    this.animation = new Tweezer({ start, end, duration });

    this.animation.on('tick', (value) => {
      this.scrollContainer.scrollTop = value;
    });
    this.animation.on('done', () => {
      this.emit('snapStop', panel);
      this.clearAnimation();
    });

    this.emit('snapStart', panel);
    this.animation.begin();
  }

  stopAnimation() {
    if (!this.animation) {
      return;
    }

    this.animation.stop();
    this.clearAnimation();
  }

  clearAnimation() {
    this.currentScrollOffset = this.scrollContainer.scrollTop;
    this.animation = null;
  }

  activatePanel(panel) {
    this.emit('activatePanel', panel);
    this.activePanel = panel;
  }
}
<|endoftext|>


<commit_before> (function(jQuery) {

  var $ = jQuery;
  var FOCUS_GENE_FAMILY = 'phytozome_10_2.59088092';
  var FOCUS_GENE = 'vigra.Vradi01g03360';
  var FOCUS_GENE_UNIQUENAME = 'Vradi01g03360.Vradi.ver6';
  var FOCUS_PROTEIN = FOCUS_GENE + '.1';
  var FOCUS_GENE_FUNCTION = 'gamma-glutamyl transpeptidase';
  var SELECTOR = {
    welcome: '#site-name',
    searchBtn: "a:contains('Gene Search')",
    descriptors: 'th.views-field-description',
    formFieldDescr: '#edit-description',
    formFieldAbbrev: '#edit-abbreviation',
    submitBtn: '#edit-submit-gene',
    searchResult: 'th.views-field-name',
    functionalDesc: 'th.views-field-description',
    geneFamilyLink:'a[href*="'+FOCUS_GENE_UNIQUENAME+'"]:contains("'+FOCUS_GENE_FAMILY+'")',
    phylotree: '#phylogram text:contains("'+FOCUS_PROTEIN+'")',
    popup: '#phylonode_popup_dialog',
    contextViewerLink: "#phylonode_popup_dialog a[href*='lis_context_viewer']",
    contextFocusGene: 'path.point.focus:first',
    contextLocus: 'g.legend:has(:contains("phytozome_10_2.59088092"))',
    contextFocus: 'h4:contains("phytozome_10_2.59088092")',
    contextFamilyLink: 'a:contains("View genes in phylogram")',
    contact: 'a:contains("Contact")',
  };

  $.fn.d3Click = function () {
    this.each(function (i, e) {
      var evt = new MouseEvent('click');
      e.dispatchEvent(evt);
    });
  };

  var tour = new Tour({
    name: 'gene-to-phylotree-to-context-viewer',
    keyboard: true,
    debug: true,
    orphan: true,
    // this template is the default one, with Prev button removed.
    // template: "

<button class='btn btn-default' data-role='next'>Next →</button><button class='btn btn-default' data-role='end'>End tour</button>

",
    steps : [
      {
        title: 'Gene Tour: Welcome to LIS!',
        content: 'Let\'s go the legumeinfo.org homepage, where the Gene Tour will begin.  Use the Next button or → (right arrow key) to advance the tour. Use the Prev button or ← (left arrow key) to step back.',
        placement: 'bottom',
        element: SELECTOR.welcome,
        reflex: true,
      }, {
        path: '/home',
        title: 'Gene Tour: Getting started',
        content: "This tour will provide an example of navigating LIS from a gene annotated with a specific function to views of its evolutionary context both with respect to other individual genes as well as to the genomic context in which it occurs.
 Now press the Gene Search button, or use the Next button or press →.",
        placement: 'bottom',
        element: SELECTOR.searchBtn,
        reflex: true,
      }, {
        title: 'Gene Tour: Gene Search',
        path: '/search/gene',
        content: 'All the legume genes in LIS have been annotated with functional descriptors based on a consistent set of homology-based methods. We\'ll use gene function as a query to limit the set of genes in the results.',
        placement: 'top',
        element: SELECTOR.descriptors,
        reflex: true,
      }, {
        title: 'Gene Tour: Gene Search',
        content: 'We can enter key terms from the expected description of the gene function in which we\'re interested here.',
        placement: 'bottom',
        element: SELECTOR.formFieldDescr,
        reflex: true,
        onShown: function(tour) {
          $(SELECTOR.formFieldDescr)[0].value=FOCUS_GENE_FUNCTION;
          $('#edit-description-op')[0].value='starts';
        },
      }, {
        title: 'Gene Tour: Filter by Species',
        content: "Supposing we are initially interested in the genes from Vigna radiata (mungbean), we would specify the five-letter species abbreviation 'vigra', composed of the first three letters of the genus and the first two letters of the species component of the scientific name.",
        onShown: function() {
          $(SELECTOR.formFieldAbbrev)[0].value='vigra';
        },
        element: SELECTOR.formFieldAbbrev,
        placement: 'bottom',
        reflex: true,
        onPrev: function(tour) {
          tour.skipStep = true;
        }
      }, {
        title: 'Gene Tour: Gene Search',
        content: 'Then click "Apply" to apply the specified filters to the genes in the result.',
        placement: 'bottom',
        element: SELECTOR.submitBtn,
        reflex: true,
        onNext: function() {
          $(SELECTOR.submitBtn)[0].click();
        }
      }, {
        title: 'Gene Tour: Gene Search',
        content: 'Please be patient, as we wait for the query results to be returned...',
        placement: 'top',
        onShown: function(tour) {
          $('.popover-navigation div').hide();
          // wait for dynamic content with a loading dialog.
          if(tour.skipStep) {
            tour.skipStep = false;
            tour.prev();
            return;
          }
          var promise = lisTours.waitForContent(
            tour,
            function() {
              return $('tr.views-row-first td.views-field-description:contains("'+FOCUS_GENE_FUNCTION+'")')[0];
            }, 40000);
          // advance automatically to next step when done loading
          promise.then(function() {
            tour.next();
          });
          return promise;
        }
      }, {
        title: 'Gene Tour: Search Results',
        content: "We've gotten paged results back for genes matching the given description in our target species.",
        element: SELECTOR.searchResult,
        placement: 'top',
        reflex: true,
        onPrev: function(tour) {
          tour.skipStep = true;
        }
       }, {
     title: 'Gene Tour: Functional description',
     content: "Notice that there is a subtle difference in the annotation of the genes, though they are all listed as belonging to the same gene family.",
     placement: 'top',
     element: SELECTOR.functionalDesc,
     reflex: true,
     onPrev: function(tour) {
       tour.skipStep = true;
     },
      }, {
        title: 'Gene Tour: Gene Family',
        content: "Following the link to the gene family will show you this gene in the context of a tree representing the orthologous, paralogous and homoeologous members of the family.",
        placement: 'left',
	reflex: false, // NB dont use reflex, because of absolute link to legumeinfo.org,
        element: SELECTOR.geneFamilyLink,
      }, {
        title: 'Gene Tour: Phylotree',
        //NB: currently, this must use path instead of click on the
        //link element due to the fact that these are being made into
        //external absolute links to legumeinfo.org probably due to
        //peanutbase
        path: '/chado_phylotree/'+FOCUS_GENE_FAMILY+'?hilite_node='+FOCUS_PROTEIN,
        content: 'Waiting for tree display...',
        placement: 'top',
        onShown: function(tour) {
          $('.popover-navigation div').hide();
          // wait for dynamic content with a loading dialog.
          if(tour.skipStep) {
            tour.skipStep = false;
            tour.prev();
            return;
          }
	  var promise = lisTours.waitForSelector(tour, SELECTOR.phylotree);
          // advance automatically to next step when done loading
          promise.then(function() {
            tour.next();
          });
          return promise;
        }
      }, {
        title: 'Gene Tour: Phylotree',
        placement: 'right',
        content : 'Here is our gene again, surrounded by orthologues from other species. ',
        element : SELECTOR.phylotree,
	onShow: function(tour) {
	  return lisTours.waitForSelector(tour, SELECTOR.phylotree)
	},
        onPrev: function(tour) {
          tour.skipStep = true;
        }
      }, {
        title: 'Gene Tour: Phylotree',
        placement: 'right',
        content : 'Notice that the two other instances of the gene family from mungbean are in a separate clade. This suggests that the gene was duplicated in an ancestral species and the two copies were retained in most of the species (possibly with subsequent duplications within some of the descendant species). This could be due to an important difference in function that evolved after the ancient duplication occurred.',
        element : SELECTOR.phylotree,
      }, {
        title: 'Gene Tour: Phylotree',
        placement: 'right',
        content : 'The nodes of the tree representing the genes (as well as the internal ancestral nodes) can be clicked for more options.',
        element : SELECTOR.phylotree,
        reflex: true,
        onNext: function() {
          $('#phylogram g.leaf:contains("'+FOCUS_PROTEIN+'")').d3Click();
          var promise = lisTours.waitForContent(
            tour,
            function() {
              return $("#phylonode_popup_dialog a[href*='lis_context_viewer']")[0];
            });
          return promise;
        }
      }, {
        title: 'Gene Tour: Gene Linkouts',
        content: 'We get a popup with a number of options relevant to the gene.',
        element: SELECTOR.popup,
        placement: 'bottom',
      }, {
        title: 'Gene Tour: Genomic Contexts',
        content: 'Let\'s follow the link for similar genomic contexts',
        element: SELECTOR.contextViewerLink,
        reflex: true,
        placement: 'right',
        onNext: function() {
          $(SELECTOR.contextViewerLink)[0].click();
        }
      }, {
        title: 'Gene Tour: Gene Search',
        content: 'Please be patient, as we wait for the page to load...',
        placement: 'top',
        onShown: function(tour) {
          $('.popover-navigation div').hide();
          // wait for dynamic content with a loading dialog.
          if(tour.skipStep) {
            tour.skipStep = false;
            tour.prev();
            return;
          }
          var promise = lisTours.waitForContent(
            tour,
            function() {
              return $('g.gene:has(:contains("'+FOCUS_GENE+'")) > path');
            });
          // advance automatically to next step when done loading
          promise.then(function() {
            tour.next();
          });
          return promise;
        }
      },{
        title: 'Gene Tour: Genome Context Viewer',
        content: 'Our gene is front and center, highlighted among the neighboring genes from the same region on the chromosome. All genes are color coded according to the gene families to which they belong, and genomic segments with similar gene content are aligned to the track containing our search gene.',
        element: SELECTOR.contextFocusGene,
        placement: 'top',
        onPrev: function(tour) {
          tour.skipStep = true;}
      }, {
        title: 'Gene Tour: Gene Family Focus',
        content: 'Hover over the gene family to see all representatives highlighted in their syntenic contexts. Click the gene family for more information.',
        element: SELECTOR.contextLocus,
        placement: 'left',
        onNext: function() {
          $(SELECTOR.contextLocus).d3Click();
        },
      }, {
        title: 'Gene Tour: Gene Family Focus',
        content: 'All representatives of the family present in the current view are listed.',
        element: SELECTOR.contextFocus,
        placement: 'right'
      }, {
        title: 'Gene Tour: Gene Family Focus',
        content: 'Following this link will take us back to the phylotree, with all of the syntenic members of the family highlighted.',
        element: SELECTOR.contextFamilyLink,
        placement: 'right',
        onNext: function() {
          $(SELECTOR.contextFamilyLink)[0].click();
        },
      }, {
        title: 'Gene Tour: Gene Family Focus',
        content: 'We see that most of the members of the clade containing our gene were found by the syntenic context matching. The context viewer alignment parameters might be relaxed to try to include more of the clade and see how the gene content of the regions may have diverged more rapidly in these species.',
        element: SELECTOR.phylotree,
        placement: 'right'
      }, {
        title: 'Gene Tour: The End',
        content: 'Congratulations, you\'ve made it to the end of the Gene Tour. Please let us know if you have any suggestions on how to improve the tools used in this tour or the tour itself by using our Contact form. Now press End Tour.' ,
        element: SELECTOR.contact,
        reflex: true,
        placement: 'bottom'
     }
    ]
  });

  lisTours.register(tour);

}(window.__jquery));

<commit_msg>Remove Prev button throughout the tour.<commit_after> (function(jQuery) {

  var $ = jQuery;
  var FOCUS_GENE_FAMILY = 'phytozome_10_2.59088092';
  var FOCUS_GENE = 'vigra.Vradi01g03360';
  var FOCUS_GENE_UNIQUENAME = 'Vradi01g03360.Vradi.ver6';
  var FOCUS_PROTEIN = FOCUS_GENE + '.1';
  var FOCUS_GENE_FUNCTION = 'gamma-glutamyl transpeptidase';
  var SELECTOR = {
    welcome: '#site-name',
    searchBtn: "a:contains('Gene Search')",
    descriptors: 'th.views-field-description',
    formFieldDescr: '#edit-description',
    formFieldAbbrev: '#edit-abbreviation',
    submitBtn: '#edit-submit-gene',
    searchResult: 'th.views-field-name',
    functionalDesc: 'th.views-field-description',
    geneFamilyLink:'a[href*="'+FOCUS_GENE_UNIQUENAME+'"]:contains("'+FOCUS_GENE_FAMILY+'")',
    phylotree: '#phylogram text:contains("'+FOCUS_PROTEIN+'")',
    popup: '#phylonode_popup_dialog',
    contextViewerLink: "#phylonode_popup_dialog a[href*='lis_context_viewer']",
    contextFocusGene: 'path.point.focus:first',
    contextLocus: 'g.legend:has(:contains("phytozome_10_2.59088092"))',
    contextFocus: 'h4:contains("phytozome_10_2.59088092")',
    contextFamilyLink: 'a:contains("View genes in phylogram")',
    contact: 'a:contains("Contact")',
  };

  $.fn.d3Click = function () {
    this.each(function (i, e) {
      var evt = new MouseEvent('click');
      e.dispatchEvent(evt);
    });
  };

  var tour = new Tour({
    name: 'gene-to-phylotree-to-context-viewer',
    keyboard: true,
    debug: true,
    orphan: true,
    // this template should be same as default, except with Prev button removed.
    template: '

<button class="btn btn-sm btn-default" data-role="next">Next →</button>
<button class="btn btn-sm btn-default" data-role="end">End tour</button>

',
    steps : [
      {
        title: 'Gene Tour: Welcome to LIS!',
        content: 'Let\'s go the legumeinfo.org homepage, where the Gene Tour will begin.  Use the Next button or → (right arrow key) to advance the tour.',
        placement: 'bottom',
        element: SELECTOR.welcome,
        reflex: true,
      }, {
        path: '/home',
        title: 'Gene Tour: Getting started',
        content: "This tour will provide an example of navigating LIS from a gene annotated with a specific function to views of its evolutionary context both with respect to other individual genes as well as to the genomic context in which it occurs.
 Now press the Gene Search button, or use the Next button or press →.",
        placement: 'bottom',
        element: SELECTOR.searchBtn,
        reflex: true,
      }, {
        title: 'Gene Tour: Gene Search',
        path: '/search/gene',
        content: 'All the legume genes in LIS have been annotated with functional descriptors based on a consistent set of homology-based methods. We\'ll use gene function as a query to limit the set of genes in the results.',
        placement: 'top',
        element: SELECTOR.descriptors,
        reflex: true,
      }, {
        title: 'Gene Tour: Gene Search',
        content: 'We can enter key terms from the expected description of the gene function in which we\'re interested here.',
        placement: 'bottom',
        element: SELECTOR.formFieldDescr,
        reflex: true,
        onShown: function(tour) {
          $(SELECTOR.formFieldDescr)[0].value=FOCUS_GENE_FUNCTION;
          $('#edit-description-op')[0].value='starts';
        },
      }, {
        title: 'Gene Tour: Filter by Species',
        content: "Supposing we are initially interested in the genes from Vigna radiata (mungbean), we would specify the five-letter species abbreviation 'vigra', composed of the first three letters of the genus and the first two letters of the species component of the scientific name.",
        onShown: function() {
          $(SELECTOR.formFieldAbbrev)[0].value='vigra';
        },
        element: SELECTOR.formFieldAbbrev,
        placement: 'bottom',
        reflex: true,
      }, {
        title: 'Gene Tour: Gene Search',
        content: 'Then click "Apply" to apply the specified filters to the genes in the result.',
        placement: 'bottom',
        element: SELECTOR.submitBtn,
        reflex: true,
        onNext: function() {
          $(SELECTOR.submitBtn)[0].click();
        }
      }, {
        title: 'Gene Tour: Gene Search',
        content: 'Please be patient, as we wait for the query results to be returned...',
        placement: 'top',
        onShown: function(tour) {
          $('.popover-navigation div').hide();
          // wait for dynamic content with a loading dialog.
          var promise = lisTours.waitForContent(
            tour,
            function() {
              return $('tr.views-row-first td.views-field-description:contains("'+FOCUS_GENE_FUNCTION+'")')[0];
            }, 40000);
          // advance automatically to next step when done loading
          promise.then(function() {
            tour.next();
          });
          return promise;
        }
      }, {
        title: 'Gene Tour: Search Results',
        content: "We've gotten paged results back for genes matching the given description in our target species.",
        element: SELECTOR.searchResult,
        placement: 'top',
        reflex: true,
       }, {
     title: 'Gene Tour: Functional description',
     content: "Notice that there is a subtle difference in the annotation of the genes, though they are all listed as belonging to the same gene family.",
     placement: 'top',
     element: SELECTOR.functionalDesc,
     reflex: true,
      }, {
        title: 'Gene Tour: Gene Family',
        content: "Following the link to the gene family will show you this gene in the context of a tree representing the orthologous, paralogous and homoeologous members of the family.",
        placement: 'left',
	reflex: false, // NB dont use reflex, because of absolute link to legumeinfo.org,
        element: SELECTOR.geneFamilyLink,
      }, {
        title: 'Gene Tour: Phylotree',
        //NB: currently, this must use path instead of click on the
        //link element due to the fact that these are being made into
        //external absolute links to legumeinfo.org probably due to
        //peanutbase
        path: '/chado_phylotree/'+FOCUS_GENE_FAMILY+'?hilite_node='+FOCUS_PROTEIN,
        content: 'Waiting for tree display...',
        placement: 'top',
        onShown: function(tour) {
          $('.popover-navigation div').hide();
          // wait for dynamic content with a loading dialog.
	  var promise = lisTours.waitForSelector(tour, SELECTOR.phylotree);
          // advance automatically to next step when done loading
          promise.then(function() {
            tour.next();
          });
          return promise;
        }
      }, {
        title: 'Gene Tour: Phylotree',
        placement: 'right',
        content : 'Here is our gene again, surrounded by orthologues from other species. ',
        element : SELECTOR.phylotree,
	onShow: function(tour) {
	  return lisTours.waitForSelector(tour, SELECTOR.phylotree)
	},
      }, {
        title: 'Gene Tour: Phylotree',
        placement: 'right',
        content : 'Notice that the two other instances of the gene family from mungbean are in a separate clade. This suggests that the gene was duplicated in an ancestral species and the two copies were retained in most of the species (possibly with subsequent duplications within some of the descendant species). This could be due to an important difference in function that evolved after the ancient duplication occurred.',
        element : SELECTOR.phylotree,
      }, {
        title: 'Gene Tour: Phylotree',
        placement: 'right',
        content : 'The nodes of the tree representing the genes (as well as the internal ancestral nodes) can be clicked for more options.',
        element : SELECTOR.phylotree,
        reflex: true,
        onNext: function() {
          $('#phylogram g.leaf:contains("'+FOCUS_PROTEIN+'")').d3Click();
          var promise = lisTours.waitForContent(
            tour,
            function() {
              return $("#phylonode_popup_dialog a[href*='lis_context_viewer']")[0];
            });
          return promise;
        }
      }, {
        title: 'Gene Tour: Gene Linkouts',
        content: 'We get a popup with a number of options relevant to the gene.',
        element: SELECTOR.popup,
        placement: 'bottom',
      }, {
        title: 'Gene Tour: Genomic Contexts',
        content: 'Let\'s follow the link for similar genomic contexts',
        element: SELECTOR.contextViewerLink,
        reflex: true,
        placement: 'right',
        onNext: function() {
          $(SELECTOR.contextViewerLink)[0].click();
        }
      }, {
        title: 'Gene Tour: Gene Search',
        content: 'Please be patient, as we wait for the page to load...',
        placement: 'top',
        onShown: function(tour) {
          $('.popover-navigation div').hide();
          // wait for dynamic content with a loading dialog.
          var promise = lisTours.waitForContent(
            tour,
            function() {
              return $('g.gene:has(:contains("'+FOCUS_GENE+'")) > path');
            });
          // advance automatically to next step when done loading
          promise.then(function() {
            tour.next();
          });
          return promise;
        }
      },{
        title: 'Gene Tour: Genome Context Viewer',
        content: 'Our gene is front and center, highlighted among the neighboring genes from the same region on the chromosome. All genes are color coded according to the gene families to which they belong, and genomic segments with similar gene content are aligned to the track containing our search gene.',
        element: SELECTOR.contextFocusGene,
        placement: 'top',
      }, {
        title: 'Gene Tour: Gene Family Focus',
        content: 'Hover over the gene family to see all representatives highlighted in their syntenic contexts. Click the gene family for more information.',
        element: SELECTOR.contextLocus,
        placement: 'left',
        onNext: function() {
          $(SELECTOR.contextLocus).d3Click();
        },
      }, {
        title: 'Gene Tour: Gene Family Focus',
        content: 'All representatives of the family present in the current view are listed.',
        element: SELECTOR.contextFocus,
        placement: 'right'
      }, {
        title: 'Gene Tour: Gene Family Focus',
        content: 'Following this link will take us back to the phylotree, with all of the syntenic members of the family highlighted.',
        element: SELECTOR.contextFamilyLink,
        placement: 'right',
        onNext: function() {
          $(SELECTOR.contextFamilyLink)[0].click();
        },
      }, {
        title: 'Gene Tour: Gene Family Focus',
        content: 'We see that most of the members of the clade containing our gene were found by the syntenic context matching. The context viewer alignment parameters might be relaxed to try to include more of the clade and see how the gene content of the regions may have diverged more rapidly in these species.',
        element: SELECTOR.phylotree,
        placement: 'right'
      }, {
        title: 'Gene Tour: The End',
        content: 'Congratulations, you\'ve made it to the end of the Gene Tour. Please let us know if you have any suggestions on how to improve the tools used in this tour or the tour itself by using our Contact form. Now press End Tour.' ,
        element: SELECTOR.contact,
        reflex: true,
        placement: 'bottom'
     }
    ]
  });

  lisTours.register(tour);

}(window.__jquery));

<|endoftext|>


<commit_before>// A controller represents an interface to the data
// stored in our database
function Controller(options) {
  this.options = options;
}

Object.assign(Controller.prototype, {
  create() {},

  read(id) {
    // `singular` is whether or not we're looking for 1
    // or all. This coercion is fine because SERIALs start at 1
    const singular = Boolean(id);

    // Our base query
    var query = `SELECT * FROM ${this.options.table}`;

    // If we're looking for one, we modify the query string
    if (singular) {
      query += ' WHERE id = $1';
    }

    const method = singular ? 'oneOrNone' : 'any';
    return this.options.store[method](query, id);
  },

  update() {},

  delete(id) {
    if (!id) {
      return Promise.reject(new TypeError('id was undefined'));
    }

    const query = `DELETE FROM ${this.options.table} WHERE id = $1 RETURNING *`;
    return this.options.store.one(query, id);
  }
});

module.exports = Controller;
<commit_msg>Add create method to controller; attach options to instance<commit_after>const _ = require('lodash');

// The options that can be passed into a Controller
const validOptions = ['table', 'store'];

// A controller represents an interface to the data
// stored in our database
function Controller(options) {
  Object.assign(this, _.pick(options, validOptions));
}

Object.assign(Controller.prototype, {
  create(data) {
    const query = `INSERT INTO ${this.table} (description, value, date) VALUES ($1, $2, $3) RETURNING *`;
    return this.store.one(query, [data.description, data.value, data.date]);
  },

  read(id) {
    // `singular` is whether or not we're looking for 1
    // or all. This coercion is fine because SERIALs start at 1
    const singular = Boolean(id);

    // Our base query
    var query = `SELECT * FROM ${this.table}`;

    // If we're looking for one, we modify the query string
    if (singular) {
      query += ' WHERE id = $1';
    }

    const method = singular ? 'oneOrNone' : 'any';
    return this.store[method](query, id);
  },

  update() {},

  delete(id) {
    if (!id) {
      return Promise.reject(new TypeError('id was undefined'));
    }

    const query = `DELETE FROM ${this.table} WHERE id = $1 RETURNING *`;
    return this.store.one(query, id);
  }
});

module.exports = Controller;
<|endoftext|>


<commit_before>var IndigoAdmin = (function() {
	'use strict';

	// var console = console ? console : {
	// 	log: function() {}
	// };
	try {
		var Unity = external.getUnityObject(1.0);
	} catch(e) {}

	function pageInit (base_url) {

		console.log('Incorrectly inline styled elements: ', $('[style]:not(.menu-gravatar)'));

		$('.datatables').each(function() {
			var dtInstance = $(this).dataTable({
				"oLanguage": {
					"sUrl": base_url + "translation/datatables.json"
				},
				"sPaginationType": "bs_full",
				"sDom": "<'panel-heading'<'pull-right'f><'pull-left'l><'pull-left'>r<'clearfix'>><'table-responsive't>p",
				"bProcessing": $(this).data('source') ? true : false,
				"bServerSide": $(this).data('source') ? true : false,
				"sAjaxSource": $(this).data('source'),
				"fnInitComplete": function(oSettings, json) {
					var datatable = this;
					// SEARCH - Add the placeholder for Search and Turn this into in-line form control
					var search_input = datatable.closest('.dataTables_wrapper').find('div[id$=_filter] input');
					search_input.attr('placeholder', (oSettings && oSettings.oLanguage && oSettings.oLanguage.sSearchPlaceholder) ? oSettings.oLanguage.sSearchPlaceholder : 'Search all fields');
					search_input.addClass('form-control input-sm');
					// LENGTH - Inline-Form control
					var length_sel = datatable.closest('.dataTables_wrapper').find('div[id$=_length] select');
					// length_sel.addClass('form-control input-sm');
					length_sel.selectpicker().selectpicker('setStyle', 'btn-sm', 'add');

					// Initial filter values
					datatable.find('.filter').each(function() {
						var inputs = $(this).find('input, select');
						inputs.each(function(index, el) {
							if ($(el).val())
							{
								datatable.fnFilter($(el).val(), index);
							}
						});
					});
				},
				"fnServerData": function(sSource, aoData, fnCallback) {
					$.each($(this).data(), function(index, val) {
						aoData.push({'name': index, 'value': val});
					});

					$.ajax( {
						"dataType": 'json',
						"type": "POST",
						"url": sSource,
						"data": aoData,
						"success": function(response, result, xhr) {
							console.log(result);
							fnCallback(response, result, xhr);
						},
						"timeout": 15000,
						"error": function(param) {
							console.log(param);
						}
					} );
				},
			});

			// All select filters should be selectpickers
			dtInstance.find('.filter select').selectpicker();

			// Filter mechanism on filter control change
			dtInstance.find('.filter').each(function() {
				var inputs = $(this).find('input, select');
				inputs.change(function() {
					if ($(this).is('select'))
					{
						dtInstance.fnFilter($(this).val(), inputs.index(this));
					}
					else
					{
						dtInstance.fnFilter(this.value, inputs.index(this));
					}
				});
			});

			// Filter reset mechanism
			dtInstance.find('.filter [type=reset]').click(function() {
				dtInstance.find('.filter select').each(function() {
					$(this).val([]).selectpicker('render');
				});

				dtInstance.find('.filter input').each(function() {
					$(this).val('');
				});

				dtInstance.fnFilterClear();
			});
		});

		try {
			Unity.init({
				name: "Indigo Admin",
				iconUrl: "https://secure.gravatar.com/avatar/7f3930c2e3af0d6dcb0115e194111898?s=256&d=mm",
				onInit: setUpUnityIntegration
			});
		} catch(e) {}

		console.log('IndigoAdmin pageInit() completed.');
	}

	function exposeMenu(prefix, menu) {
		menu.find(' > li').each(function() {
			var anchor = $(this).find('> a');
			var button = $(this).find('> button');
			if (anchor.length) {
				Unity.addAction(prefix + '/' + $.trim(anchor.text()), function() { anchor[0].click(); });
			} else {
				exposeMenu(prefix + '/' + $.trim(button.text()), $(this).find(' > ul'));
			}
		});
	}

	function setUpUnityIntegration() { // private
		exposeMenu('', $('.side-navigation .primary'));
		$(this).trigger('unityready');
	}

	function setMenuLabel(menuItemId, text, type) {
		type = (typeof type !== 'undefined') ? type : 'primary';
		$('#menu-item-' + menuItemId + ' > .label')
			.removeClass('label-danger')
			.removeClass('label-info')
			.removeClass('label-warning')
			.removeClass('label-default')
			.removeClass('label-primary')
			.addClass('label-' + type)
			.html(text);
	}

	function addFastButton (title, icon, callback) {
		icon = (typeof icon !== 'undefined' && icon !== null && icon !== '') ? '' : '';
		title = (typeof title !== 'undefined' && title !== null && title !== '') ? title : '';
		if (title + icon == '') {
			console.error('You must supply at least one of title and icon.');
			return;
		}
		var elementHtml = '
' + icon + ((title != '' && icon != '') ? '   ' : '' ) + title + '
';
		var element = $(elementHtml);
		element.find('a').click(function() { callback(); return false; });
		element.prependTo('html body div.navbar div.hidden-xs ul.nav');
	}

	function addProxyButton (title, icon, selector) {
		addFastButton(title, icon, function() {
			$(selector)[0].click();
		});
	}

	function displayNotification (title, body, icon) {
		Unity.Notification.showNotification(title, body, icon);
	}

	return {
		pageInit:            pageInit,
		setMenuLabel:        setMenuLabel,
		addFastButton:       addFastButton,
		addProxyButton:      addProxyButton,
		displayNotification: displayNotification
	};

}());<commit_msg>Datatable filtering, initial filters and filter syncing<commit_after>var IndigoAdmin = (function() {
	'use strict';

	// var console = console ? console : {
	// 	log: function() {}
	// };
	try {
		var Unity = external.getUnityObject(1.0);
	} catch(e) {}

	function pageInit (base_url) {

		console.log('Incorrectly inline styled elements: ', $('[style]:not(.menu-gravatar)'));

		$('.datatables').each(function() {

			var filter = []
			$(this).find('.filter:first').find('input, select').each(function(index, el) {
				if ($(el).val())
				{
					filter.push({'sSearch': $(el).val()});
				}
				else
				{
					filter.push(null);
				}
			});

			var dtInstance = $(this).dataTable({
				"oLanguage": {
					"sUrl": base_url + "translation/datatables.json"
				},
				"sPaginationType": "bs_full",
				"sDom": "<'panel-heading'<'pull-right'f><'pull-left'l><'pull-left'>r<'clearfix'>><'table-responsive't>p",
				"bProcessing": $(this).data('source') ? true : false,
				"bServerSide": $(this).data('source') ? true : false,
				"sAjaxSource": $(this).data('source'),
				"aoSearchCols": filter,
				"fnInitComplete": function(oSettings, json) {
					var datatable = this;
					// SEARCH - Add the placeholder for Search and Turn this into in-line form control
					var search_input = datatable.closest('.dataTables_wrapper').find('div[id$=_filter] input');
					search_input.attr('placeholder', (oSettings && oSettings.oLanguage && oSettings.oLanguage.sSearchPlaceholder) ? oSettings.oLanguage.sSearchPlaceholder : 'Search all fields');
					search_input.addClass('form-control input-sm');
					// LENGTH - Inline-Form control
					var length_sel = datatable.closest('.dataTables_wrapper').find('div[id$=_length] select');
					// length_sel.addClass('form-control input-sm');
					length_sel.selectpicker().selectpicker('setStyle', 'btn-sm', 'add');
				},
				"fnServerData": function(sSource, aoData, fnCallback) {
					$.each($(this).data(), function(index, val) {
						aoData.push({'name': index, 'value': val});
					});

					$(this).find('.filter').each(function() {
						var inputs = $(this).find('input, select');
						inputs.each(function(index, el) {
							if ($(el).is('select')) {
								aoData.push({'name': 'cType_'+index, 'value': 'select'});
							}
							else if($(el).is('input')) {
								aoData.push({'name': 'cType_'+index, 'value': $(el).attr('type')});
							}
						});
					});

					$.ajax( {
						"dataType": 'json',
						"type": "POST",
						"url": sSource,
						"data": aoData,
						"success": function(response, result, xhr) {
							console.log(result);
							fnCallback(response, result, xhr);
						},
						"timeout": 15000,
						"error": function(param) {
							console.log(param);
						}
					} );
				},
			});

			// All select filters should be selectpickers
			dtInstance.find('.filter select').selectpicker();

			// Find filters
			var filters = dtInstance.find('.filter');

			// Filter mechanism on filter control change
			// Filter syn mechanism across the same inputs, selects
			// (Assuming that all filter elements are identical to the others)
			filters.each(function() {
				// Do the filtering
				var controls = $(this).find('input, select');
				// var controls = $(this).find('input, select').change(function() {
				// 	dtInstance.fnFilter($(this).val(), controls.index(this));
				// });

				// Sync inputs
				var inputs = $(this).find('input').on('keypress keyup paste change', function() {
					filters.not(this).find('input:eq(' + inputs.index(this) + ')').val($(this).val());
				}).change(function() {
					dtInstance.fnFilter($(this).val(), controls.index(this));
				});

				// Sync selects
				var selects = $(this).find('select').on('change', function() {
					filters.not(this).find('select:eq(' + selects.index(this) + ')').val($(this).val()).selectpicker('refresh');
				}).change(function() {
					dtInstance.fnFilter($(this).val(), controls.index(this));
				});
			});

			// Filter reset mechanism
			dtInstance.find('.filter [type=reset]').click(function() {
				dtInstance.find('.filter select').each(function() {
					$(this).val([]).selectpicker('render');
				});

				dtInstance.find('.filter input').each(function() {
					$(this).val('');
				});

				dtInstance.fnFilterClear();
			});
		});

		try {
			Unity.init({
				name: "Indigo Admin",
				iconUrl: "https://secure.gravatar.com/avatar/7f3930c2e3af0d6dcb0115e194111898?s=256&d=mm",
				onInit: setUpUnityIntegration
			});
		} catch(e) {}

		console.log('IndigoAdmin pageInit() completed.');
	}

	function exposeMenu(prefix, menu) {
		menu.find(' > li').each(function() {
			var anchor = $(this).find('> a');
			var button = $(this).find('> button');
			if (anchor.length) {
				Unity.addAction(prefix + '/' + $.trim(anchor.text()), function() { anchor[0].click(); });
			} else {
				exposeMenu(prefix + '/' + $.trim(button.text()), $(this).find(' > ul'));
			}
		});
	}

	function setUpUnityIntegration() { // private
		exposeMenu('', $('.side-navigation .primary'));
		$(this).trigger('unityready');
	}

	function setMenuLabel(menuItemId, text, type) {
		type = (typeof type !== 'undefined') ? type : 'primary';
		$('#menu-item-' + menuItemId + ' > .label')
			.removeClass('label-danger')
			.removeClass('label-info')
			.removeClass('label-warning')
			.removeClass('label-default')
			.removeClass('label-primary')
			.addClass('label-' + type)
			.html(text);
	}

	function addFastButton (title, icon, callback) {
		icon = (typeof icon !== 'undefined' && icon !== null && icon !== '') ? '' : '';
		title = (typeof title !== 'undefined' && title !== null && title !== '') ? title : '';
		if (title + icon == '') {
			console.error('You must supply at least one of title and icon.');
			return;
		}
		var elementHtml = '
' + icon + ((title != '' && icon != '') ? '   ' : '' ) + title + '
';
		var element = $(elementHtml);
		element.find('a').click(function() { callback(); return false; });
		element.prependTo('html body div.navbar div.hidden-xs ul.nav');
	}

	function addProxyButton (title, icon, selector) {
		addFastButton(title, icon, function() {
			$(selector)[0].click();
		});
	}

	function displayNotification (title, body, icon) {
		Unity.Notification.showNotification(title, body, icon);
	}

	return {
		pageInit:            pageInit,
		setMenuLabel:        setMenuLabel,
		addFastButton:       addFastButton,
		addProxyButton:      addProxyButton,
		displayNotification: displayNotification
	};

}());<|endoftext|>


<commit_before>'use strict';

exports.init = function(req, res){
  if (req.isAuthenticated()) {
    res.redirect(req.user.defaultReturnUrl());
  }
  else {
    res.render('login/forgot/index');
  }
};

exports.send = function(req, res, next){
  var workflow = req.app.utility.workflow(req, res);

  workflow.on('validate', function() {
    if (!req.body.email) {
      workflow.outcome.errfor.email = 'required';
      return workflow.emit('response');
    }

    workflow.emit('generateToken');
  });

  workflow.on('generateToken', function() {
    var crypto = require('crypto');
    crypto.randomBytes(21, function(err, buf) {
      if (err) {
        return next(err);
      }

      var token = buf.toString('hex');
      req.app.db.models.User.encryptPassword(token, function(err, hash) {
        if (err) {
          return next(err);
        }

        workflow.emit('patchUser', token, hash);
      });
    });
  });

  workflow.on('patchUser', function(token, hash) {
    var conditions = { email: req.body.email.toLowerCase() };
    var fieldsToSet = {
      resetPasswordToken: hash,
      resetPasswordExpires: Date.now() + 10000000
    };
    req.app.db.models.User.findOneAndUpdate(conditions, fieldsToSet, function(err, user) {
      if (err) {
        return workflow.emit('exception', err);
      }

      if (!user) {
        return workflow.emit('response');
      }

      workflow.emit('sendEmail', token, user);
    });
  });

  workflow.on('sendEmail', function(token, user) {
    req.app.utility.sendmail(req, res, {
      from: req.app.config.smtp.from.name +' <'+ req.app.config.smtp.from.address +'>',
      to: user.email,
      subject: 'Reset your '+ req.app.config.projectName +' password',
      textPath: 'login/forgot/email-text',
      htmlPath: 'login/forgot/email-html',
      locals: {
        username: user.username,
        resetLink: req.protocol +'://'+ req.headers.host +'/login/reset/'+ user.email +'/'+ token +'/',
        projectName: req.app.config.projectName
      },
      success: function(message) {
        workflow.emit('response');
      },
      error: function(err) {
        workflow.outcome.errors.push('Error Sending: '+ err);
        workflow.emit('response');
      }
    });
  });

  workflow.emit('validate');
};
<commit_msg>Fix database commands for resetting password<commit_after>'use strict';

exports.init = function(req, res){
  if (req.isAuthenticated()) {
    res.redirect(req.user.defaultReturnUrl());
  }
  else {
    res.render('login/forgot/index');
  }
};

exports.send = function(req, res, next){
  var workflow = req.app.utility.workflow(req, res);

  workflow.on('validate', function() {
    if (!req.body.email) {
      workflow.outcome.errfor.email = 'required';
      return workflow.emit('response');
    }

    workflow.emit('generateToken');
  });

  workflow.on('generateToken', function() {
    var crypto = require('crypto');
    crypto.randomBytes(21, function(err, buf) {
      if (err) {
        return next(err);
      }

      var token = buf.toString('hex');
      req.app.db.User.encryptPassword(token, function(err, hash) {
        if (err) {
          return next(err);
        }

        workflow.emit('patchUser', token, hash);
      });
    });
  });

  workflow.on('patchUser', function(token, hash) {
    var conditions = { email: req.body.email.toLowerCase() };
    var fieldsToSet = {
      resetPasswordToken: hash,
      resetPasswordExpires: Date.now() + 10000000
    };

      req.app.db.User.findOne( {where: conditions})
          .then( function(User) {
              if (User) {
                  return User.updateAttributes( fieldsToSet)
              }
              else {
                  return workflow.emit('exception', 'Sorry, we don\'t recognize that email address.' );
              }
          })
          .then(function (User) {
              workflow.emit('sendEmail', token, User);

          })
          .catch(function (err) {
              return workflow.emit('exception', err);
          });

  });

  workflow.on('sendEmail', function(token, user) {
    req.app.utility.sendmail(req, res, {
      from: req.app.config.smtp.from.name +' <'+ req.app.config.smtp.from.address +'>',
      to: user.email,
      subject: 'Reset your '+ req.app.config.projectName +' password',
      textPath: 'login/forgot/email-text',
      htmlPath: 'login/forgot/email-html',
      locals: {
        username: user.username,
        resetLink: req.protocol +'://'+ req.headers.host +'/login/reset/'+ user.email +'/'+ token +'/',
        projectName: req.app.config.projectName
      },
      success: function(message) {
        workflow.emit('response');
      },
      error: function(err) {
        workflow.outcome.errors.push('Error Sending: '+ err);
        workflow.emit('response');
      }
    });
  });

  workflow.emit('validate');
};
<|endoftext|>


<commit_before>/*********************************************
 * Production build for deployment
 *********************************************/

// For excluding /node_modules/
var webpack = require("webpack");
var nodeExternals = require('webpack-node-externals');
var Fs = require('fs')
var nodeModules = {}
var ExtractTextPlugin = require('extract-text-webpack-plugin');
var cssnext = require('postcss-cssnext');

Fs.readdirSync('node_modules').forEach(function (module) {
  if (module !== '.bin') {
    nodeModules[module] = true
  }
})

module.exports = [{
// Front end
  target: 'node',
  entry: './src/server.js',
  output: {
    path: __dirname,
    filename: "./build/server.js",
  },
  externals: [nodeExternals()],
  resolve: {
    root: __dirname,
    modulesDirectories: [
      'node_modules',
      './src/',
    ],
    extensions: ['', '.js', '.jsx'],
  },

  plugins: [
    new ExtractTextPlugin('./static/build/main.css', {
      allChunks: true,
    }),
    new webpack.DefinePlugin({
      'process.env':{
        'NODE_ENV': '"production"', // compiles React as production build
	'PORT': 8001, // we use a reverse proxy to forward this to port 80
      },
    }),
    new webpack.optimize.UglifyJsPlugin({
      compress: {
        warnings: true,
      },
    }),
    new webpack.optimize.DedupePlugin(),
    new webpack.optimize.AggressiveMergingPlugin(),
    new webpack.optimize.OccurrenceOrderPlugin(true),
  ],

  module: {
    loaders: [
      {
        loader: 'babel-loader',
        test: /\.jsx?$/,
        exclude: /node_modules/,
        query: {
          plugins: ['lodash'],
          presets: ['es2015', 'react'],
        },
      },

      /*
       * Parse SCSS to minified CSS, then postprocess with postcss autoprefixer plugin
       */
      {
        test: /\.scss$/,
        loader: ExtractTextPlugin.extract('style', ['css?minimize', 'postcss', 'sass']),
      },

      /*
       * Load in files with CSS alone
       */
      {
        test: /\.(png|woff|woff2|eot|ttf|svg)$/,
        loader: 'url-loader?limit=10000',
      },
    ],
  },
  postcss: function () {
    return [cssnext];
  },
}, {
  target: 'web',
  entry: './src/client.js',
  output: {
    filename: "./static/build/client.js",
  },
  resolve: {
    root: __dirname,
    modulesDirectories: [
      'node_modules',
      './src/',
    ],
    extensions: ['', '.js', '.jsx'],
  },

  plugins: [
    new webpack.OldWatchingPlugin(),
  ],

  module: {
    loaders: [
      {
        loader: 'babel',
        test: /\.jsx?$/,
        exclude: /node_modules/,
        query: {
          plugins: ['lodash'],
          presets: ['es2015', 'react'],
        },
      },
      {
        test: /\.scss$/,
        loader: "null",
      },
    ],
  },
},
// Editor tools copy
{
  target: 'node',
  entry: './src/editor-server.js',
  output: {
    path: __dirname,
    filename: "./build/editor-server.js",
  },
  externals: [nodeExternals()],
  resolve: {
    root: __dirname,
    modulesDirectories: [
      'node_modules',
      './src/',
    ],
    extensions: ['', '.js', '.jsx'],
  },

  plugins: [
    new webpack.OldWatchingPlugin(),
  ],

  module: {
    loaders: [
      {
        loader: 'babel-loader',
        test: /\.jsx?$/,
        exclude: /node_modules/,
        query: {
          presets: ['es2015', 'react'],
        },
      },
    ],
  },
}, {
  target: 'web',
  entry: './src/editor-client.js',
  output: {
    filename: "./static/build/editor-client.js",
  },
  resolve: {
    root: __dirname,
    modulesDirectories: [
      'node_modules',
      './src/',
    ],
    extensions: ['', '.js', '.jsx'],
  },
  module: {
    loaders: [
      {
        loader: 'babel',
        test: /\.jsx?$/,
        exclude: /node_modules/,
        query: {
          presets: ['es2015', 'react'],
        },
      },
    ],
  },
}]
<commit_msg>Added the production flag to the client.js webpack build<commit_after>/*********************************************
 * Production build for deployment
 *********************************************/

// For excluding /node_modules/
var webpack = require("webpack");
var nodeExternals = require('webpack-node-externals');
var Fs = require('fs')
var nodeModules = {}
var ExtractTextPlugin = require('extract-text-webpack-plugin');
var cssnext = require('postcss-cssnext');

Fs.readdirSync('node_modules').forEach(function (module) {
  if (module !== '.bin') {
    nodeModules[module] = true
  }
})

module.exports = [{
// Front end
  target: 'node',
  entry: './src/server.js',
  output: {
    path: __dirname,
    filename: "./build/server.js",
  },
  externals: [nodeExternals()],
  resolve: {
    root: __dirname,
    modulesDirectories: [
      'node_modules',
      './src/',
    ],
    extensions: ['', '.js', '.jsx'],
  },

  plugins: [
    new ExtractTextPlugin('./static/build/main.css', {
      allChunks: true,
    }),
    new webpack.DefinePlugin({
      'process.env':{
        'NODE_ENV': '"production"', // compiles React as production build
	'PORT': 8001, // we use a reverse proxy to forward this to port 80
      },
    }),
    new webpack.optimize.UglifyJsPlugin({
      compress: {
        warnings: true,
      },
    }),
    new webpack.optimize.DedupePlugin(),
    new webpack.optimize.AggressiveMergingPlugin(),
    new webpack.optimize.OccurrenceOrderPlugin(true),
  ],

  module: {
    loaders: [
      {
        loader: 'babel-loader',
        test: /\.jsx?$/,
        exclude: /node_modules/,
        query: {
          plugins: ['lodash'],
          presets: ['es2015', 'react'],
        },
      },

      /*
       * Parse SCSS to minified CSS, then postprocess with postcss autoprefixer plugin
       */
      {
        test: /\.scss$/,
        loader: ExtractTextPlugin.extract('style', ['css?minimize', 'postcss', 'sass']),
      },

      /*
       * Load in files with CSS alone
       */
      {
        test: /\.(png|woff|woff2|eot|ttf|svg)$/,
        loader: 'url-loader?limit=10000',
      },
    ],
  },
  postcss: function () {
    return [cssnext];
  },
}, {
  target: 'web',
  entry: './src/client.js',
  output: {
    filename: "./static/build/client.js",
  },
  resolve: {
    root: __dirname,
    modulesDirectories: [
      'node_modules',
      './src/',
    ],
    extensions: ['', '.js', '.jsx'],
  },

  plugins: [
    new webpack.OldWatchingPlugin(),
    new webpack.DefinePlugin({
      'process.env':{
        'NODE_ENV': '"production"', // compiles React as production build
  'PORT': 8001, // we use a reverse proxy to forward this to port 80
      },
    }),
  ],

  module: {
    loaders: [
      {
        loader: 'babel',
        test: /\.jsx?$/,
        exclude: /node_modules/,
        query: {
          plugins: ['lodash'],
          presets: ['es2015', 'react'],
        },
      },
      {
        test: /\.scss$/,
        loader: "null",
      },
    ],
  },
},
// Editor tools copy
{
  target: 'node',
  entry: './src/editor-server.js',
  output: {
    path: __dirname,
    filename: "./build/editor-server.js",
  },
  externals: [nodeExternals()],
  resolve: {
    root: __dirname,
    modulesDirectories: [
      'node_modules',
      './src/',
    ],
    extensions: ['', '.js', '.jsx'],
  },

  plugins: [
    new webpack.OldWatchingPlugin(),
  ],

  module: {
    loaders: [
      {
        loader: 'babel-loader',
        test: /\.jsx?$/,
        exclude: /node_modules/,
        query: {
          presets: ['es2015', 'react'],
        },
      },
    ],
  },
}, {
  target: 'web',
  entry: './src/editor-client.js',
  output: {
    filename: "./static/build/editor-client.js",
  },
  resolve: {
    root: __dirname,
    modulesDirectories: [
      'node_modules',
      './src/',
    ],
    extensions: ['', '.js', '.jsx'],
  },
  module: {
    loaders: [
      {
        loader: 'babel',
        test: /\.jsx?$/,
        exclude: /node_modules/,
        query: {
          presets: ['es2015', 'react'],
        },
      },
    ],
  },
}]
<|endoftext|>


<commit_before>import Immutable from 'immutable';
import React, { Component, PropTypes } from 'react';
import { ListView, InteractionManager } from 'react-native';

/**
 * Return the keys from a set of data.
 *
 * @example
 * - getKeys({ foo: 'bar', baz: 'qux' }) will return [foo, baz]
 * - getKeys([2, 3, 5]) will return [0, 1, 2]
 *
 * @param {Immutable.Iterable} immutableData
 * @returns {Array} An array of keys for the data.
 */
function getKeys(immutableData) {
  // TODO: Allow any type of arbitrary data
  return immutableData.keySeq().toArray();
}

/**
 * @param immutableSectionData
 * @returns {Array}
 */
// TODO: Make this getRows instead? Then pass rows into getKeys.
function getRowIdentities(immutableSectionData) {
  // TODO: Allow any type of arbitrary data.
  const sectionKeys = immutableSectionData.map((section) => Object.keys(section));
  return sectionKeys.valueSeq().toArray();
}

/**
 * @param {String|Number} key
 * @param {Immutable.Iterable|Object|Array} data
 * @returns {*} The value at the given key, whether the data is Immutable or not.
 */
function getValueFromKey(key, data) {
  return (typeof data.get === 'function') ? data.get(key) : data[key];
}

/**
 * A ListView capable of displaying {@link https://facebook.github.io/immutable-js/ Immutable} data
 * out of the box.
 */
class ImmutableListView extends Component {

  static propTypes = {
    // Pass through any props that ListView would normally take.
    ...ListView.propTypes,

    // ImmutableListView handles creating the dataSource, so don't pass it in.
    dataSource: PropTypes.oneOf([undefined]),

    // TODO: Allow any type of arbitrary data.
    // eslint-disable-next-line consistent-return
    immutableData: (props, propName, componentName) => {
      // Note: It's not enough to simply validate PropTypes.instanceOf(Immutable.Iterable),
      // because different imports of Immutable.js across files have different class prototypes.
      if (!Immutable.Iterable.isIterable(props[propName])) {
        return new Error(`Invalid prop ${propName} supplied to ${componentName}: Must be Immutable.Iterable.`);
      }
    },

    /**
     * How many rows of data to display while waiting for interactions to finish (e.g. Navigation animations).
     * You can use this to improve the animation performance of longer lists when pushing new routes.
     *
     * @see https://facebook.github.io/react-native/docs/performance.html#slow-navigator-transitions
     */
    rowsDuringInteraction: PropTypes.number,
  };

  static defaultProps = ListView.getDefaultProps();

  state = {
    dataSource: new ListView.DataSource({
      rowHasChanged: (r1, r2) => !Immutable.is(r1, r2),

      getRowData: (dataBlob, sectionID, rowID) => {
        const rowData = getValueFromKey(sectionID, dataBlob);
        return getValueFromKey(rowID, rowData);
      },

      // TODO: This might still return true if just the section data has changed; verify this.
      sectionHeaderHasChanged: (s1, s2) => !Immutable.is(s1, s2),

      getSectionHeaderData: (dataBlob, sectionID) => getValueFromKey(sectionID, dataBlob),
    }),

    interactionOngoing: true,
  };

  componentWillMount() {
    this.setStateFromPropsAfterInteraction(this.props);
  }

  componentWillReceiveProps(newProps) {
    this.setStateFromPropsAfterInteraction(newProps);
  }

  setStateFromPropsAfterInteraction(props) {
    // Always set state right away before the interaction.
    this.setStateFromProps(props, false);

    // If set, wait for animations etc. to complete before rendering the full list of data.
    if (props.rowsDuringInteraction >= 0) {
      InteractionManager.runAfterInteractions(() => {
        this.setStateFromProps(props, true);
      });
    }
  }

  setStateFromProps(props, interactionHasJustFinished) {
    const { dataSource, interactionOngoing } = this.state;
    const { immutableData, rowsDuringInteraction, renderSectionHeader } = props;

    const shouldDisplayPartialData = rowsDuringInteraction >= 0 && interactionOngoing && !interactionHasJustFinished;

    const displayData = (shouldDisplayPartialData
      ? immutableData.slice(0, rowsDuringInteraction)
      : immutableData);

    this.setState({
      dataSource: (renderSectionHeader
        ? dataSource.cloneWithRowsAndSections(displayData, getKeys(displayData), getRowIdentities(displayData))
        : dataSource.cloneWithRows(displayData, getKeys(displayData))
      ),
      interactionOngoing: interactionHasJustFinished ? false : interactionOngoing,
    });
  }

  render() {
    const { dataSource } = this.state;

    // Note: enableEmptySections is being used to mimic the default behavior of the upcoming version.
    return (
      <ListView
        dataSource={dataSource}
        enableEmptySections
        {...this.props}
      />
    );
  }

}

export default ImmutableListView;
<commit_msg>🏇  Inherit from PureComponent, since data is immutable<commit_after>import Immutable from 'immutable';
import React, { PureComponent, PropTypes } from 'react';
import { ListView, InteractionManager } from 'react-native';

/**
 * Return the keys from a set of data.
 *
 * @example
 * - getKeys({ foo: 'bar', baz: 'qux' }) will return [foo, baz]
 * - getKeys([2, 3, 5]) will return [0, 1, 2]
 *
 * @param {Immutable.Iterable} immutableData
 * @returns {Array} An array of keys for the data.
 */
function getKeys(immutableData) {
  // TODO: Allow any type of arbitrary data
  return immutableData.keySeq().toArray();
}

/**
 * @param immutableSectionData
 * @returns {Array}
 */
// TODO: Make this getRows instead? Then pass rows into getKeys.
function getRowIdentities(immutableSectionData) {
  // TODO: Allow any type of arbitrary data.
  const sectionKeys = immutableSectionData.map((section) => Object.keys(section));
  return sectionKeys.valueSeq().toArray();
}

/**
 * @param {String|Number} key
 * @param {Immutable.Iterable|Object|Array} data
 * @returns {*} The value at the given key, whether the data is Immutable or not.
 */
function getValueFromKey(key, data) {
  return (typeof data.get === 'function') ? data.get(key) : data[key];
}

/**
 * A ListView capable of displaying {@link https://facebook.github.io/immutable-js/ Immutable} data
 * out of the box.
 */
class ImmutableListView extends PureComponent {

  static propTypes = {
    // Pass through any props that ListView would normally take.
    ...ListView.propTypes,

    // ImmutableListView handles creating the dataSource, so don't pass it in.
    dataSource: PropTypes.oneOf([undefined]),

    // TODO: Allow any type of arbitrary data.
    // eslint-disable-next-line consistent-return
    immutableData: (props, propName, componentName) => {
      // Note: It's not enough to simply validate PropTypes.instanceOf(Immutable.Iterable),
      // because different imports of Immutable.js across files have different class prototypes.
      if (!Immutable.Iterable.isIterable(props[propName])) {
        return new Error(`Invalid prop ${propName} supplied to ${componentName}: Must be Immutable.Iterable.`);
      }
    },

    /**
     * How many rows of data to display while waiting for interactions to finish (e.g. Navigation animations).
     * You can use this to improve the animation performance of longer lists when pushing new routes.
     *
     * @see https://facebook.github.io/react-native/docs/performance.html#slow-navigator-transitions
     */
    rowsDuringInteraction: PropTypes.number,
  };

  static defaultProps = ListView.getDefaultProps();

  state = {
    dataSource: new ListView.DataSource({
      rowHasChanged: (r1, r2) => !Immutable.is(r1, r2),

      getRowData: (dataBlob, sectionID, rowID) => {
        const rowData = getValueFromKey(sectionID, dataBlob);
        return getValueFromKey(rowID, rowData);
      },

      // TODO: This might still return true if just the section data has changed; verify this.
      sectionHeaderHasChanged: (s1, s2) => !Immutable.is(s1, s2),

      getSectionHeaderData: (dataBlob, sectionID) => getValueFromKey(sectionID, dataBlob),
    }),

    interactionOngoing: true,
  };

  componentWillMount() {
    this.setStateFromPropsAfterInteraction(this.props);
  }

  componentWillReceiveProps(newProps) {
    this.setStateFromPropsAfterInteraction(newProps);
  }

  setStateFromPropsAfterInteraction(props) {
    // Always set state right away before the interaction.
    this.setStateFromProps(props, false);

    // If set, wait for animations etc. to complete before rendering the full list of data.
    if (props.rowsDuringInteraction >= 0) {
      InteractionManager.runAfterInteractions(() => {
        this.setStateFromProps(props, true);
      });
    }
  }

  setStateFromProps(props, interactionHasJustFinished) {
    const { dataSource, interactionOngoing } = this.state;
    const { immutableData, rowsDuringInteraction, renderSectionHeader } = props;

    const shouldDisplayPartialData = rowsDuringInteraction >= 0 && interactionOngoing && !interactionHasJustFinished;

    const displayData = (shouldDisplayPartialData
      ? immutableData.slice(0, rowsDuringInteraction)
      : immutableData);

    this.setState({
      dataSource: (renderSectionHeader
        ? dataSource.cloneWithRowsAndSections(displayData, getKeys(displayData), getRowIdentities(displayData))
        : dataSource.cloneWithRows(displayData, getKeys(displayData))
      ),
      interactionOngoing: interactionHasJustFinished ? false : interactionOngoing,
    });
  }

  render() {
    const { dataSource } = this.state;

    // Note: enableEmptySections is being used to mimic the default behavior of the upcoming version.
    return (
      <ListView
        dataSource={dataSource}
        enableEmptySections
        {...this.props}
      />
    );
  }

}

export default ImmutableListView;
<|endoftext|>