<commit_before>import Vue from 'vue'

import QIcon from '../icon/QIcon.js'
import QSpinner from '../spinner/QSpinner.js'

import ValidateMixin from '../../mixins/validate.js'
import DarkMixin from '../../mixins/dark.js'
import { slot } from '../../utils/slot.js'
import uid from '../../utils/uid.js'
import { stop, prevent } from '../../utils/event.js'
import { fromSSR } from '../../plugins/Platform.js'

function getTargetUid (val) {
  return val === void 0 ? `f_${uid()}` : val
}

export default Vue.extend({
  name: 'QField',

  mixins: [ DarkMixin, ValidateMixin ],

  inheritAttrs: false,

  props: {
    label: String,
    stackLabel: Boolean,
    hint: String,
    hideHint: Boolean,
    prefix: String,
    suffix: String,

    color: String,
    bgColor: String,

    filled: Boolean,
    outlined: Boolean,
    borderless: Boolean,
    standout: [Boolean, String],

    square: Boolean,

    loading: Boolean,

    bottomSlots: Boolean,
    hideBottomSpace: Boolean,

    rounded: Boolean,
    dense: Boolean,
    itemAligned: Boolean,

    counter: Boolean,

    clearable: Boolean,
    clearIcon: String,

    disable: Boolean,
    readonly: Boolean,

    autofocus: Boolean,

    for: [String],
    maxlength: [Number, String],
    maxValues: [Number, String] // private, do not add to JSON; internally needed by QSelect
  },

  data () {
    return {
      focused: false,
      targetUid: getTargetUid(this.for),

      // used internally by validation for QInput
      // or menu handling for QSelect
      innerLoading: false
    }
  },

  watch: {
    for (val) {
      // don't transform targetUid into a computed
      // prop as it will break SSR
      this.targetUid = getTargetUid(val)
    }
  },

  computed: {
    editable () {
      return this.disable !== true && this.readonly !== true
    },

    hasValue () {
      const value = this.__getControl === void 0 ? this.value : this.innerValue

      return value !== void 0 &&
        value !== null &&
        ('' + value).length > 0
    },

    computedCounter () {
      if (this.counter !== false) {
        const len = typeof this.value === 'string' || typeof this.value === 'number'
          ? ('' + this.value).length
          : (Array.isArray(this.value) === true ? this.value.length : 0)
        const max = this.maxlength !== void 0 ? this.maxlength : this.maxValues

        return len + (max !== void 0 ? ' / ' + max : '')
      }
    },

    floatingLabel () {
      return this.hasError === true ||
        this.stackLabel === true ||
        this.focused === true ||
        (
          this.inputValue !== void 0 && this.hideSelected === true
            ? this.inputValue.length > 0
            : this.hasValue === true
        ) ||
        (
          this.displayValue !== void 0 &&
          this.displayValue !== null &&
          ('' + this.displayValue).length > 0
        )
    },

    shouldRenderBottom () {
      return this.bottomSlots === true ||
        this.hint !== void 0 ||
        this.rules !== void 0 ||
        this.counter === true ||
        this.error !== null
    },

    classes () {
      return {
        [this.fieldClass]: this.fieldClass !== void 0,

        [`q-field--${this.styleType}`]: true,
        'q-field--rounded': this.rounded,
        'q-field--square': this.square,

        'q-field--focused': this.focused === true || this.hasError === true,
        'q-field--float': this.floatingLabel,
        'q-field--labeled': this.label !== void 0,

        'q-field--dense': this.dense,
        'q-field--item-aligned q-item-type': this.itemAligned,
        'q-field--dark': this.isDark,

        'q-field--auto-height': this.__getControl === void 0,

        'q-field--with-bottom': this.hideBottomSpace !== true && this.shouldRenderBottom === true,
        'q-field--error': this.hasError,

        'q-field--readonly': this.readonly === true && this.disable !== true,
        'q-field--disabled': this.disable
      }
    },

    styleType () {
      if (this.filled === true) { return 'filled' }
      if (this.outlined === true) { return 'outlined' }
      if (this.borderless === true) { return 'borderless' }
      if (this.standout) { return 'standout' }
      return 'standard'
    },

    contentClass () {
      const cls = []

      if (this.hasError === true) {
        cls.push('text-negative')
      }
      else if (typeof this.standout === 'string' && this.standout.length > 0 && this.focused === true) {
        return this.standout
      }
      else if (this.color !== void 0) {
        cls.push('text-' + this.color)
      }

      if (this.bgColor !== void 0) {
        cls.push(`bg-${this.bgColor}`)
      }

      return cls
    },

    controlSlotScope () {
      return {
        id: this.targetUid,
        field: this.$el,
        editable: this.editable,
        focused: this.focused,
        floatingLabel: this.floatingLabel,
        value: this.value,
        emitValue: this.__emitValue
      }
    }
  },

  methods: {
    focus () {
      if (this.showPopup !== void 0 && this.hasDialog === true) {
        this.showPopup()
        return
      }

      this.__focus()
    },

    blur () {
      const el = document.activeElement
      // IE can have null document.activeElement
      if (el !== null && this.$el.contains(el)) {
        el.blur()
      }
    },

    __focus () {
      const el = document.activeElement
      let target = this.$refs.target
      // IE can have null document.activeElement
      if (target !== void 0 && (el === null || el.id !== this.targetUid)) {
        target.hasAttribute('tabindex') === true || (target = target.querySelector('[tabindex]'))
        target !== null && target !== el && target.focus()
      }
    },

    __getContent (h) {
      const node = []

      this.$scopedSlots.prepend !== void 0 && node.push(
        h('div', {
          staticClass: 'q-field__prepend q-field__marginal row no-wrap items-center',
          key: 'prepend',
          on: this.slotsEvents
        }, this.$scopedSlots.prepend())
      )

      node.push(
        h('div', {
          staticClass: 'q-field__control-container col relative-position row no-wrap q-anchor--skip'
        }, this.__getControlContainer(h))
      )

      this.$scopedSlots.append !== void 0 && node.push(
        h('div', {
          staticClass: 'q-field__append q-field__marginal row no-wrap items-center',
          key: 'append',
          on: this.slotsEvents
        }, this.$scopedSlots.append())
      )

      this.hasError === true && this.noErrorIcon === false && node.push(
        this.__getInnerAppendNode(h, 'error', [
          h(QIcon, { props: { name: this.$q.iconSet.field.error, color: 'negative' } })
        ])
      )

      if (this.loading === true || this.innerLoading === true) {
        node.push(
          this.__getInnerAppendNode(
            h,
            'inner-loading-append',
            this.$scopedSlots.loading !== void 0
              ? this.$scopedSlots.loading()
              : [ h(QSpinner, { props: { color: this.color } }) ]
          )
        )
      }
      else if (this.clearable === true && this.hasValue === true && this.editable === true) {
        node.push(
          this.__getInnerAppendNode(h, 'inner-clearable-append', [
            h(QIcon, {
              staticClass: 'cursor-pointer',
              props: { name: this.clearIcon || this.$q.iconSet.field.clear },
              on: this.clearableEvents
            })
          ])
        )
      }

      this.__getInnerAppend !== void 0 && node.push(
        this.__getInnerAppendNode(h, 'inner-append', this.__getInnerAppend(h))
      )

      this.__getPopup !== void 0 && node.push(
        this.__getPopup(h)
      )

      return node
    },

    __getControlContainer (h) {
      const node = []

      this.prefix !== void 0 && this.prefix !== null && node.push(
        h('div', {
          staticClass: 'q-field__prefix no-pointer-events row items-center'
        }, [ this.prefix ])
      )

      if (this.__getControl !== void 0) {
        node.push(
          this.__getControl(h)
        )
      }
      // internal usage only:
      else if (this.$scopedSlots.rawControl !== void 0) {
        node.push(this.$scopedSlots.rawControl())
      }
      else if (this.$scopedSlots.control !== void 0) {
        node.push(
          h('div', {
            ref: 'target',
            staticClass: 'q-field__native row',
            attrs: {
              ...this.$attrs,
              'data-autofocus': this.autofocus
            }
          }, this.$scopedSlots.control(this.controlSlotScope))
        )
      }

      this.label !== void 0 && node.push(
        h('div', {
          staticClass: 'q-field__label no-pointer-events absolute ellipsis'
        }, [ this.label ])
      )

      this.suffix !== void 0 && this.suffix !== null && node.push(
        h('div', {
          staticClass: 'q-field__suffix no-pointer-events row items-center'
        }, [ this.suffix ])
      )

      return node.concat(
        this.__getDefaultSlot !== void 0
          ? this.__getDefaultSlot(h)
          : slot(this, 'default')
      )
    },

    __getBottom (h) {
      let msg, key

      if (this.hasError === true) {
        if (this.computedErrorMessage !== void 0) {
          msg = [ h('div', [ this.computedErrorMessage ]) ]
          key = this.computedErrorMessage
        }
        else {
          msg = slot(this, 'error')
          key = 'q--slot-error'
        }
      }
      else if (this.hideHint !== true || this.focused === true) {
        if (this.hint !== void 0) {
          msg = [ h('div', [ this.hint ]) ]
          key = this.hint
        }
        else {
          msg = slot(this, 'hint')
          key = 'q--slot-hint'
        }
      }

      const hasCounter = this.counter === true || this.$scopedSlots.counter !== void 0

      if (this.hideBottomSpace === true && hasCounter === false && msg === void 0) {
        return
      }

      const main = h('div', {
        key,
        staticClass: 'q-field__messages col'
      }, msg)

      return h('div', {
        staticClass: 'q-field__bottom row items-start q-field__bottom--' +
          (this.hideBottomSpace !== true ? 'animated' : 'stale')
      }, [
        this.hideBottomSpace === true
          ? main
          : h('transition', { props: { name: 'q-transition--field-message' } }, [
            main
          ]),

        hasCounter === true
          ? h('div', {
            staticClass: 'q-field__counter'
          }, this.$scopedSlots.counter !== void 0 ? this.$scopedSlots.counter() : [ this.computedCounter ])
          : null
      ])
    },

    __getInnerAppendNode (h, key, content) {
      return content === null ? null : h('div', {
        staticClass: 'q-field__append q-field__marginal row no-wrap items-center q-anchor--skip',
        key
      }, content)
    },

    __onControlPopupShow (e) {
      e !== void 0 && stop(e)
      this.$emit('popup-show', e)
      this.hasPopupOpen = true
      this.__onControlFocusin(e)
    },

    __onControlPopupHide (e) {
      e !== void 0 && stop(e)
      this.$emit('popup-hide', e)
      this.hasPopupOpen = false
      this.__onControlFocusout(e)
    },

    __onControlFocusin (e) {
      if (this.editable === true && this.focused === false) {
        this.focused = true
        this.$emit('focus', e)
      }
    },

    __onControlFocusout (e, then) {
      clearTimeout(this.focusoutTimer)
      this.focusoutTimer = setTimeout(() => {
        if (
          document.hasFocus() === true && (
            this.hasPopupOpen === true ||
            this.$refs === void 0 ||
            this.$refs.control === void 0 ||
            this.$refs.control.contains(document.activeElement) !== false
          )
        ) {
          return
        }

        if (this.focused === true) {
          this.focused = false
          this.$emit('blur', e)
        }

        then !== void 0 && then()
      })
    },

    __clearValue (e) {
      stop(e)
      if (this.type === 'file') {
        // do not let focus be triggered
        // as it will make the native file dialog
        // appear for another selection
        prevent(e)
        this.$refs.input.value = null
      }
      this.$emit('input', null)
      this.$emit('clear', this.value)
    },

    __emitValue (value) {
      this.$emit('input', value)
    }
  },

  render (h) {
    this.__onPreRender !== void 0 && this.__onPreRender()
    this.__onPostRender !== void 0 && this.$nextTick(this.__onPostRender)

    return h('label', {
      staticClass: 'q-field row no-wrap items-start',
      class: this.classes,
      attrs: {
        for: this.targetUid
      }
    }, [
      this.$scopedSlots.before !== void 0 ? h('div', {
        staticClass: 'q-field__before q-field__marginal row no-wrap items-center',
        on: this.slotsEvents
      }, this.$scopedSlots.before()) : null,

      h('div', {
        staticClass: 'q-field__inner relative-position col self-stretch column justify-center'
      }, [
        h('div', {
          ref: 'control',
          staticClass: 'q-field__control relative-position row no-wrap',
          class: this.contentClass,
          attrs: { tabindex: -1 },
          on: this.controlEvents
        }, this.__getContent(h)),

        this.shouldRenderBottom === true
          ? this.__getBottom(h)
          : null
      ]),

      this.$scopedSlots.after !== void 0 ? h('div', {
        staticClass: 'q-field__after q-field__marginal row no-wrap items-center',
        on: this.slotsEvents
      }, this.$scopedSlots.after()) : null
    ])
  },

  created () {
    this.__onPreRender !== void 0 && this.__onPreRender()

    this.slotsEvents = { click: prevent }

    this.clearableEvents = { click: this.__clearValue }

    this.controlEvents = this.__getControlEvents !== void 0
      ? this.__getControlEvents()
      : {
        focusin: this.__onControlFocusin,
        focusout: this.__onControlFocusout,
        'popup-show': this.__onControlPopupShow,
        'popup-hide': this.__onControlPopupHide
      }
  },

  mounted () {
    if (fromSSR === true && this.for === void 0) {
      this.targetUid = getTargetUid()
    }

    this.autofocus === true && this.focus()
  },

  beforeDestroy () {
    clearTimeout(this.focusoutTimer)
  }
})
<commit_msg>fix(QField): Do not float label on empty field with error #6065 (#6066)<commit_after>import Vue from 'vue'

import QIcon from '../icon/QIcon.js'
import QSpinner from '../spinner/QSpinner.js'

import ValidateMixin from '../../mixins/validate.js'
import DarkMixin from '../../mixins/dark.js'
import { slot } from '../../utils/slot.js'
import uid from '../../utils/uid.js'
import { stop, prevent } from '../../utils/event.js'
import { fromSSR } from '../../plugins/Platform.js'

function getTargetUid (val) {
  return val === void 0 ? `f_${uid()}` : val
}

export default Vue.extend({
  name: 'QField',

  mixins: [ DarkMixin, ValidateMixin ],

  inheritAttrs: false,

  props: {
    label: String,
    stackLabel: Boolean,
    hint: String,
    hideHint: Boolean,
    prefix: String,
    suffix: String,

    color: String,
    bgColor: String,

    filled: Boolean,
    outlined: Boolean,
    borderless: Boolean,
    standout: [Boolean, String],

    square: Boolean,

    loading: Boolean,

    bottomSlots: Boolean,
    hideBottomSpace: Boolean,

    rounded: Boolean,
    dense: Boolean,
    itemAligned: Boolean,

    counter: Boolean,

    clearable: Boolean,
    clearIcon: String,

    disable: Boolean,
    readonly: Boolean,

    autofocus: Boolean,

    for: [String],
    maxlength: [Number, String],
    maxValues: [Number, String] // private, do not add to JSON; internally needed by QSelect
  },

  data () {
    return {
      focused: false,
      targetUid: getTargetUid(this.for),

      // used internally by validation for QInput
      // or menu handling for QSelect
      innerLoading: false
    }
  },

  watch: {
    for (val) {
      // don't transform targetUid into a computed
      // prop as it will break SSR
      this.targetUid = getTargetUid(val)
    }
  },

  computed: {
    editable () {
      return this.disable !== true && this.readonly !== true
    },

    hasValue () {
      const value = this.__getControl === void 0 ? this.value : this.innerValue

      return value !== void 0 &&
        value !== null &&
        ('' + value).length > 0
    },

    computedCounter () {
      if (this.counter !== false) {
        const len = typeof this.value === 'string' || typeof this.value === 'number'
          ? ('' + this.value).length
          : (Array.isArray(this.value) === true ? this.value.length : 0)
        const max = this.maxlength !== void 0 ? this.maxlength : this.maxValues

        return len + (max !== void 0 ? ' / ' + max : '')
      }
    },

    floatingLabel () {
      return this.stackLabel === true ||
        this.focused === true ||
        (
          this.inputValue !== void 0 && this.hideSelected === true
            ? this.inputValue.length > 0
            : this.hasValue === true
        ) ||
        (
          this.displayValue !== void 0 &&
          this.displayValue !== null &&
          ('' + this.displayValue).length > 0
        )
    },

    shouldRenderBottom () {
      return this.bottomSlots === true ||
        this.hint !== void 0 ||
        this.rules !== void 0 ||
        this.counter === true ||
        this.error !== null
    },

    classes () {
      return {
        [this.fieldClass]: this.fieldClass !== void 0,

        [`q-field--${this.styleType}`]: true,
        'q-field--rounded': this.rounded,
        'q-field--square': this.square,

        'q-field--focused': this.focused === true || this.hasError === true,
        'q-field--float': this.floatingLabel,
        'q-field--labeled': this.label !== void 0,

        'q-field--dense': this.dense,
        'q-field--item-aligned q-item-type': this.itemAligned,
        'q-field--dark': this.isDark,

        'q-field--auto-height': this.__getControl === void 0,

        'q-field--with-bottom': this.hideBottomSpace !== true && this.shouldRenderBottom === true,
        'q-field--error': this.hasError,

        'q-field--readonly': this.readonly === true && this.disable !== true,
        'q-field--disabled': this.disable
      }
    },

    styleType () {
      if (this.filled === true) { return 'filled' }
      if (this.outlined === true) { return 'outlined' }
      if (this.borderless === true) { return 'borderless' }
      if (this.standout) { return 'standout' }
      return 'standard'
    },

    contentClass () {
      const cls = []

      if (this.hasError === true) {
        cls.push('text-negative')
      }
      else if (typeof this.standout === 'string' && this.standout.length > 0 && this.focused === true) {
        return this.standout
      }
      else if (this.color !== void 0) {
        cls.push('text-' + this.color)
      }

      if (this.bgColor !== void 0) {
        cls.push(`bg-${this.bgColor}`)
      }

      return cls
    },

    controlSlotScope () {
      return {
        id: this.targetUid,
        field: this.$el,
        editable: this.editable,
        focused: this.focused,
        floatingLabel: this.floatingLabel,
        value: this.value,
        emitValue: this.__emitValue
      }
    }
  },

  methods: {
    focus () {
      if (this.showPopup !== void 0 && this.hasDialog === true) {
        this.showPopup()
        return
      }

      this.__focus()
    },

    blur () {
      const el = document.activeElement
      // IE can have null document.activeElement
      if (el !== null && this.$el.contains(el)) {
        el.blur()
      }
    },

    __focus () {
      const el = document.activeElement
      let target = this.$refs.target
      // IE can have null document.activeElement
      if (target !== void 0 && (el === null || el.id !== this.targetUid)) {
        target.hasAttribute('tabindex') === true || (target = target.querySelector('[tabindex]'))
        target !== null && target !== el && target.focus()
      }
    },

    __getContent (h) {
      const node = []

      this.$scopedSlots.prepend !== void 0 && node.push(
        h('div', {
          staticClass: 'q-field__prepend q-field__marginal row no-wrap items-center',
          key: 'prepend',
          on: this.slotsEvents
        }, this.$scopedSlots.prepend())
      )

      node.push(
        h('div', {
          staticClass: 'q-field__control-container col relative-position row no-wrap q-anchor--skip'
        }, this.__getControlContainer(h))
      )

      this.$scopedSlots.append !== void 0 && node.push(
        h('div', {
          staticClass: 'q-field__append q-field__marginal row no-wrap items-center',
          key: 'append',
          on: this.slotsEvents
        }, this.$scopedSlots.append())
      )

      this.hasError === true && this.noErrorIcon === false && node.push(
        this.__getInnerAppendNode(h, 'error', [
          h(QIcon, { props: { name: this.$q.iconSet.field.error, color: 'negative' } })
        ])
      )

      if (this.loading === true || this.innerLoading === true) {
        node.push(
          this.__getInnerAppendNode(
            h,
            'inner-loading-append',
            this.$scopedSlots.loading !== void 0
              ? this.$scopedSlots.loading()
              : [ h(QSpinner, { props: { color: this.color } }) ]
          )
        )
      }
      else if (this.clearable === true && this.hasValue === true && this.editable === true) {
        node.push(
          this.__getInnerAppendNode(h, 'inner-clearable-append', [
            h(QIcon, {
              staticClass: 'cursor-pointer',
              props: { name: this.clearIcon || this.$q.iconSet.field.clear },
              on: this.clearableEvents
            })
          ])
        )
      }

      this.__getInnerAppend !== void 0 && node.push(
        this.__getInnerAppendNode(h, 'inner-append', this.__getInnerAppend(h))
      )

      this.__getPopup !== void 0 && node.push(
        this.__getPopup(h)
      )

      return node
    },

    __getControlContainer (h) {
      const node = []

      this.prefix !== void 0 && this.prefix !== null && node.push(
        h('div', {
          staticClass: 'q-field__prefix no-pointer-events row items-center'
        }, [ this.prefix ])
      )

      if (this.__getControl !== void 0) {
        node.push(
          this.__getControl(h)
        )
      }
      // internal usage only:
      else if (this.$scopedSlots.rawControl !== void 0) {
        node.push(this.$scopedSlots.rawControl())
      }
      else if (this.$scopedSlots.control !== void 0) {
        node.push(
          h('div', {
            ref: 'target',
            staticClass: 'q-field__native row',
            attrs: {
              ...this.$attrs,
              'data-autofocus': this.autofocus
            }
          }, this.$scopedSlots.control(this.controlSlotScope))
        )
      }

      this.label !== void 0 && node.push(
        h('div', {
          staticClass: 'q-field__label no-pointer-events absolute ellipsis'
        }, [ this.label ])
      )

      this.suffix !== void 0 && this.suffix !== null && node.push(
        h('div', {
          staticClass: 'q-field__suffix no-pointer-events row items-center'
        }, [ this.suffix ])
      )

      return node.concat(
        this.__getDefaultSlot !== void 0
          ? this.__getDefaultSlot(h)
          : slot(this, 'default')
      )
    },

    __getBottom (h) {
      let msg, key

      if (this.hasError === true) {
        if (this.computedErrorMessage !== void 0) {
          msg = [ h('div', [ this.computedErrorMessage ]) ]
          key = this.computedErrorMessage
        }
        else {
          msg = slot(this, 'error')
          key = 'q--slot-error'
        }
      }
      else if (this.hideHint !== true || this.focused === true) {
        if (this.hint !== void 0) {
          msg = [ h('div', [ this.hint ]) ]
          key = this.hint
        }
        else {
          msg = slot(this, 'hint')
          key = 'q--slot-hint'
        }
      }

      const hasCounter = this.counter === true || this.$scopedSlots.counter !== void 0

      if (this.hideBottomSpace === true && hasCounter === false && msg === void 0) {
        return
      }

      const main = h('div', {
        key,
        staticClass: 'q-field__messages col'
      }, msg)

      return h('div', {
        staticClass: 'q-field__bottom row items-start q-field__bottom--' +
          (this.hideBottomSpace !== true ? 'animated' : 'stale')
      }, [
        this.hideBottomSpace === true
          ? main
          : h('transition', { props: { name: 'q-transition--field-message' } }, [
            main
          ]),

        hasCounter === true
          ? h('div', {
            staticClass: 'q-field__counter'
          }, this.$scopedSlots.counter !== void 0 ? this.$scopedSlots.counter() : [ this.computedCounter ])
          : null
      ])
    },

    __getInnerAppendNode (h, key, content) {
      return content === null ? null : h('div', {
        staticClass: 'q-field__append q-field__marginal row no-wrap items-center q-anchor--skip',
        key
      }, content)
    },

    __onControlPopupShow (e) {
      e !== void 0 && stop(e)
      this.$emit('popup-show', e)
      this.hasPopupOpen = true
      this.__onControlFocusin(e)
    },

    __onControlPopupHide (e) {
      e !== void 0 && stop(e)
      this.$emit('popup-hide', e)
      this.hasPopupOpen = false
      this.__onControlFocusout(e)
    },

    __onControlFocusin (e) {
      if (this.editable === true && this.focused === false) {
        this.focused = true
        this.$emit('focus', e)
      }
    },

    __onControlFocusout (e, then) {
      clearTimeout(this.focusoutTimer)
      this.focusoutTimer = setTimeout(() => {
        if (
          document.hasFocus() === true && (
            this.hasPopupOpen === true ||
            this.$refs === void 0 ||
            this.$refs.control === void 0 ||
            this.$refs.control.contains(document.activeElement) !== false
          )
        ) {
          return
        }

        if (this.focused === true) {
          this.focused = false
          this.$emit('blur', e)
        }

        then !== void 0 && then()
      })
    },

    __clearValue (e) {
      stop(e)
      if (this.type === 'file') {
        // do not let focus be triggered
        // as it will make the native file dialog
        // appear for another selection
        prevent(e)
        this.$refs.input.value = null
      }
      this.$emit('input', null)
      this.$emit('clear', this.value)
    },

    __emitValue (value) {
      this.$emit('input', value)
    }
  },

  render (h) {
    this.__onPreRender !== void 0 && this.__onPreRender()
    this.__onPostRender !== void 0 && this.$nextTick(this.__onPostRender)

    return h('label', {
      staticClass: 'q-field row no-wrap items-start',
      class: this.classes,
      attrs: {
        for: this.targetUid
      }
    }, [
      this.$scopedSlots.before !== void 0 ? h('div', {
        staticClass: 'q-field__before q-field__marginal row no-wrap items-center',
        on: this.slotsEvents
      }, this.$scopedSlots.before()) : null,

      h('div', {
        staticClass: 'q-field__inner relative-position col self-stretch column justify-center'
      }, [
        h('div', {
          ref: 'control',
          staticClass: 'q-field__control relative-position row no-wrap',
          class: this.contentClass,
          attrs: { tabindex: -1 },
          on: this.controlEvents
        }, this.__getContent(h)),

        this.shouldRenderBottom === true
          ? this.__getBottom(h)
          : null
      ]),

      this.$scopedSlots.after !== void 0 ? h('div', {
        staticClass: 'q-field__after q-field__marginal row no-wrap items-center',
        on: this.slotsEvents
      }, this.$scopedSlots.after()) : null
    ])
  },

  created () {
    this.__onPreRender !== void 0 && this.__onPreRender()

    this.slotsEvents = { click: prevent }

    this.clearableEvents = { click: this.__clearValue }

    this.controlEvents = this.__getControlEvents !== void 0
      ? this.__getControlEvents()
      : {
        focusin: this.__onControlFocusin,
        focusout: this.__onControlFocusout,
        'popup-show': this.__onControlPopupShow,
        'popup-hide': this.__onControlPopupHide
      }
  },

  mounted () {
    if (fromSSR === true && this.for === void 0) {
      this.targetUid = getTargetUid()
    }

    this.autofocus === true && this.focus()
  },

  beforeDestroy () {
    clearTimeout(this.focusoutTimer)
  }
})
<|endoftext|>


<commit_before>function removeAnimations (element) {
	$(element).removeClass("animated " +
		//Attention seekers
		"flash " +
		"bounce " +
		"shake " +
		"tada " +
		"swing " +
		"wobble " +
		"wiggle " +
		"pulse " +

		//Flippers
		"flip " +
		"flipInX " +
		"flipOutX " +
		"flipInY " +
		"flipOutY " +

		//Fading entrances
		"fadeIn " +
		"fadeInUp " +
		"fadeInDown " +
		"fadeInLeft " +
		"fadeInRight " +
		"fadeInUpBig " +
		"fadeInDownBig " +
		"fadeInLeftBig " +
		"fadeInRightBig " +

		//Fading exits
		"fadeOut " +
		"fadeOutUp " +
		"fadeOutDown " +
		"fadeOutLeft " +
		"fadeOutRight " +
		"fadeOutUpBig " +
		"fadeOutDownBig " +
		"fadeOutLeftBig " +
		"fadeOutRightBig " +

		//Bouncing entrances
		"bounceIn " +
		"bounceInDown " +
		"bounceInUp " +
		"bounceInLeft " +
		"bounceInRight " +

		//Bouncing exits
		"bounceOut " +
		"bounceOutDown " +
		"bounceOutUp " +
		"bounceOutLeft " +
		"bounceOutRight " +

		//Rotating entrances
		"rotateIn " +
		"rotateInDownLeft " +
		"rotateInDownRight " +
		"rotateInUpLeft " +
		"rotateInUpRight " +

		//Rotating exits
		"rotateOut " +
		"rotateOutDownLeft " +
		"rotateOutDownRight " +
		"rotateOutUpLeft " +
		"rotateOutUpRight " +

		//Lightspeed
		"lightSpeedIn " +
		"lightSpeedOut " +

		//Specials
		"hinge " +
		"rollIn " +
		"rollOut");
}

function cleanUpAnimationAfterTimeout(element, duration) {
    return setTimeout(function() {
    	removeAnimations(element);
    }, duration);
}

function keyCodeToAction(keycode) {
	switch(keycode) {
		case 86: //v
			return "v";

		case 96: //keypad 0
		case 48: //top 0
			return "0";
		case 97: //keypad 1
		case 49: //top 1
			return "1";
		case 98: //keypad 2
		case 50: //top 2
			return "2";
		case 99: //keypad 3
		case 51: //top 3
			return "3";
		case 100: //keypad 4
		case 52: //top 4
			return "4";
		case 101: //keypad 5
		case 53: //top 5
			return "5";
		case 102: //keypad 6
		case 54: //top 6
			return "6";
		case 103: //keypad 7
		case 55: //top 7
			return "7";
		case 104: //keypad 8
		case 56: //top 8
			return "8";
		case 105: //keypad 9
		case 57: //top 9
			return "9";

        case 83: //s
		case 40: //down
			return "down";
        case 65: //a
		case 37: //left
			return "left";
        case 87: //w
		case 38: //up
			return "up";
        case 68: //d
		case 39: //right
			return "right";

		case 13: //enter
			return "enter";

		case 27: //escape
			return "escape";
	}
}<commit_msg>Renamed parameters to better reflect their purpose<commit_after>function removeAnimations (selector) {
	$(selector).removeClass("animated " +
		//Attention seekers
		"flash " +
		"bounce " +
		"shake " +
		"tada " +
		"swing " +
		"wobble " +
		"wiggle " +
		"pulse " +

		//Flippers
		"flip " +
		"flipInX " +
		"flipOutX " +
		"flipInY " +
		"flipOutY " +

		//Fading entrances
		"fadeIn " +
		"fadeInUp " +
		"fadeInDown " +
		"fadeInLeft " +
		"fadeInRight " +
		"fadeInUpBig " +
		"fadeInDownBig " +
		"fadeInLeftBig " +
		"fadeInRightBig " +

		//Fading exits
		"fadeOut " +
		"fadeOutUp " +
		"fadeOutDown " +
		"fadeOutLeft " +
		"fadeOutRight " +
		"fadeOutUpBig " +
		"fadeOutDownBig " +
		"fadeOutLeftBig " +
		"fadeOutRightBig " +

		//Bouncing entrances
		"bounceIn " +
		"bounceInDown " +
		"bounceInUp " +
		"bounceInLeft " +
		"bounceInRight " +

		//Bouncing exits
		"bounceOut " +
		"bounceOutDown " +
		"bounceOutUp " +
		"bounceOutLeft " +
		"bounceOutRight " +

		//Rotating entrances
		"rotateIn " +
		"rotateInDownLeft " +
		"rotateInDownRight " +
		"rotateInUpLeft " +
		"rotateInUpRight " +

		//Rotating exits
		"rotateOut " +
		"rotateOutDownLeft " +
		"rotateOutDownRight " +
		"rotateOutUpLeft " +
		"rotateOutUpRight " +

		//Lightspeed
		"lightSpeedIn " +
		"lightSpeedOut " +

		//Specials
		"hinge " +
		"rollIn " +
		"rollOut");
}

function cleanUpAnimationAfterTimeout(selector, timeout) {
    return setTimeout(function() {
    	removeAnimations(selector);
    }, timeout);
}

function keyCodeToAction(keycode) {
	switch(keycode) {
		case 86: //v
			return "v";

		case 96: //keypad 0
		case 48: //top 0
			return "0";
		case 97: //keypad 1
		case 49: //top 1
			return "1";
		case 98: //keypad 2
		case 50: //top 2
			return "2";
		case 99: //keypad 3
		case 51: //top 3
			return "3";
		case 100: //keypad 4
		case 52: //top 4
			return "4";
		case 101: //keypad 5
		case 53: //top 5
			return "5";
		case 102: //keypad 6
		case 54: //top 6
			return "6";
		case 103: //keypad 7
		case 55: //top 7
			return "7";
		case 104: //keypad 8
		case 56: //top 8
			return "8";
		case 105: //keypad 9
		case 57: //top 9
			return "9";

        case 83: //s
		case 40: //down
			return "down";
        case 65: //a
		case 37: //left
			return "left";
        case 87: //w
		case 38: //up
			return "up";
        case 68: //d
		case 39: //right
			return "right";

		case 13: //enter
			return "enter";

		case 27: //escape
			return "escape";
	}
}<|endoftext|>


<commit_before>'use strict';

var modules = [
  'ngCookies',
  'ngResource',
  'ngSanitize',
  'ngAnimate',
  'ui.router',
  'infinite-scroll'
];

if (dev) {
  modules.push('mockBackend');
}

angular.module('SmithsonianApp', modules)
  .value('route', {
    api: dev ? 'http://localhost:9000/api' : 'http://localhost:8080/api'
  })
  .config(function ($stateProvider, $urlRouterProvider) {
    $urlRouterProvider.when('', '/');
    $stateProvider.state('main', {
      url: '/',
      views: {
        'main': {
          controller: 'MainCtrl',
          templateUrl: 'views/main.html'
        }
      }
    })
    .state('tour', {
      url: '/tour',
      views: {
        'main': {
          controller: 'TourCtrl',
          templateUrl: 'views/tour.html'
        }
      }
    })
    .state('browse', {
      url: '/browse',
      views: {
        'main': {
          controller: 'BrowseCtrl',
          templateUrl: 'views/browse.html'
        }
      }
    })
    .state('favorite', {
      url: '/favorite',
      views: {
        'main': {
          controller: 'FavoriteCtrl',
          templateUrl: 'views/favorite.html'
        }
      }
    })
    .state('item', {
      url: '/item/:itemId',
      views: {
        'main': {
          controller: 'ObjectCtrl',
          templateUrl: 'views/object.html'
        }
      }
    })
    ;
  });
<commit_msg>Changed to items in state url<commit_after>'use strict';

var modules = [
  'ngCookies',
  'ngResource',
  'ngSanitize',
  'ngAnimate',
  'ui.router',
  'infinite-scroll'
];

if (dev) {
  modules.push('mockBackend');
}

angular.module('SmithsonianApp', modules)
  .value('route', {
    api: dev ? 'http://localhost:9000/api' : 'http://localhost:8080/api'
  })
  .config(function ($stateProvider, $urlRouterProvider) {
    $urlRouterProvider.when('', '/');
    $stateProvider.state('main', {
      url: '/',
      views: {
        'main': {
          controller: 'MainCtrl',
          templateUrl: 'views/main.html'
        }
      }
    })
    .state('tour', {
      url: '/tour',
      views: {
        'main': {
          controller: 'TourCtrl',
          templateUrl: 'views/tour.html'
        }
      }
    })
    .state('browse', {
      url: '/browse',
      views: {
        'main': {
          controller: 'BrowseCtrl',
          templateUrl: 'views/browse.html'
        }
      }
    })
    .state('favorite', {
      url: '/favorite',
      views: {
        'main': {
          controller: 'FavoriteCtrl',
          templateUrl: 'views/favorite.html'
        }
      }
    })
    .state('item', {
      url: '/items/:itemId',
      views: {
        'main': {
          controller: 'ObjectCtrl',
          templateUrl: 'views/object.html'
        }
      }
    })
    ;
  });
<|endoftext|>


<commit_before>var chessground = require('chessground');
var partial = chessground.util.partial;
var editor = require('./editor');
var m = require('mithril');
var keyboard = require('./keyboard');

module.exports = function(cfg) {

  this.data = editor.init(cfg);

  this.chessground = new chessground.controller({
    fen: cfg.fen,
    orientation: 'white',
    movable: {
      free: true,
      color: 'both',
      dropOff: 'trash'
    },
    animation: {
      duration: cfg.animation.duration
    },
    premovable: {
      enabled: false
    },
    draggable: {
      showGhost: false
    },
    events: {
      change: m.redraw
    },
    disableContextMenu: true
  });

  this.computeFen = partial(editor.computeFen, this.data, this.chessground.getFen);

  this.trans = partial(editor.trans, this.data);

  this.startPosition = function() {
    this.chessground.set({
      fen: 'start'
    });
    this.data.castles = editor.castlesAt(true);
    this.data.color('w');
  }.bind(this);

  this.clearBoard = function() {
    this.chessground.set({
      fen: '8/8/8/8/8/8/8/8'
    });
    this.data.castles = editor.castlesAt(false);
  }.bind(this);

  this.loadNewFen = function(fen) {
    window.location = editor.makeUrl(this.data.baseUrl, fen);
  }.bind(this);

  this.positionLooksLegit = function() {
    var kings = {
      white: 0,
      black: 0
    };
    var pieces = this.chessground.data.pieces;
    for (var pos in pieces) {
      if (pieces[pos] && pieces[pos].role === 'king') kings[pieces[pos].color]++;
    }
    return kings.white === 1 && kings.black === 1;
  }.bind(this);

  keyboard(this);
};
<commit_msg>disable chessground autodistance in board editor<commit_after>var chessground = require('chessground');
var partial = chessground.util.partial;
var editor = require('./editor');
var m = require('mithril');
var keyboard = require('./keyboard');

module.exports = function(cfg) {

  this.data = editor.init(cfg);

  this.chessground = new chessground.controller({
    fen: cfg.fen,
    orientation: 'white',
    movable: {
      free: true,
      color: 'both',
      dropOff: 'trash'
    },
    animation: {
      duration: cfg.animation.duration
    },
    premovable: {
      enabled: false
    },
    draggable: {
      showGhost: false,
      autoDistance: false
    },
    events: {
      change: m.redraw
    },
    disableContextMenu: true
  });

  this.computeFen = partial(editor.computeFen, this.data, this.chessground.getFen);

  this.trans = partial(editor.trans, this.data);

  this.startPosition = function() {
    this.chessground.set({
      fen: 'start'
    });
    this.data.castles = editor.castlesAt(true);
    this.data.color('w');
  }.bind(this);

  this.clearBoard = function() {
    this.chessground.set({
      fen: '8/8/8/8/8/8/8/8'
    });
    this.data.castles = editor.castlesAt(false);
  }.bind(this);

  this.loadNewFen = function(fen) {
    window.location = editor.makeUrl(this.data.baseUrl, fen);
  }.bind(this);

  this.positionLooksLegit = function() {
    var kings = {
      white: 0,
      black: 0
    };
    var pieces = this.chessground.data.pieces;
    for (var pos in pieces) {
      if (pieces[pos] && pieces[pos].role === 'king') kings[pieces[pos].color]++;
    }
    return kings.white === 1 && kings.black === 1;
  }.bind(this);

  keyboard(this);
};
<|endoftext|>


<commit_before>function makeAckable(send) {

  var currentId = 1; // increment with each ackable message sent

  var messages = [];

  function resend() {
    var resendCutoff = performance.now() - 2500;
    messages.forEach(function(m) {
      if (m.at < resendCutoff) send(m.t, m.d);
    });
  }

  setInterval(resend, 1000);

  return {
    resend: resend,
    register: function(t, d) {
      d.a = currentId++;
      messages.push({
        t: t,
        d: d,
        at: performance.now()
      });
    },
    gotAck: function(id) {
      messages = messages.filter(function(m) {
        return m.d.a !== id;
      });
    }
  };
}

// versioned events, acks, retries, resync
lichess.StrongSocket = function(url, version, settings) {

  var settings = $.extend(true, {}, lichess.StrongSocket.defaults, settings);
  var options = settings.options;
  var ws;
  var pingSchedule;
  var connectSchedule;
  var ackable = makeAckable((t, d) => send(t, d));
  var lastPingTime = performance.now();
  var pongCount = 0;
  var averageLag = 0;
  var tryOtherUrl = false;
  var autoReconnect = true;
  var nbConnects = 0;
  var storage = lichess.storage.make('surl7');

  var connect = function() {
    destroy();
    autoReconnect = true;
    var params = $.param(settings.params);
    if (version !== false) params += (params ? '&' : '') + 'v=' + version;
    var fullUrl = options.protocol + '//' + baseUrl() + url + '?' + params;
    debug("connection attempt to " + fullUrl);
    try {
      ws = new WebSocket(fullUrl);
      ws.onerror = function(e) {
        onError(e);
      };
      ws.onclose = function() {
        lichess.pubsub.emit('socket.close');
        if (autoReconnect) {
          debug('Will autoreconnect in ' + options.autoReconnectDelay);
          scheduleConnect(options.autoReconnectDelay);
        }
      };
      ws.onopen = function() {
        debug("connected to " + fullUrl);
        onSuccess();
        $('body').removeClass('offline').addClass('online').addClass(nbConnects > 1 ? 'reconnected' : '');
        pingNow();
        lichess.pubsub.emit('socket.open');
        ackable.resend();
      };
      ws.onmessage = function(e) {
        if (e.data == 0) return pong();
        const m = JSON.parse(e.data);
        if (m.t === 'n') pong();
        handle(m);
      };
    } catch (e) {
      onError(e);
    }
    scheduleConnect(options.pingMaxLag);
  };

  var send = function(t, d, o, noRetry) {
    o = o || {};
    var msg = { t: t };
    if (d !== undefined) {
      if (o.withLag) d.l = Math.round(averageLag);
      if (o.millis >= 0) d.s = Math.round(o.millis * 0.1).toString(36);
      msg.d = d;
    }
    if (o.ackable) {
      msg.d = msg.d || {}; // can't ack message without data
      ackable.register(t, msg.d); // adds d.a, the ack ID we expect to get back
    }
    var message = JSON.stringify(msg);
    debug("send " + message);
    try {
      ws.send(message);
    } catch (e) {
      // maybe sent before socket opens,
      // try again a second later.
      if (!noRetry) setTimeout(function() {
        send(t, msg.d, o, true);
      }, 1000);
    }
  };
  lichess.pubsub.on('socket.send', send);

  var scheduleConnect = function(delay) {
    if (options.idle) delay = 10 * 1000 + Math.random() * 10 * 1000;
    // debug('schedule connect ' + delay);
    clearTimeout(pingSchedule);
    clearTimeout(connectSchedule);
    connectSchedule = setTimeout(function() {
      $('body').addClass('offline').removeClass('online');
      tryOtherUrl = true;
      connect();
    }, delay);
  };

  var schedulePing = function(delay) {
    clearTimeout(pingSchedule);
    pingSchedule = setTimeout(pingNow, delay);
  };

  var pingNow = function() {
    clearTimeout(pingSchedule);
    clearTimeout(connectSchedule);
    var pingData = (options.isAuth && pongCount % 8 == 2) ? JSON.stringify({
      t: 'p',
      l: Math.round(0.1 * averageLag)
    }) : null;
    try {
      ws.send(pingData);
      lastPingTime = performance.now();
    } catch (e) {
      debug(e, true);
    }
    scheduleConnect(options.pingMaxLag);
  };

  var computePingDelay = function() {
    return options.pingDelay + (options.idle ? 1000 : 0);
  };

  var pong = function() {
    clearTimeout(connectSchedule);
    schedulePing(computePingDelay());
    var currentLag = Math.min(performance.now() - lastPingTime, 10000);
    pongCount++;

    // Average first 4 pings, then switch to decaying average.
    var mix = pongCount > 4 ? 0.1 : 1 / pongCount;
    averageLag += mix * (currentLag - averageLag);

    lichess.pubsub.emit('socket.lag', averageLag);
  };

  var handle = function(m) {
    if (m.v) {
      if (m.v <= version) {
        debug("already has event " + m.v);
        return;
      }
      // it's impossible but according to previous login, it happens nonetheless
      if (m.v > version + 1) return lichess.reload();
      version = m.v;
    }
    switch (m.t || false) {
      case false:
        break;
      case 'resync':
        lichess.reload();
        break;
      case 'ack':
        ackable.gotAck(m.d);
        break;
      default:
        lichess.pubsub.emit('socket.in.' + m.t, m.d);
        var processed = settings.receive && settings.receive(m.t, m.d);
        if (!processed && settings.events[m.t]) settings.events[m.t](m.d || null, m);
    }
  };

  var debug = function(msg, always) {
    if (always || options.debug) {
      console.debug("[" + options.name + " " + settings.params.sri + "]", msg);
    }
  };

  var destroy = function() {
    clearTimeout(pingSchedule);
    clearTimeout(connectSchedule);
    disconnect();
    ws = null;
  };

  var disconnect = function() {
    if (ws) {
      debug("Disconnect");
      autoReconnect = false;
      ws.onerror = ws.onclose = ws.onopen = ws.onmessage = $.noop;
      ws.close();
    }
  };

  var onError = function(e) {
    options.debug = true;
    debug('error: ' + JSON.stringify(e));
    tryOtherUrl = true;
    clearTimeout(pingSchedule);
  };

  var onSuccess = function() {
    nbConnects++;
    if (nbConnects == 1) {
      options.onFirstConnect();
      var disconnectTimeout;
      lichess.idleTimer(10 * 60 * 1000, function() {
        options.idle = true;
        disconnectTimeout = setTimeout(destroy, 2 * 60 * 60 * 1000);
      }, function() {
        options.idle = false;
        if (ws) clearTimeout(disconnectTimeout);
        else location.reload();
      });
    }
  };

  const baseUrls = document.body.getAttribute('data-socket-domains').split(',');

  const baseUrl = function() {
    let url = storage.get();
    if (!url || tryOtherUrl) {
      url = baseUrls[Math.floor(Math.random() * baseUrls.length)];
      storage.set(url);
    }
    return url;
  };

  connect();
  window.addEventListener('unload', destroy);

  return {
    disconnect: disconnect,
    send: send,
    options: options,
    pingInterval: function() {
      return computePingDelay() + averageLag;
    },
    averageLag: function() {
      return averageLag;
    },
    getVersion: function() {
      return version;
    }
  };
};

lichess.StrongSocket.defaults = {
  events: {
    fen: function(e) {
      $('.mini-board-' + e.id).each(function() {
        lichess.parseFen($(this).data("fen", e.fen).data("lastmove", e.lm));
      });
    },
    challenges: function(d) {
      lichess.challengeApp.update(d);
    },
    notifications: function(d) {
      lichess.notifyApp.update(d, true);
    }
  },
  params: {
    sri: lichess.sri
  },
  options: {
    name: "unnamed",
    idle: false,
    pingMaxLag: 9000, // time to wait for pong before reseting the connection
    pingDelay: 2500, // time between pong and ping
    autoReconnectDelay: 3500,
    protocol: location.protocol === 'https:' ? 'wss:' : 'ws:',
    onFirstConnect: $.noop
  }
};
<commit_msg>bump socket url storage key to rabalance<commit_after>function makeAckable(send) {

  var currentId = 1; // increment with each ackable message sent

  var messages = [];

  function resend() {
    var resendCutoff = performance.now() - 2500;
    messages.forEach(function(m) {
      if (m.at < resendCutoff) send(m.t, m.d);
    });
  }

  setInterval(resend, 1000);

  return {
    resend: resend,
    register: function(t, d) {
      d.a = currentId++;
      messages.push({
        t: t,
        d: d,
        at: performance.now()
      });
    },
    gotAck: function(id) {
      messages = messages.filter(function(m) {
        return m.d.a !== id;
      });
    }
  };
}

// versioned events, acks, retries, resync
lichess.StrongSocket = function(url, version, settings) {

  var settings = $.extend(true, {}, lichess.StrongSocket.defaults, settings);
  var options = settings.options;
  var ws;
  var pingSchedule;
  var connectSchedule;
  var ackable = makeAckable((t, d) => send(t, d));
  var lastPingTime = performance.now();
  var pongCount = 0;
  var averageLag = 0;
  var tryOtherUrl = false;
  var autoReconnect = true;
  var nbConnects = 0;
  var storage = lichess.storage.make('surl8');

  var connect = function() {
    destroy();
    autoReconnect = true;
    var params = $.param(settings.params);
    if (version !== false) params += (params ? '&' : '') + 'v=' + version;
    var fullUrl = options.protocol + '//' + baseUrl() + url + '?' + params;
    debug("connection attempt to " + fullUrl);
    try {
      ws = new WebSocket(fullUrl);
      ws.onerror = function(e) {
        onError(e);
      };
      ws.onclose = function() {
        lichess.pubsub.emit('socket.close');
        if (autoReconnect) {
          debug('Will autoreconnect in ' + options.autoReconnectDelay);
          scheduleConnect(options.autoReconnectDelay);
        }
      };
      ws.onopen = function() {
        debug("connected to " + fullUrl);
        onSuccess();
        $('body').removeClass('offline').addClass('online').addClass(nbConnects > 1 ? 'reconnected' : '');
        pingNow();
        lichess.pubsub.emit('socket.open');
        ackable.resend();
      };
      ws.onmessage = function(e) {
        if (e.data == 0) return pong();
        const m = JSON.parse(e.data);
        if (m.t === 'n') pong();
        handle(m);
      };
    } catch (e) {
      onError(e);
    }
    scheduleConnect(options.pingMaxLag);
  };

  var send = function(t, d, o, noRetry) {
    o = o || {};
    var msg = { t: t };
    if (d !== undefined) {
      if (o.withLag) d.l = Math.round(averageLag);
      if (o.millis >= 0) d.s = Math.round(o.millis * 0.1).toString(36);
      msg.d = d;
    }
    if (o.ackable) {
      msg.d = msg.d || {}; // can't ack message without data
      ackable.register(t, msg.d); // adds d.a, the ack ID we expect to get back
    }
    var message = JSON.stringify(msg);
    debug("send " + message);
    try {
      ws.send(message);
    } catch (e) {
      // maybe sent before socket opens,
      // try again a second later.
      if (!noRetry) setTimeout(function() {
        send(t, msg.d, o, true);
      }, 1000);
    }
  };
  lichess.pubsub.on('socket.send', send);

  var scheduleConnect = function(delay) {
    if (options.idle) delay = 10 * 1000 + Math.random() * 10 * 1000;
    // debug('schedule connect ' + delay);
    clearTimeout(pingSchedule);
    clearTimeout(connectSchedule);
    connectSchedule = setTimeout(function() {
      $('body').addClass('offline').removeClass('online');
      tryOtherUrl = true;
      connect();
    }, delay);
  };

  var schedulePing = function(delay) {
    clearTimeout(pingSchedule);
    pingSchedule = setTimeout(pingNow, delay);
  };

  var pingNow = function() {
    clearTimeout(pingSchedule);
    clearTimeout(connectSchedule);
    var pingData = (options.isAuth && pongCount % 8 == 2) ? JSON.stringify({
      t: 'p',
      l: Math.round(0.1 * averageLag)
    }) : null;
    try {
      ws.send(pingData);
      lastPingTime = performance.now();
    } catch (e) {
      debug(e, true);
    }
    scheduleConnect(options.pingMaxLag);
  };

  var computePingDelay = function() {
    return options.pingDelay + (options.idle ? 1000 : 0);
  };

  var pong = function() {
    clearTimeout(connectSchedule);
    schedulePing(computePingDelay());
    var currentLag = Math.min(performance.now() - lastPingTime, 10000);
    pongCount++;

    // Average first 4 pings, then switch to decaying average.
    var mix = pongCount > 4 ? 0.1 : 1 / pongCount;
    averageLag += mix * (currentLag - averageLag);

    lichess.pubsub.emit('socket.lag', averageLag);
  };

  var handle = function(m) {
    if (m.v) {
      if (m.v <= version) {
        debug("already has event " + m.v);
        return;
      }
      // it's impossible but according to previous login, it happens nonetheless
      if (m.v > version + 1) return lichess.reload();
      version = m.v;
    }
    switch (m.t || false) {
      case false:
        break;
      case 'resync':
        lichess.reload();
        break;
      case 'ack':
        ackable.gotAck(m.d);
        break;
      default:
        lichess.pubsub.emit('socket.in.' + m.t, m.d);
        var processed = settings.receive && settings.receive(m.t, m.d);
        if (!processed && settings.events[m.t]) settings.events[m.t](m.d || null, m);
    }
  };

  var debug = function(msg, always) {
    if (always || options.debug) {
      console.debug("[" + options.name + " " + settings.params.sri + "]", msg);
    }
  };

  var destroy = function() {
    clearTimeout(pingSchedule);
    clearTimeout(connectSchedule);
    disconnect();
    ws = null;
  };

  var disconnect = function() {
    if (ws) {
      debug("Disconnect");
      autoReconnect = false;
      ws.onerror = ws.onclose = ws.onopen = ws.onmessage = $.noop;
      ws.close();
    }
  };

  var onError = function(e) {
    options.debug = true;
    debug('error: ' + JSON.stringify(e));
    tryOtherUrl = true;
    clearTimeout(pingSchedule);
  };

  var onSuccess = function() {
    nbConnects++;
    if (nbConnects == 1) {
      options.onFirstConnect();
      var disconnectTimeout;
      lichess.idleTimer(10 * 60 * 1000, function() {
        options.idle = true;
        disconnectTimeout = setTimeout(destroy, 2 * 60 * 60 * 1000);
      }, function() {
        options.idle = false;
        if (ws) clearTimeout(disconnectTimeout);
        else location.reload();
      });
    }
  };

  const baseUrls = document.body.getAttribute('data-socket-domains').split(',');

  const baseUrl = function() {
    let url = storage.get();
    if (!url || tryOtherUrl) {
      url = baseUrls[Math.floor(Math.random() * baseUrls.length)];
      storage.set(url);
    }
    return url;
  };

  connect();
  window.addEventListener('unload', destroy);

  return {
    disconnect: disconnect,
    send: send,
    options: options,
    pingInterval: function() {
      return computePingDelay() + averageLag;
    },
    averageLag: function() {
      return averageLag;
    },
    getVersion: function() {
      return version;
    }
  };
};

lichess.StrongSocket.defaults = {
  events: {
    fen: function(e) {
      $('.mini-board-' + e.id).each(function() {
        lichess.parseFen($(this).data("fen", e.fen).data("lastmove", e.lm));
      });
    },
    challenges: function(d) {
      lichess.challengeApp.update(d);
    },
    notifications: function(d) {
      lichess.notifyApp.update(d, true);
    }
  },
  params: {
    sri: lichess.sri
  },
  options: {
    name: "unnamed",
    idle: false,
    pingMaxLag: 9000, // time to wait for pong before reseting the connection
    pingDelay: 2500, // time between pong and ping
    autoReconnectDelay: 3500,
    protocol: location.protocol === 'https:' ? 'wss:' : 'ws:',
    onFirstConnect: $.noop
  }
};
<|endoftext|>


<commit_before>var async  = require('async');
var utils  = require('../utils');

var TSV    = require('./tsv');
var json   = require('./json');
var NIFTI  = require('./nii');

var BIDS = {

    options:  {},
    issues: [],

    /**
     * Start
     *
     * Takes either a filelist array or
     * a path to a BIDS directory and an
     * options object and starts
     * the validation process and
     * returns the errors and warnings as
     * arguments to the callback.
     */
    start: function (dir, options, callback) {
        var self = BIDS;
        self.options = options ? self.parseOptions(options) : {};
        BIDS.reset();
        utils.files.readDir(dir, function (files) {
            self.quickTest(files, function (couldBeBIDS) {
                if (couldBeBIDS) {
                    self.fullTest(files, callback);
                } else {
                    callback('Invalid');
                }
            });
        });
    },

    /**
     * Quick Test
     *
     * A quick test to see if it could be a BIDS
     * dataset based on structure/naming. If it
     * could be it will trigger the full validation
     * otherwise it will throw a callback with a
     * generic error.
     */
    quickTest: function (fileList, callback) {
        var couldBeBIDS = false;
        for (var key in fileList) {
            if (fileList.hasOwnProperty(key)) {
                var file = fileList[key];
                var path = utils.files.relativePath(file);
                if (path) {
                    path = path.split('/');
                    if (path.length > 5) {couldBeBIDS = false; break;}
                    path = path.reverse();

                    if (
                        path[0].includes('.nii') &&
                        (
                            path[1] == 'anat' ||
                            path[1] == 'func' ||
                            path[1] == 'dwi'
                        ) &&
                        (
                            (path[2] && path[2].indexOf('ses-') == 0) ||
                            (path[2] && path[2].indexOf('sub-') == 0)
                        )
                    ) {
                        couldBeBIDS = true;
                        break;
                    }
                }
            }
        }
        callback(couldBeBIDS);
    },

    /**
     * Full Test
     *
     * Takes on an array of files and starts
     * the validation process for a BIDS
     * package.
     */
    fullTest: function (fileList, callback) {
        var self = this;

        var jsonContentsDict = {},
            bContentsDict    = {},
            events           = [],
            niftis           = [];

        // validate individual files
        async.forEachOf(fileList, function (file, key, cb) {
            file.relativePath = utils.files.relativePath(file);

            // validate path naming
            if (!utils.type.isBIDS(file.relativePath)) {
                self.issues.push(new utils.Issue({
                    file: file,
                    evidence: file.name,
                    code: 1
                }));
                cb();
            }

            // capture niftis for later validation
            else if (file.name.includes('.nii')) {
                niftis.push(file);
                cb();
            }

            // validate tsv
            else if (file.name && file.name.endsWith('.tsv')) {
                utils.files.readFile(file, function (contents) {
                    var isEvents = file.name.endsWith('_events.tsv');
                    if (isEvents) {events.push(file.relativePath);}
                    TSV(file, contents, isEvents, function (issues) {
                        self.issues = self.issues.concat(issues);
                        cb();
                    });
                });
            }

            // capture bvec and bval contents
            else if (file.name && (file.name.endsWith('.bvec') || file.name.endsWith('.bval'))) {
                utils.files.readFile(file, function (contents) {
                    bContentsDict[file.relativePath] = contents;
                    cb();
                });
            }

            // validate json
            else if (file.name && file.name.endsWith('.json')) {
                utils.files.readFile(file, function (contents) {
                    json(file, contents, function (issues, jsObj) {
                        self.issues = self.issues.concat(issues);
                        jsonContentsDict[file.relativePath] = jsObj;
                        cb();
                    });
                });
            } else {
                cb();
            }

        }, function () {
            async.forEachOf(niftis, function (file, key, cb) {
                if (self.options.ignoreNiftiHeaders) {
                    NIFTI(null, file, jsonContentsDict, bContentsDict, events, function (issues) {
                        self.issues = self.issues.concat(issues);
                        return cb();
                    });
                } else {
                    utils.files.readNiftiHeader(file, function (header) {
                        NIFTI(header, file, jsonContentsDict, bContentsDict, events, function (issues) {
                            self.issues = self.issues.concat(issues);
                            return cb();
                        });
                    });
                }
            }, function(){
                var issues = self.formatIssues(self.issues);
                callback(issues.errors, issues.warnings);
            });
        });
    },

    /**
     * Format Issues
     */
    formatIssues: function () {
        var errors = [], warnings = [];

        // organize by issue code
        var categorized = {};
        for (var i = 0; i < this.issues.length; i++) {
            var issue = this.issues[i];
            if (!categorized[issue.code]) {
                categorized[issue.code] = utils.issues[issue.code];
                categorized[issue.code].files = [];
            }
            categorized[issue.code].files.push(issue);
        }

        // organize by severity
        for (var key in categorized) {
            var issue = categorized[key];
            issue.code = key;
            if (issue.severity === 'error') {
                errors.push(issue);
            } else if (issue.severity === 'warning' && !this.options.ignoreWarnings) {
                warnings.push(issue);
            }

        }

        return {errors: errors, warnings: warnings};
    },

    /**
     * Reset
     *
     * Resets the in object data back to original values.
     */
    reset: function () {
        this.issues = [];
    },

    /**
     * Parse Options
     */
    parseOptions: function (options) {
        return {
            ignoreWarnings:     options.ignoreWarnings     ? true : false,
            ignoreNiftiHeaders: options.ignoreNiftiHeaders ? true : false
        };
    }
};

module.exports = BIDS;<commit_msg>separated bvec and bval checks<commit_after>var async  = require('async');
var utils  = require('../utils');

var TSV    = require('./tsv');
var json   = require('./json');
var NIFTI  = require('./nii');

var BIDS = {

    options:  {},
    issues: [],

    /**
     * Start
     *
     * Takes either a filelist array or
     * a path to a BIDS directory and an
     * options object and starts
     * the validation process and
     * returns the errors and warnings as
     * arguments to the callback.
     */
    start: function (dir, options, callback) {
        var self = BIDS;
        self.options = options ? self.parseOptions(options) : {};
        BIDS.reset();
        utils.files.readDir(dir, function (files) {
            self.quickTest(files, function (couldBeBIDS) {
                if (couldBeBIDS) {
                    self.fullTest(files, callback);
                } else {
                    callback('Invalid');
                }
            });
        });
    },

    /**
     * Quick Test
     *
     * A quick test to see if it could be a BIDS
     * dataset based on structure/naming. If it
     * could be it will trigger the full validation
     * otherwise it will throw a callback with a
     * generic error.
     */
    quickTest: function (fileList, callback) {
        var couldBeBIDS = false;
        for (var key in fileList) {
            if (fileList.hasOwnProperty(key)) {
                var file = fileList[key];
                var path = utils.files.relativePath(file);
                if (path) {
                    path = path.split('/');
                    if (path.length > 5) {couldBeBIDS = false; break;}
                    path = path.reverse();

                    if (
                        path[0].includes('.nii') &&
                        (
                            path[1] == 'anat' ||
                            path[1] == 'func' ||
                            path[1] == 'dwi'
                        ) &&
                        (
                            (path[2] && path[2].indexOf('ses-') == 0) ||
                            (path[2] && path[2].indexOf('sub-') == 0)
                        )
                    ) {
                        couldBeBIDS = true;
                        break;
                    }
                }
            }
        }
        callback(couldBeBIDS);
    },

    /**
     * Full Test
     *
     * Takes on an array of files and starts
     * the validation process for a BIDS
     * package.
     */
    fullTest: function (fileList, callback) {
        var self = this;

        var jsonContentsDict = {},
            bContentsDict    = {},
            events           = [],
            niftis           = [];

        // validate individual files
        async.forEachOf(fileList, function (file, key, cb) {
            file.relativePath = utils.files.relativePath(file);

            // validate path naming
            if (!utils.type.isBIDS(file.relativePath)) {
                self.issues.push(new utils.Issue({
                    file: file,
                    evidence: file.name,
                    code: 1
                }));
                cb();
            }

            // capture niftis for later validation
            else if (file.name.includes('.nii')) {
                niftis.push(file);
                cb();
            }

            // validate tsv
            else if (file.name && file.name.endsWith('.tsv')) {
                utils.files.readFile(file, function (contents) {
                    var isEvents = file.name.endsWith('_events.tsv');
                    if (isEvents) {events.push(file.relativePath);}
                    TSV(file, contents, isEvents, function (issues) {
                        self.issues = self.issues.concat(issues);
                        cb();
                    });
                });
            }

            // validate bvec
            else if (file.name && file.name.endsWith('.bvec')) {
                utils.files.readFile(file, function (contents) {
                    bContentsDict[file.relativePath] = contents;
                    cb();
                });
            }

            // validate bval
            else if (file.name && file.name.endsWith('.bval')) {
                utils.files.readFile(file, function (contents) {
                    bContentsDict[file.relativePath] = contents;
                    cb();
                });
            }

            // validate json
            else if (file.name && file.name.endsWith('.json')) {
                utils.files.readFile(file, function (contents) {
                    json(file, contents, function (issues, jsObj) {
                        self.issues = self.issues.concat(issues);
                        jsonContentsDict[file.relativePath] = jsObj;
                        cb();
                    });
                });
            } else {
                cb();
            }

        }, function () {
            async.forEachOf(niftis, function (file, key, cb) {
                if (self.options.ignoreNiftiHeaders) {
                    NIFTI(null, file, jsonContentsDict, bContentsDict, events, function (issues) {
                        self.issues = self.issues.concat(issues);
                        return cb();
                    });
                } else {
                    utils.files.readNiftiHeader(file, function (header) {
                        NIFTI(header, file, jsonContentsDict, bContentsDict, events, function (issues) {
                            self.issues = self.issues.concat(issues);
                            return cb();
                        });
                    });
                }
            }, function(){
                var issues = self.formatIssues(self.issues);
                callback(issues.errors, issues.warnings);
            });
        });
    },

    /**
     * Format Issues
     */
    formatIssues: function () {
        var errors = [], warnings = [];

        // organize by issue code
        var categorized = {};
        for (var i = 0; i < this.issues.length; i++) {
            var issue = this.issues[i];
            if (!categorized[issue.code]) {
                categorized[issue.code] = utils.issues[issue.code];
                categorized[issue.code].files = [];
            }
            categorized[issue.code].files.push(issue);
        }

        // organize by severity
        for (var key in categorized) {
            var issue = categorized[key];
            issue.code = key;
            if (issue.severity === 'error') {
                errors.push(issue);
            } else if (issue.severity === 'warning' && !this.options.ignoreWarnings) {
                warnings.push(issue);
            }

        }

        return {errors: errors, warnings: warnings};
    },

    /**
     * Reset
     *
     * Resets the in object data back to original values.
     */
    reset: function () {
        this.issues = [];
    },

    /**
     * Parse Options
     */
    parseOptions: function (options) {
        return {
            ignoreWarnings:     options.ignoreWarnings     ? true : false,
            ignoreNiftiHeaders: options.ignoreNiftiHeaders ? true : false
        };
    }
};

module.exports = BIDS;<|endoftext|>


<commit_before>var async  = require('async');
var fs     = require('fs');
var utils  = require('../utils');

var TSV    = require('./tsv');
var json   = require('./json');
var NIFTI  = require('./nii');
var bval   = require('./bval');
var bvec   = require('./bvec');
var session = require('./session');
var headerFields = require('./headerFields');

var BIDS = {

    options:  {},
    issues: [],

    /**
     * Start
     *
     * Takes either a filelist array or
     * a path to a BIDS directory and an
     * options object and starts
     * the validation process and
     * returns the errors and warnings as
     * arguments to the callback.
     */
    start: function (dir, options, callback) {
        var self = BIDS;
        self.options = options ? self.parseOptions(options) : {};
        BIDS.reset();
        utils.files.readDir(dir, function (files) {
            self.quickTest(files, function (couldBeBIDS) {
                if (couldBeBIDS) {
                    self.fullTest(files, callback);
                } else {
                    callback('Invalid');
                }
            });
        });
    },

    /**
     * Quick Test
     *
     * A quick test to see if it could be a BIDS
     * dataset based on structure/naming. If it
     * could be it will trigger the full validation
     * otherwise it will throw a callback with a
     * generic error.
     */
    quickTest: function (fileList, callback) {
        var couldBeBIDS = false;
        for (var key in fileList) {
            if (fileList.hasOwnProperty(key)) {
                var file = fileList[key];
                var path = utils.files.relativePath(file);
                if (path) {
                    path = path.split('/');
                    if (path[1] === 'derivatives') {continue;}
                    path = path.reverse();

                    if (
                        path[0].includes('.nii') &&
                        (
                            path[1] == 'anat' ||
                            path[1] == 'func' ||
                            path[1] == 'dwi'
                        ) &&
                        (
                            (path[2] && path[2].indexOf('ses-') == 0) ||
                            (path[2] && path[2].indexOf('sub-') == 0)
                        )
                    ) {
                        couldBeBIDS = true;
                        break;
                    }
                }
            }
        }
        callback(couldBeBIDS);
    },

    /**
     * Full Test
     *
     * Takes on an array of files and starts
     * the validation process for a BIDS
     * package.
     */
    fullTest: function (fileList, callback) {
        var self = this;

        var jsonContentsDict = {},
            bContentsDict    = {},
            events           = [],
            niftis           = [],
            headers          = [];

        var summary = {
            sessions: [],
            subjects: [],
            tasks:    [],
            modalities: [],
            totalFiles: Object.keys(fileList).length,
            size: 0
        };

        // validate individual files
        async.forEachOf(fileList, function (file, key, cb) {
            var path = utils.files.relativePath(file);
            file.relativePath = path;

            // ignore associated data
            if (
                file.relativePath.startsWith('/code/') ||
                file.relativePath.startsWith('/derivatives/') ||
                file.relativePath.startsWith('/sourcedata/')
            ) {cb();}

            // validate path naming
            else if (!utils.type.isBIDS(file.relativePath)) {
                self.issues.push(new utils.Issue({
                    file: file,
                    evidence: file.name,
                    code: 1
                }));
                cb();
            }

            // capture niftis for later validation
            else if (file.name.endsWith('.nii') || file.name.endsWith('.nii.gz')) {
                niftis.push(file);

                // collect modality summary
                var pathParts = path.split('_');
                var suffix    = pathParts[pathParts.length -1];
                    suffix    = suffix.slice(0, suffix.indexOf('.'));
                if (summary.modalities.indexOf(suffix) === -1) {summary.modalities.push(suffix);}

                cb();
            }

            // validate tsv
            else if (file.name && file.name.endsWith('.tsv')) {
                utils.files.readFile(file, function (contents) {
                    var isEvents = file.name.endsWith('_events.tsv');
                    if (isEvents) {events.push(file.relativePath);}
                    TSV(file, contents, isEvents, function (issues) {
                        self.issues = self.issues.concat(issues);
                        cb();
                    });
                });
            }

            // validate bvec
            else if (file.name && file.name.endsWith('.bvec')) {
                utils.files.readFile(file, function (contents) {
                    bContentsDict[file.relativePath] = contents;
                    bvec(file, contents, function (issues) {
                        self.issues = self.issues.concat(issues);
                        cb();
                    });
                });
            }

            // validate bval
            else if (file.name && file.name.endsWith('.bval')) {
                utils.files.readFile(file, function (contents) {
                    bContentsDict[file.relativePath] = contents;
                    bval(file, contents, function (issues) {
                        self.issues = self.issues.concat(issues);
                        cb();
                    });
                });
            }

            // validate json
            else if (file.name && file.name.endsWith('.json')) {
                utils.files.readFile(file, function (contents) {
                    json(file, contents, function (issues, jsObj) {
                        self.issues = self.issues.concat(issues);
                        jsonContentsDict[file.relativePath] = jsObj;

                        // collect task summary
                        if (file.name.indexOf('task') > -1) {
                            var task = jsObj ? jsObj.TaskName : null;
                            if (task && summary.tasks.indexOf(task) === -1) {
                                summary.tasks.push(task);
                            }
                        }
                        cb();
                    });
                });
            } else {
                cb();
            }

            // collect file stats
            if (typeof window !== 'undefined') {
                if (file.size) {summary.size += file.size;}
            } else {
                if (!file.stats) {file.stats = fs.lstatSync(file.path);}
                summary.size += file.stats.size;
            }

            // collect sessions subjects
            var checks = {'ses':  'sessions', 'sub':  'subjects'};
            for (var checkKey in checks) {
                if (path && path.indexOf(checkKey + '-') > -1) {
                    var item = path.slice(path.indexOf(checkKey + '-'));
                        item = item.slice(0, item.indexOf('/'));
                        if (item.indexOf('_') > -1) {item = item.slice(0, item.indexOf('_'));}
                        item = item.slice(checkKey.length + 1);
                    if (summary[checks[checkKey]].indexOf(item) === -1) {summary[checks[checkKey]].push(item);}
                }
            }

        }, function () {
            async.forEachOf(niftis, function (file, key, cb) {
                if (self.options.ignoreNiftiHeaders) {
                    NIFTI(null, file, jsonContentsDict, bContentsDict, fileList, events, function (issues) {
                        self.issues = self.issues.concat(issues);
                        cb();
                    });
                } else {
                    utils.files.readNiftiHeader(file, function (header) {
                        // check if header could be read
                        if (header && header.hasOwnProperty('error')) {
                            self.issues.push(header.error);
                            cb();
                        } else {
                            headers.push([file, header]);
                            NIFTI(header, file, jsonContentsDict, bContentsDict, fileList, events, function (issues) {
                                self.issues = self.issues.concat(issues);
                                cb();
                            });
                        }
                    });
                }

            }, function(){
                self.issues = self.issues.concat(headerFields(headers));
                self.issues = self.issues.concat(session(fileList));
                var issues  = self.formatIssues(self.issues);
                summary.modalities = self.groupModalities(summary.modalities);
                //remove fieldmap related warnings if no fieldmaps are present
                if(summary.modalities.indexOf("fieldmap") < 0) {
                    var filteredWarnings = [];
                    var fieldmapRelatedCodes = ["6", "7", "8", "9"];
                    for (var i in issues.warnings) {
                        if (fieldmapRelatedCodes.indexOf(issues.warnings[i].code) < 0) {
                            filteredWarnings.push(issues.warnings[i]);
                        }
                    }
                    issues.warnings = filteredWarnings;
                }
                callback(issues.errors, issues.warnings, summary);
            });
        });
    },

    /**
     * Format Issues
     */
    formatIssues: function () {
        var errors = [], warnings = [];

        // organize by issue code
        var categorized = {};
        for (var i = 0; i < this.issues.length; i++) {
            var issue = this.issues[i];
            if (!categorized[issue.code]) {
                categorized[issue.code] = utils.issues[issue.code];
                categorized[issue.code].files = [];
            }
            categorized[issue.code].files.push(issue);
        }

        // organize by severity
        for (var key in categorized) {
            issue = categorized[key];
            issue.code = key;
            // sort alphabetically by relative path of files
            issue.files.sort(function(a,b) {return (a.file.relativePath > b.file.relativePath) ? 1 : ((b.file.relativePath > a.file.relativePath) ? -1 : 0);} );

            if (issue.severity === 'error') {
                errors.push(issue);
            } else if (issue.severity === 'warning' && !this.options.ignoreWarnings) {
                warnings.push(issue);
            }

        }

        return {errors: errors, warnings: warnings};
    },

    /**
     * Group Modalities
     *
     * Takes an array of modalities and looks for
     * groupings defined in 'modalityGroups' and
     * replaces any perfectly matched groupings with
     * the grouping object key.
     */
    groupModalities: function (modalities) {

        var modalityGroups = [
            [[
                'magnitude1',
                'magnitude2',
                'phase1',
                'phase2'
            ], "fieldmap"],
            [[
                'magnitude1',
                'magnitude2',
                'phasediff'
            ], "fieldmap"],
            [[
                'magnitude',
                'fieldmap'
            ], "fieldmap"],
            [['epi'], "fieldmap"]
        ];

        for (var groupTouple_i in modalityGroups) {
            var groupSet = modalityGroups[groupTouple_i][0];
            var groupName = modalityGroups[groupTouple_i][1];
            var match = true;
            for (var i = 0; i < groupSet.length; i++) {
                if (modalities.indexOf(groupSet[i]) === -1) {
                    match = false;
                }
            }
            if (match) {
                modalities.push(groupName);
                for (var j = 0; j < groupSet.length; j++) {
                    modalities.splice(modalities.indexOf(groupSet[j]), 1);
                }
            }
        }

        return modalities;
    },

    /**
     * Reset
     *
     * Resets the in object data back to original values.
     */
    reset: function () {
        this.issues = [];
    },

    /**
     * Parse Options
     */
    parseOptions: function (options) {
        return {
            ignoreWarnings:     options.ignoreWarnings     ? true : false,
            ignoreNiftiHeaders: options.ignoreNiftiHeaders ? true : false
        };
    }
};

module.exports = BIDS;
<commit_msg>removed unnecessary quick test check<commit_after>var async  = require('async');
var fs     = require('fs');
var utils  = require('../utils');

var TSV    = require('./tsv');
var json   = require('./json');
var NIFTI  = require('./nii');
var bval   = require('./bval');
var bvec   = require('./bvec');
var session = require('./session');
var headerFields = require('./headerFields');

var BIDS = {

    options:  {},
    issues: [],

    /**
     * Start
     *
     * Takes either a filelist array or
     * a path to a BIDS directory and an
     * options object and starts
     * the validation process and
     * returns the errors and warnings as
     * arguments to the callback.
     */
    start: function (dir, options, callback) {
        var self = BIDS;
        self.options = options ? self.parseOptions(options) : {};
        BIDS.reset();
        utils.files.readDir(dir, function (files) {
            self.quickTest(files, function (couldBeBIDS) {
                if (couldBeBIDS) {
                    self.fullTest(files, callback);
                } else {
                    callback('Invalid');
                }
            });
        });
    },

    /**
     * Quick Test
     *
     * A quick test to see if it could be a BIDS
     * dataset based on structure/naming. If it
     * could be it will trigger the full validation
     * otherwise it will throw a callback with a
     * generic error.
     */
    quickTest: function (fileList, callback) {
        var couldBeBIDS = false;
        for (var key in fileList) {
            if (fileList.hasOwnProperty(key)) {
                var file = fileList[key];
                var path = utils.files.relativePath(file);
                if (path) {
                    path = path.split('/');
                    path = path.reverse();
                    if (
                        path[0].includes('.nii') &&
                        (
                            path[1] == 'anat' ||
                            path[1] == 'func' ||
                            path[1] == 'dwi'
                        ) &&
                        (
                            (path[2] && path[2].indexOf('ses-') == 0) ||
                            (path[2] && path[2].indexOf('sub-') == 0)
                        )
                    ) {
                        couldBeBIDS = true;
                        break;
                    }
                }
            }
        }
        callback(couldBeBIDS);
    },

    /**
     * Full Test
     *
     * Takes on an array of files and starts
     * the validation process for a BIDS
     * package.
     */
    fullTest: function (fileList, callback) {
        var self = this;

        var jsonContentsDict = {},
            bContentsDict    = {},
            events           = [],
            niftis           = [],
            headers          = [];

        var summary = {
            sessions: [],
            subjects: [],
            tasks:    [],
            modalities: [],
            totalFiles: Object.keys(fileList).length,
            size: 0
        };

        // validate individual files
        async.forEachOf(fileList, function (file, key, cb) {
            var path = utils.files.relativePath(file);
            file.relativePath = path;

            // ignore associated data
            if (
                file.relativePath.startsWith('/code/') ||
                file.relativePath.startsWith('/derivatives/') ||
                file.relativePath.startsWith('/sourcedata/')
            ) {cb();}

            // validate path naming
            else if (!utils.type.isBIDS(file.relativePath)) {
                self.issues.push(new utils.Issue({
                    file: file,
                    evidence: file.name,
                    code: 1
                }));
                cb();
            }

            // capture niftis for later validation
            else if (file.name.endsWith('.nii') || file.name.endsWith('.nii.gz')) {
                niftis.push(file);

                // collect modality summary
                var pathParts = path.split('_');
                var suffix    = pathParts[pathParts.length -1];
                    suffix    = suffix.slice(0, suffix.indexOf('.'));
                if (summary.modalities.indexOf(suffix) === -1) {summary.modalities.push(suffix);}

                cb();
            }

            // validate tsv
            else if (file.name && file.name.endsWith('.tsv')) {
                utils.files.readFile(file, function (contents) {
                    var isEvents = file.name.endsWith('_events.tsv');
                    if (isEvents) {events.push(file.relativePath);}
                    TSV(file, contents, isEvents, function (issues) {
                        self.issues = self.issues.concat(issues);
                        cb();
                    });
                });
            }

            // validate bvec
            else if (file.name && file.name.endsWith('.bvec')) {
                utils.files.readFile(file, function (contents) {
                    bContentsDict[file.relativePath] = contents;
                    bvec(file, contents, function (issues) {
                        self.issues = self.issues.concat(issues);
                        cb();
                    });
                });
            }

            // validate bval
            else if (file.name && file.name.endsWith('.bval')) {
                utils.files.readFile(file, function (contents) {
                    bContentsDict[file.relativePath] = contents;
                    bval(file, contents, function (issues) {
                        self.issues = self.issues.concat(issues);
                        cb();
                    });
                });
            }

            // validate json
            else if (file.name && file.name.endsWith('.json')) {
                utils.files.readFile(file, function (contents) {
                    json(file, contents, function (issues, jsObj) {
                        self.issues = self.issues.concat(issues);
                        jsonContentsDict[file.relativePath] = jsObj;

                        // collect task summary
                        if (file.name.indexOf('task') > -1) {
                            var task = jsObj ? jsObj.TaskName : null;
                            if (task && summary.tasks.indexOf(task) === -1) {
                                summary.tasks.push(task);
                            }
                        }
                        cb();
                    });
                });
            } else {
                cb();
            }

            // collect file stats
            if (typeof window !== 'undefined') {
                if (file.size) {summary.size += file.size;}
            } else {
                if (!file.stats) {file.stats = fs.lstatSync(file.path);}
                summary.size += file.stats.size;
            }

            // collect sessions subjects
            var checks = {'ses':  'sessions', 'sub':  'subjects'};
            for (var checkKey in checks) {
                if (path && path.indexOf(checkKey + '-') > -1) {
                    var item = path.slice(path.indexOf(checkKey + '-'));
                        item = item.slice(0, item.indexOf('/'));
                        if (item.indexOf('_') > -1) {item = item.slice(0, item.indexOf('_'));}
                        item = item.slice(checkKey.length + 1);
                    if (summary[checks[checkKey]].indexOf(item) === -1) {summary[checks[checkKey]].push(item);}
                }
            }

        }, function () {
            async.forEachOf(niftis, function (file, key, cb) {
                if (self.options.ignoreNiftiHeaders) {
                    NIFTI(null, file, jsonContentsDict, bContentsDict, fileList, events, function (issues) {
                        self.issues = self.issues.concat(issues);
                        cb();
                    });
                } else {
                    utils.files.readNiftiHeader(file, function (header) {
                        // check if header could be read
                        if (header && header.hasOwnProperty('error')) {
                            self.issues.push(header.error);
                            cb();
                        } else {
                            headers.push([file, header]);
                            NIFTI(header, file, jsonContentsDict, bContentsDict, fileList, events, function (issues) {
                                self.issues = self.issues.concat(issues);
                                cb();
                            });
                        }
                    });
                }

            }, function(){
                self.issues = self.issues.concat(headerFields(headers));
                self.issues = self.issues.concat(session(fileList));
                var issues  = self.formatIssues(self.issues);
                summary.modalities = self.groupModalities(summary.modalities);
                //remove fieldmap related warnings if no fieldmaps are present
                if(summary.modalities.indexOf("fieldmap") < 0) {
                    var filteredWarnings = [];
                    var fieldmapRelatedCodes = ["6", "7", "8", "9"];
                    for (var i in issues.warnings) {
                        if (fieldmapRelatedCodes.indexOf(issues.warnings[i].code) < 0) {
                            filteredWarnings.push(issues.warnings[i]);
                        }
                    }
                    issues.warnings = filteredWarnings;
                }
                callback(issues.errors, issues.warnings, summary);
            });
        });
    },

    /**
     * Format Issues
     */
    formatIssues: function () {
        var errors = [], warnings = [];

        // organize by issue code
        var categorized = {};
        for (var i = 0; i < this.issues.length; i++) {
            var issue = this.issues[i];
            if (!categorized[issue.code]) {
                categorized[issue.code] = utils.issues[issue.code];
                categorized[issue.code].files = [];
            }
            categorized[issue.code].files.push(issue);
        }

        // organize by severity
        for (var key in categorized) {
            issue = categorized[key];
            issue.code = key;
            // sort alphabetically by relative path of files
            issue.files.sort(function(a,b) {return (a.file.relativePath > b.file.relativePath) ? 1 : ((b.file.relativePath > a.file.relativePath) ? -1 : 0);} );

            if (issue.severity === 'error') {
                errors.push(issue);
            } else if (issue.severity === 'warning' && !this.options.ignoreWarnings) {
                warnings.push(issue);
            }

        }

        return {errors: errors, warnings: warnings};
    },

    /**
     * Group Modalities
     *
     * Takes an array of modalities and looks for
     * groupings defined in 'modalityGroups' and
     * replaces any perfectly matched groupings with
     * the grouping object key.
     */
    groupModalities: function (modalities) {

        var modalityGroups = [
            [[
                'magnitude1',
                'magnitude2',
                'phase1',
                'phase2'
            ], "fieldmap"],
            [[
                'magnitude1',
                'magnitude2',
                'phasediff'
            ], "fieldmap"],
            [[
                'magnitude',
                'fieldmap'
            ], "fieldmap"],
            [['epi'], "fieldmap"]
        ];

        for (var groupTouple_i in modalityGroups) {
            var groupSet = modalityGroups[groupTouple_i][0];
            var groupName = modalityGroups[groupTouple_i][1];
            var match = true;
            for (var i = 0; i < groupSet.length; i++) {
                if (modalities.indexOf(groupSet[i]) === -1) {
                    match = false;
                }
            }
            if (match) {
                modalities.push(groupName);
                for (var j = 0; j < groupSet.length; j++) {
                    modalities.splice(modalities.indexOf(groupSet[j]), 1);
                }
            }
        }

        return modalities;
    },

    /**
     * Reset
     *
     * Resets the in object data back to original values.
     */
    reset: function () {
        this.issues = [];
    },

    /**
     * Parse Options
     */
    parseOptions: function (options) {
        return {
            ignoreWarnings:     options.ignoreWarnings     ? true : false,
            ignoreNiftiHeaders: options.ignoreNiftiHeaders ? true : false
        };
    }
};

module.exports = BIDS;
<|endoftext|>