<commit_before>import { XMLDocumentImporter } from 'substance'
import JATSSchema from '../../TextureArticle'
import InternalArticleSchema from '../../InternalArticleSchema'
import InternalArticle from '../../InternalArticleDocument'
import { createXMLConverters } from '../../shared/xmlSchemaHelpers'
import BodyConverter from './BodyConverter'

/*
  TextureJATs Reference: (Please keep this up-to-date)
  article:
    (
      front,
      body?,
      back?,
    )
  front:
    (
      journal-meta?,    // not supported yet
      article-meta,     // -> metadata and others
      // TODO: define a strict schema here ( why multiple ones? )
      (def-list|list|ack|bio|fn-group|glossary|notes)*
    )
  article-meta:
    (
      article-id*,      // not supported yet
      article-categories?,  //   -> article-record
      title-group?,     // this is not optional internally, at least it contains the main title
      contrib-group*,   // -> mapped to authors, editors, and contributors
      aff*,             // -> affiliations
      author-notes?,    // not supported yet
      pub-date*,        // -> article-record
      volume?,          // -> article-record
      issue?,           // -> article-record
      isbn?,            // -> article-record
      (((fpage,lpage?)?,page-range?)|elocation-id)?,  // -> article-record
      history?,         // -> article-record
      permissions?,
      self-uri*,        // not supported yet
      (related-article,related-object)*, // not supported yet
      abstract?,        // -> content.abstract
      trans-abstract*,  // -> translations
      kwd-group*,       // -> keywords
      funding-group*,   // not supported yet
      conference*,      // not supported yet
      counts?,          // not supported yet
      custom-meta-group?  // not supported yet
    )
  back:
    (
      label?,   // not supported
      title*, // not supported
      (ack|app-group|bio|fn-group|glossary|ref-list|notes|sec)* // not supported
    )

  TODO:
    Allow only one place for '<ack>', '<bio>', '<fn-group>', '<glossary>', '<notes>'
*/

export default function jats2internal (jats, api) {
  let doc = InternalArticle.createEmptyArticle(InternalArticleSchema)
  // this is used to for parts of the DOM where we use JATS in the internal model
  let jatsImporter = _createImporter(doc)

  // metadata
  _populateAffiliations(doc, jats)
  _populateAuthors(doc, jats)
  _populateEditors(doc, jats)
  _populateAwards(doc, jats)
  _populateArticleRecord(doc, jats)
  _populateKeywords(doc, jats)
  _populateSubjects(doc, jats)

  // content
  _populateTitle(doc, jats, jatsImporter)
  _populateAbstract(doc, jats, jatsImporter)
  _populateBody(doc, jats, jatsImporter)
  // _populateFootnotes(doc, jats)
  // _populateReferences(doc, jats)

  return doc
}

function _createImporter (doc) {
  // Note: we are applying a hybrid approach, i.e. we create XML importers for the JATS schema
  // but only for those elements which are supported by our internal article schema.
  let jatsSchema = JATSSchema.xmlSchema
  let tagNames = jatsSchema.getTagNames().filter(name => Boolean(InternalArticleSchema.getNodeClass(name)))
  let jatsConverters = createXMLConverters(JATSSchema.xmlSchema, tagNames)
  let converters = [
    // Note: this is actually not used ATM because we populate the body node 'manually'
    HeadingConverter
  ].concat(jatsConverters)
  let jatsImporter = new _HybridJATSImporter({
    schema: InternalArticleSchema,
    xmlSchema: jatsSchema,
    idAttribute: 'id',
    converters
  })
  // ATTENTION: this looks hacky, but we know what we are doing (hopefully)
  jatsImporter.state.doc = doc
  return jatsImporter
}

class _HybridJATSImporter extends XMLDocumentImporter {
  _getConverterForElement (el, mode) {
    let converter = super._getConverterForElement(el, mode)
    if (!converter) {
      if (mode === 'inline') {
        return UnsupportedInlineNodeConverter
      } else {
        return UnsupportedNodeConverter
      }
    }
    return converter
  }
}

function _populateAffiliations (doc, jats) {
  const affiliations = doc.get('affiliations')
  const affEls = jats.findAll('article-meta > aff')
  affEls.forEach(el => {
    let aff = {
      id: el.id,
      type: 'organisation',
      name: getText(el, 'institution[content-type=orgname]'),
      division1: getText(el, 'institution[content-type=orgdiv1]'),
      division2: getText(el, 'institution[content-type=orgdiv2]'),
      division3: getText(el, 'institution[content-type=orgdiv3]'),
      street: getText(el, 'addr-line[content-type=street-address]'),
      addressComplements: getText(el, 'addr-line[content-type=complements]'),
      city: getText(el, 'city'),
      state: getText(el, 'state'),
      postalCode: getText(el, 'postal-code'),
      country: getText(el, 'country'),
      phone: getText(el, 'phone'),
      fax: getText(el, 'fax'),
      email: getText(el, 'email'),
      uri: getText(el, 'uri[content-type=link]')
    }
    affiliations.append(doc.create(aff))
  })
}

function _populateAuthors (doc, jats) {
  let authors = doc.get('authors')
  let authorEls = jats.findAll(`contrib-group[content-type=author] > contrib`)
  _populateContribs(doc, jats, authors, authorEls)
}

function _populateEditors (doc, jats) {
  let editors = doc.get('editors')
  let editorEls = jats.findAll(`contrib-group[content-type=editor] > contrib`)
  _populateContribs(doc, jats, editors, editorEls)
}

function _populateContribs (doc, jats, contribs, contribEls, groupId) {
  contribEls.forEach(contribEl => {
    if (contribEl.attr('contrib-type') === 'group') {
      // ATTENTION: groups are defined 'inplace'
      // the members of the group are appended to the list of persons
      let groups = doc.get('groups')
      let group = {
        id: contribEl.id,
        type: 'group',
        name: getText(contribEl, 'named-content[content-type=name]'),
        email: getText(contribEl, 'email'),
        affiliations: _getAffiliationIds(contribEl, true),
        equalContrib: contribEl.getAttribute('equal-contrib') === 'yes',
        corresp: contribEl.getAttribute('corresp') === 'yes',
        awards: _getAwardIds(contribEl)
      }
      groups.append(doc.create(group))

      let memberEls = contribEl.findAll('contrib')
      _populateContribs(doc, jats, contribs, memberEls, group.id)
    } else {
      contribs.append(doc.create({
        type: 'person',
        givenNames: getText(contribEl, 'given-names'),
        surname: getText(contribEl, 'surname'),
        email: getText(contribEl, 'email'),
        prefix: getText(contribEl, 'prefix'),
        suffix: getText(contribEl, 'suffix'),
        affiliations: _getAffiliationIds(contribEl),
        awards: _getAwardIds(contribEl),
        equalContrib: contribEl.getAttribute('equal-contrib') === 'yes',
        corresp: contribEl.getAttribute('corresp') === 'yes',
        deceased: contribEl.getAttribute('deceased') === 'yes'
      }))
    }
  })
}

function _getAffiliationIds (el, isGroup) {
  // let dom = el.ownerDocument
  let xrefs = el.findAll('xref[ref-type=aff]')
  // NOTE: for groups we need to extract only affiliations of group, without members
  if (isGroup) {
    xrefs = el.findAll('collab > xref[ref-type=aff]')
  }
  let affs = xrefs.map(xref => xref.attr('rid'))
  return affs
}

function _getAwardIds (el) {
  let xrefs = el.findAll('xref[ref-type=award]')
  let awardIds = xrefs.map(xref => xref.attr('rid'))
  return awardIds
}

function _populateAwards (doc, jats) {
  const awards = doc.get('awards')
  const awardEls = jats.findAll('article-meta > funding-group > award-group')
  // Convert <award-group> elements to award entities
  awardEls.forEach(el => {
    let award = {
      id: el.id,
      type: 'award',
      institution: getText(el, 'institution'),
      fundRefId: getText(el, 'institution-id'),
      awardId: getText(el, 'award-id')
    }
    awards.append(doc.create(award))
  })
}

function _populateArticleRecord (doc, jats) {
  console.error('FIXME: populate article-record')
}
// This is the original implementation
// function _importArticleRecord(dom, api) {
//   let el = dom.find('article-meta')

//   let node = {
//     id: 'article-record',
//     type: 'article-record',
//     elocationId: getText(el, 'elocation-id'),
//     fpage: getText(el, 'fpage'),
//     lpage: getText(el, 'lpage'),
//     issue: getText(el, 'issue'),
//     volume: getText(el, 'volume'),
//     pageRange: getText(el, 'page-range')
//   }
//   const articleDates = el.findAll('history > date, pub-date')
//   articleDates.forEach(dateEl => {
//     const date = _extractDate(dateEl)
//     node[date.type] = date.value
//   })
// }
// const dateTypesMap = {
//   'pub': 'publishedDate',
//   'accepted': 'acceptedDate',
//   'received': 'receivedDate',
//   'rev-recd': 'revReceivedDate',
//   'rev-request': 'revRequestedDate'
// }
// function _extractDate(el) {
//   const dateType = el.getAttribute('date-type')
//   const value = el.getAttribute('iso-8601-date')
//   const entityProp = dateTypesMap[dateType]

//   return {
//     value: value,
//     type: entityProp
//   }
// }

function _populateKeywords (doc, jats) {
  let keywords = doc.get('keywords')
  let kwdEls = jats.findAll('article-meta > kwd-group > kwd')
  kwdEls.forEach(kwdEl => {
    const kwd = {
      type: 'keyword',
      name: kwdEl.textContent,
      category: kwdEl.getAttribute('content-type'),
      language: kwdEl.getParent().getAttribute('xml:lang')
    }
    keywords.append(doc.create(kwd))
  })
}

function _populateSubjects (doc, jats) {
  let subjects = doc.get('subjects')
  let subjectEls = jats.findAll('article-meta > article-categories > subj-group > subject')
  subjectEls.forEach(subjectEl => {
    const subject = {
      // HACK: trying to merge EntitDb into Article model, avoiding type collision
      type: '_subject',
      name: subjectEl.textContent,
      category: subjectEl.getAttribute('content-type'),
      language: subjectEl.getParent().getAttribute('xml:lang')
    }
    subjects.append(doc.create(subject))
  })
}

function _populateTitle (doc, jats, jatsImporter) {
  let titleEl = jats.find('article > front > article-meta > title-group > article-title')
  if (titleEl) {
    let title = doc.get('title')
    _convertAnnotatedText(jatsImporter, titleEl, title)
  }
}

function _populateAbstract (doc, jats, jatsImporter) {
  // ATTENTION: JATS can have multiple abstracts
  // ATM we only take the first, loosing the others
  let abstractEls = jats.findAll('article > front > article-meta > abstract')
  if (abstractEls.length > 0) {
    let abstractEl = abstractEls[0]
    if (abstractEls.length > 1) {
      console.error('FIXME: Texture only supports one <abstract>.')
    }
    let abstract = doc.get('abstract')
    abstractEl.children.forEach(el => {
      abstract.append(jatsImporter.convertElement(el))
    })
  }
}

function _populateBody (doc, jats, jatsImporter) {
  // ATTENTION: JATS can have multiple abstracts
  // ATM we only take the first, loosing the others
  let bodyEl = jats.find('article > body')
  if (bodyEl) {
    let body = doc.get('body')
    BodyConverter.instance().import(bodyEl, body, jatsImporter)
  }
}

// Customized Element converters

const UnsupportedNodeConverter = {
  type: '@unsupported-node',
  matchElement (el) {
    return false
  },
  import (el, node, converter) {
    node.data = el.serialize()
  }
}

const UnsupportedInlineNodeConverter = {
  type: '@unsupported-inline-node',
  matchElement (el) {
    return false
  },
  import (el, node, converter) {
    node.data = el.serialize()
  }
}

const HeadingConverter = {
  type: 'heading',
  tagName: 'heading',
  import (el, node, importer) {
    // Note: attributes are converted automatically
    node.content = importer.annotatedText(el, [node.id, 'content'])
  },
  export (node, el, exporter) {
    console.error('FIXME: implement HeadingConverter.export()')
  }
}

// Helpers

function getText (rootEl, selector) {
  let el = rootEl.find(selector)
  if (el) {
    return el.textContent
  } else {
    return ''
  }
}

function _convertAnnotatedText (jatsImporter, el, textNode) {
  // NOTE: this is a bit difficult but necessary
  // The importer maintains a stack of 'scopes' to deal with recursive calls
  // triggered by converters for nesteded element (annotations and inline nodes)
  jatsImporter.state.pushContext(el.tagName)
  textNode.content = jatsImporter.annotatedText(el, textNode.getPath())
  jatsImporter.state.popContext()
}
<commit_msg>Populate ArticleRecord.<commit_after>import { XMLDocumentImporter } from 'substance'
import JATSSchema from '../../TextureArticle'
import InternalArticleSchema from '../../InternalArticleSchema'
import InternalArticle from '../../InternalArticleDocument'
import { createXMLConverters } from '../../shared/xmlSchemaHelpers'
import BodyConverter from './BodyConverter'

/*
  TextureJATs Reference: (Please keep this up-to-date)
  article:
    (
      front,
      body?,
      back?,
    )
  front:
    (
      journal-meta?,    // not supported yet
      article-meta,     // -> metadata and others
      // TODO: define a strict schema here ( why multiple ones? )
      (def-list|list|ack|bio|fn-group|glossary|notes)*
    )
  article-meta:
    (
      article-id*,      // not supported yet
      article-categories?,  //   -> article-record
      title-group?,     // this is not optional internally, at least it contains the main title
      contrib-group*,   // -> mapped to authors, editors, and contributors
      aff*,             // -> affiliations
      author-notes?,    // not supported yet
      pub-date*,        // -> article-record
      volume?,          // -> article-record
      issue?,           // -> article-record
      isbn?,            // -> article-record
      (((fpage,lpage?)?,page-range?)|elocation-id)?,  // -> article-record
      history?,         // -> article-record
      permissions?,
      self-uri*,        // not supported yet
      (related-article,related-object)*, // not supported yet
      abstract?,        // -> content.abstract
      trans-abstract*,  // -> translations
      kwd-group*,       // -> keywords
      funding-group*,   // not supported yet
      conference*,      // not supported yet
      counts?,          // not supported yet
      custom-meta-group?  // not supported yet
    )
  back:
    (
      label?,   // not supported
      title*, // not supported
      (ack|app-group|bio|fn-group|glossary|ref-list|notes|sec)* // not supported
    )

  TODO:
    Allow only one place for '<ack>', '<bio>', '<fn-group>', '<glossary>', '<notes>'
*/

export default function jats2internal (jats, api) {
  let doc = InternalArticle.createEmptyArticle(InternalArticleSchema)
  // this is used to for parts of the DOM where we use JATS in the internal model
  let jatsImporter = _createImporter(doc)

  // metadata
  _populateAffiliations(doc, jats)
  _populateAuthors(doc, jats)
  _populateEditors(doc, jats)
  _populateAwards(doc, jats)
  _populateArticleRecord(doc, jats)
  _populateKeywords(doc, jats)
  _populateSubjects(doc, jats)

  // content
  _populateTitle(doc, jats, jatsImporter)
  _populateAbstract(doc, jats, jatsImporter)
  _populateBody(doc, jats, jatsImporter)
  // _populateFootnotes(doc, jats)
  // _populateReferences(doc, jats)

  return doc
}

function _createImporter (doc) {
  // Note: we are applying a hybrid approach, i.e. we create XML importers for the JATS schema
  // but only for those elements which are supported by our internal article schema.
  let jatsSchema = JATSSchema.xmlSchema
  let tagNames = jatsSchema.getTagNames().filter(name => Boolean(InternalArticleSchema.getNodeClass(name)))
  let jatsConverters = createXMLConverters(JATSSchema.xmlSchema, tagNames)
  let converters = [
    // Note: this is actually not used ATM because we populate the body node 'manually'
    HeadingConverter
  ].concat(jatsConverters)
  let jatsImporter = new _HybridJATSImporter({
    schema: InternalArticleSchema,
    xmlSchema: jatsSchema,
    idAttribute: 'id',
    converters
  })
  // ATTENTION: this looks hacky, but we know what we are doing (hopefully)
  jatsImporter.state.doc = doc
  return jatsImporter
}

class _HybridJATSImporter extends XMLDocumentImporter {
  _getConverterForElement (el, mode) {
    let converter = super._getConverterForElement(el, mode)
    if (!converter) {
      if (mode === 'inline') {
        return UnsupportedInlineNodeConverter
      } else {
        return UnsupportedNodeConverter
      }
    }
    return converter
  }
}

function _populateAffiliations (doc, jats) {
  const affiliations = doc.get('affiliations')
  const affEls = jats.findAll('article-meta > aff')
  affEls.forEach(el => {
    let aff = {
      id: el.id,
      type: 'organisation',
      name: getText(el, 'institution[content-type=orgname]'),
      division1: getText(el, 'institution[content-type=orgdiv1]'),
      division2: getText(el, 'institution[content-type=orgdiv2]'),
      division3: getText(el, 'institution[content-type=orgdiv3]'),
      street: getText(el, 'addr-line[content-type=street-address]'),
      addressComplements: getText(el, 'addr-line[content-type=complements]'),
      city: getText(el, 'city'),
      state: getText(el, 'state'),
      postalCode: getText(el, 'postal-code'),
      country: getText(el, 'country'),
      phone: getText(el, 'phone'),
      fax: getText(el, 'fax'),
      email: getText(el, 'email'),
      uri: getText(el, 'uri[content-type=link]')
    }
    affiliations.append(doc.create(aff))
  })
}

function _populateAuthors (doc, jats) {
  let authors = doc.get('authors')
  let authorEls = jats.findAll(`contrib-group[content-type=author] > contrib`)
  _populateContribs(doc, jats, authors, authorEls)
}

function _populateEditors (doc, jats) {
  let editors = doc.get('editors')
  let editorEls = jats.findAll(`contrib-group[content-type=editor] > contrib`)
  _populateContribs(doc, jats, editors, editorEls)
}

function _populateContribs (doc, jats, contribs, contribEls, groupId) {
  contribEls.forEach(contribEl => {
    if (contribEl.attr('contrib-type') === 'group') {
      // ATTENTION: groups are defined 'inplace'
      // the members of the group are appended to the list of persons
      let groups = doc.get('groups')
      let group = {
        id: contribEl.id,
        type: 'group',
        name: getText(contribEl, 'named-content[content-type=name]'),
        email: getText(contribEl, 'email'),
        affiliations: _getAffiliationIds(contribEl, true),
        equalContrib: contribEl.getAttribute('equal-contrib') === 'yes',
        corresp: contribEl.getAttribute('corresp') === 'yes',
        awards: _getAwardIds(contribEl)
      }
      groups.append(doc.create(group))

      let memberEls = contribEl.findAll('contrib')
      _populateContribs(doc, jats, contribs, memberEls, group.id)
    } else {
      contribs.append(doc.create({
        type: 'person',
        givenNames: getText(contribEl, 'given-names'),
        surname: getText(contribEl, 'surname'),
        email: getText(contribEl, 'email'),
        prefix: getText(contribEl, 'prefix'),
        suffix: getText(contribEl, 'suffix'),
        affiliations: _getAffiliationIds(contribEl),
        awards: _getAwardIds(contribEl),
        equalContrib: contribEl.getAttribute('equal-contrib') === 'yes',
        corresp: contribEl.getAttribute('corresp') === 'yes',
        deceased: contribEl.getAttribute('deceased') === 'yes'
      }))
    }
  })
}

function _getAffiliationIds (el, isGroup) {
  // let dom = el.ownerDocument
  let xrefs = el.findAll('xref[ref-type=aff]')
  // NOTE: for groups we need to extract only affiliations of group, without members
  if (isGroup) {
    xrefs = el.findAll('collab > xref[ref-type=aff]')
  }
  let affs = xrefs.map(xref => xref.attr('rid'))
  return affs
}

function _getAwardIds (el) {
  let xrefs = el.findAll('xref[ref-type=award]')
  let awardIds = xrefs.map(xref => xref.attr('rid'))
  return awardIds
}

function _populateAwards (doc, jats) {
  const awards = doc.get('awards')
  const awardEls = jats.findAll('article-meta > funding-group > award-group')
  // Convert <award-group> elements to award entities
  awardEls.forEach(el => {
    let award = {
      id: el.id,
      type: 'award',
      institution: getText(el, 'institution'),
      fundRefId: getText(el, 'institution-id'),
      awardId: getText(el, 'award-id')
    }
    awards.append(doc.create(award))
  })
}

function _populateArticleRecord (doc, jats) {
  let articleMetaEl = jats.find('article-meta')
  let articleRecord = doc.get('article-record')
  Object.assign(articleRecord, {
    elocationId: getText(articleMetaEl, 'elocation-id'),
    fpage: getText(articleMetaEl, 'fpage'),
    lpage: getText(articleMetaEl, 'lpage'),
    issue: getText(articleMetaEl, 'issue'),
    volume: getText(articleMetaEl, 'volume'),
    pageRange: getText(articleMetaEl, 'page-range')
  })
  const articleDateEls = articleMetaEl.findAll('history > date, pub-date')
  articleDateEls.forEach(dateEl => {
    const date = _extractDate(dateEl)
    articleRecord[date.type] = date.value
  })
}

const DATE_TYPES_MAP = {
  'pub': 'publishedDate',
  'accepted': 'acceptedDate',
  'received': 'receivedDate',
  'rev-recd': 'revReceivedDate',
  'rev-request': 'revRequestedDate'
}

function _extractDate (el) {
  const dateType = el.getAttribute('date-type')
  const value = el.getAttribute('iso-8601-date')
  const entityProp = DATE_TYPES_MAP[dateType]
  return {
    value: value,
    type: entityProp
  }
}

function _populateKeywords (doc, jats) {
  let keywords = doc.get('keywords')
  let kwdEls = jats.findAll('article-meta > kwd-group > kwd')
  kwdEls.forEach(kwdEl => {
    const kwd = {
      type: 'keyword',
      name: kwdEl.textContent,
      category: kwdEl.getAttribute('content-type'),
      language: kwdEl.getParent().getAttribute('xml:lang')
    }
    keywords.append(doc.create(kwd))
  })
}

function _populateSubjects (doc, jats) {
  let subjects = doc.get('subjects')
  let subjectEls = jats.findAll('article-meta > article-categories > subj-group > subject')
  subjectEls.forEach(subjectEl => {
    const subject = {
      // HACK: trying to merge EntitDb into Article model, avoiding type collision
      type: '_subject',
      name: subjectEl.textContent,
      category: subjectEl.getAttribute('content-type'),
      language: subjectEl.getParent().getAttribute('xml:lang')
    }
    subjects.append(doc.create(subject))
  })
}

function _populateTitle (doc, jats, jatsImporter) {
  let titleEl = jats.find('article > front > article-meta > title-group > article-title')
  if (titleEl) {
    let title = doc.get('title')
    _convertAnnotatedText(jatsImporter, titleEl, title)
  }
}

function _populateAbstract (doc, jats, jatsImporter) {
  // ATTENTION: JATS can have multiple abstracts
  // ATM we only take the first, loosing the others
  let abstractEls = jats.findAll('article > front > article-meta > abstract')
  if (abstractEls.length > 0) {
    let abstractEl = abstractEls[0]
    if (abstractEls.length > 1) {
      console.error('FIXME: Texture only supports one <abstract>.')
    }
    let abstract = doc.get('abstract')
    abstractEl.children.forEach(el => {
      abstract.append(jatsImporter.convertElement(el))
    })
  }
}

function _populateBody (doc, jats, jatsImporter) {
  // ATTENTION: JATS can have multiple abstracts
  // ATM we only take the first, loosing the others
  let bodyEl = jats.find('article > body')
  if (bodyEl) {
    let body = doc.get('body')
    BodyConverter.instance().import(bodyEl, body, jatsImporter)
  }
}

// Customized Element converters

const UnsupportedNodeConverter = {
  type: '@unsupported-node',
  matchElement (el) {
    return false
  },
  import (el, node, converter) {
    node.data = el.serialize()
  }
}

const UnsupportedInlineNodeConverter = {
  type: '@unsupported-inline-node',
  matchElement (el) {
    return false
  },
  import (el, node, converter) {
    node.data = el.serialize()
  }
}

const HeadingConverter = {
  type: 'heading',
  tagName: 'heading',
  import (el, node, importer) {
    // Note: attributes are converted automatically
    node.content = importer.annotatedText(el, [node.id, 'content'])
  },
  export (node, el, exporter) {
    console.error('FIXME: implement HeadingConverter.export()')
  }
}

// Helpers

function getText (rootEl, selector) {
  let el = rootEl.find(selector)
  if (el) {
    return el.textContent
  } else {
    return ''
  }
}

function _convertAnnotatedText (jatsImporter, el, textNode) {
  // NOTE: this is a bit difficult but necessary
  // The importer maintains a stack of 'scopes' to deal with recursive calls
  // triggered by converters for nesteded element (annotations and inline nodes)
  jatsImporter.state.pushContext(el.tagName)
  textNode.content = jatsImporter.annotatedText(el, textNode.getPath())
  jatsImporter.state.popContext()
}
<|endoftext|>


<commit_before>// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

console.log('instant_message init');

// 模块互相调用的测试
call_room_storage();
call_danmaku_rendering();

var url = window.location.href;
var appId = '3v0yxbrcuatjjsplug51eou78dy77jsyrg63hdx9bytn2jmc';

// 每个客户端自定义的 id
var clientId = guid();

console.log('my client id is :'+clientId);
// 如果想加入一个已有房间，可以传入 roomId
var roomId;
var rt;
var conv;
var convOld;
var firstFlag = true;

// 创建聊天实例（支持单页多实例）
rt = AV.realtime({
    appId: appId,
    clientId: clientId,
    // 是否开启 HTML 转义，防止 XSS
    encodeHTML: true
    // 是否开启服务器端认证
    // auth: authFun
});

// 当前 SDK 版本
console.log('欢迎使用 LeanCloud 实时通信，当前 SDK 版本是 ' + AV.realtime.version);

// 实时通信服务连接成功
rt.on('open', function() {
    console.log('实时通信服务建立成功！');
    findRoomId(url,function(result){
    	console.log('call back get result:'+result);
    	if(result == 'none')
    	{
	    		conv = rt.conv({
	            name: url,

	            transient: false,

	            attr: {
	            }
	        }, function(data) {
	            if (data) {
	            	roomId = data.id;
	                console.log('新的群组创建成功：', data.id);
	                setRoomId(url,roomId,function(result){
	                	console.log('storage get result:'+result);
	                });
	            }
	        });
    	}
    	else if(result == 'failed')
    	{
    		console.log('call back get failed');
    	}
    	else
    	{
    		roomId = result;
    		rt.conv(roomId, function(obj) {

        		// 判断这个 conv 是否在服务器端存在
        		if (obj) {
            		console.log('已经获取已有房间的实例');
            		convOld = obj;
            		convOld.join(function(data) {
        				console.log('加入现有群组。。当前用户成功加入 Conversation');
    				});
            		console.log('房间名字： ', convOld.name);
            		console.log('获取房间的初始化数据', convOld.attr);
            		conv = convOld;
            		convOld.receive(function(data) {
        				ReceiveMessage(data);
    				});
        		} else {
            		console.log('你想获取的房间不存在');
        		}
    		});
    	}
    });
});

// 当聊天断开时触发
rt.on('close', function() {
    console.log('实时通信服务被断开！');
});

// 当 Conversation 被创建时触发，当然您可以使用回调函数来处理，不一定要监听这个事件
rt.on('create', function(data) {

    // 当前用户加入这个 Conversation
    conv.join(function(data) {
        console.log('当前用户成功加入 Conversation');
    });

    // 当前 Conversation 接收到消息
    conv.receive(function(data) {
        ReceiveMessage(data);
    });
});

// 接收断线或者网络状况不佳的事件（断网可测试）
rt.on('reuse', function() {
    console.log('正在重新连接。。。');
});

// 监听所有用户加入的情况
rt.on('join', function(data) {
    console.log('有用户加入某个当前用户在的 Conversation：', data.initBy);
    sendMsg('fuck');
});

function sendMsg(msg){
    conv.send({
        text: msg
    },{
        type: 'text'
    }, function(data) {
        console.log('发送的消息服务端已收到：', msg);
    });
}

function ReceiveMessage(data) {
	console.log('当前 Conversation 收到消息：', data.msg.text)
};

function S4() {
   return (((1+Math.random())*0x10000)|0).toString(16).substring(1);
};
// Generate a pseudo-GUID by concatenating random hexadecimal.
function guid() {
   return (S4()+S4()+"-"+S4()+"-"+S4()+"-"+S4()+"-"+S4()+S4()+S4());
};

<commit_msg>function name changed<commit_after>// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

console.log('instant_message init');

// 模块互相调用的测试
call_room_storage();
call_danmaku_rendering();

var url = window.location.href;
var appId = '3v0yxbrcuatjjsplug51eou78dy77jsyrg63hdx9bytn2jmc';

// 每个客户端自定义的 id
var clientId = guid();

console.log('my client id is :'+clientId);
// 如果想加入一个已有房间，可以传入 roomId
var roomId;
var rt;
var conv;
var convOld;
var firstFlag = true;

// 创建聊天实例（支持单页多实例）
rt = AV.realtime({
    appId: appId,
    clientId: clientId,
    // 是否开启 HTML 转义，防止 XSS
    encodeHTML: true
    // 是否开启服务器端认证
    // auth: authFun
});

// 当前 SDK 版本
console.log('欢迎使用 LeanCloud 实时通信，当前 SDK 版本是 ' + AV.realtime.version);

// 实时通信服务连接成功
rt.on('open', function() {
    console.log('实时通信服务建立成功！');
    findRoomId(url,function(result){
    	console.log('call back get result:'+result);
    	if(result == 'none')
    	{
	    		conv = rt.conv({
	            name: url,

	            transient: false,

	            attr: {
	            }
	        }, function(data) {
	            if (data) {
	            	roomId = data.id;
	                console.log('新的群组创建成功：', data.id);
	                setRoomId(url,roomId,function(result){
	                	console.log('storage get result:'+result);
	                });
	            }
	        });
    	}
    	else if(result == 'failed')
    	{
    		console.log('call back get failed');
    	}
    	else
    	{
    		roomId = result;
    		rt.conv(roomId, function(obj) {

        		// 判断这个 conv 是否在服务器端存在
        		if (obj) {
            		console.log('已经获取已有房间的实例');
            		convOld = obj;
            		convOld.join(function(data) {
        				console.log('加入现有群组。。当前用户成功加入 Conversation');
    				});
            		console.log('房间名字： ', convOld.name);
            		console.log('获取房间的初始化数据', convOld.attr);
            		conv = convOld;
            		convOld.receive(function(data) {
        				ReceiveMsg(data);
    				});
        		} else {
            		console.log('你想获取的房间不存在');
        		}
    		});
    	}
    });
});

// 当聊天断开时触发
rt.on('close', function() {
    console.log('实时通信服务被断开！');
});

// 当 Conversation 被创建时触发，当然您可以使用回调函数来处理，不一定要监听这个事件
rt.on('create', function(data) {

    // 当前用户加入这个 Conversation
    conv.join(function(data) {
        console.log('当前用户成功加入 Conversation');
    });

    // 当前 Conversation 接收到消息
    conv.receive(function(data) {
        ReceiveMsg(data);
    });
});

// 接收断线或者网络状况不佳的事件（断网可测试）
rt.on('reuse', function() {
    console.log('正在重新连接。。。');
});

// 监听所有用户加入的情况
rt.on('join', function(data) {
    console.log('有用户加入某个当前用户在的 Conversation：', data.initBy);
    sendMsg('fuck');
});

function sendMsg(msg){
    conv.send({
        text: msg
    },{
        type: 'text'
    }, function(data) {
        console.log('发送的消息服务端已收到：', msg);
    });
}

function ReceiveMsg(data) {
	console.log('当前 Conversation 收到消息：', data.msg.text)
};

function S4() {
   return (((1+Math.random())*0x10000)|0).toString(16).substring(1);
};
// Generate a pseudo-GUID by concatenating random hexadecimal.
function guid() {
   return (S4()+S4()+"-"+S4()+"-"+S4()+"-"+S4()+"-"+S4()+S4()+S4());
};

<|endoftext|>


<commit_before>/**
 * Module Name: Timers
 * Description: Persistent timers and shit
 */
var _ = require('underscore')._;

var timers = function(dbot) {
    this.timers = dbot.db.timers;
    this.runningTimeouts = [];
    this.runningIntervals = [];

    this.api = {
        'addTimer': function(timeout, callback, firstDate) {
            var now = new Date().getTime();
            if(firstDate) {
                console.log('Setting first timer to run at ' + firstDate);
                firstTimeout = firstDate.getTime() - now;
                setTimeout(function() {
                    console.log('Running first timer at ' + new Date().toUTCString());
                    this.runningIntervals.push(this.api.addTimer(timeout, callback));
                    try {
                        callback();
                    } catch(err) {
                        console.log('Callback failed: ' + err);
                    }
                }.bind(this), firstTimeout);
            } else {
                this.runningIntervals.push(setInterval(function() {
                    console.log('Running subsequent timer at ' + new Date().toUTCString());
                    try {
                        callback();
                    } catch(err) {
                        console.log('Callback failed: ' + err);
                    }
                }.bind(this), timeout));
            }
        }
    };

    this.onDestroy = function() {
        for(var i=0;i<this.runningTimeouts.length;i++) {
            clearTimeout(this.runningTimeouts[i]);
        }
        for(i=0;i<this.runningIntervals.length;i++) {
            clearInterval(this.runningIntervals[i]);
        }
    }.bind(this);
};

exports.fetch = function(dbot) {
    return new timers(dbot);
};
<commit_msg>fix #249<commit_after>/**
 * Module Name: Timers
 * Description: Persistent timers and shit
 */
var _ = require('underscore')._;

var timers = function(dbot) {
    this.timers = dbot.db.timers;
    this.runningTimeouts = [];
    this.runningIntervals = [];

    this.api = {
        'addTimer': function(timeout, callback, firstDate) {
            var now = new Date().getTime();
            if(firstDate) {
                console.log('Setting first timer to run at ' + firstDate);
                firstTimeout = firstDate.getTime() - now;
                this.runningTimeouts.push(setTimeout(function() {
                    console.log('Running first timer at ' + new Date().toUTCString());
                    this.runningIntervals.push(this.api.addTimer(timeout, callback));
                    try {
                        callback();
                    } catch(err) {
                        console.log('Callback failed: ' + err);
                    }
                }.bind(this), firstTimeout));
            } else {
                this.runningIntervals.push(setInterval(function() {
                    console.log('Running subsequent timer at ' + new Date().toUTCString());
                    try {
                        callback();
                    } catch(err) {
                        console.log('Callback failed: ' + err);
                    }
                }.bind(this), timeout));
            }
        }
    };

    this.onDestroy = function() {
        for(var i=0;i<this.runningTimeouts.length;i++) {
            console.log('destroying ' +this.runningTimeouts[i]);
            clearTimeout(this.runningTimeouts[i]);
        }
        for(i=0;i<this.runningIntervals.length;i++) {
            console.log('destroying ' +this.runningIntervals[i]);
            clearInterval(this.runningIntervals[i]);
        }
    }.bind(this);
};

exports.fetch = function(dbot) {
    return new timers(dbot);
};
<|endoftext|>


<commit_before>"use strict";

let d3 = require('d3');
let _ = require('lodash');

// Calculates where points on the circumference of a circle lie.
class Circle {
  constructor(cx, cy, r) {
    this.cx = cx;
    this.cy = cy;
    this.r = r;
  }
  at(frac) {
    let radian = frac * 2 * Math.PI;
    return {
      x: _.round(this.cx + this.r * Math.sin(radian), 2),
      y: _.round(this.cy - this.r * Math.cos(radian), 2),
    };
  }
}

class Markers {
  constructor(defs) {
    this.defs = defs;
    this.added = [];
  }
  has(id) {
    return _.includes(this.added, id);
  }
  append(id) {
    return this.defs.append('marker')
      .attr('id', id);
  }
  ref(id) {
    return `url(#${id})`;
  }
}

class Transform {
  constructor() {
    this.operations = [];
  }
  toString() {
    return _.reverse(_.clone(this.operations)).join(' ');
  }
  translate(x, y) {
    this.operations.push(`translate(${x}, ${y})`);
    return this;
  };
  rotate(angle) {
    this.operations.push(`rotate(${angle})`);
    return this;
  }
}

let View = function(controller, svg, module) {

let markers = new Markers(d3.select(svg).append('defs'));

d3.select('head').append('style')
  .text(`
      .server .serverbg {
        stroke: black;
      }
      .server.follower .serverbg {
        fill: gray;
      }
      .server.candidate .serverbg {
        fill: #aa6666;
      }
      .server.leader .serverbg {
        fill: #00aa00;
      }
      .server g.votes {
        visibility: hidden;
      }
      .server.candidate g.votes {
        visibility: visible;
        fill: white;
      }
      .server.candidate g.votes .granted {
        fill: black;
      }

      .logs .bg rect {
        fill: #dddddd;
        stroke: gray;
      }
      .logs .entry rect {
        fill: #bcff29;
        stroke: black;
        stroke-width: 8;
      }
      .logs .entry.uncommitted rect {
        stroke-dasharray: 20, 15;
      }
  `);

let model = module.env;

let numServers = model.vars.get('servers').size();
let numIndexes = model.vars.get('servers').index(1).lookup('log').capacity();
let electionTimeout = 100000;
let ringLayout = new Circle(250, 500, 200);

// Wraps the model's Server Record with additional information for drawing
class Server {
  constructor(serverId, serverVar) {
    this.serverVar = serverVar;
    this.serverId = serverId;
    this.frac = (this.serverId - 1) / numServers;
    this.point = ringLayout.at(this.frac);
    this.peersCircle = new Circle(this.point.x, this.point.y, 40);

    this.timeoutArc = d3.svg.arc()
      .innerRadius(50)
      .outerRadius(60)
      .startAngle(0)
      .endAngle(0);
  }

  getVotes() {
    return this.serverVar.lookup('state').match({
      Candidate: cstate => cstate.peers.map(peer => {
        if (peer.lookup('voteGranted').toString() == 'True') {
          return 'granted';
        } else if (peer.lookup('voteResponded').toString() == 'True') {
          return 'denied';
        } else {
          return 'noreply';
        }
      }),
      Follower: model.vars.get('servers').map(s => 'N/A'),
      Leader: model.vars.get('servers').map(s => 'N/A'),
    });
  }

  update(clock) {
    let timeoutFrac = ((this.serverVar.lookup('timeoutAt').value - clock) /
       (electionTimeout * 2));
    this.serverVar.lookup('state').match({
      Leader: () => { timeoutFrac = 1; },
    });
    this.timeoutArc.endAngle(2 * Math.PI * timeoutFrac);
    return this;
  }
}

let serverData = model.getVar('servers').map((v, id) => new Server(id, v));

class Servers {
  constructor() {
  }

  draw(selection, changes) {
    if (changes === undefined) {
      changes = [''];
    }
    //Changesets.affected(changes, `servers[${serverId}]`);

    serverData.forEach(s => s.update(controller.workspace.clock));
    let updateG = serversG
      .selectAll('g.server')
      .data(serverData);

    // Server enter
    let enterG = updateG.enter()
      .append('g');
    enterG.append('circle')
      .attr('class', 'serverbg')
      .attr('cx', s => s.point.x)
      .attr('cy', s => s.point.y)
      .attr('r', 50);
    enterG.append('path')
      .attr('class', 'timeout')
      .attr('transform', s => new Transform()
        .translate(s.point.x, s.point.y)
        .toString());
    enterG.append('text')
      .attr('class', 'term')
      .attr('x', s => s.point.x)
      .attr('y', s => s.point.y + 30)
      .style({
        'text-anchor': 'middle',
        'font-size': 80,
      });
    enterG.append('g')
      .attr('class', 'votes');

    // Server update
    updateG.attr('class', s => ('server ' + s.serverVar.lookup('state').match({
          Follower: 'follower',
          Candidate: 'candidate',
          Leader: 'leader',
        })));
    updateG.select('path.timeout')
      .attr('d', s => s.timeoutArc());
    updateG.select('text')
      .text(s => s.serverVar.lookup('currentTerm').toString());

    // Votes
    this.drawVotes(updateG.select('g.votes'));
  } // Servers.draw()

  drawVotes(votesSel) {
    let updateSel = votesSel.selectAll('circle')
      .data(s => s.getVotes().map((vote, i) => ({
        server: s,
        vote: vote,
        point: s.peersCircle.at(i / numServers),
      })));
    let enterSel = updateSel.enter();
    enterSel
      .append('circle')
        .attr('cx', v => v.point.x)
        .attr('cy', v => v.point.y)
        .attr('r', 5);
    updateSel
      .attr('class', v => v.vote);
  } // Servers.drawVotes()

} // class Servers

let radianToAngle = radian => radian / (2 * Math.PI) * 360;

class Message {
  constructor(messageVar) {
    this.messageVar = messageVar;
    this.fromPoint = ringLayout.at((messageVar.lookup('from').value - 1) / numServers);
    this.toPoint = ringLayout.at((messageVar.lookup('to').value - 1) / numServers);
    let rise = this.toPoint.y - this.fromPoint.y;
    let run = this.toPoint.x - this.fromPoint.x;
    this.angle = radianToAngle(Math.atan(rise / run));
    if (run < 0) {
      this.angle += 180;
    }
    this.point = {
      x: 0,
      y: 0,
    };
  }
  update(clock) {
    let sentAt = this.messageVar.lookup('sentAt').value;
    let deliverAt = this.messageVar.lookup('deliverAt').value;
    let frac = .7;
    if (sentAt > 0) {
      frac = _.clamp((clock - sentAt) / (deliverAt - sentAt),
                     0, 1);
    }
    this.point.x = _.round(this.fromPoint.x + (this.toPoint.x - this.fromPoint.x) * frac, 2);
    this.point.y = _.round(this.fromPoint.y + (this.toPoint.y - this.fromPoint.y) * frac, 2);
    return this;
  }
}

class Messages {
  constructor() {
    markers.append('arrow')
      .attr({
        viewBox: '0 -5 10 10',
        refX: 5,
        refY: 0,
        markerWidth: 4,
        markerHeight: 4,
        orient:'auto',
      })
      .append('path')
        .attr('d', 'M0,-5L10,0L0,5')
        .attr('class', 'arrowHead');
  }

  draw(selection, changes) {
    /*
    Changesets.affected(changes, [
      'clock',
      `network[${index}]`,
    ]);
    */

    let messageData = model.getVar('network').map(v =>
      new Message(v).update(controller.workspace.clock));
    let updateSel = messagesG
      .selectAll('g.message')
      .data(messageData);

    // Message enter
    let enterSel = updateSel.enter()
      .append('g');
    enterSel.append('circle')
      .attr('r', 15);
    enterSel.append('line')
      .attr({
        x1: 0,
        y1: 0,
        x2: 30,
        y2: 0,
        'stroke-width': 4,
        'stroke': 'black',
        'marker-end': markers.ref('arrow'),
      });

    // Message update
    updateSel.attr('class', m => ('message ' + m.messageVar.lookup('payload').match({
          RequestVoteRequest: 'RequestVote request',
          RequestVoteResponse: 'RequestVote response',
          AppendEntriesRequest: 'AppendEntries request',
          AppendEntriesResponse: 'AppendEntries response',
        })));
    updateSel.select('circle')
      .attr('cx', m => m.point.x)
      .attr('cy', m => m.point.y);
    updateSel.select('line')
      .attr('transform', m => new Transform()
        .rotate(m.angle)
        .translate(m.point.x, m.point.y)
        .toString());

    // Message exit
    updateSel.exit().remove();

  } // Messages.draw()
}

class Logs {
  constructor() {
    this.x = 500;
    this.y = 100;
    this.width = 500;
    this.height = 800;
    this.serverLabelWidth = 50;
    this.indexHeight = 50;
    this.rowHeight = this.height / 8;
    this.columnWidth = .9 * (this.width - this.serverLabelWidth) / numIndexes;
    this.indexes = _.range(numIndexes).map(i => i + 1);
  }

  entryBBox(serverId, index) {
    return {
      x: this.x + this.serverLabelWidth + (index - 1) * this.columnWidth,
      y: this.y + this.indexHeight + (serverId - 1 + .1) * this.rowHeight,
      width: this.columnWidth,
      height: .8 * this.rowHeight,
    };
  }

  drawFixed(selection) {
    let enterSel = selection
      .append('g')
        .attr('class', 'indexes')
        .selectAll('text')
        .data(this.indexes).enter();
    enterSel.append('text')
      .attr('class', 'index')
      .attr('x', (index, i) => this.x + this.serverLabelWidth + i * this.columnWidth)
      .attr('y', this.y)
      .style('font-size', 60)
      .text(index => index);
  }

  draw(selection, changes) {
    /*
    Changesets.affected(changes, [
      'servers',
    ]);
    */
    let updateSel = logsG
      .selectAll('g.log')
      .data(serverData);

    // Log enter
    let enterSel = updateSel.enter()
      .append('g')
      .attr('class', 'log');
    enterSel.append('text')
      .attr('class', 'serverId')
      .attr('x', (s, i) => this.x)
      .attr('y', (s, i) => this.y + this.indexHeight + (i + .8) * this.rowHeight)
      .style('font-size', 60)
      .text(s => s.serverId);

    let bg = enterSel.append('g')
      .attr('class', 'bg');
    bg.selectAll('rect')
      .data(server => this.indexes.map(index => ({
        server: server,
        index: index,
        bbox: this.entryBBox(server.serverId, index),
      })))
      .enter()
      .append('rect')
        .attr('x', si => si.bbox.x)
        .attr('y', si => si.bbox.y)
        .attr('width', si => si.bbox.width)
        .attr('height', si => si.bbox.height);
    updateSel.append('g')
      .attr('class', 'entries');

    // Log update
    let entriesUpdateSel = updateSel.select('g.entries').selectAll('g')
      .data(server => server.serverVar.lookup('log').map((entry, index) => ({
        server: server,
        entry: entry,
        index: index,
        bbox: this.entryBBox(server.serverId, index),
      })));
    let entriesEnterSel = entriesUpdateSel.enter()
        .append('g');
    entriesEnterSel.append('rect')
        .attr('x', entry => entry.bbox.x)
        .attr('y', entry => entry.bbox.y)
        .attr('width', entry => entry.bbox.width)
        .attr('height', entry => entry.bbox.height);
    entriesEnterSel.append('text')
        .attr('x', entry => entry.bbox.x)
        .attr('y', entry => entry.bbox.y + .8 * entry.bbox.height)
        .attr('width', entry => entry.bbox.width)
        .attr('height', entry => entry.bbox.height)
        .style('font-size', 60);
    entriesUpdateSel
      .attr('class', entry => ('entry ' +
        (entry.server.serverVar.lookup('commitIndex').value >= entry.index
          ? 'committed'
          : 'uncommitted')));
    entriesUpdateSel.select('text')
      .text(entry => entry.entry.lookup('term').toString());
    entriesUpdateSel.exit().remove();

    // Log exit
    updateSel.exit().remove();

  } // Logs.draw()
}

d3.select(svg).append('circle')
  .attr({
    cx: ringLayout.cx,
    cy: ringLayout.cy,
    r: ringLayout.r,
  })
  .style({
    stroke: 'black',
    'stroke-width': 5,
    fill: 'none',
  });

let serversG = d3.select(svg)
  .append('g')
    .attr('class', 'servers');
let messagesG = d3.select(svg)
  .append('g')
    .attr('class', 'messages');
let logsG = d3.select(svg)
  .append('g')
    .attr('class', 'logs');

let servers = new Servers();
servers.draw(serversG);
let messages = new Messages();
messages.draw(messagesG);
let logs = new Logs();
logs.drawFixed(logsG);
logs.draw(logsG);

return {
  bigView: true,
  name: 'RaftView',
  update: function(changes) {
    servers.draw(serversG, changes);
    messages.draw(messagesG, changes);
    logs.draw(logsG, changes);
  }
};

}; // View

module.exports = View;
<commit_msg>d3: color based on terms<commit_after>"use strict";

let d3 = require('d3');
let _ = require('lodash');

// Calculates where points on the circumference of a circle lie.
class Circle {
  constructor(cx, cy, r) {
    this.cx = cx;
    this.cy = cy;
    this.r = r;
  }
  at(frac) {
    let radian = frac * 2 * Math.PI;
    return {
      x: _.round(this.cx + this.r * Math.sin(radian), 2),
      y: _.round(this.cy - this.r * Math.cos(radian), 2),
    };
  }
}

class Markers {
  constructor(defs) {
    this.defs = defs;
    this.added = [];
  }
  has(id) {
    return _.includes(this.added, id);
  }
  append(id) {
    return this.defs.append('marker')
      .attr('id', id);
  }
  ref(id) {
    return `url(#${id})`;
  }
}

class Transform {
  constructor() {
    this.operations = [];
  }
  toString() {
    return _.reverse(_.clone(this.operations)).join(' ');
  }
  translate(x, y) {
    this.operations.push(`translate(${x}, ${y})`);
    return this;
  };
  rotate(angle) {
    this.operations.push(`rotate(${angle})`);
    return this;
  }
}

let View = function(controller, svg, module) {

let markers = new Markers(d3.select(svg).append('defs'));

d3.select('head').append('style')
  .text(`
      .server .serverbg {
        stroke: black;
      }
      .server g.votes {
        visibility: hidden;
      }
      .server.candidate g.votes {
        visibility: visible;
        fill: white;
      }
      .server.candidate g.votes .granted {
        fill: black;
      }

      .logs .bg rect {
        fill: #dddddd;
        stroke: gray;
      }
      .logs .entry rect {
        stroke: black;
        stroke-width: 8;
      }
      .logs .entry.uncommitted rect {
        stroke-dasharray: 20, 15;
      }
  `);

let model = module.env;

let numServers = model.vars.get('servers').size();
let numIndexes = model.vars.get('servers').index(1).lookup('log').capacity();
let electionTimeout = 100000;
let ringLayout = new Circle(250, 500, 200);

let termColor = d3.scale.category10();

// Wraps the model's Server Record with additional information for drawing
class Server {
  constructor(serverId, serverVar) {
    this.serverVar = serverVar;
    this.serverId = serverId;
    this.frac = (this.serverId - 1) / numServers;
    this.point = ringLayout.at(this.frac);
    this.peersCircle = new Circle(this.point.x, this.point.y, 40);

    this.timeoutArc = d3.svg.arc()
      .innerRadius(50)
      .outerRadius(60)
      .startAngle(0)
      .endAngle(0);
  }

  getVotes() {
    return this.serverVar.lookup('state').match({
      Candidate: cstate => cstate.peers.map(peer => {
        if (peer.lookup('voteGranted').toString() == 'True') {
          return 'granted';
        } else if (peer.lookup('voteResponded').toString() == 'True') {
          return 'denied';
        } else {
          return 'noreply';
        }
      }),
      Follower: model.vars.get('servers').map(s => 'N/A'),
      Leader: model.vars.get('servers').map(s => 'N/A'),
    });
  }

  update(clock) {
    let timeoutFrac = ((this.serverVar.lookup('timeoutAt').value - clock) /
       (electionTimeout * 2));
    this.serverVar.lookup('state').match({
      Leader: () => { timeoutFrac = 1; },
    });
    this.timeoutArc.endAngle(2 * Math.PI * timeoutFrac);
    return this;
  }
}

let serverData = model.getVar('servers').map((v, id) => new Server(id, v));

class Servers {
  constructor() {
  }

  draw(selection, changes) {
    if (changes === undefined) {
      changes = [''];
    }
    //Changesets.affected(changes, `servers[${serverId}]`);

    serverData.forEach(s => s.update(controller.workspace.clock));
    let updateG = serversG
      .selectAll('g.server')
      .data(serverData);

    // Server enter
    let enterG = updateG.enter()
      .append('g');
    enterG.append('circle')
      .attr('class', 'serverbg')
      .attr('cx', s => s.point.x)
      .attr('cy', s => s.point.y)
      .attr('r', 50);
    enterG.append('path')
      .attr('class', 'timeout')
      .attr('transform', s => new Transform()
        .translate(s.point.x, s.point.y)
        .toString());
    enterG.append('text')
      .attr('class', 'term')
      .attr('x', s => s.point.x)
      .attr('y', s => s.point.y + 30)
      .style({
        'text-anchor': 'middle',
        'font-size': 80,
      });
    enterG.append('g')
      .attr('class', 'votes');

    // Server update
    updateG.attr('class', s => ('server ' + s.serverVar.lookup('state').match({
          Follower: 'follower',
          Candidate: 'candidate',
          Leader: 'leader',
        })));
    updateG.select('.serverbg')
      .style('fill', s => termColor(s.serverVar.lookup('currentTerm').value));
    updateG.select('path.timeout')
      .attr('d', s => s.timeoutArc());
    updateG.select('text')
      .text(s => s.serverVar.lookup('currentTerm').toString());

    // Votes
    this.drawVotes(updateG.select('g.votes'));
  } // Servers.draw()

  drawVotes(votesSel) {
    let updateSel = votesSel.selectAll('circle')
      .data(s => s.getVotes().map((vote, i) => ({
        server: s,
        vote: vote,
        point: s.peersCircle.at(i / numServers),
      })));
    let enterSel = updateSel.enter();
    enterSel
      .append('circle')
        .attr('cx', v => v.point.x)
        .attr('cy', v => v.point.y)
        .attr('r', 5);
    updateSel
      .attr('class', v => v.vote);
  } // Servers.drawVotes()

} // class Servers

let radianToAngle = radian => radian / (2 * Math.PI) * 360;

class Message {
  constructor(messageVar) {
    this.messageVar = messageVar;
    this.fromPoint = ringLayout.at((messageVar.lookup('from').value - 1) / numServers);
    this.toPoint = ringLayout.at((messageVar.lookup('to').value - 1) / numServers);
    let rise = this.toPoint.y - this.fromPoint.y;
    let run = this.toPoint.x - this.fromPoint.x;
    this.angle = radianToAngle(Math.atan(rise / run));
    if (run < 0) {
      this.angle += 180;
    }
    this.point = {
      x: 0,
      y: 0,
    };
  }
  update(clock) {
    let sentAt = this.messageVar.lookup('sentAt').value;
    let deliverAt = this.messageVar.lookup('deliverAt').value;
    let frac = .7;
    if (sentAt > 0) {
      frac = _.clamp((clock - sentAt) / (deliverAt - sentAt),
                     0, 1);
    }
    this.point.x = _.round(this.fromPoint.x + (this.toPoint.x - this.fromPoint.x) * frac, 2);
    this.point.y = _.round(this.fromPoint.y + (this.toPoint.y - this.fromPoint.y) * frac, 2);
    return this;
  }
}

class Messages {
  constructor() {
    markers.append('arrow')
      .attr({
        viewBox: '0 -5 10 10',
        refX: 5,
        refY: 0,
        markerWidth: 4,
        markerHeight: 4,
        orient:'auto',
      })
      .append('path')
        .attr('d', 'M0,-5L10,0L0,5')
        .attr('class', 'arrowHead');
  }

  draw(selection, changes) {
    /*
    Changesets.affected(changes, [
      'clock',
      `network[${index}]`,
    ]);
    */

    let messageData = model.getVar('network').map(v =>
      new Message(v).update(controller.workspace.clock));
    let updateSel = messagesG
      .selectAll('g.message')
      .data(messageData);

    // Message enter
    let enterSel = updateSel.enter()
      .append('g');
    enterSel.append('circle')
      .attr('r', 15);
    enterSel.append('line')
      .attr({
        x1: 0,
        y1: 0,
        x2: 30,
        y2: 0,
        'stroke-width': 4,
        'stroke': 'black',
        'marker-end': markers.ref('arrow'),
      });

    // Message update
    updateSel.attr('class', m => ('message ' + m.messageVar.lookup('payload').match({
          RequestVoteRequest: 'RequestVote request',
          RequestVoteResponse: 'RequestVote response',
          AppendEntriesRequest: 'AppendEntries request',
          AppendEntriesResponse: 'AppendEntries response',
        })));
    updateSel.select('circle')
      .attr('cx', m => m.point.x)
      .attr('cy', m => m.point.y);
    updateSel.select('line')
      .attr('transform', m => new Transform()
        .rotate(m.angle)
        .translate(m.point.x, m.point.y)
        .toString());

    // Message exit
    updateSel.exit().remove();

  } // Messages.draw()
}

class Logs {
  constructor() {
    this.x = 500;
    this.y = 100;
    this.width = 500;
    this.height = 800;
    this.serverLabelWidth = 50;
    this.indexHeight = 50;
    this.rowHeight = this.height / 8;
    this.columnWidth = .9 * (this.width - this.serverLabelWidth) / numIndexes;
    this.indexes = _.range(numIndexes).map(i => i + 1);
  }

  entryBBox(serverId, index) {
    return {
      x: this.x + this.serverLabelWidth + (index - 1) * this.columnWidth,
      y: this.y + this.indexHeight + (serverId - 1 + .1) * this.rowHeight,
      width: this.columnWidth,
      height: .8 * this.rowHeight,
    };
  }

  drawFixed(selection) {
    let enterSel = selection
      .append('g')
        .attr('class', 'indexes')
        .selectAll('text')
        .data(this.indexes).enter();
    enterSel.append('text')
      .attr('class', 'index')
      .attr('x', (index, i) => this.x + this.serverLabelWidth + i * this.columnWidth)
      .attr('y', this.y + .8 * this.indexHeight)
      .style('font-size', 60)
      .text(index => index);
  }

  draw(selection, changes) {
    /*
    Changesets.affected(changes, [
      'servers',
    ]);
    */
    let updateSel = logsG
      .selectAll('g.log')
      .data(serverData);

    // Log enter
    let enterSel = updateSel.enter()
      .append('g')
      .attr('class', 'log');
    enterSel.append('text')
      .attr('class', 'serverId')
      .attr('x', (s, i) => this.x)
      .attr('y', (s, i) => this.y + this.indexHeight + (i + .8) * this.rowHeight)
      .style('font-size', 60)
      .text(s => s.serverId);

    let bg = enterSel.append('g')
      .attr('class', 'bg');
    bg.selectAll('rect')
      .data(server => this.indexes.map(index => ({
        server: server,
        index: index,
        bbox: this.entryBBox(server.serverId, index),
      })))
      .enter()
      .append('rect')
        .attr('x', si => si.bbox.x)
        .attr('y', si => si.bbox.y)
        .attr('width', si => si.bbox.width)
        .attr('height', si => si.bbox.height);
    updateSel.append('g')
      .attr('class', 'entries');

    // Log update
    let entriesUpdateSel = updateSel.select('g.entries').selectAll('g')
      .data(server => server.serverVar.lookup('log').map((entry, index) => ({
        server: server,
        entry: entry,
        index: index,
        bbox: this.entryBBox(server.serverId, index),
      })));
    let entriesEnterSel = entriesUpdateSel.enter()
        .append('g');
    entriesEnterSel.append('rect')
        .attr('x', entry => entry.bbox.x)
        .attr('y', entry => entry.bbox.y)
        .attr('width', entry => entry.bbox.width)
        .attr('height', entry => entry.bbox.height);
    entriesEnterSel.append('text')
        .attr('x', entry => entry.bbox.x)
        .attr('y', entry => entry.bbox.y + .8 * entry.bbox.height)
        .attr('width', entry => entry.bbox.width)
        .attr('height', entry => entry.bbox.height)
        .style('font-size', 60);
    entriesUpdateSel
      .attr('class', entry => ('entry ' +
        (entry.server.serverVar.lookup('commitIndex').value >= entry.index
          ? 'committed'
          : 'uncommitted')));
    entriesUpdateSel.select('rect')
      .style('fill', entry => termColor(entry.entry.lookup('term').value));
    entriesUpdateSel.select('text')
      .text(entry => entry.entry.lookup('term').toString());
    entriesUpdateSel.exit().remove();

    // Log exit
    updateSel.exit().remove();

  } // Logs.draw()
}

d3.select(svg).append('circle')
  .attr({
    cx: ringLayout.cx,
    cy: ringLayout.cy,
    r: ringLayout.r,
  })
  .style({
    stroke: 'black',
    'stroke-width': 5,
    fill: 'none',
  });

let serversG = d3.select(svg)
  .append('g')
    .attr('class', 'servers');
let messagesG = d3.select(svg)
  .append('g')
    .attr('class', 'messages');
let logsG = d3.select(svg)
  .append('g')
    .attr('class', 'logs');

let servers = new Servers();
servers.draw(serversG);
let messages = new Messages();
messages.draw(messagesG);
let logs = new Logs();
logs.drawFixed(logsG);
logs.draw(logsG);

return {
  bigView: true,
  name: 'RaftView',
  update: function(changes) {
    servers.draw(serversG, changes);
    messages.draw(messagesG, changes);
    logs.draw(logsG, changes);
  }
};

}; // View

module.exports = View;
<|endoftext|>


<commit_before>/**
 * @file
 */

/**
 * Extract the hit part of the ES result, if it exist
 *
 * @param esRes
 * @param pos
 * @param fld
 * @returns {*}
 */
export function getEsField(esRes, pos, fld) {
  if (esRes && Array.isArray(esRes.hits) && esRes.hits[pos] && esRes.hits[pos]._source && Array.isArray(esRes.hits[pos]._source[fld])) {
    return esRes.hits[pos]._source[fld];
  }
  return [];
}

/**
 * return first occurrence of a given list of fields
 *
 * @param esRes
 * @param pos
 * @param fldList
 * @returns {*}
 */
export function getFirstField(esRes, pos, fldList) {
  let field = '';
  fldList.forEach(function (tag) {
    const fld = getEsField(esRes, pos, tag);
    if (!field && fld[0]) {
      field = fld[0];
    }
  });
  return field;
}

/**
 * Merge default and function given parameters, and translate to ES names
 *
 * @param pars
 * @param defaultParameters
 * @param esParMap
 * @returns {{}}
 */
export function setAndMap(pars, defaultParameters, esParMap) {
  let ret = {};
  Object.keys(defaultParameters).forEach(function (key) {
    ret[esParMap[key]] = pars[key] ? pars[key] : defaultParameters[key];
  });
  return ret;
}

/**
 * sort on ascending distance
 *
 * @param buf
 * @param elements
 * @returns {*|Blob|string|ArrayBuffer}
 */
export function sortDistanceAndSlice(buf, elements) {
  return buf.sort(function (a, b) {
    return (parseInt(a.distance, 10) - parseInt(b.distance, 10));
  }).slice(0, elements);
}

/**
 * sort an array on the title field
 *
 * @param arr
 * @returns {*}
 */
export function titleSort(arr) {
  return arr.sort(function (a, b) {
    return (a.title > b.title ? 1 : -1);
  });
}

/**
 * parse a register record and return a consolidated structure
 *
 * @param esRes
 * @param pos
 * @param dk5Tab
 * @returns {*}
 */
export function parseRegisterRecord(esRes, pos, dk5Tab) {
  const ret = {};
  ret.title = getFirstField(esRes, pos, ['630a', '633a', '640a', '600a', '610a', 'a20a']);
  ret.titleDetails = getFirstField(esRes, pos, ['630e', '633e', '640e', '600f', '610e', 'a20b']);
  ret.titleFull = ret.title + (ret.titleDetails ? ' - ' + ret.titleDetails : '');
  ret.index = getFirstField(esRes, pos, ['652m']);
  ret.id = getFirstField(esRes, pos, ['001a']);
  ret.parent = Object.assign({}, dk5Tab[ret.index]);
  const aspectDk5 = getEsField(esRes, pos, 'b52m');
  if (aspectDk5.length === 0) {
    return ret;
  }

  const aspectTitle = getEsField(esRes, pos, 'b52y');
  const items = [];
  for (let i = 0; i < aspectDk5.length; i++) {
    items.push({index: aspectDk5[i], title: aspectTitle[i], parent: dk5Tab[aspectDk5[i]]});
  }
  return Object.assign({}, ret, {items: items});
}

/**
 * Collect systematic notes. Notes are found in field a40
 *
 * @param systRec
 * @param hitPos
 * @returns {string}
 */
export function createTaggedSystematicNote(systRec, hitPos) {
  let note = getEsField(systRec, hitPos, 'a40').join('
');
  if (note) {
    note = parseTextAndTagSyst(note, getEsField(systRec, hitPos, 'a40b'));
  }
  return note;
}

/**
 * Collect register notes. Notes are found in field 651 and b00
 *
 * @param regRecs
 * @param hitPos
 * @returns {*|string|String}
 */
export function createTaggedRegisterNote(regRecs, hitPos) {
  let note = [];
  ['651', 'b00'].forEach((noteTag) => {
    let noteText = getEsField(regRecs, hitPos, noteTag).join('
');
    if (noteText) {
      note.push(parseTextAndTagSyst(noteText, getEsField(regRecs, hitPos, noteTag + 'b')));
    }
  });
  return note.join('
');
}

/**
 * Parse all register records for notes
 * Notes for the same index, can be found in more than one record
 *
 * @param regRecs
 * @returns {{}}
 */
export function parseRegisterForNotes(regRecs) {
  const notes = {};
  for (let hitPos = 0; hitPos < regRecs.hits.length; hitPos++) {
    const index = getFirstField(regRecs, hitPos, ['652m', '652n', '652d']);
    const note = createTaggedRegisterNote(regRecs, hitPos);
    if (notes[index]) {
      if (notes[index].indexOf(note) === -1) {
        notes[index] += '
' + note;
      }
    }
    else {
      notes[index] = note;

    }
  }
  return notes;
}

/**
 * Parse register records and extract unique words
 *
 * @param regRecs
 * @param wordFields
 * @returns {Array}
 */
export function parseRegisterForUniqueWords(regRecs, wordFields) {
  let uniqueWords = {};
  for (let hitPos = 0; hitPos < regRecs.hits.length; hitPos++) {
    wordFields.forEach((fld) => {
      const wordArr = getEsField(regRecs, hitPos, fld);
      for (let i = 0; i < wordArr.length; i++) {
        wordArr[i].split(/[\s,]+/).forEach((word) => {
          word = word.replace(/^[:\-\.#]+|[:\-\.#]+$|[\"()]+/g, '');
          let num = word.replace(/[\.:-]/g, '');
          if (num.length > 2 && parseInt(num, 10) !== num) {
            uniqueWords[word.toLowerCase()] = true;
          }
        });
      }
    });
  }
  return Object.keys(uniqueWords);
}

/**
 * Parse the note and put systematics in tags
 *
 * @param note
 * @param noteSyst
 * @returns {*}
 */
function parseTextAndTagSyst(note, noteSyst) {
  let ret = note;
  let notePos = 0;
  for (let i = 0; i < noteSyst.length; i++) {
    let syst = noteSyst[i];
    const replace = '<dk>' + syst + '</dk>';
    const p = ret.indexOf(syst, notePos);
    if (p > -1) {
      ret = ret.substr(0, p) + replace + ret.substr(p + syst.length);
      notePos = p + replace.length;
    }
  }
  return ret;
}
<commit_msg>#63: refactor to const<commit_after>/**
 * @file
 */

/**
 * Extract the hit part of the ES result, if it exist
 *
 * @param esRes
 * @param pos
 * @param fld
 * @returns {*}
 */
export function getEsField(esRes, pos, fld) {
  if (esRes && Array.isArray(esRes.hits) && esRes.hits[pos] && esRes.hits[pos]._source && Array.isArray(esRes.hits[pos]._source[fld])) {
    return esRes.hits[pos]._source[fld];
  }
  return [];
}

/**
 * return first occurrence of a given list of fields
 *
 * @param esRes
 * @param pos
 * @param fldList
 * @returns {*}
 */
export function getFirstField(esRes, pos, fldList) {
  let field = '';
  fldList.forEach(function (tag) {
    const fld = getEsField(esRes, pos, tag);
    if (!field && fld[0]) {
      field = fld[0];
    }
  });
  return field;
}

/**
 * Merge default and function given parameters, and translate to ES names
 *
 * @param pars
 * @param defaultParameters
 * @param esParMap
 * @returns {{}}
 */
export function setAndMap(pars, defaultParameters, esParMap) {
  let ret = {};
  Object.keys(defaultParameters).forEach(function (key) {
    ret[esParMap[key]] = pars[key] ? pars[key] : defaultParameters[key];
  });
  return ret;
}

/**
 * sort on ascending distance
 *
 * @param buf
 * @param elements
 * @returns {*|Blob|string|ArrayBuffer}
 */
export function sortDistanceAndSlice(buf, elements) {
  return buf.sort(function (a, b) {
    return (parseInt(a.distance, 10) - parseInt(b.distance, 10));
  }).slice(0, elements);
}

/**
 * sort an array on the title field
 *
 * @param arr
 * @returns {*}
 */
export function titleSort(arr) {
  return arr.sort(function (a, b) {
    return (a.title > b.title ? 1 : -1);
  });
}

/**
 * parse a register record and return a consolidated structure
 *
 * @param esRes
 * @param pos
 * @param dk5Tab
 * @returns {*}
 */
export function parseRegisterRecord(esRes, pos, dk5Tab) {
  const ret = {};
  ret.title = getFirstField(esRes, pos, ['630a', '633a', '640a', '600a', '610a', 'a20a']);
  ret.titleDetails = getFirstField(esRes, pos, ['630e', '633e', '640e', '600f', '610e', 'a20b']);
  ret.titleFull = ret.title + (ret.titleDetails ? ' - ' + ret.titleDetails : '');
  ret.index = getFirstField(esRes, pos, ['652m']);
  ret.id = getFirstField(esRes, pos, ['001a']);
  ret.parent = Object.assign({}, dk5Tab[ret.index]);
  const aspectDk5 = getEsField(esRes, pos, 'b52m');
  if (aspectDk5.length === 0) {
    return ret;
  }

  const aspectTitle = getEsField(esRes, pos, 'b52y');
  const items = [];
  for (let i = 0; i < aspectDk5.length; i++) {
    items.push({index: aspectDk5[i], title: aspectTitle[i], parent: dk5Tab[aspectDk5[i]]});
  }
  return Object.assign({}, ret, {items: items});
}

/**
 * Collect systematic notes. Notes are found in field a40
 *
 * @param systRec
 * @param hitPos
 * @returns {string}
 */
export function createTaggedSystematicNote(systRec, hitPos) {
  let note = getEsField(systRec, hitPos, 'a40').join('
');
  if (note) {
    note = parseTextAndTagSyst(note, getEsField(systRec, hitPos, 'a40b'));
  }
  return note;
}

/**
 * Collect register notes. Notes are found in field 651 and b00
 *
 * @param regRecs
 * @param hitPos
 * @returns {*|string|String}
 */
export function createTaggedRegisterNote(regRecs, hitPos) {
  const note = [];
  ['651', 'b00'].forEach((noteTag) => {
    const noteText = getEsField(regRecs, hitPos, noteTag).join('
');
    if (noteText) {
      note.push(parseTextAndTagSyst(noteText, getEsField(regRecs, hitPos, noteTag + 'b')));
    }
  });
  return note.join('
');
}

/**
 * Parse all register records for notes
 * Notes for the same index, can be found in more than one record
 *
 * @param regRecs
 * @returns {{}}
 */
export function parseRegisterForNotes(regRecs) {
  const notes = {};
  for (let hitPos = 0; hitPos < regRecs.hits.length; hitPos++) {
    const index = getFirstField(regRecs, hitPos, ['652m', '652n', '652d']);
    const note = createTaggedRegisterNote(regRecs, hitPos);
    if (notes[index]) {
      if (notes[index].indexOf(note) === -1) {
        notes[index] += '
' + note;
      }
    }
    else {
      notes[index] = note;

    }
  }
  return notes;
}

/**
 * Parse register records and extract unique words
 *
 * @param regRecs
 * @param wordFields
 * @returns {Array}
 */
export function parseRegisterForUniqueWords(regRecs, wordFields) {
  let uniqueWords = {};
  for (let hitPos = 0; hitPos < regRecs.hits.length; hitPos++) {
    wordFields.forEach((fld) => {
      const wordArr = getEsField(regRecs, hitPos, fld);
      for (let i = 0; i < wordArr.length; i++) {
        wordArr[i].split(/[\s,]+/).forEach((word) => {
          word = word.replace(/^[:\-\.#]+|[:\-\.#]+$|[\"()]+/g, '');
          let num = word.replace(/[\.:-]/g, '');
          if (num.length > 2 && parseInt(num, 10) !== num) {
            uniqueWords[word.toLowerCase()] = true;
          }
        });
      }
    });
  }
  return Object.keys(uniqueWords);
}

/**
 * Parse the note and put systematics in tags
 *
 * @param note
 * @param noteSyst
 * @returns {*}
 */
function parseTextAndTagSyst(note, noteSyst) {
  let ret = note;
  let notePos = 0;
  for (let i = 0; i < noteSyst.length; i++) {
    let syst = noteSyst[i];
    const replace = '<dk>' + syst + '</dk>';
    const p = ret.indexOf(syst, notePos);
    if (p > -1) {
      ret = ret.substr(0, p) + replace + ret.substr(p + syst.length);
      notePos = p + replace.length;
    }
  }
  return ret;
}
<|endoftext|>


<commit_before>import { getObjectValueByPath } from '../../../util/helpers'

/**
 * Select generators
 *
 * @mixin
 *
 * Used for creating the DOM elements for VSelect
 */
export default {
  methods: {
    genMenu () {
      const offsetY = this.isAutocomplete || this.offset || this.isDropdown
      let nudgeTop = 0

      if (this.auto) nudgeTop = -18
      else if (this.solo) nudgeTop = 0
      else if (this.isDropdown) nudgeTop = 26
      else if (offsetY) nudgeTop = 24

      const data = {
        ref: 'menu',
        props: {
          activator: this.$el,
          auto: this.auto,
          closeOnClick: false,
          closeOnContentClick: !this.isMultiple,
          contentClass: this.computedContentClass,
          disabled: this.disabled,
          maxHeight: this.maxHeight,
          nudgeTop,
          offsetY,
          offsetOverflow: this.isAutocomplete,
          openOnClick: false,
          value: this.menuIsActive &&
            (!this.tags || this.filteredItems.length > 0) &&
            (!this.combobox || this.filteredItems.length > 0),
          zIndex: this.menuZIndex
        },
        on: {
          input: val => {
            if (!val) {
              this.menuIsActive = false
            }
          }
        }
      }

      if (this.isAutocomplete) data.props.transition = ''

      this.minWidth && (data.props.minWidth = this.minWidth)

      return this.$createElement('v-menu', data, [this.genList()])
    },
    genSelectionsAndSearch () {
      const data = {
        staticClass: 'input-group--select__autocomplete',
        'class': {
          'input-group--select__autocomplete--index': this.selectedIndex > -1
        },
        style: {
          flex: this.shouldBreak ? '1 0 100%' : null
        },
        attrs: {
          ...this.$attrs,
          disabled: this.disabled || !this.isAutocomplete,
          readonly: this.readonly,
          tabindex: this.disabled || !this.isAutocomplete ? -1 : this.tabindex
        },
        domProps: {
          value: this.maskText(this.lazySearch || '')
        },
        directives: [{
          name: 'show',
          value: (this.isAutocomplete) ||
            (this.placeholder && !this.selectedItems.length)
        }],
        ref: 'input',
        key: 'input'
      }

      if (this.isAutocomplete) {
        data.attrs.role = 'combobox'
        data.domProps.autocomplete = this.browserAutocomplete

        data.on = {
          ...this.genListeners(),
          input: e => {
            this.searchValue = this.unmaskText(e.target.value)
          }
        }

        if (this.combobox) {
          // When using the combobox
          // update inputValue and
          // set the menu status
          data.on.blur = () => {
            this.inputValue = this.lazySearch
          }
        }

        data.directives = data.directives.concat(this.genDirectives())
      }

      if (this.placeholder) data.domProps.placeholder = this.placeholder

      return this.$createElement('div', {
        'class': 'input-group__selections',
        style: { 'overflow': 'hidden' },
        ref: 'activator'
      }, [
        this.genSelections(),
        this.$createElement('input', data)
      ])
    },
    genSelections () {
      if (this.hideSelections) return []

      const children = []
      const chips = this.chips
      const slots = this.$scopedSlots.selection
      const length = this.selectedItems.length
      this.selectedItems.forEach((item, i) => {
        if (slots) {
          children.push(this.genSlotSelection(item, i))
        } else if (chips) {
          children.push(this.genChipSelection(item, i))
        } else if (this.segmented) {
          children.push(this.genSegmentedBtn(item, i))
        } else {
          children.push(this.genCommaSelection(item, i < length - 1, i))
        }
      })

      return children
    },
    genSegmentedBtn (item) {
      if (!item.text || !item.callback) {
        console.warn('[Vuetify] Warn: When using the v-select component with \'segmented\' prop without a selection slot, items must contain both a text and callback property')
        return null
      }

      return this.$createElement('v-btn', {
        props: {
          flat: true
        },
        on: {
          click (e) {
            e.stopPropagation()
            item.callback(e)
          }
        }
      }, [item.text])
    },
    genSlotSelection (item, index) {
      return this.$scopedSlots.selection({
        parent: this,
        item,
        index,
        selected: index === this.selectedIndex,
        disabled: this.disabled || this.readonly
      })
    },
    genChipSelection (item, index) {
      const isDisabled = this.disabled || this.readonly
      const click = e => {
        if (isDisabled) return

        e.stopPropagation()
        this.focus()
        this.selectedIndex = index
      }

      return this.$createElement('v-chip', {
        staticClass: 'chip--select-multi',
        attrs: { tabindex: '-1' },
        props: {
          close: !isDisabled,
          dark: this.dark,
          disabled: isDisabled,
          selected: index === this.selectedIndex
        },
        on: {
          click: click,
          focus: click,
          input: () => this.selectItem(item)
        },
        key: this.getValue(item)
      }, this.getText(item))
    },
    genCommaSelection (item, comma, index) {
      return this.$createElement('div', {
        staticClass: 'input-group__selections__comma',
        'class': {
          'input-group__selections__comma--active': index === this.selectedIndex
        },
        key: JSON.stringify(this.getValue(item)) // Item may be an object
      }, `${this.getText(item)}${comma ? ', ' : ''}`)
    },
    genList () {
      const children = this.filteredItems.map(o => {
        if (o.header) return this.genHeader(o)
        if (o.divider) return this.genDivider(o)
        else return this.genTile(o)
      })

      if (!children.length) {
        const noData = this.$slots['no-data']
        if (noData) {
          children.push(noData)
        } else {
          children.push(this.genTile(this.noDataText, true))
        }
      }

      return this.$createElement('v-card', [
        this.$createElement('v-list', {
          ref: 'list'
        }, children)
      ])
    },
    genHeader (item) {
      return this.$createElement('v-subheader', {
        props: item
      }, item.header)
    },
    genDivider (item) {
      return this.$createElement('v-divider', {
        props: item
      })
    },
    genTile (item, disabled) {
      const active = this.selectedItems.indexOf(item) !== -1

      if (typeof disabled === 'undefined') {
        disabled = getObjectValueByPath(item, this.itemDisabled)
      }

      const data = {
        on: {
          click: e => {
            if (disabled) return

            this.selectItem(item)
          }
        },
        props: {
          avatar: item === Object(item) && this.itemAvatar in item,
          ripple: true,
          value: active
        }
      }

      if (disabled) {
        data.props.disabled = disabled
      }

      if (this.$scopedSlots.item) {
        return this.$createElement('v-list-tile', data,
          [this.$scopedSlots.item({ parent: this, item })]
        )
      }

      return this.$createElement('v-list-tile', data,
        [this.genAction(item, active), this.genContent(item)]
      )
    },
    genAction (item, active) {
      if (!this.isMultiple) return null

      const data = {
        staticClass: 'list__tile__action--select-multi',
        on: {
          click: e => {
            e.stopPropagation()
            this.selectItem(item)
          }
        }
      }

      return this.$createElement('v-list-tile-action', data, [
        this.$createElement('v-checkbox', { props: { inputValue: active } })
      ])
    },
    genContent (item) {
      const text = this.getText(item)

      return this.$createElement('v-list-tile-content',
        [this.$createElement('v-list-tile-title', {
          domProps: {
            innerHTML: this.genFiltered(text)
          }
        })]
      )
    }
  }
}
<commit_msg>fix: Don't show 'undefined' in empty v-selects with Vue 2.5.0<commit_after>import { getObjectValueByPath } from '../../../util/helpers'

/**
 * Select generators
 *
 * @mixin
 *
 * Used for creating the DOM elements for VSelect
 */
export default {
  methods: {
    genMenu () {
      const offsetY = this.isAutocomplete || this.offset || this.isDropdown
      let nudgeTop = 0

      if (this.auto) nudgeTop = -18
      else if (this.solo) nudgeTop = 0
      else if (this.isDropdown) nudgeTop = 26
      else if (offsetY) nudgeTop = 24

      const data = {
        ref: 'menu',
        props: {
          activator: this.$el,
          auto: this.auto,
          closeOnClick: false,
          closeOnContentClick: !this.isMultiple,
          contentClass: this.computedContentClass,
          disabled: this.disabled,
          maxHeight: this.maxHeight,
          nudgeTop,
          offsetY,
          offsetOverflow: this.isAutocomplete,
          openOnClick: false,
          value: this.menuIsActive &&
            (!this.tags || this.filteredItems.length > 0) &&
            (!this.combobox || this.filteredItems.length > 0),
          zIndex: this.menuZIndex
        },
        on: {
          input: val => {
            if (!val) {
              this.menuIsActive = false
            }
          }
        }
      }

      if (this.isAutocomplete) data.props.transition = ''

      this.minWidth && (data.props.minWidth = this.minWidth)

      return this.$createElement('v-menu', data, [this.genList()])
    },
    genSelectionsAndSearch () {
      const data = {
        staticClass: 'input-group--select__autocomplete',
        'class': {
          'input-group--select__autocomplete--index': this.selectedIndex > -1
        },
        style: {
          flex: this.shouldBreak ? '1 0 100%' : null
        },
        attrs: {
          ...this.$attrs,
          disabled: this.disabled || !this.isAutocomplete,
          readonly: this.readonly,
          tabindex: this.disabled || !this.isAutocomplete ? -1 : this.tabindex
        },
        domProps: {
          value: this.maskText(this.lazySearch || '')
        },
        directives: [{
          name: 'show',
          value: (this.isAutocomplete) ||
            (this.placeholder && !this.selectedItems.length)
        }],
        ref: 'input',
        key: 'input'
      }

      if (this.isAutocomplete) {
        data.attrs.role = 'combobox'
        data.domProps.autocomplete = this.browserAutocomplete

        data.on = {
          ...this.genListeners(),
          input: e => {
            this.searchValue = this.unmaskText(e.target.value)
          }
        }

        if (this.combobox) {
          // When using the combobox
          // update inputValue and
          // set the menu status
          data.on.blur = () => {
            this.inputValue = this.lazySearch
          }
        }

        data.directives = data.directives.concat(this.genDirectives())
      }

      if (this.placeholder) data.domProps.placeholder = this.placeholder

      return this.$createElement('div', {
        'class': 'input-group__selections',
        style: { 'overflow': 'hidden' },
        ref: 'activator'
      }, [
        ...this.genSelections(),
        this.$createElement('input', data)
      ])
    },
    genSelections () {
      if (this.hideSelections) return []

      const children = []
      const chips = this.chips
      const slots = this.$scopedSlots.selection
      const length = this.selectedItems.length
      this.selectedItems.forEach((item, i) => {
        if (slots) {
          children.push(this.genSlotSelection(item, i))
        } else if (chips) {
          children.push(this.genChipSelection(item, i))
        } else if (this.segmented) {
          children.push(this.genSegmentedBtn(item, i))
        } else {
          children.push(this.genCommaSelection(item, i < length - 1, i))
        }
      })

      return children
    },
    genSegmentedBtn (item) {
      if (!item.text || !item.callback) {
        console.warn('[Vuetify] Warn: When using the v-select component with \'segmented\' prop without a selection slot, items must contain both a text and callback property')
        return null
      }

      return this.$createElement('v-btn', {
        props: {
          flat: true
        },
        on: {
          click (e) {
            e.stopPropagation()
            item.callback(e)
          }
        }
      }, [item.text])
    },
    genSlotSelection (item, index) {
      return this.$scopedSlots.selection({
        parent: this,
        item,
        index,
        selected: index === this.selectedIndex,
        disabled: this.disabled || this.readonly
      })
    },
    genChipSelection (item, index) {
      const isDisabled = this.disabled || this.readonly
      const click = e => {
        if (isDisabled) return

        e.stopPropagation()
        this.focus()
        this.selectedIndex = index
      }

      return this.$createElement('v-chip', {
        staticClass: 'chip--select-multi',
        attrs: { tabindex: '-1' },
        props: {
          close: !isDisabled,
          dark: this.dark,
          disabled: isDisabled,
          selected: index === this.selectedIndex
        },
        on: {
          click: click,
          focus: click,
          input: () => this.selectItem(item)
        },
        key: this.getValue(item)
      }, this.getText(item))
    },
    genCommaSelection (item, comma, index) {
      return this.$createElement('div', {
        staticClass: 'input-group__selections__comma',
        'class': {
          'input-group__selections__comma--active': index === this.selectedIndex
        },
        key: JSON.stringify(this.getValue(item)) // Item may be an object
      }, `${this.getText(item)}${comma ? ', ' : ''}`)
    },
    genList () {
      const children = this.filteredItems.map(o => {
        if (o.header) return this.genHeader(o)
        if (o.divider) return this.genDivider(o)
        else return this.genTile(o)
      })

      if (!children.length) {
        const noData = this.$slots['no-data']
        if (noData) {
          children.push(noData)
        } else {
          children.push(this.genTile(this.noDataText, true))
        }
      }

      return this.$createElement('v-card', [
        this.$createElement('v-list', {
          ref: 'list'
        }, children)
      ])
    },
    genHeader (item) {
      return this.$createElement('v-subheader', {
        props: item
      }, item.header)
    },
    genDivider (item) {
      return this.$createElement('v-divider', {
        props: item
      })
    },
    genTile (item, disabled) {
      const active = this.selectedItems.indexOf(item) !== -1

      if (typeof disabled === 'undefined') {
        disabled = getObjectValueByPath(item, this.itemDisabled)
      }

      const data = {
        on: {
          click: e => {
            if (disabled) return

            this.selectItem(item)
          }
        },
        props: {
          avatar: item === Object(item) && this.itemAvatar in item,
          ripple: true,
          value: active
        }
      }

      if (disabled) {
        data.props.disabled = disabled
      }

      if (this.$scopedSlots.item) {
        return this.$createElement('v-list-tile', data,
          [this.$scopedSlots.item({ parent: this, item })]
        )
      }

      return this.$createElement('v-list-tile', data,
        [this.genAction(item, active), this.genContent(item)]
      )
    },
    genAction (item, active) {
      if (!this.isMultiple) return null

      const data = {
        staticClass: 'list__tile__action--select-multi',
        on: {
          click: e => {
            e.stopPropagation()
            this.selectItem(item)
          }
        }
      }

      return this.$createElement('v-list-tile-action', data, [
        this.$createElement('v-checkbox', { props: { inputValue: active } })
      ])
    },
    genContent (item) {
      const text = this.getText(item)

      return this.$createElement('v-list-tile-content',
        [this.$createElement('v-list-tile-title', {
          domProps: {
            innerHTML: this.genFiltered(text)
          }
        })]
      )
    }
  }
}
<|endoftext|>


<commit_before>//TODO: refactor this whole thing!

define([
    'jquery',
    'underscore',
    'base/utils',
    'base/component'
], function($, _, utils, Component) {

    var geo_picker;

    var ButtonList = Component.extend({
        init: function(core, options) {
            //set properties
            this.name = 'buttonlist';
            this.template = "components/_gapminder/" + this.name + "/" + this.name;
            this.data = options.data;

            this.addButtons(options.buttons);
            this._super(core, options);

        },

        postRender: function() {
            this.placeholder = utils.d3ToJquery(this.placeholder);
        },

        //make button list responsive
        resize: function() {
            var $buttons = this.placeholder.find('.vzb-buttonlist .vzb-buttonlist-btn'),
                $button_more = this.placeholder.find('.vzb-buttonlist .vzb-buttonlist-btn-more'),
                offset = 30;

            var size_button = {
                width: $buttons.first().outerWidth(),
                height: $buttons.first().outerHeight()
            };

            var size_container = {
                    width: this.placeholder.outerWidth(),
                    height: this.placeholder.outerHeight()
                },
                vertical = size_container.width < size_container.height,

                number_buttons = $buttons.length - 1,
                compare = (vertical) ? "height" : "width";

            offset = size_button[compare] / 2;

            if ((number_buttons * size_button[compare]) <= size_container[compare] - offset) {
                $buttons.removeClass('vzb-hidden');
                $button_more.addClass('vzb-hidden');
            } else {
                var max = Math.floor((size_container[compare] - offset) / size_button[compare]) - 1,
                    i = 0;
                $buttons.addClass('vzb-hidden');
                $buttons.each(function() {
                    i++;
                    $(this).removeClass('vzb-hidden');
                    if (i >= max) return false;
                });
                $button_more.removeClass('vzb-hidden');
            }
        },

        geopicker: function(button_list) {
            geo_picker = this.components['picker-geo'];

            //show the picker when the correct button is pressed
            var geo_button = this.placeholder.find('.vzb-buttonlist-geo');
            geo_button.click(function() {
                geo_picker.show();
            });
        },

        addButtons: function(button_list) {
            var _this = this;

            button_list.map(function(btn) {
                switch (btn) {
                    case 'full-screen':
                        _this.addComponent('_gapminder/buttonlist/buttons/full-screen-button', {
                            data: _this.data,
                            root: '.vzb-tool-buttonlist .vzb-buttonlist'
                        });
                        break;

                    case 'add':
                        _this.addComponent('_gapminder/buttonlist/buttons/add-button', {
                            data: _this.data,
                            root: '.vzb-tool-buttonlist .vzb-buttonlist'
                        });
                        break;
                    case 'colors':
                        _this.addComponent('_gapminder/buttonlist/buttons/colors-button', {
                            data: _this.data,
                            root: '.vzb-tool-buttonlist .vzb-buttonlist'
                        });
                        break;
                    case 'find':
                        _this.addComponent('_gapminder/buttonlist/buttons/find-button', {
                            data: _this.data,
                            root: '.vzb-tool-buttonlist .vzb-buttonlist'
                        });
                        break;
                    case 'play':
                        _this.addComponent('_gapminder/buttonlist/buttons/play-button', {
                            data: _this.data,
                            root: '.vzb-tool-buttonlist .vzb-buttonlist'
                        });
                        break;
                    case 'more-options':
                        _this.addComponent('_gapminder/buttonlist/buttons/more-options-button', {
                            data: _this.data,
                            root: '.vzb-tool-buttonlist .vzb-buttonlist'
                        });
                        break;
                }
            })
        }

    });

    return ButtonList;
});<commit_msg>Add add button function for dynamic adding of buttons<commit_after>//TODO: refactor this whole thing!

define([
    'jquery',
    'underscore',
    'base/utils',
    'base/component'
], function($, _, utils, Component) {

    var geo_picker;

    var ButtonList = Component.extend({
        init: function(core, options) {
            //set properties
            this.name = 'buttonlist';
            this.template = "components/_gapminder/" + this.name + "/" + this.name;
            this.data = options.data;

            this.addButtons(options.buttons);
            this._super(core, options);

        },

        postRender: function() {
            this.placeholder = utils.d3ToJquery(this.placeholder);
        },

        //make button list responsive
        resize: function() {
            var $buttons = this.placeholder.find('.vzb-buttonlist .vzb-buttonlist-btn'),
                $button_more = this.placeholder.find('.vzb-buttonlist .vzb-buttonlist-btn-more'),
                offset = 30;

            var size_button = {
                width: $buttons.first().outerWidth(),
                height: $buttons.first().outerHeight()
            };

            var size_container = {
                    width: this.placeholder.outerWidth(),
                    height: this.placeholder.outerHeight()
                },
                vertical = size_container.width < size_container.height,

                number_buttons = $buttons.length - 1,
                compare = (vertical) ? "height" : "width";

            offset = size_button[compare] / 2;

            if ((number_buttons * size_button[compare]) <= size_container[compare] - offset) {
                $buttons.removeClass('vzb-hidden');
                $button_more.addClass('vzb-hidden');
            } else {
                var max = Math.floor((size_container[compare] - offset) / size_button[compare]) - 1,
                    i = 0;
                $buttons.addClass('vzb-hidden');
                $buttons.each(function() {
                    i++;
                    $(this).removeClass('vzb-hidden');
                    if (i >= max) return false;
                });
                $button_more.removeClass('vzb-hidden');
            }
        },

        geopicker: function(button_list) {
            geo_picker = this.components['picker-geo'];

            //show the picker when the correct button is pressed
            var geo_button = this.placeholder.find('.vzb-buttonlist-geo');
            geo_button.click(function() {
                geo_picker.show();
            });
        },

        addButtons: function(button_list) {
            var _this = this;

            button_list.map(function(btn) {
                switch (btn) {
                    case 'full-screen':
                        _this.addComponent('_gapminder/buttonlist/buttons/full-screen-button', {
                            data: _this.data,
                            root: '.vzb-tool-buttonlist .vzb-buttonlist'
                        });
                        break;

                    case 'add':
                        _this.addComponent('_gapminder/buttonlist/buttons/add-button', {
                            data: _this.data,
                            root: '.vzb-tool-buttonlist .vzb-buttonlist'
                        });
                        break;
                    case 'colors':
                        _this.addComponent('_gapminder/buttonlist/buttons/colors-button', {
                            data: _this.data,
                            root: '.vzb-tool-buttonlist .vzb-buttonlist'
                        });
                        break;
                    case 'find':
                        _this.addComponent('_gapminder/buttonlist/buttons/find-button', {
                            data: _this.data,
                            root: '.vzb-tool-buttonlist .vzb-buttonlist'
                        });
                        break;
                    case 'play':
                        _this.addComponent('_gapminder/buttonlist/buttons/play-button', {
                            data: _this.data,
                            root: '.vzb-tool-buttonlist .vzb-buttonlist'
                        });
                        break;
                    case 'more-options':
                        _this.addComponent('_gapminder/buttonlist/buttons/more-options-button', {
                            data: _this.data,
                            root: '.vzb-tool-buttonlist .vzb-buttonlist'
                        });
                        break;
                }
            })
        },

        addButton: function(btn) {
            this.addButtons(btn.split(" "));
        }

    });

    return ButtonList;
});<|endoftext|>