Use short Splatfest team names on mobile

<template>

                {{ title }}

            <SplatfestResultsBox :festival="festival" :results="results" v-if="results && !screenshotMode" />

            <SplatfestResultsBox :festival="festival" :results="results" v-if="results && !screenshotMode" />

                <template v-if="!screenshotMode">

                        {{ festival.times.start | date(dateOptions) }}
                        {{ festival.times.start | time }}

                    –

                        {{ festival.times.end | date }}
                        {{ festival.times.end | time }}

                </template>
                <template v-else>
                    <template v-if="state == 'upcoming'">
                        {{ festival.times.start - now | shortDuration | time.in }}
                    </template>
                    <template v-else>
                        {{ festival.times.end - now | durationHours | time.remaining }}
                    </template>
                </template>

            <SplatfestWinnerBar
                v-else
                :festival="festival"
                :results="results"
                />

            <template v-if="state == 'upcoming'">
                {{ festival.times.start - now | duration | time.in }}
            </template>

            <template v-else-if="state == 'active'">
                {{ festival.times.end - now | duration | time.remaining }}
            </template>

            <template v-else-if="state == 'past' && !results && festival.times.result > now">
                {{ festival.times.result - now | duration | resultsIn }}
            </template>

</template>


Add the word "upcoming" to the Splatfest title

<template>

                {{ title }}

                {{ festival.names.alpha_short }}

                {{ festival.names.bravo_short }}

                {{ festival.times.start | date }}
                {{ festival.times.start | time }}
                –
                {{ festival.times.end | date }}
                {{ festival.times.end | time }}

            <template v-if="festival.times.start <= now">
                {{ festival.times.end - now | duration }}
                remaining
            </template>
            <template v-else>
                in
                {{ festival.times.start - now | duration }}
            </template>

</template>

<template>

                Splatfest

                {{ festival.names.alpha_short }}

                {{ festival.names.bravo_short }}

                {{ festival.times.start | date }}
                {{ festival.times.start | time }}
                –
                {{ festival.times.end | date }}
                {{ festival.times.end | time }}

            <template v-if="festival.times.start <= now">
                {{ festival.times.end - now | duration }}
                remaining
            </template>
            <template v-else>
                in
                {{ festival.times.start - now | duration }}
            </template>

</template>


Add action bar to main component

Add action bar to main component


Add new shortcut

Add new shortcut

<template>

		<x-sub :note="p.reply"/>

		<mk-avatar class="avatar" :user="note.user"/>
		%fa:retweet%
		{{ '%i18n:@reposted-by%'.substr(0, '%i18n:@reposted-by%'.indexOf('{')) }}
		{{ note.user | userName }}
		{{ '%i18n:@reposted-by%'.substr('%i18n:@reposted-by%'.indexOf('}') + 1) }}
		<mk-time :time="note.createdAt"/>

		<mk-avatar class="avatar" :user="p.user"/>

			<mk-note-header class="header" :note="p"/>

					{{ p.cw }}
					<mk-cw-button v-model="showContent"/>

						%i18n:@private%
						%i18n:@deleted%
						%fa:reply%
						<misskey-flavored-markdown v-if="p.text" :text="p.text" :i="$store.state.i" :class="$style.text"/>
						RP:

 0">
						<mk-media-list :media-list="p.files"/>

					<mk-poll v-if="p.poll" :note="p" ref="pollViewer"/>
					<a class="location" v-if="p.geo" :href="`https://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% 位置情報

<mk-note-preview :note="p.renote"/>

					<mk-url-preview v-for="url in urls" :url="url" :key="url"/>

				<mk-reactions-viewer :note="p" ref="reactionsViewer"/>
				<button class="replyButton" @click="reply()" title="%i18n:@reply%">
					<template v-if="p.reply">%fa:reply-all%</template>
					<template v-else>%fa:reply%</template>

 0">{{ p.repliesCount }}

				</button>
				<button class="renoteButton" @click="renote()" title="%i18n:@renote%">
					%fa:retweet%
 0">{{ p.renoteCount }}

				</button>
				<button class="reactionButton" :class="{ reacted: p.myReaction != null }" @click="react()" ref="reactButton" title="%i18n:@add-reaction%">
					%fa:plus%
 0">{{ p.reactions_count }}

				</button>
				<button @click="menu()" ref="menuButton">
					%fa:ellipsis-h%
				</button>

		<mk-note-status-graph width="462" height="130" :note="p"/>

</template>


tooltips for tray items

tooltips for tray items

<template>

        <AposLogoPadless class="apos-admin-bar__logo" />

 0">
            <AposButton
              type="subtle" label="Page Tree"
              icon="file-tree-icon" class="apos-admin-bar__btn"
              :modifiers="['no-motion']"
              @click="emitEvent('@apostrophecms/page:manager')"
            />

            <AposButton
              v-if="item.options" type="subtle"
              @click="emitEvent(item.action)"
              :label="item.label"
              :modifiers="['no-motion']"
              class="apos-admin-bar__btn"
            />
            <AposContextMenu
              v-else-if="item.items" class="apos-admin-bar__sub"
              :menu="item.items" :button="{
                label: item.label,
                modifiers: ['no-motion'],
                class: 'apos-admin-bar__btn',
                type: 'subtle'
              }"
              @item-clicked="emitEvent"
            />

 0">
            <AposContextMenu
              class="apos-admin-bar__create"
              :menu="createMenu"
              :button="{
                label: 'New item',
                iconOnly: true,
                icon: 'plus-icon',
                type: 'primary',
                modifiers: ['round', 'no-motion']
              }"
              @item-clicked="emitEvent"
            />

        <TheAposAdminBarUser class="apos-admin-bar__user" />

        <transition-group
          tag="div"
          class="apos-admin-bar__control-set apos-admin-bar__control-set--context-controls"
          name="flip"
        >

            <AposButton
              :disabled="patchesSinceLoaded.length === 0"
              type="subtle" :modifiers="['small', 'no-motion']"
              label="Undo" class="apos-admin-bar__context-button"
              icon="undo-icon" :icon-only="true"
              @click="undo"
            />

            <AposButton
              :disabled="undone.length === 0"
              type="subtle" :modifiers="['small', 'no-motion']"
              label="Redo" class="apos-admin-bar__context-button"
              icon="redo-icon" :icon-only="true"
              @click="redo"
            />

              <component
                :is="savingIndicator.el"
                v-bind="savingIndicator.options"
                class="apos-admin-bar__status__icon"
              />

                {{ savingLabel }}

        </transition-group>
        <transition-group
          tag="div"
          class="apos-admin-bar__control-set apos-admin-bar__control-set--title"
          name="flip"
        >

            <AposIndicator
              icon="information-outline-icon"
              fill-color="var(--a-primary)"
              :tooltip="docTooltip"
              class="apos-admin-bar__title__indicator"
            />

              {{ context.title }}

              —

            <AposContextMenu
              class="apos-admin-bar__title__document"
              :button="draftButton"
              :menu="draftMenu"
              :disabled="contextStack.length > 0"
              @item-clicked="switchDraftMode"
              menu-offset="13, 10"
              menu-placement="bottom-end"
            />

        </transition-group>
        <transition-group
          tag="div"
          class="apos-admin-bar__control-set apos-admin-bar__control-set--mode-and-settings"
          name="flip"
        >

            <AposButton
              class="apos-admin-bar__context-button"
              label="Edit" type="subtle"
              :modifiers="['small', 'no-motion']"
              :tooltip="{
                content: 'Toggle Edit Mode',
                placement: 'bottom'
              }"
              @click="switchEditMode(true)"
              :disabled="contextStack.length > 0"
            />

            <AposButton
              v-for="item in trayItems"
              :key="item.name"
              type="subtle" :modifiers="['small', 'no-motion']"
              :tooltip="trayItemTooltip(item)" class="apos-admin-bar__context-button"
              :icon="item.options.icon" :icon-only="true"
              :state="trayItemState[item.name] ? [ 'active' ] : []"
              @click="emitEvent(item.action)"
            />
            <AposButton
              v-if="context._id"
              class="apos-admin-bar__context-button"
              label="Page Settings" :tooltip="{
                content: 'Page Settings',
                placement: 'bottom'
              }"
              type="subtle" :modifiers="['small', 'no-motion']"
              icon="cog-icon" :icon-only="true"
              @click="emitEvent({
                itemName: contextEditorName,
                props: {
                  docId: context._id
                }
              })"
              :disabled="contextStack.length > 0"
            />

            <AposDocMoreMenu
              :doc-id="context._id"
              :disabled="!context.modified"
              :is-modified="context.modified"
              :can-discard-draft="context.modified"
              :is-modified-from-published="context.modified"
              :is-published="!!context.lastPublishedAt"
              :options="{ saveDraft: false }"
              @discardDraft="onDiscardDraft"
            />
            <AposButton
              class="apos-admin-bar__context-button"
              label="Preview" :tooltip="{
                content: 'Toggle Preview Mode',
                placement: 'bottom'
              }"
              type="subtle" :modifiers="['small', 'no-motion']"
              @click="switchEditMode(false)"
              :disabled="contextStack.length > 0"
            />
            <AposButton
              v-if="editMode"
              type="primary" :label="publishLabel"
              :disabled="!readyToPublish"
              class="apos-admin-bar__btn apos-admin-bar__context-button"
              @click="onPublish"
              :modifiers="['no-motion']"
            />

        </transition-group>

</template>

<template>

        <AposLogoPadless class="apos-admin-bar__logo" />

 0">
            <AposButton
              type="subtle" label="Page Tree"
              icon="file-tree-icon" class="apos-admin-bar__btn"
              :modifiers="['no-motion']"
              @click="emitEvent('@apostrophecms/page:manager')"
            />

            <AposButton
              v-if="item.options" type="subtle"
              @click="emitEvent(item.action)"
              :label="item.label"
              :modifiers="['no-motion']"
              class="apos-admin-bar__btn"
            />
            <AposContextMenu
              v-else-if="item.items" class="apos-admin-bar__sub"
              :menu="item.items" :button="{
                label: item.label,
                modifiers: ['no-motion'],
                class: 'apos-admin-bar__btn',
                type: 'subtle'
              }"
              @item-clicked="emitEvent"
            />

 0">
            <AposContextMenu
              class="apos-admin-bar__create"
              :menu="createMenu"
              :button="{
                label: 'New item',
                iconOnly: true,
                icon: 'plus-icon',
                type: 'primary',
                modifiers: ['round', 'no-motion']
              }"
              @item-clicked="emitEvent"
            />

        <TheAposAdminBarUser class="apos-admin-bar__user" />

        <transition-group
          tag="div"
          class="apos-admin-bar__control-set apos-admin-bar__control-set--context-controls"
          name="flip"
        >

            <AposButton
              :disabled="patchesSinceLoaded.length === 0"
              type="subtle" :modifiers="['small', 'no-motion']"
              label="Undo" class="apos-admin-bar__context-button"
              icon="undo-icon" :icon-only="true"
              @click="undo"
            />

            <AposButton
              :disabled="undone.length === 0"
              type="subtle" :modifiers="['small', 'no-motion']"
              label="Redo" class="apos-admin-bar__context-button"
              icon="redo-icon" :icon-only="true"
              @click="redo"
            />

              <component
                :is="savingIndicator.el"
                v-bind="savingIndicator.options"
                class="apos-admin-bar__status__icon"
              />

                {{ savingLabel }}

        </transition-group>
        <transition-group
          tag="div"
          class="apos-admin-bar__control-set apos-admin-bar__control-set--title"
          name="flip"
        >

            <AposIndicator
              icon="information-outline-icon"
              fill-color="var(--a-primary)"
              :tooltip="docTooltip"
              class="apos-admin-bar__title__indicator"
            />

              {{ context.title }}

              —

            <AposContextMenu
              class="apos-admin-bar__title__document"
              :button="draftButton"
              :menu="draftMenu"
              :disabled="contextStack.length > 0"
              @item-clicked="switchDraftMode"
              menu-offset="13, 10"
              menu-placement="bottom-end"
            />

        </transition-group>
        <transition-group
          tag="div"
          class="apos-admin-bar__control-set apos-admin-bar__control-set--mode-and-settings"
          name="flip"
        >

            <AposButton
              class="apos-admin-bar__context-button"
              label="Edit" type="subtle"
              :modifiers="['small', 'no-motion']"
              :tooltip="{
                content: 'Toggle Edit Mode',
                placement: 'bottom'
              }"
              @click="switchEditMode(true)"
              :disabled="contextStack.length > 0"
            />

            <AposButton
              v-for="item in trayItems"
              :key="item.name"
              type="subtle" :modifiers="['small', 'no-motion']"
              label="trayItemLabel(item)" class="apos-admin-bar__context-button"
              :icon="item.options.icon" :icon-only="true"
              :state="trayItemState[item.name] ? [ 'active' ] : []"
              @click="emitEvent(item.action)"
            />
            <AposButton
              v-if="context._id"
              class="apos-admin-bar__context-button"
              label="Page Settings" :tooltip="{
                content: 'Page Settings',
                placement: 'bottom'
              }"
              type="subtle" :modifiers="['small', 'no-motion']"
              icon="cog-icon" :icon-only="true"
              @click="emitEvent({
                itemName: contextEditorName,
                props: {
                  docId: context._id
                }
              })"
              :disabled="contextStack.length > 0"
            />

            <AposDocMoreMenu
              :doc-id="context._id"
              :disabled="!context.modified"
              :is-modified="context.modified"
              :can-discard-draft="context.modified"
              :is-modified-from-published="context.modified"
              :is-published="!!context.lastPublishedAt"
              :options="{ saveDraft: false }"
              @discardDraft="onDiscardDraft"
            />
            <AposButton
              class="apos-admin-bar__context-button"
              label="Preview" :tooltip="{
                content: 'Toggle Preview Mode',
                placement: 'bottom'
              }"
              type="subtle" :modifiers="['small', 'no-motion']"
              @click="switchEditMode(false)"
              :disabled="contextStack.length > 0"
            />
            <AposButton
              v-if="editMode"
              type="primary" :label="publishLabel"
              :disabled="!readyToPublish"
              class="apos-admin-bar__btn apos-admin-bar__context-button"
              @click="onPublish"
              :modifiers="['no-motion']"
            />

        </transition-group>

</template>


perf[Login]: support other query (#2013)

<template>

    <el-form ref="loginForm" :model="loginForm" :rules="loginRules" class="login-form" auto-complete="on" label-position="left">

Login Form

      <el-form-item prop="username">

          <svg-icon icon-class="user" />

        <el-input
          ref="username"
          v-model="loginForm.username"
          placeholder="Username"
          name="username"
          type="text"
          tabindex="1"
          auto-complete="on"
        />
      </el-form-item>

      <el-tooltip v-model="capsTooltip" content="Caps lock is On" placement="right" manual>
        <el-form-item prop="password">

            <svg-icon icon-class="password" />

          <el-input
            :key="passwordType"
            ref="password"
            v-model="loginForm.password"
            :type="passwordType"
            placeholder="Password"
            name="password"
            tabindex="2"
            auto-complete="on"
            @keyup.native="checkCapslock"
            @blur="capsTooltip = false"
            @keyup.enter.native="handleLogin"
          />

            <svg-icon :icon-class="passwordType === 'password' ? 'eye' : 'eye-open'" />

        </el-form-item>
      </el-tooltip>

      <el-button :loading="loading" type="primary" style="width:100%;margin-bottom:30px;" @click.native.prevent="handleLogin">Login</el-button>

          Username : admin
          Password : any

          Username : editor
          Password : any

        <el-button class="thirdparty-button" type="primary" @click="showDialog=true">
          Or connect with
        </el-button>

    </el-form>

    <el-dialog title="Or connect with" :visible.sync="showDialog">
      Can not be simulated on local, so please combine you own business simulation! ! !

      <social-sign />
    </el-dialog>

</template>


Removed not needed method

Removed not needed method

<template>

  <core-base>
    <side-nav class='nav'></side-nav>

      <search-button class='search-btn'></search-button>

      <error-page v-show='error'></error-page>

        <explore-page v-if='showExplorePage'></explore-page>
        <content-page v-if='showContentPage'></content-page>
        <learn-page  v-if='showLearnPage'></learn-page>
        <scratchpad-page v-if='showScratchpadPage'></scratchpad-page>

          <search-widget
            :show-topics="exploreMode">
          </search-widget>

    <router-view></router-view>

  </core-base>

</template>


Making stroke-width reactive

Making stroke-width reactive

<script>
import resize from 'vue-resize-directive'
import { interpolatePath } from 'd3-interpolate-path'

import horizontal from './layout/horizontal'
import vertical from './layout/vertical'
import circular from './layout/circular'
import { drawLink as bezier } from './linkLayout/bezier'
import { drawLink as orthogonal } from './linkLayout/orthogonal'
import standardBehavior from './behaviors/StandardBehavior'
import {compareString, toPromise, findInParents, mapMany, translate} from './d3-utils'
import {renderInVueContext, renderTemplateSlot} from './vueHelper'
import {setUpZoom} from './zoom/zoomBehavior'

import * as d3 from 'd3'

const layout = {
  horizontal,
  circular,
  vertical
}

const linkLayouts = {
  bezier,
  orthogonal
}

var i = 0
const types = ['tree', 'cluster']
const layouts = ['circular', 'horizontal', 'vertical']
const nodeDisplays = ['all', 'leaves', 'extremities']
const linkLayoutsType = ['bezier', 'orthogonal']

const props = {
  data: {
    type: Object,
    required: false
  },
  selected: {
    type: Object,
    required: false
  },
  duration: {
    type: Number,
    default: 750
  },
  type: {
    type: String,
    default: 'tree',
    validator (value) {
      return types.includes(value)
    }
  },
  layoutType: {
    type: String,
    default: 'horizontal',
    validator (value) {
      return layouts.includes(value)
    }
  },
  linkLayout: {
    type: String,
    default: 'bezier',
    validator (value) {
      return linkLayoutsType.includes(value)
    }
  },
  marginX: {
    type: Number,
    default: 20
  },
  marginY: {
    type: Number,
    default: 20
  },
  nodeText: {
    type: String,
    default: 'name'
  },
  identifier: {
    type: Function,
    default: () => i++
  },
  zoomable: {
    type: Boolean,
    default: false
  },
  minZoom: {
    type: Number,
    default: 0.8
  },
  maxZoom: {
    type: Number,
    default: 9
  },
  nodeTextDisplay: {
    type: String,
    default: 'all',
    validator (value) {
      return nodeDisplays.includes(value)
    }
  },
  radius: {
    type: Number,
    default: 3
  },
  strokeWidth: {
    type: Number,
    default: 1.5
  },
  leafTextMargin: {
    type: Number,
    default: 6
  },
  nodeTextMargin: {
    type: Number,
    default: 6
  }
}

const directives = {
  resize
}

function hasChildren (d) {
  return d.children || d._children
}

function getChildren (d) {
  return d.children ? {children: d.children, visible: true} : (d._children ? {children: d._children, visible: false} : null)
}

function onAllChilddren (d, callback, fatherVisible = undefined) {
  if (callback(d, fatherVisible) === false) {
    return
  }
  var directChildren = getChildren(d)
  directChildren && directChildren.children.forEach(child => onAllChilddren(child, callback, directChildren.visible))
}

function filterTextNode (nodeTextDisplay, root) {
  switch (nodeTextDisplay) {
    case 'all':
      return d => true

    case 'leaves':
      return d => !hasChildren(d)

    case 'extremities':
      return d => !hasChildren(d) || d === root
  }
}

export default {
  name: 'D3Tree',

  props,

  directives,

  model: {
    prop: 'selected',
    event: 'change'
  },

  render (h) {
    const {setSelected, collapse, collapseAll, expand, expandAll, show, toggleExpandCollapse} = this
    const rawActions = {setSelected, collapse, collapseAll, expand, expandAll, show, toggleExpandCollapse}
    this.actions = Object.keys(rawActions).reduce((current, key) => {
      current[key] = rawActions[key].bind(this)
      return current
    }, {})
    const getProps = () => {
      const {actions, graphNodes: nodes} = this
      return {nodes, actions}
    }
    this._behaviour = renderTemplateSlot(getProps, this.$scopedSlots.behavior, standardBehavior)
    return h('div', {class: 'viewport treeclass', directives: [{name: 'resize', value: this.resize}]})
  },

  data () {
    return {
      currentTransform: null,
      maxTextLenght: {
        first: 0,
        last: 0
      },
      graphNodes: {
        clickedNode: null,
        clickedText: null
      }
    }
  },

  mounted () {
    const size = this.getSize()
    const svg = d3.select(this.$el).append('svg')
          .attr('width', size.width)
          .attr('height', size.height)
    const {zoomable, tree} = this
    const g = zoomable ? svg.append('g') : this.transformSvg(svg.append('g'), size)

    this.internaldata = {
      svg,
      g,
      tree
    }

    this.internaldata.zoom = zoomable ? this.setUpZoom() : null
    this.data && this.onData(this.data)
  },

  methods: {
    setSelected (node) {
      this.$emit('change', node)
    },

    getSize () {
      const {$el: {clientWidth: width, clientHeight: height}} = this
      return { width, height }
    },

    resize () {
      const size = this.getSize()
      this.internaldata.svg
              .attr('width', size.width)
              .attr('height', size.height)
      this.layout.size(this.internaldata.tree, size, this.margin, this.maxTextLenght)
      this.applyZoom(size)
      this.redraw()
    },

    setUpZoom () {
      const { currentTransform, minZoom, maxZoom, onZoomed, internaldata: { svg } } = this
      return setUpZoom({ currentTransform, minZoom, maxZoom, svg }, onZoomed)
    },

    onZoomed ({transform}) {
      this.$emit('zoom', {transform})
      this._originalZoom = transform
      this.currentTransform = this.updateTransform(transform)
      this.redraw({transitionDuration: 0})
    },

    removeZoom () {
      const { internaldata } = this
      internaldata.zoom.on('zoom', null)
      internaldata.zoom = null
    },

    updateZoom () {
      if (!this.zoomable) {
        return
      }
      const {minZoom, maxZoom} = this
      this.internaldata.zoom.scaleExtent([minZoom, maxZoom])
    },

    completeRedraw ({margin = null, layout = null}) {
      const size = this.getSize()
      this.layout.size(this.internaldata.tree, size, this.margin, this.maxTextLenght)
      this.applyZoom(size, true)
      this.redraw()
    },

    transformSvg (g, size) {
      size = size || this.getSize()
      return this.layout.transformSvg(g, this.margin, size, this.maxTextLenght)
    },

    updateTransform (transform, size) {
      size = size || this.getSize()
      return this.layout.updateTransform(transform, this.margin, size, this.maxTextLenght)
    },

    updateGraph (source, {transitionDuration = undefined} = {}) {
      source = source || this.internaldata.root
      let originBuilder = source
      let forExit = source
      const originAngle = source.layoutInfo ? source.layoutInfo.rotate : 0
      const origin = {x: source.x0, y: source.y0}

      if (arguments.length === 0) {
        originBuilder = d => {
          if (d.parent == null) {
            return origin
          }
          if (d.parent.x0 !== undefined) {
            return {x: d.parent.x0, y: d.parent.y0}
          }
          if (d.parent._x0 !== undefined) {
            return {x: d.parent._x0, y: d.parent._y0}
          }
          return origin
        }
        forExit = d => ({x: source.x, y: source.y})
        source = this.internaldata.root
      } else if (typeof source === 'object') {
        originBuilder = d => origin
        forExit = d => ({x: source.x, y: source.y})
      }

      const root = this.internaldata.root
      const links = this.internaldata.g.selectAll('.linktree')
         .data(this.internaldata.tree(root).descendants().slice(1), d => d.id)

      const newLinks = links.enter().append('path').attr('class', 'linktree').lower()
      const nodes = this.internaldata.g.selectAll('.nodetree').data(root.descendants(), d => d.id)
      const newNodes = nodes.enter().append('g').attr('class', d => `nodetree node-rank-${d.depth}`)
      const allNodes = newNodes.merge(nodes)

      nodes.each(function (d) {
        d._x0 = d.x
        d._y0 = d.y
      })

      const { strokeWidth, layout, duration, drawLink } = this
      transitionDuration = (transitionDuration === undefined) ? duration : transitionDuration
      const transform = this.currentTransform || d3.zoomIdentity
      const strokeWidthFinal = `${strokeWidth / transform.k}px`

      newLinks.attr('d', d => drawLink(originBuilder(d), originBuilder(d), layout))
        .attr('transform', transform)
        .attr('stroke-width', strokeWidthFinal)
      const updateAndNewLinks = links.merge(newLinks)
      const updateAndNewLinksPromise = toPromise(updateAndNewLinks
        .transition().duration(transitionDuration)
        .attr('transform', transform)
        .attr('stroke-width', strokeWidthFinal)
        .attrTween('d', function (d) {
          const previous = d3.select(this).attr('d')
          const final = drawLink(d, d.parent, layout)
          return interpolatePath(previous, final)
        })
      )
      const exitingLinksPromise = toPromise(links.exit().transition().duration(transitionDuration).attr('d', d => drawLink(forExit(d), forExit(d), layout)).remove())
      const {actions, radius, selected, $scopedSlots: {node}} = this
      const getHtml = node ? d => renderInVueContext({
        scope: node,
        props: {
          actions,
          radius,
          node: d,
          data: d.data,
          isRetracted: !!d._children,
          isSelected: d.data === selected
        }
      }, this.redraw) : d => `<circle r="${radius}"/>`

      newNodes.attr('transform', d => `${translate(originBuilder(d), layout, transform)} rotate(${originAngle}) scale(0.1)`)
        .append('g')
        .attr('class', 'node')

      newNodes
        .append('text')
        .attr('dy', '.35em')
        .attr('x', 0)
        .attr('dx', 0)
        .on('click', this.onNodeTextClick)

      allNodes
        .select('.node')
        .html(getHtml)

      allNodes.classed('node--internal', d => hasChildren(d))
        .classed('node--leaf', d => !hasChildren(d))
        .classed('selected', d => d.data === selected)
        .on('click', this.onNodeClick)

      const { leafTextMargin, nodeTextMargin, layout: {layoutNode}, nodeTextDisplay } = this
      const showNode = filterTextNode(nodeTextDisplay, root)
      allNodes.filter(d => !showNode(d)).select('text').text('')
      const text = allNodes.filter(showNode).select('text').text(d => d.data[this.nodeText])

      allNodes.each((d) => {
        d.layoutInfo = layoutNode(hasChildren(d), {leaf: leafTextMargin, node: nodeTextMargin}, d)
      })

      const allNodesPromise = toPromise(allNodes.transition().duration(transitionDuration)
        .attr('transform', d => `${translate(d, layout, transform)} rotate(${d.layoutInfo.rotate})`)
        .attr('opacity', 1))

      text.attr('x', d => d.layoutInfo.x)
          .attr('y', d => d.layoutInfo.y)
          .attr('text-anchor', d => d.layoutInfo.anchor)
          .attr('transform', d => `rotate(${d.layoutInfo.textRotate})`)

      allNodes.each((d) => {
        d.x0 = d.x
        d.y0 = d.y
      })

      const exitingNodes = nodes.exit()
      exitingNodes.select('.node').transition().duration(transitionDuration)
                  .attr('transform', 'scale(0.1)')

      const exitingNodesPromise = toPromise(exitingNodes.transition().duration(transitionDuration)
                  .attr('transform', d => `${translate(forExit(d), layout, transform)} rotate(${d.parent.layoutInfo.rotate})`)
                  .attr('opacity', 0).remove())

      const leaves = root.leaves()
      const extremeNodes = text.filter(d => leaves.indexOf(d) !== -1).nodes()
      const last = Math.max(...extremeNodes.map(node => node.getComputedTextLength())) + leafTextMargin
      const first = text.node().getComputedTextLength() + leafTextMargin
      if (last <= this.maxTextLenght.last && first <= this.maxTextLenght.first) {
        this._scheduledRedraw = false
        return Promise.all([allNodesPromise, exitingNodesPromise, updateAndNewLinksPromise, exitingLinksPromise])
      }

      this.maxTextLenght = {first, last}
      const size = this.getSize()
      this.applyZoom(size)
      this.layout.size(this.internaldata.tree, size, this.margin, this.maxTextLenght)
      return this.updateGraph(source)
    },

    onNodeTextClick (d) {
      this.graphNodes.clickedNode = null
      this.onEvent('clickedText', d)
    },

    onNodeClick (d) {
      this.graphNodes.clickedText = null
      this.onEvent('clickedNode', d)
    },

    onEvent (name, d) {
      this.graphNodes[name] = null
      this.graphNodes[name] = d
      this.$emit(name, {element: d, data: d.data})
      d3.event.stopPropagation()
    },

    toggleExpandCollapse (d) {
      if (!d) {
        return Promise.resolve(false)
      }
      return d.children ? this.collapse(d) : this.expand(d)
    },

    onData (data) {
      if (!data) {
        this.internaldata.root = null
        this.clean()
        return
      }
      const root = d3.hierarchy(data).sort((a, b) => { return compareString(a.data.text, b.data.text) })
      this.internaldata.root = root
      root.each(d => { d.id = this.identifier(d.data) })
      const size = this.getSize()
      root.x = size.height / 2
      root.y = 0
      root.x0 = root.x
      root.y0 = root.y
      this.redraw()
    },

    clean () {
      ['.linktree', '.nodetree', 'text', 'circle'].forEach(selector => {
        this.internaldata.g.selectAll(selector).transition().duration(this.duration).attr('opacity', 0).remove()
      })
    },

    redraw (option) {
      const { internaldata: { root }, _scheduledRedraw } = this
      if (!root || _scheduledRedraw) {
        return
      }
      this._scheduledRedraw = true
      this.$nextTick(() => this.updateGraph(root, option))
    },

    getNodeOriginComputer (originalVisibleNodes) {
      return node => {
        const parentVisible = findInParents(node, originalVisibleNodes)
        return {x: parentVisible.x0, y: parentVisible.y0}
      }
    },

    applyZoom (size, transition) {
      const { internaldata: {g, zoom}, zoomable } = this
      if (zoomable && zoom) {
        this.currentTransform = this.updateTransform(this._originalZoom)
        return
      }
      const element = transition ? g.transition().duration(this.duration) : g
      this.transformSvg(element, size)
    },

    updateIfNeeded (d, update) {
      return update ? this.updateGraph(d).then(() => true) : Promise.resolve(true)
    },

    // API
    collapse (d, update = true) {
      if (!d.children) {
        return Promise.resolve(false)
      }

      d._children = d.children
      d.children = null
      this.$emit('retract', {element: d, data: d.data})
      return this.updateIfNeeded(d, update)
    },

    expand (d, update = true) {
      if (!d._children) {
        return Promise.resolve(false)
      }

      d.children = d._children
      d._children = null
      this.$emit('expand', {element: d, data: d.data})
      return this.updateIfNeeded(d, update)
    },

    expandAll (d, update = true) {
      const lastVisible = d.leaves()
      onAllChilddren(d, child => { this.expand(child, false) })
      return this.updateIfNeeded(this.getNodeOriginComputer(lastVisible), update)
    },

    collapseAll (d, update = true) {
      onAllChilddren(d, child => this.collapse(child, false))
      return this.updateIfNeeded(d, update)
    },

    show (d, update = true) {
      const path = d.ancestors().reverse()
      const root = path.find(node => node.children === null) || d
      path.forEach(node => this.expand(node, false))
      return this.updateIfNeeded(root, update)
    },

    showOnly (d) {
      const root = this.internaldata.root
      const path = d.ancestors().reverse()
      const shouldBeRetracted = mapMany(path, p => p.children ? p.children : []).filter(node => node && (path.indexOf(node) === -1))
      const mapped = {}
      shouldBeRetracted.filter(node => node.children).forEach(rectractedNode => rectractedNode.each(c => { mapped[c.id] = rectractedNode }))
      const origin = node => {
        const reference = mapped[node.id]
        return !reference ? node : {x: reference.x, y: reference.y}
      }
      const updater = node => {
        if (shouldBeRetracted.indexOf(node) !== -1) {
          this.collapse(node, false)
          return false
        }
        return (node !== d)
      }
      onAllChilddren(root, updater)
      return this.updateGraph(origin).then(() => true)
    },

    resetZoom () {
      if (!this.zoomable) {
        return Promise.resolve(false)
      }
      const {svg, zoom} = this.internaldata
      const transitionPromise = toPromise(svg.transition().duration(this.duration).call(zoom.transform, () => d3.zoomIdentity))
      return transitionPromise.then(() => true)
    }
  },

  computed: {
    tree () {
      const size = this.getSize()
      const tree = this.type === 'cluster' ? d3.cluster() : d3.tree()
      this.layout.size(tree, size, this.margin, this.maxTextLenght)
      return tree
    },

    margin () {
      return {x: this.marginX, y: this.marginY}
    },

    layout () {
      return layout[this.layoutType]
    },

    drawLink () {
      return linkLayouts[this.linkLayout]
    }
  },

  watch: {
    data: {
      handler: function (current, old) {
        this.onData(current)
      },
      deep: true
    },

    type () {
      if (!this.internaldata.tree) {
        return
      }
      this.internaldata.tree = this.tree
      this.redraw()
    },

    marginX (newMarginX, oldMarginX) {
      this.completeRedraw({margin: {x: oldMarginX, y: this.marginY}})
    },

    marginY (newMarginY, oldMarginY) {
      this.completeRedraw({margin: {x: this.marginX, y: oldMarginY}})
    },

    layout (newLayout, oldLayout) {
      this.completeRedraw({layout: oldLayout})
    },

    selected () {
      this.completeRedraw({layout: this.layout})
    },

    radius () {
      this.completeRedraw({layout: this.layout})
    },

    leafTextMargin () {
      this.completeRedraw({layout: this.layout})
    },

    nodeTextMargin () {
      this.completeRedraw({layout: this.layout})
    },

    nodeTextDisplay () {
      this.completeRedraw({layout: this.layout})
    },

    linkLayout () {
      this.completeRedraw({layout: this.layout})
    },

    strokeWidth () {
      this.completeRedraw({layout: this.layout})
    },

    minZoom () {
      this.updateZoom()
    },

    maxZoom () {
      this.updateZoom()
    },

    zoomable (newValue) {
      if (newValue) {
        this.internaldata.zoom = this.setUpZoom()
        return
      }
      this.removeZoom()
    }
  }
}
</script>

<style>
.treeclass .nodetree  circle {
  fill: #999;
}

.treeclass .node--internal circle {
  cursor: pointer;
  fill:  #555;
}

.treeclass .nodetree text {
  font: 10px sans-serif;
  cursor: pointer;
}

.treeclass .nodetree.selected text {
  font-weight: bold;
}

.treeclass .node--internal text {
  text-shadow: 0 1px 0 #fff, 0 -1px 0 #fff, 1px 0 0 #fff, -1px 0 0 #fff;
}

.treeclass .linktree {
  fill: none;
  stroke: #555;
  stroke-opacity: 0.4;
}

.treeclass {
  max-height: 100%;
  width: 100%;
}
</style>

<script>
import resize from 'vue-resize-directive'
import { interpolatePath } from 'd3-interpolate-path'

import horizontal from './layout/horizontal'
import vertical from './layout/vertical'
import circular from './layout/circular'
import { drawLink as bezier } from './linkLayout/bezier'
import { drawLink as orthogonal } from './linkLayout/orthogonal'
import standardBehavior from './behaviors/StandardBehavior'
import {compareString, toPromise, findInParents, mapMany, translate} from './d3-utils'
import {renderInVueContext, renderTemplateSlot} from './vueHelper'
import {setUpZoom} from './zoom/zoomBehavior'

import * as d3 from 'd3'

const layout = {
  horizontal,
  circular,
  vertical
}

const linkLayouts = {
  bezier,
  orthogonal
}

var i = 0
const types = ['tree', 'cluster']
const layouts = ['circular', 'horizontal', 'vertical']
const nodeDisplays = ['all', 'leaves', 'extremities']
const linkLayoutsType = ['bezier', 'orthogonal']

const props = {
  data: {
    type: Object,
    required: false
  },
  selected: {
    type: Object,
    required: false
  },
  duration: {
    type: Number,
    default: 750
  },
  type: {
    type: String,
    default: 'tree',
    validator (value) {
      return types.includes(value)
    }
  },
  layoutType: {
    type: String,
    default: 'horizontal',
    validator (value) {
      return layouts.includes(value)
    }
  },
  linkLayout: {
    type: String,
    default: 'bezier',
    validator (value) {
      return linkLayoutsType.includes(value)
    }
  },
  marginX: {
    type: Number,
    default: 20
  },
  marginY: {
    type: Number,
    default: 20
  },
  nodeText: {
    type: String,
    default: 'name'
  },
  identifier: {
    type: Function,
    default: () => i++
  },
  zoomable: {
    type: Boolean,
    default: false
  },
  minZoom: {
    type: Number,
    default: 0.8
  },
  maxZoom: {
    type: Number,
    default: 9
  },
  nodeTextDisplay: {
    type: String,
    default: 'all',
    validator (value) {
      return nodeDisplays.includes(value)
    }
  },
  radius: {
    type: Number,
    default: 3
  },
  strokeWidth: {
    type: Number,
    default: 1.5
  },
  leafTextMargin: {
    type: Number,
    default: 6
  },
  nodeTextMargin: {
    type: Number,
    default: 6
  }
}

const directives = {
  resize
}

function hasChildren (d) {
  return d.children || d._children
}

function getChildren (d) {
  return d.children ? {children: d.children, visible: true} : (d._children ? {children: d._children, visible: false} : null)
}

function onAllChilddren (d, callback, fatherVisible = undefined) {
  if (callback(d, fatherVisible) === false) {
    return
  }
  var directChildren = getChildren(d)
  directChildren && directChildren.children.forEach(child => onAllChilddren(child, callback, directChildren.visible))
}

function filterTextNode (nodeTextDisplay, root) {
  switch (nodeTextDisplay) {
    case 'all':
      return d => true

    case 'leaves':
      return d => !hasChildren(d)

    case 'extremities':
      return d => !hasChildren(d) || d === root
  }
}

export default {
  name: 'D3Tree',

  props,

  directives,

  model: {
    prop: 'selected',
    event: 'change'
  },

  render (h) {
    const {setSelected, collapse, collapseAll, expand, expandAll, show, toggleExpandCollapse} = this
    const rawActions = {setSelected, collapse, collapseAll, expand, expandAll, show, toggleExpandCollapse}
    this.actions = Object.keys(rawActions).reduce((current, key) => {
      current[key] = rawActions[key].bind(this)
      return current
    }, {})
    const getProps = () => {
      const {actions, graphNodes: nodes} = this
      return {nodes, actions}
    }
    this._behaviour = renderTemplateSlot(getProps, this.$scopedSlots.behavior, standardBehavior)
    return h('div', {class: 'viewport treeclass', directives: [{name: 'resize', value: this.resize}]})
  },

  data () {
    return {
      currentTransform: null,
      maxTextLenght: {
        first: 0,
        last: 0
      },
      graphNodes: {
        clickedNode: null,
        clickedText: null
      }
    }
  },

  mounted () {
    const size = this.getSize()
    const svg = d3.select(this.$el).append('svg')
          .attr('width', size.width)
          .attr('height', size.height)
    const {zoomable, tree} = this
    const g = zoomable ? svg.append('g') : this.transformSvg(svg.append('g'), size)

    this.internaldata = {
      svg,
      g,
      tree
    }

    this.internaldata.zoom = zoomable ? this.setUpZoom() : null
    this.data && this.onData(this.data)
  },

  methods: {
    setSelected (node) {
      this.$emit('change', node)
    },

    getSize () {
      const {$el: {clientWidth: width, clientHeight: height}} = this
      return { width, height }
    },

    resize () {
      const size = this.getSize()
      this.internaldata.svg
              .attr('width', size.width)
              .attr('height', size.height)
      this.layout.size(this.internaldata.tree, size, this.margin, this.maxTextLenght)
      this.applyZoom(size)
      this.redraw()
    },

    setUpZoom () {
      const { currentTransform, minZoom, maxZoom, onZoomed, internaldata: { svg } } = this
      return setUpZoom({ currentTransform, minZoom, maxZoom, svg }, onZoomed)
    },

    onZoomed ({transform}) {
      this.$emit('zoom', {transform})
      this._originalZoom = transform
      this.currentTransform = this.updateTransform(transform)
      this.redraw({transitionDuration: 0})
    },

    removeZoom () {
      const { internaldata } = this
      internaldata.zoom.on('zoom', null)
      internaldata.zoom = null
    },

    updateZoom () {
      if (!this.zoomable) {
        return
      }
      const {minZoom, maxZoom} = this
      this.internaldata.zoom.scaleExtent([minZoom, maxZoom])
    },

    completeRedraw ({margin = null, layout = null}) {
      const size = this.getSize()
      this.layout.size(this.internaldata.tree, size, this.margin, this.maxTextLenght)
      this.applyZoom(size, true)
      this.redraw()
    },

    transformSvg (g, size) {
      size = size || this.getSize()
      return this.layout.transformSvg(g, this.margin, size, this.maxTextLenght)
    },

    updateTransform (transform, size) {
      size = size || this.getSize()
      return this.layout.updateTransform(transform, this.margin, size, this.maxTextLenght)
    },

    updateGraph (source, {transitionDuration = undefined} = {}) {
      source = source || this.internaldata.root
      let originBuilder = source
      let forExit = source
      const originAngle = source.layoutInfo ? source.layoutInfo.rotate : 0
      const origin = {x: source.x0, y: source.y0}

      if (arguments.length === 0) {
        originBuilder = d => {
          if (d.parent == null) {
            return origin
          }
          if (d.parent.x0 !== undefined) {
            return {x: d.parent.x0, y: d.parent.y0}
          }
          if (d.parent._x0 !== undefined) {
            return {x: d.parent._x0, y: d.parent._y0}
          }
          return origin
        }
        forExit = d => ({x: source.x, y: source.y})
        source = this.internaldata.root
      } else if (typeof source === 'object') {
        originBuilder = d => origin
        forExit = d => ({x: source.x, y: source.y})
      }

      const root = this.internaldata.root
      const links = this.internaldata.g.selectAll('.linktree')
         .data(this.internaldata.tree(root).descendants().slice(1), d => d.id)

      const newLinks = links.enter().append('path').attr('class', 'linktree').lower()
      const nodes = this.internaldata.g.selectAll('.nodetree').data(root.descendants(), d => d.id)
      const newNodes = nodes.enter().append('g').attr('class', d => `nodetree node-rank-${d.depth}`)
      const allNodes = newNodes.merge(nodes)

      nodes.each(function (d) {
        d._x0 = d.x
        d._y0 = d.y
      })

      const { strokeWidth, layout, duration, drawLink } = this
      transitionDuration = (transitionDuration === undefined) ? duration : transitionDuration
      const transform = this.currentTransform || d3.zoomIdentity
      const strokeWidthFinal = `${strokeWidth / transform.k}px`

      newLinks.attr('d', d => drawLink(originBuilder(d), originBuilder(d), layout))
        .attr('transform', transform)
        .attr('stroke-width', strokeWidthFinal)
      const updateAndNewLinks = links.merge(newLinks)
      const updateAndNewLinksPromise = toPromise(updateAndNewLinks
        .transition().duration(transitionDuration)
        .attr('transform', transform)
        .attr('stroke-width', strokeWidthFinal)
        .attrTween('d', function (d) {
          const previous = d3.select(this).attr('d')
          const final = drawLink(d, d.parent, layout)
          return interpolatePath(previous, final)
        })
      )
      const exitingLinksPromise = toPromise(links.exit().transition().duration(transitionDuration).attr('d', d => drawLink(forExit(d), forExit(d), layout)).remove())
      const {actions, radius, selected, $scopedSlots: {node}} = this
      const getHtml = node ? d => renderInVueContext({
        scope: node,
        props: {
          actions,
          radius,
          node: d,
          data: d.data,
          isRetracted: !!d._children,
          isSelected: d.data === selected
        }
      }, this.redraw) : d => `<circle r="${radius}"/>`

      newNodes.attr('transform', d => `${translate(originBuilder(d), layout, transform)} rotate(${originAngle}) scale(0.1)`)
        .append('g')
        .attr('class', 'node')

      newNodes
        .append('text')
        .attr('dy', '.35em')
        .attr('x', 0)
        .attr('dx', 0)
        .on('click', this.onNodeTextClick)

      allNodes
        .select('.node')
        .html(getHtml)

      allNodes.classed('node--internal', d => hasChildren(d))
        .classed('node--leaf', d => !hasChildren(d))
        .classed('selected', d => d.data === selected)
        .on('click', this.onNodeClick)

      const { leafTextMargin, nodeTextMargin, layout: {layoutNode}, nodeTextDisplay } = this
      const showNode = filterTextNode(nodeTextDisplay, root)
      allNodes.filter(d => !showNode(d)).select('text').text('')
      const text = allNodes.filter(showNode).select('text').text(d => d.data[this.nodeText])

      allNodes.each((d) => {
        d.layoutInfo = layoutNode(hasChildren(d), {leaf: leafTextMargin, node: nodeTextMargin}, d)
      })

      const allNodesPromise = toPromise(allNodes.transition().duration(transitionDuration)
        .attr('transform', d => `${translate(d, layout, transform)} rotate(${d.layoutInfo.rotate})`)
        .attr('opacity', 1))

      text.attr('x', d => d.layoutInfo.x)
          .attr('y', d => d.layoutInfo.y)
          .attr('text-anchor', d => d.layoutInfo.anchor)
          .attr('transform', d => `rotate(${d.layoutInfo.textRotate})`)

      allNodes.each((d) => {
        d.x0 = d.x
        d.y0 = d.y
      })

      const exitingNodes = nodes.exit()
      exitingNodes.select('.node').transition().duration(transitionDuration)
                  .attr('transform', 'scale(0.1)')

      const exitingNodesPromise = toPromise(exitingNodes.transition().duration(transitionDuration)
                  .attr('transform', d => `${translate(forExit(d), layout, transform)} rotate(${d.parent.layoutInfo.rotate})`)
                  .attr('opacity', 0).remove())

      const leaves = root.leaves()
      const extremeNodes = text.filter(d => leaves.indexOf(d) !== -1).nodes()
      const last = Math.max(...extremeNodes.map(node => node.getComputedTextLength())) + leafTextMargin
      const first = text.node().getComputedTextLength() + leafTextMargin
      if (last <= this.maxTextLenght.last && first <= this.maxTextLenght.first) {
        this._scheduledRedraw = false
        return Promise.all([allNodesPromise, exitingNodesPromise, updateAndNewLinksPromise, exitingLinksPromise])
      }

      this.maxTextLenght = {first, last}
      const size = this.getSize()
      this.applyZoom(size)
      this.layout.size(this.internaldata.tree, size, this.margin, this.maxTextLenght)
      return this.updateGraph(source)
    },

    onNodeTextClick (d) {
      this.graphNodes.clickedNode = null
      this.onEvent('clickedText', d)
    },

    onNodeClick (d) {
      this.graphNodes.clickedText = null
      this.onEvent('clickedNode', d)
    },

    onEvent (name, d) {
      this.graphNodes[name] = null
      this.graphNodes[name] = d
      this.$emit(name, {element: d, data: d.data})
      d3.event.stopPropagation()
    },

    toggleExpandCollapse (d) {
      if (!d) {
        return Promise.resolve(false)
      }
      return d.children ? this.collapse(d) : this.expand(d)
    },

    onData (data) {
      if (!data) {
        this.internaldata.root = null
        this.clean()
        return
      }
      const root = d3.hierarchy(data).sort((a, b) => { return compareString(a.data.text, b.data.text) })
      this.internaldata.root = root
      root.each(d => { d.id = this.identifier(d.data) })
      const size = this.getSize()
      root.x = size.height / 2
      root.y = 0
      root.x0 = root.x
      root.y0 = root.y
      this.redraw()
    },

    clean () {
      ['.linktree', '.nodetree', 'text', 'circle'].forEach(selector => {
        this.internaldata.g.selectAll(selector).transition().duration(this.duration).attr('opacity', 0).remove()
      })
    },

    redraw (option) {
      const { internaldata: { root }, _scheduledRedraw } = this
      if (!root || _scheduledRedraw) {
        return
      }
      this._scheduledRedraw = true
      this.$nextTick(() => this.updateGraph(root, option))
    },

    getNodeOriginComputer (originalVisibleNodes) {
      return node => {
        const parentVisible = findInParents(node, originalVisibleNodes)
        return {x: parentVisible.x0, y: parentVisible.y0}
      }
    },

    applyZoom (size, transition) {
      const { internaldata: {g, zoom}, zoomable } = this
      if (zoomable && zoom) {
        this.currentTransform = this.updateTransform(this._originalZoom)
        return
      }
      const element = transition ? g.transition().duration(this.duration) : g
      this.transformSvg(element, size)
    },

    updateIfNeeded (d, update) {
      return update ? this.updateGraph(d).then(() => true) : Promise.resolve(true)
    },

    // API
    collapse (d, update = true) {
      if (!d.children) {
        return Promise.resolve(false)
      }

      d._children = d.children
      d.children = null
      this.$emit('retract', {element: d, data: d.data})
      return this.updateIfNeeded(d, update)
    },

    expand (d, update = true) {
      if (!d._children) {
        return Promise.resolve(false)
      }

      d.children = d._children
      d._children = null
      this.$emit('expand', {element: d, data: d.data})
      return this.updateIfNeeded(d, update)
    },

    expandAll (d, update = true) {
      const lastVisible = d.leaves()
      onAllChilddren(d, child => { this.expand(child, false) })
      return this.updateIfNeeded(this.getNodeOriginComputer(lastVisible), update)
    },

    collapseAll (d, update = true) {
      onAllChilddren(d, child => this.collapse(child, false))
      return this.updateIfNeeded(d, update)
    },

    show (d, update = true) {
      const path = d.ancestors().reverse()
      const root = path.find(node => node.children === null) || d
      path.forEach(node => this.expand(node, false))
      return this.updateIfNeeded(root, update)
    },

    showOnly (d) {
      const root = this.internaldata.root
      const path = d.ancestors().reverse()
      const shouldBeRetracted = mapMany(path, p => p.children ? p.children : []).filter(node => node && (path.indexOf(node) === -1))
      const mapped = {}
      shouldBeRetracted.filter(node => node.children).forEach(rectractedNode => rectractedNode.each(c => { mapped[c.id] = rectractedNode }))
      const origin = node => {
        const reference = mapped[node.id]
        return !reference ? node : {x: reference.x, y: reference.y}
      }
      const updater = node => {
        if (shouldBeRetracted.indexOf(node) !== -1) {
          this.collapse(node, false)
          return false
        }
        return (node !== d)
      }
      onAllChilddren(root, updater)
      return this.updateGraph(origin).then(() => true)
    },

    resetZoom () {
      if (!this.zoomable) {
        return Promise.resolve(false)
      }
      const {svg, zoom} = this.internaldata
      const transitionPromise = toPromise(svg.transition().duration(this.duration).call(zoom.transform, () => d3.zoomIdentity))
      return transitionPromise.then(() => true)
    }
  },

  computed: {
    tree () {
      const size = this.getSize()
      const tree = this.type === 'cluster' ? d3.cluster() : d3.tree()
      this.layout.size(tree, size, this.margin, this.maxTextLenght)
      return tree
    },

    margin () {
      return {x: this.marginX, y: this.marginY}
    },

    layout () {
      return layout[this.layoutType]
    },

    drawLink () {
      return linkLayouts[this.linkLayout]
    }
  },

  watch: {
    data: {
      handler: function (current, old) {
        this.onData(current)
      },
      deep: true
    },

    type () {
      if (!this.internaldata.tree) {
        return
      }
      this.internaldata.tree = this.tree
      this.redraw()
    },

    marginX (newMarginX, oldMarginX) {
      this.completeRedraw({margin: {x: oldMarginX, y: this.marginY}})
    },

    marginY (newMarginY, oldMarginY) {
      this.completeRedraw({margin: {x: this.marginX, y: oldMarginY}})
    },

    layout (newLayout, oldLayout) {
      this.completeRedraw({layout: oldLayout})
    },

    selected () {
      this.completeRedraw({layout: this.layout})
    },

    radius () {
      this.completeRedraw({layout: this.layout})
    },

    leafTextMargin () {
      this.completeRedraw({layout: this.layout})
    },

    nodeTextMargin () {
      this.completeRedraw({layout: this.layout})
    },

    nodeTextDisplay () {
      this.completeRedraw({layout: this.layout})
    },

    linkLayout () {
      this.completeRedraw({layout: this.layout})
    },

    minZoom () {
      this.updateZoom()
    },

    maxZoom () {
      this.updateZoom()
    },

    zoomable (newValue) {
      if (newValue) {
        this.internaldata.zoom = this.setUpZoom()
        return
      }
      this.removeZoom()
    }
  }
}
</script>

<style>
.treeclass .nodetree  circle {
  fill: #999;
}

.treeclass .node--internal circle {
  cursor: pointer;
  fill:  #555;
}

.treeclass .nodetree text {
  font: 10px sans-serif;
  cursor: pointer;
}

.treeclass .nodetree.selected text {
  font-weight: bold;
}

.treeclass .node--internal text {
  text-shadow: 0 1px 0 #fff, 0 -1px 0 #fff, 1px 0 0 #fff, -1px 0 0 #fff;
}

.treeclass .linktree {
  fill: none;
  stroke: #555;
  stroke-opacity: 0.4;
}

.treeclass {
  max-height: 100%;
  width: 100%;
}
</style>


Make the logo square-shaped

Make the logo square-shaped

<template>
<q-layout view="lHh Lpr lFf">
  <q-header elevated>
    <q-toolbar class="bg-grey-2  text-primary">
      <q-btn type="a"
             href="https://www.scilifelab.se/data/"
             flat
             round>
	<q-avatar square>
          <img :src="require('assets/sll-logo.svg')"
	       alt="Data Centre logo"/>
	</q-avatar>
      </q-btn>
      <q-toolbar-title>
	{{ today }}
      </q-toolbar-title>

      <q-space />

      <q-btn flat
             round
             icon="fab fa-github"
             type="a"
             href="https://github.com/ScilifelabDataCentre/lunch-menu"/>
    </q-toolbar>
  </q-header>
  <q-page-container>
    <router-view />
  </q-page-container>
</q-layout>
</template>

<script>
export default {
  name: 'MainLayout',

  computed: {
    showSolna: {
      get () {
        return this.$store.state.main.showSolna;
      },
      set (val) {
        this.$store.dispatch('main/setShowSolna', val)
      }
    },

    showUppsala: {
      get () {
        return this.$store.state.main.showUppsala;
      },
      set (val) {
        this.$store.dispatch('main/setShowUppsala', val)
      }
    },

    onlyFavourites: {
      get () {
        return this.$store.state.main.onlyFavourites;
      },
      set (val) {
        this.$store.dispatch('main/setOnlyFavourites', val)
      }
    }
  },

  data () {
    return {
      today: '',
    }
  },

  created () {
    const days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday',
                  'Thursday', 'Friday', 'Saturday'];
    const months = ['January', 'February', 'March',
                    'April', 'May', 'June',
                    'July', 'August', 'September',
                    'October', 'November', 'December'];
    let day = new Date();
    this.today = days[day.getDay()] + ' ' + day.getDate() + ' ' + months[day.getMonth()];
  }

}
</script>

<template>
<q-layout view="lHh Lpr lFf">
  <q-header elevated>
    <q-toolbar class="bg-grey-2  text-primary">
      <q-btn type="a"
             href="https://www.scilifelab.se/data/"
             flat
             round>
	<q-avatar>
          <img :src="require('assets/sll-logo.svg')"
	       alt="Data Centre logo"/>
	</q-avatar>
      </q-btn>
      <q-toolbar-title>
	{{ today }}
      </q-toolbar-title>

      <q-space />

      <q-btn flat
             round
             icon="fab fa-github"
             type="a"
             href="https://github.com/ScilifelabDataCentre/lunch-menu"/>
    </q-toolbar>
  </q-header>
  <q-page-container>
    <router-view />
  </q-page-container>
</q-layout>
</template>

<script>
export default {
  name: 'MainLayout',

  computed: {
    showSolna: {
      get () {
        return this.$store.state.main.showSolna;
      },
      set (val) {
        this.$store.dispatch('main/setShowSolna', val)
      }
    },

    showUppsala: {
      get () {
        return this.$store.state.main.showUppsala;
      },
      set (val) {
        this.$store.dispatch('main/setShowUppsala', val)
      }
    },

    onlyFavourites: {
      get () {
        return this.$store.state.main.onlyFavourites;
      },
      set (val) {
        this.$store.dispatch('main/setOnlyFavourites', val)
      }
    }
  },

  data () {
    return {
      today: '',
    }
  },

  created () {
    const days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday',
                  'Thursday', 'Friday', 'Saturday'];
    const months = ['January', 'February', 'March',
                    'April', 'May', 'June',
                    'July', 'August', 'September',
                    'October', 'November', 'December'];
    let day = new Date();
    this.today = days[day.getDay()] + ' ' + day.getDate() + ' ' + months[day.getMonth()];
  }

}
</script>


Scroll

Scroll

<template>

Route Info

              Current route is broken!

              No matching route found :(

             No info

                        Methods

                        Middleware

                        Name

                        Action

</template>


mobile buttom (navigation drawer) - delete some style to let Bulma make work

<template>

      <router-link v-for="item in menu" :key="item.index" tag="li" :to="item.path" class="menu-item">

{{ item.name }}

          <router-link v-for="content in contents" :key="content['.key']" tag="li" :to="'/admin/content/' + content['.key']">

              {{ content.name }}

          </router-link>
          <router-link :to="item.dropdown[0].path" tag="li">
            {{item.dropdown.path}}

              {{ item.dropdown[0].name }}

          </router-link>

      </router-link>

{{ helpbtn.name }}

</template>


Moving observers to setTimeout (#2201)

Moving observers to setTimeout (#2201)

Co-authored-by: hastom <2c8a74993c3385c06a9a708847569030bc2411b9@onvis.ru>

<template>
  <md-popover :md-settings="popperSettings" :md-active="shouldRender">
    <transition name="md-menu-content" :css="didMount" v-if="shouldRender" v-on="$listeners">
      <div
        :class="[menuClasses, mdContentClass, $mdActiveTheme]"
        :style="menuStyles"
        ref="menu">
        <div class="md-menu-content-container md-scrollbar" :class="$mdActiveTheme" ref="container">
          <md-list :class="listClasses" v-bind="filteredAttrs">
            <slot />
          </md-list>
        </div>
      </div>
    </transition>
  </md-popover>
</template>

<script>
  import MdComponent from 'core/MdComponent'
  import MdObserveEvent from 'core/utils/MdObserveEvent'
  import MdResizeObserver from 'core/utils/MdResizeObserver'
  import MdPopover from 'components/MdPopover/MdPopover'
  import MdFocusTrap from 'components/MdFocusTrap/MdFocusTrap'
  import MdList from 'components/MdList/MdList'

  export default new MdComponent({
    name: 'MdMenuContent',
    components: {
      MdPopover,
      MdFocusTrap,
      MdList
    },
    props: {
      mdListClass: [String, Boolean],
      mdContentClass: [String, Boolean]
    },
    inject: ['MdMenu'],
    data: () => ({
      highlightIndex: -1,
      didMount: false,
      highlightItems: [],
      popperSettings: null,
      menuStyles: ''
    }),
    computed: {
      filteredAttrs () {
        const attrs = this.$attrs
        delete attrs.id
        return attrs
      },
      highlightedItem () {
        return this.highlightItems[this.highlightIndex]
      },
      shouldRender () {
        return this.MdMenu.active
      },
      menuClasses () {
        const prefix = 'md-menu-content-'

        return {
          [prefix + this.MdMenu.direction]: true,
          [prefix + this.MdMenu.size]: true,
          'md-menu-content': this.didMount,
          'md-shallow': !this.didMount
        }
      },
      listClasses () {
        return {
          'md-dense': this.MdMenu.dense,
          ...this.mdListClass
        }
      }
    },
    watch: {
      shouldRender (shouldRender) {
        if (shouldRender) {
          this.setPopperSettings()
          setTimeout(() => {
            this.setInitialHighlightIndex()
            this.createClickEventObserver()
            this.createResizeObserver()
            this.createKeydownListener()
          }, 0)
        }
      }
    },
    methods: {
      setPopperSettings () {
        const { direction, alignTrigger } = this.MdMenu

        let { offsetX, offsetY } = this.getOffsets()

        if (!this.hasCustomOffsets()) {
          if (this.MdMenu.instance.$el && this.MdMenu.instance.$el.offsetHeight) {
            offsetY = -this.MdMenu.instance.$el.offsetHeight - 11
          }

          if (direction.includes('start')) {
            offsetX = -8
          } else if (direction.includes('end')) {
            offsetX = 8
          }
        }

        this.popperSettings = {
          placement: direction,
          modifiers: {
            keepTogether: {
              enabled: true
            },
            flip: {
              enabled: false
            },
            offset: {
              offset: `${offsetX}, ${offsetY}`
            }
          }
        }
      },
      setInitialHighlightIndex () {
        this.setHighlightItems()
        this.highlightItems.forEach((item, index) => {
          if (item.classList.contains('md-selected')) {
            this.highlightIndex = index - 1
          }
        })
      },
      setHighlightItems () {
        if (this.$el.querySelectorAll) {
          const items = this.$el.querySelectorAll('.md-list-item-container:not(.md-list-item-default):not([disabled])')

          this.highlightItems = Array.from(items)
        }
      },
      setHighlight (direction) {
        this.setHighlightItems()

        if (this.highlightItems.length) {
          if (direction === 'down') {
            if (this.highlightIndex === this.highlightItems.length - 1) {
              this.highlightIndex = 0
            } else {
              this.highlightIndex++
            }
          } else {
            if (this.highlightIndex === 0) {
              this.highlightIndex = this.highlightItems.length - 1
            } else {
              this.highlightIndex--
            }
          }

          this.clearAllHighlights()
          this.setItemHighlight()
        }
      },
      clearAllHighlights () {
        this.highlightItems.forEach(item => {
          item.parentNode.__vue__.highlighted = false
        })
      },
      setItemHighlight () {
        if (this.highlightedItem) {
          this.highlightedItem.parentNode.__vue__.highlighted = true
          if (this.$parent.$parent.setOffsets) {
            this.$parent.$parent.setOffsets(this.highlightedItem.parentNode)
          }
        }
      },
      setSelection () {
        if (this.highlightedItem) {
          this.highlightedItem.parentNode.click()
        }
      },
      onEsc () {
        this.MdMenu.active = false
        this.destroyKeyDownListener()
      },
      getOffsets () {
        const relativePosition = this.getBodyPosition()

        const offsetX = this.MdMenu.offsetX || 0
        const offsetY = this.MdMenu.offsetY || 0

        return {
          offsetX: offsetX - relativePosition.x,
          offsetY: offsetY - relativePosition.y
        }
      },
      hasCustomOffsets () {
        const { offsetX, offsetY, alignTrigger } = this.MdMenu

        return Boolean(alignTrigger || offsetY || offsetX)
      },
      isMenu ({ target }) {
        return this.MdMenu.$el ? this.MdMenu.$el.contains(target) : false
      },
      isMenuContentEl ({ target }) {
        return this.$refs.menu ? this.$refs.menu.contains(target) : false
      },
      isBackdropExpectMenu ($event) {
        return !this.$el.contains($event.target) && !this.isMenu($event)
      },
      createClickEventObserver () {
        if (document) {
          this.MdMenu.bodyClickObserver = new MdObserveEvent(document.body, 'click', $event => {
            $event.stopPropagation()
            if (!this.isMenu($event) && (this.MdMenu.closeOnClick || !this.isMenuContentEl($event))) {
              this.MdMenu.active = false
              this.MdMenu.bodyClickObserver.destroy()
              this.MdMenu.windowResizeObserver.destroy()
              this.destroyKeyDownListener()
            }
          })
        }
      },
      createKeydownListener () {
        window.addEventListener('keydown', this.keyNavigation)
      },
      destroyKeyDownListener () {
        window.removeEventListener('keydown', this.keyNavigation)
      },
      keyNavigation (event) {
        switch (event.key) {
        case 'ArrowUp':
          event.preventDefault()
          this.setHighlight('up')
          break

        case 'ArrowDown':
          event.preventDefault()
          this.setHighlight('down')
          break

        case 'Enter':
          this.setSelection()
          break

        case 'Space':
          this.setSelection()
          break

        case 'Escape':
          this.onEsc()
        }
      },
      createResizeObserver () {
        this.MdMenu.windowResizeObserver = new MdResizeObserver(window, this.setStyles)
      },
      setupWatchers () {
        this.$watch('MdMenu.direction', this.setPopperSettings)
        this.$watch('MdMenu.alignTrigger', this.setPopperSettings)
        this.$watch('MdMenu.offsetX', this.setPopperSettings)
        this.$watch('MdMenu.offsetY', this.setPopperSettings)
      },
      setStyles () {
        if (this.MdMenu.fullWidth) {
          this.menuStyles = `
            width: ${this.MdMenu.instance.$el.offsetWidth}px;
            max-width: ${this.MdMenu.instance.$el.offsetWidth}px
          `
        }
      },
      getBodyPosition() {
        const body = document.body
        const { top, left } = body.getBoundingClientRect()

        const scrollLeft = window.pageXOffset !== undefined ? window.pageXOffset : body.scrollLeft
        const scrollTop = window.pageYOffset !== undefined ? window.pageYOffset : body.scrollTop

        return { x: left + scrollLeft, y: top + scrollTop }
      }
    },
    mounted () {
      this.$nextTick().then(() => {
        this.setHighlightItems()
        this.setupWatchers()
        this.setStyles()
        this.didMount = true
      })
    },
    beforeDestroy () {
      if (this.MdMenu.bodyClickObserver) {
        this.MdMenu.bodyClickObserver.destroy()
      }

      if (this.MdMenu.windowResizeObserver) {
        this.MdMenu.windowResizeObserver.destroy()
      }
      this.destroyKeyDownListener()
    }
  })
</script>

<style lang="scss">
  @import "~components/MdAnimation/variables";
  @import "~components/MdElevation/mixins";
  @import "~components/MdLayout/mixins";

  $md-menu-base-width: 56px;

  .md-menu-content {
    @include md-elevation(8);
    min-width: $md-menu-base-width * 2;
    max-width: $md-menu-base-width * 5;
    max-height: 35vh;
    display: flex;
    flex-direction: row;
    position: absolute;
    z-index: 60;
    border-radius: 2px;
    transition: transform .2s $md-transition-stand-timing,
                opacity .3s $md-transition-stand-timing;
    will-change: opacity, transform, top, left !important;

    &.md-shallow {
      position: fixed !important;
      top: -9999em !important;
      left: -9999em !important;
      pointer-events: none;
    }

    &.md-menu-content-enter-active {
      opacity: 1;
      transform: translate3d(0, 0, 0);
    }

    &.md-menu-content-leave-active {
      transition: opacity .4s $md-transition-default-timing;
      opacity: 0;
    }

    &.md-menu-content-enter {
      &.md-menu-content-top-start {
        transform-origin: bottom left;
        transform: translate3d(0, 8px, 0) scaleY(.95);
      }

      &.md-menu-content-top-end {
        transform-origin: bottom right;
        transform: translate3d(0, 8px, 0) scaleY(.95);
      }

      &.md-menu-content-right-start {
        transform-origin: left top;
        transform: translate3d(0, -8px, 0) scaleY(.95);
      }

      &.md-menu-content-right-end {
        transform-origin: left bottom;
        transform: translate3d(0, 8px, 0) scaleY(.95);
      }

      &.md-menu-content-bottom-start {
        transform-origin: top left;
        transform: translate3d(0, -8px, 0) scaleY(.95);
      }

      &.md-menu-content-bottom-end {
        transform-origin: top right;
        transform: translate3d(0, -8px, 0) scaleY(.95);
      }

      &.md-menu-content-left-start {
        transform-origin: right top;
        transform: translate3d(0, -8px, 0) scaleY(.95);
      }

      &.md-menu-content-left-end {
        transform-origin: right bottom;
        transform: translate3d(0, 8px, 0) scaleY(.95);
      }

      .md-list {
        opacity: 0;
      }
    }

    &.md-menu-content-medium {
      min-width: $md-menu-base-width * 3;
    }

    &.md-menu-content-big {
      min-width: $md-menu-base-width * 4;
    }

    &.md-menu-content-huge {
      min-width: $md-menu-base-width * 5;
    }
  }

  .md-menu-content-container {
    flex: 1;
    overflow: auto;

    .md-list {
      transition: opacity .3s $md-transition-stand-timing;
      will-change: opacity;
      font-family: 'Roboto', sans-serif;
      text-transform: none;
      white-space: nowrap;

      .md-list-item-container {
        height: 100%;
      }

      @include md-layout-small {
        font-size: 14px;
      }
    }
  }
</style>

<template>
  <md-popover :md-settings="popperSettings" :md-active="shouldRender">
    <transition name="md-menu-content" :css="didMount" v-if="shouldRender" v-on="$listeners">
      <div
        :class="[menuClasses, mdContentClass, $mdActiveTheme]"
        :style="menuStyles"
        ref="menu">
        <div class="md-menu-content-container md-scrollbar" :class="$mdActiveTheme" ref="container">
          <md-list :class="listClasses" v-bind="filteredAttrs">
            <slot />
          </md-list>
        </div>
      </div>
    </transition>
  </md-popover>
</template>

<script>
  import MdComponent from 'core/MdComponent'
  import MdPropValidator from 'core/utils/MdPropValidator'
  import MdObserveEvent from 'core/utils/MdObserveEvent'
  import MdResizeObserver from 'core/utils/MdResizeObserver'
  import MdPopover from 'components/MdPopover/MdPopover'
  import MdFocusTrap from 'components/MdFocusTrap/MdFocusTrap'
  import MdList from 'components/MdList/MdList'

  export default new MdComponent({
    name: 'MdMenuContent',
    components: {
      MdPopover,
      MdFocusTrap,
      MdList
    },
    props: {
      mdListClass: [String, Boolean],
      mdContentClass: [String, Boolean]
    },
    inject: ['MdMenu'],
    data: () => ({
      highlightIndex: -1,
      didMount: false,
      highlightItems: [],
      popperSettings: null,
      menuStyles: ''
    }),
    computed: {
      filteredAttrs () {
        const attrs = this.$attrs
        delete attrs.id
        return attrs
      },
      highlightedItem () {
        return this.highlightItems[this.highlightIndex]
      },
      shouldRender () {
        return this.MdMenu.active
      },
      menuClasses () {
        const prefix = 'md-menu-content-'

        return {
          [prefix + this.MdMenu.direction]: true,
          [prefix + this.MdMenu.size]: true,
          'md-menu-content': this.didMount,
          'md-shallow': !this.didMount
        }
      },
      listClasses () {
        return {
          'md-dense': this.MdMenu.dense,
          ...this.mdListClass
        }
      }
    },
    watch: {
      shouldRender (shouldRender) {
        if (shouldRender) {
          this.setPopperSettings()

          this.$nextTick().then(() => {
            this.setInitialHighlightIndex()
            this.createClickEventObserver()
            this.createResizeObserver()
            this.createKeydownListener()
          })
        }
      }
    },
    methods: {
      setPopperSettings () {
        const { direction, alignTrigger } = this.MdMenu

        let { offsetX, offsetY } = this.getOffsets()

        if (!this.hasCustomOffsets()) {
          if (this.MdMenu.instance.$el && this.MdMenu.instance.$el.offsetHeight) {
            offsetY = -this.MdMenu.instance.$el.offsetHeight - 11
          }

          if (direction.includes('start')) {
            offsetX = -8
          } else if (direction.includes('end')) {
            offsetX = 8
          }
        }

        this.popperSettings = {
          placement: direction,
          modifiers: {
            keepTogether: {
              enabled: true
            },
            flip: {
              enabled: false
            },
            offset: {
              offset: `${offsetX}, ${offsetY}`
            }
          }
        }
      },
      setInitialHighlightIndex () {
        this.setHighlightItems()
        this.highlightItems.forEach((item, index) => {
          if (item.classList.contains('md-selected')) {
            this.highlightIndex = index - 1
          }
        })
      },
      setHighlightItems () {
        if (this.$el.querySelectorAll) {
          const items = this.$el.querySelectorAll('.md-list-item-container:not(.md-list-item-default):not([disabled])')

          this.highlightItems = Array.from(items)
        }
      },
      setHighlight (direction) {
        this.setHighlightItems()

        if (this.highlightItems.length) {
          if (direction === 'down') {
            if (this.highlightIndex === this.highlightItems.length - 1) {
              this.highlightIndex = 0
            } else {
              this.highlightIndex++
            }
          } else {
            if (this.highlightIndex === 0) {
              this.highlightIndex = this.highlightItems.length - 1
            } else {
              this.highlightIndex--
            }
          }

          this.clearAllHighlights()
          this.setItemHighlight()
        }
      },
      clearAllHighlights () {
        this.highlightItems.forEach(item => {
          item.parentNode.__vue__.highlighted = false
        })
      },
      setItemHighlight () {
        if (this.highlightedItem) {
          this.highlightedItem.parentNode.__vue__.highlighted = true
          if (this.$parent.$parent.setOffsets) {
            this.$parent.$parent.setOffsets(this.highlightedItem.parentNode)
          }
        }
      },
      setSelection () {
        if (this.highlightedItem) {
          this.highlightedItem.parentNode.click()
        }
      },
      onEsc () {
        this.MdMenu.active = false
        this.destroyKeyDownListener()
      },
      getOffsets () {
        const relativePosition = this.getBodyPosition()

        const offsetX = this.MdMenu.offsetX || 0
        const offsetY = this.MdMenu.offsetY || 0

        return {
          offsetX: offsetX - relativePosition.x,
          offsetY: offsetY - relativePosition.y
        }
      },
      hasCustomOffsets () {
        const { offsetX, offsetY, alignTrigger } = this.MdMenu

        return Boolean(alignTrigger || offsetY || offsetX)
      },
      isMenu ({ target }) {
        return this.MdMenu.$el ? this.MdMenu.$el.contains(target) : false
      },
      isMenuContentEl ({ target }) {
        return this.$refs.menu ? this.$refs.menu.contains(target) : false
      },
      isBackdropExpectMenu ($event) {
        return !this.$el.contains($event.target) && !this.isMenu($event)
      },
      createClickEventObserver () {
        if (document) {
          this.MdMenu.bodyClickObserver = new MdObserveEvent(document.body, 'click', $event => {
            $event.stopPropagation()

            if (!this.isMenuContentEl($event) && (this.MdMenu.closeOnClick || this.isBackdropExpectMenu($event))) {
              this.MdMenu.active = false
              this.MdMenu.bodyClickObserver.destroy()
              this.MdMenu.windowResizeObserver.destroy()
              this.destroyKeyDownListener()
            }
          })
        }
      },
      createKeydownListener () {
        window.addEventListener('keydown', this.keyNavigation)
      },
      destroyKeyDownListener () {
        window.removeEventListener('keydown', this.keyNavigation)
      },
      keyNavigation (event) {
        switch (event.key) {
        case 'ArrowUp':
          event.preventDefault()
          this.setHighlight('up')
          break

        case 'ArrowDown':
          event.preventDefault()
          this.setHighlight('down')
          break

        case 'Enter':
          this.setSelection()
          break

        case 'Space':
          this.setSelection()
          break

        case 'Escape':
          this.onEsc()
        }
      },
      createResizeObserver () {
        this.MdMenu.windowResizeObserver = new MdResizeObserver(window, this.setStyles)
      },
      setupWatchers () {
        this.$watch('MdMenu.direction', this.setPopperSettings)
        this.$watch('MdMenu.alignTrigger', this.setPopperSettings)
        this.$watch('MdMenu.offsetX', this.setPopperSettings)
        this.$watch('MdMenu.offsetY', this.setPopperSettings)
      },
      setStyles () {
        if (this.MdMenu.fullWidth) {
          this.menuStyles = `
            width: ${this.MdMenu.instance.$el.offsetWidth}px;
            max-width: ${this.MdMenu.instance.$el.offsetWidth}px
          `
        }
      },
      getBodyPosition() {
        const body = document.body
        const { top, left } = body.getBoundingClientRect()

        const scrollLeft = window.pageXOffset !== undefined ? window.pageXOffset : body.scrollLeft
        const scrollTop = window.pageYOffset !== undefined ? window.pageYOffset : body.scrollTop

        return { x: left + scrollLeft, y: top + scrollTop }
      }
    },
    mounted () {
      this.$nextTick().then(() => {
        this.setHighlightItems()
        this.setupWatchers()
        this.setStyles()
        this.didMount = true
      })
    },
    beforeDestroy () {
      if (this.MdMenu.bodyClickObserver) {
        this.MdMenu.bodyClickObserver.destroy()
      }

      if (this.MdMenu.windowResizeObserver) {
        this.MdMenu.windowResizeObserver.destroy()
      }
      this.destroyKeyDownListener()
    }
  })
</script>

<style lang="scss">
  @import "~components/MdAnimation/variables";
  @import "~components/MdElevation/mixins";
  @import "~components/MdLayout/mixins";

  $md-menu-base-width: 56px;

  .md-menu-content {
    @include md-elevation(8);
    min-width: $md-menu-base-width * 2;
    max-width: $md-menu-base-width * 5;
    max-height: 35vh;
    display: flex;
    flex-direction: row;
    position: absolute;
    z-index: 60;
    border-radius: 2px;
    transition: transform .2s $md-transition-stand-timing,
                opacity .3s $md-transition-stand-timing;
    will-change: opacity, transform, top, left !important;

    &.md-shallow {
      position: fixed !important;
      top: -9999em !important;
      left: -9999em !important;
      pointer-events: none;
    }

    &.md-menu-content-enter-active {
      opacity: 1;
      transform: translate3d(0, 0, 0);
    }

    &.md-menu-content-leave-active {
      transition: opacity .4s $md-transition-default-timing;
      opacity: 0;
    }

    &.md-menu-content-enter {
      &.md-menu-content-top-start {
        transform-origin: bottom left;
        transform: translate3d(0, 8px, 0) scaleY(.95);
      }

      &.md-menu-content-top-end {
        transform-origin: bottom right;
        transform: translate3d(0, 8px, 0) scaleY(.95);
      }

      &.md-menu-content-right-start {
        transform-origin: left top;
        transform: translate3d(0, -8px, 0) scaleY(.95);
      }

      &.md-menu-content-right-end {
        transform-origin: left bottom;
        transform: translate3d(0, 8px, 0) scaleY(.95);
      }

      &.md-menu-content-bottom-start {
        transform-origin: top left;
        transform: translate3d(0, -8px, 0) scaleY(.95);
      }

      &.md-menu-content-bottom-end {
        transform-origin: top right;
        transform: translate3d(0, -8px, 0) scaleY(.95);
      }

      &.md-menu-content-left-start {
        transform-origin: right top;
        transform: translate3d(0, -8px, 0) scaleY(.95);
      }

      &.md-menu-content-left-end {
        transform-origin: right bottom;
        transform: translate3d(0, 8px, 0) scaleY(.95);
      }

      .md-list {
        opacity: 0;
      }
    }

    &.md-menu-content-medium {
      min-width: $md-menu-base-width * 3;
    }

    &.md-menu-content-big {
      min-width: $md-menu-base-width * 4;
    }

    &.md-menu-content-huge {
      min-width: $md-menu-base-width * 5;
    }
  }

  .md-menu-content-container {
    flex: 1;
    overflow: auto;

    .md-list {
      transition: opacity .3s $md-transition-stand-timing;
      will-change: opacity;
      font-family: 'Roboto', sans-serif;
      text-transform: none;
      white-space: nowrap;

      .md-list-item-container {
        height: 100%;
      }

      @include md-layout-small {
        font-size: 14px;
      }
    }
  }
</style>


update

update

<template>

      实时曲线

        选择曲线参数

          <select v-model="selected">
            <option>A</option>
            <option>B</option>
            <option>C</option>
          </select>

</template>

<template>
  <div>
    wqwqwqwe245656
  </div>
</template>

<script>
  export default {
    data () {
      return {

      }
    }
  }
</script>


undo mistaken removal of Recipients component

<template>

  <span>
    <template v-if="groupNames.length">
      <TruncatedItemList :items="groupNames" />
    </template>
    <template v-else-if="hasAssignments">
      {{ $tr('assignmentClass') }}
    </template>
    <template v-else>
      {{ $tr('noOne') }}
    </template>
  </span>

</template>

<script>
  import TruncatedItemList from './TruncatedItemList';

  export default {
    name: 'Recipients',
    components: {
      TruncatedItemList,
    },
    props: {
      groupNames: {
        type: Array,
        required: true,
      },
      hasAssignments: {
        type: Boolean,
        required: true,
      },
    },
    $trs: {
      assignmentClass: 'Entire class',
      noOne: 'No one',
    },
  };

</script>

<style lang="scss" scoped></style>

<template>

  <span>
    <template v-if="groupNames.length">
      <TruncatedItemList :items="groupNames" />
    </template>
    <template v-else-if="hasAssignments">
      {{ $tr('assignmentClass') }}
    </template>
    <template v-else>
      {{ $tr('noOne') }}
    </template>
  </span>

</template>

<script>

  export default {
    name: 'Recipients',
    props: {
      groupNames: {
        type: Array,
        required: true,
      },
    },
    $trs: {
      assignmentClass: 'Entire class',
      noOne: 'No one',
    },
  };

</script>

<style lang="scss" scoped></style>


Fixed search throttle.

Fixed search throttle.

<template>

            <input
              type="search"
              ref="search"
              aria-label="Type to find content"
              placeholder="Find content..."
              autocomplete="off"
              v-focus="searchOpen"
              v-model="localSearchTerm"
              id="search"
              name="search"
              @keyup="search()"
              @keydown.esc.prevent="clear()"
            >
            <button
              aria-label="Reset"
              class="reset"
              type="reset"
              @click="clear()"
              :style="{ visibility: localSearchTerm ? 'inherit' : 'hidden' }"
            >
              <svg class="clear-icon" src="./clear.svg"></svg>
            </button>

            <button @click="toggleSearch" class="search-btn">Cancel</button>

        {{ message }}

        Topic

      <card-list class="card-list" v-if="topics.length && showTopics">
        <topic-list-item
          v-for="topic in topics"
          class="card"
          :id="topic.id"
          :title="topic.title"
          :ntotal="topic.n_total"
          :ncomplete="topic.n_complete">
        </topic-list-item>
      </card-list>

        Content

      <card-grid v-if="contents.length">
        <content-grid-item
          v-for="content in contents"
          class="card"
          :title="content.title"
          :thumbnail="content.thumbnail"
          :kind="content.kind"
          :progress="content.progress"
          :id="content.id">
        </content-grid-item>
      </card-grid>

</template>


Adjust left margin for btn-text so collectively it looks more centered

<template>

  <button class="icon-button">
    <slot></slot>
    {{ text }}
  </button>

</template>