Fix ‘repeat’ and ‘eraseStyle’ prop defaults

<template lang='pug'>
//- TODO:
//- Ideally we'd just have span.left and span.right contain all the chars to the left and
//- right of the cursor, but line-wrapping becomes tricky on some browsers (FF/IE/Edge).
//- Until we can find a solution for this, we just create one span per character.
span.vue-typer
  span.left
    span.char.custom.typed(v-for='l in numLeftChars') {{ currentTextArray[l-1] }}
  caret(:class='caretClasses', :animation='caretAnimation')
  span.right
    span.char.custom(v-for='r in numRightChars', :class='rightCharClasses') {{ currentTextArray[numLeftChars + r-1] }}
</template>

<script>
import Caret from './Caret'
import shuffle from 'utils/shuffle'
import { nonNegativeNumberValidator,
         stringArrayValidator } from 'utils/validators'

const STATE = {
  IDLE: 'idle',
  TYPING: 'typing',
  ERASING: 'erasing',
  COMPLETE: 'complete'
}

const ERASE_STYLE = {
  BACKSPACE: 'backspace',
  SELECT_BACK: 'select-back',
  SELECT_ALL: 'select-all',
  CLEAR: 'clear'
}

export default {
  components: {
    Caret
  },
  props: {
    /* GENERAL */
    text: {
      type: [String, Array],
      required: true,
      validator(value) {
        if (typeof value === 'string') {
          return value.length
        }
        return stringArrayValidator(value)
      }
    },
    repeat: {
      type: Number,
      default: Infinity,
      validator: nonNegativeNumberValidator
    },
    shuffle: {
      type: Boolean,
      default: false
    },
    initialAction: {
      type: String,
      default: STATE.TYPING,
      validator(value) {
        return value === STATE.TYPING || value === STATE.ERASING
      }
    },
    /* TYPE */
    typeDelay: {
      type: Number,
      default: 70,
      validator: nonNegativeNumberValidator
    },
    preTypeDelay: {
      type: Number,
      default: 70,
      validator: nonNegativeNumberValidator
    },
    /* ERASE */
    eraseDelay: {
      type: Number,
      default: 250,
      validator: nonNegativeNumberValidator
    },
    preEraseDelay: {
      type: Number,
      default: 2000,
      validator: nonNegativeNumberValidator
    },
    eraseStyle: {
      type: String,
      default: ERASE_STYLE.SELECT_ALL,
      validator() {
        // TODO
        return true
      }
    },
    eraseFinalText: {
      type: Boolean,
      default: false
    },
    /* CARET */
    caretAnimation: String
  },
  data() {
    return {
      state: STATE.IDLE,
      repeatCounter: 0,
      actionTimeout: 0,
      actionInterval: 0,

      spool: [],
      spoolIndex: -1,
      previousTextIndex: -1,
      currentTextIndex: -1
    }
  },
  computed: {
    caretClasses() {
      return {
        idle: this.state === STATE.IDLE,
        typing: this.state === STATE.TYPING,
        selecting: this.state === STATE.ERASING && this.isSelectionBasedEraseStyle,
        erasing: this.state === STATE.ERASING && !this.isSelectionBasedEraseStyle,
        complete: this.state === STATE.COMPLETE
      }
    },
    rightCharClasses() {
      return {
        selected: this.state === STATE.ERASING && this.isSelectionBasedEraseStyle,
        erased: this.state === STATE.IDLE ||
                this.state === STATE.TYPING ||
                this.state === STATE.COMPLETE ||
                this.state === STATE.ERASING && !this.isSelectionBasedEraseStyle
      }
    },
    isSelectionBasedEraseStyle() {
      return !!this.eraseStyle.match(`^${ERASE_STYLE.SELECT_BACK}|${ERASE_STYLE.SELECT_ALL}$`)
    },
    isEraseAllStyle() {
      return !!this.eraseStyle.match(`^${ERASE_STYLE.CLEAR}|${ERASE_STYLE.SELECT_ALL}$`)
    },
    isDoneTyping() {
      return this.currentTextIndex >= this.currentText.length
    },
    isDoneErasing() {
      // Selection-based erase styles must stay in the highlight stage for one iteration before erasing is finished.
      if (this.isSelectionBasedEraseStyle) {
        return this.currentTextIndex <= 0 && this.previousTextIndex <= 0
      }
      return this.currentTextIndex <= 0
    },
    onLastWord() {
      return this.spoolIndex === this.spool.length - 1
    },
    shouldRepeat() {
      return this.repeatCounter < this.repeat
    },
    currentText() {
      if (this.spoolIndex >= 0 && this.spoolIndex < this.spool.length) {
        return this.spool[this.spoolIndex]
      }
      return ''
    },
    currentTextArray() {
      return this.currentText.split('')
    },
    numLeftChars() {
      return this.currentTextIndex < 0 ? 0 : this.currentTextIndex
    },
    numRightChars() {
      return this.currentText.length - this.numLeftChars
    }
  },
  mounted() {
    this.init()
  },
  beforeDestroy() {
    this.cancelCurrentAction()
  },
  methods: {
    init() {
      // Process the 'text' prop into a typing spool
      if (typeof this.text === 'string') {
        this.spool = [this.text]
      } else {
        // Don't violate one-way binding, make a copy! Vue doesn't make a copy for us to keep things reactive
        let textCopy = this.text.slice()

        textCopy = textCopy.filter(textToType => textToType.length)

        if (this.shuffle && textCopy.length > 1) {
          shuffle(textCopy)
        }

        this.spool = textCopy
      }

      this.spoolIndex = 0
      this.repeatCounter = 0

      if (this.initialAction === STATE.TYPING) {
        this.startTyping()
      } else if (this.initialAction === STATE.ERASING) {
        // This is a special case when we start off in erasing mode. The first text is already considered typed, and
        // it may even be the only text in the spool. So don't jump directly into erasing mode (in-case 'repeat' and
        // 'eraseFinalText' are configured to false), and instead jump to the "we just finished typing a word" phase.
        this.moveCursorToEnd()
        this.onTyped()
      }
    },
    reset() {
      this.cancelCurrentAction()
      this.init()
    },
    cancelCurrentAction() {
      if (this.actionInterval) {
        clearInterval(this.actionInterval)
        this.actionInterval = 0
      }
      if (this.actionTimeout) {
        clearTimeout(this.actionTimeout)
        this.actionTimeout = 0
      }
    },
    shiftCursor(delta) {
      this.previousTextIndex = this.currentTextIndex
      const newCursorIndex = this.currentTextIndex + delta
      this.currentTextIndex = Math.min(Math.max(newCursorIndex, 0), this.currentText.length)
    },
    moveCursorToStart() {
      this.previousTextIndex = this.currentTextIndex
      this.currentTextIndex = 0
    },
    moveCursorToEnd() {
      this.previousTextIndex = this.currentTextIndex
      this.currentTextIndex = this.currentText.length
    },
    typeStep() {
      if (!this.isDoneTyping) {
        this.shiftCursor(1)
      }

      if (this.isDoneTyping) {
        this.cancelCurrentAction()
        // Ensure the last typed character is rendered before proceeding
        // Note that $nextTick is not required after typing the previous characters due to setInterval
        this.$nextTick(this.onTyped)
      }
    },
    eraseStep() {
      if (!this.isDoneErasing) {
        if (this.isEraseAllStyle) {
          this.moveCursorToStart()
        } else {
          this.shiftCursor(-1)
        }
      }

      if (this.isDoneErasing) {
        this.cancelCurrentAction()
        // Ensure every last character is 'erased' in the DOM before proceeding
        this.$nextTick(this.onErased)
      }
    },
    startTyping() {
      if (this.actionTimeout || this.actionInterval) {
        return
      }

      this.moveCursorToStart()

      this.state = STATE.IDLE
      this.actionTimeout = setTimeout(() => {
        this.state = STATE.TYPING
        this.typeStep()
        if (!this.isDoneTyping) {
          this.actionInterval = setInterval(this.typeStep, this.typeDelay)
        }
      }, this.preTypeDelay)
    },
    startErasing() {
      if (this.actionTimeout || this.actionInterval) {
        return
      }

      this.moveCursorToEnd()

      this.state = STATE.IDLE
      this.actionTimeout = setTimeout(() => {
        this.state = STATE.ERASING
        this.eraseStep()
        if (!this.isDoneErasing) {
          this.actionInterval = setInterval(this.eraseStep, this.eraseDelay)
        }
      }, this.preEraseDelay)
    },
    onTyped() {
      this.$emit('typed', this.currentText)

      if (this.onLastWord) {
        if (this.eraseFinalText || this.shouldRepeat) {
          this.startErasing()
        } else {
          this.onComplete()
        }
      } else {
        this.startErasing()
      }
    },
    onErased() {
      this.$emit('erased', this.currentText)

      if (this.onLastWord) {
        if (this.shouldRepeat) {
          this.repeatCounter++
          this.spoolIndex = 0
          this.startTyping()
        } else {
          this.onComplete()
        }
      } else {
        this.spoolIndex++
        this.startTyping()
      }
    },
    onComplete() {
      this.state = STATE.COMPLETE
      this.$emit('complete')
    }
  },
  watch: {
    text() {
      this.reset()
    },
    repeat() {
      this.reset()
    },
    shuffle() {
      this.reset()
    }
  }
}
</script>

<style scoped lang='scss'>
@import 'colors';

span.vue-typer {
  word-break: break-all;

  span.char {
    white-space: pre-wrap;
  }

  span.left, span.right {
    display: inline;
  }
}

/* Keep the following .custom.char styles as low-specificity as possible so they are more easily overridden */
span {
  display: inline-block;
}

.typed {
  color: $char-typed-color;
  background-color: $char-typed-background-color;
}

.selected {
  color: $char-selected-color;
  background-color: $char-selected-background-color;
}

.erased {
  display: none;
}
</style>

<template lang='pug'>
//- TODO:
//- Ideally we'd just have span.left and span.right contain all the chars to the left and
//- right of the cursor, but line-wrapping becomes tricky on some browsers (FF/IE/Edge).
//- Until we can find a solution for this, we just create one span per character.
span.vue-typer
  span.left
    span.char.custom.typed(v-for='l in numLeftChars') {{ currentTextArray[l-1] }}
  caret(:class='caretClasses', :animation='caretAnimation')
  span.right
    span.char.custom(v-for='r in numRightChars', :class='rightCharClasses') {{ currentTextArray[numLeftChars + r-1] }}
</template>

<script>
import Caret from './Caret'
import shuffle from 'utils/shuffle'
import { nonNegativeNumberValidator,
         stringArrayValidator } from 'utils/validators'

const STATE = {
  IDLE: 'idle',
  TYPING: 'typing',
  ERASING: 'erasing',
  COMPLETE: 'complete'
}

const ERASE_STYLE = {
  BACKSPACE: 'backspace',
  SELECT_BACK: 'select-back',
  SELECT_ALL: 'select-all',
  CLEAR: 'clear'
}

export default {
  components: {
    Caret
  },
  props: {
    /* GENERAL */
    text: {
      type: [String, Array],
      required: true,
      validator(value) {
        if (typeof value === 'string') {
          return value.length
        }
        return stringArrayValidator(value)
      }
    },
    repeat: {
      type: Number,
      default: 0,
      validator: nonNegativeNumberValidator
    },
    shuffle: {
      type: Boolean,
      default: false
    },
    initialAction: {
      type: String,
      default: STATE.TYPING,
      validator(value) {
        return value === STATE.TYPING || value === STATE.ERASING
      }
    },
    /* TYPE */
    typeDelay: {
      type: Number,
      default: 70,
      validator: nonNegativeNumberValidator
    },
    preTypeDelay: {
      type: Number,
      default: 70,
      validator: nonNegativeNumberValidator
    },
    /* ERASE */
    eraseDelay: {
      type: Number,
      default: 250,
      validator: nonNegativeNumberValidator
    },
    preEraseDelay: {
      type: Number,
      default: 2000,
      validator: nonNegativeNumberValidator
    },
    eraseStyle: {
      type: String,
      default: ERASE_STYLE.BACKSPACE,
      validator() {
        // TODO
        return true
      }
    },
    eraseFinalText: {
      type: Boolean,
      default: false
    },
    /* CARET */
    caretAnimation: String
  },
  data() {
    return {
      state: STATE.IDLE,
      repeatCounter: 0,
      actionTimeout: 0,
      actionInterval: 0,

      spool: [],
      spoolIndex: -1,
      previousTextIndex: -1,
      currentTextIndex: -1
    }
  },
  computed: {
    caretClasses() {
      return {
        idle: this.state === STATE.IDLE,
        typing: this.state === STATE.TYPING,
        selecting: this.state === STATE.ERASING && this.isSelectionBasedEraseStyle,
        erasing: this.state === STATE.ERASING && !this.isSelectionBasedEraseStyle,
        complete: this.state === STATE.COMPLETE
      }
    },
    rightCharClasses() {
      return {
        selected: this.state === STATE.ERASING && this.isSelectionBasedEraseStyle,
        erased: this.state === STATE.IDLE ||
                this.state === STATE.TYPING ||
                this.state === STATE.COMPLETE ||
                this.state === STATE.ERASING && !this.isSelectionBasedEraseStyle
      }
    },
    isSelectionBasedEraseStyle() {
      return !!this.eraseStyle.match(`^${ERASE_STYLE.SELECT_BACK}|${ERASE_STYLE.SELECT_ALL}$`)
    },
    isEraseAllStyle() {
      return !!this.eraseStyle.match(`^${ERASE_STYLE.CLEAR}|${ERASE_STYLE.SELECT_ALL}$`)
    },
    isDoneTyping() {
      return this.currentTextIndex >= this.currentText.length
    },
    isDoneErasing() {
      // Selection-based erase styles must stay in the highlight stage for one iteration before erasing is finished.
      if (this.isSelectionBasedEraseStyle) {
        return this.currentTextIndex <= 0 && this.previousTextIndex <= 0
      }
      return this.currentTextIndex <= 0
    },
    onLastWord() {
      return this.spoolIndex === this.spool.length - 1
    },
    shouldRepeat() {
      return this.repeatCounter < this.repeat
    },
    currentText() {
      if (this.spoolIndex >= 0 && this.spoolIndex < this.spool.length) {
        return this.spool[this.spoolIndex]
      }
      return ''
    },
    currentTextArray() {
      return this.currentText.split('')
    },
    numLeftChars() {
      return this.currentTextIndex < 0 ? 0 : this.currentTextIndex
    },
    numRightChars() {
      return this.currentText.length - this.numLeftChars
    }
  },
  mounted() {
    this.init()
  },
  beforeDestroy() {
    this.cancelCurrentAction()
  },
  methods: {
    init() {
      // Process the 'text' prop into a typing spool
      if (typeof this.text === 'string') {
        this.spool = [this.text]
      } else {
        // Don't violate one-way binding, make a copy! Vue doesn't make a copy for us to keep things reactive
        let textCopy = this.text.slice()

        textCopy = textCopy.filter(textToType => textToType.length)

        if (this.shuffle && textCopy.length > 1) {
          shuffle(textCopy)
        }

        this.spool = textCopy
      }

      this.spoolIndex = 0
      this.repeatCounter = 0

      if (this.initialAction === STATE.TYPING) {
        this.startTyping()
      } else if (this.initialAction === STATE.ERASING) {
        // This is a special case when we start off in erasing mode. The first text is already considered typed, and
        // it may even be the only text in the spool. So don't jump directly into erasing mode (in-case 'repeat' and
        // 'eraseFinalText' are configured to false), and instead jump to the "we just finished typing a word" phase.
        this.moveCursorToEnd()
        this.onTyped()
      }
    },
    reset() {
      this.cancelCurrentAction()
      this.init()
    },
    cancelCurrentAction() {
      if (this.actionInterval) {
        clearInterval(this.actionInterval)
        this.actionInterval = 0
      }
      if (this.actionTimeout) {
        clearTimeout(this.actionTimeout)
        this.actionTimeout = 0
      }
    },
    shiftCursor(delta) {
      this.previousTextIndex = this.currentTextIndex
      const newCursorIndex = this.currentTextIndex + delta
      this.currentTextIndex = Math.min(Math.max(newCursorIndex, 0), this.currentText.length)
    },
    moveCursorToStart() {
      this.previousTextIndex = this.currentTextIndex
      this.currentTextIndex = 0
    },
    moveCursorToEnd() {
      this.previousTextIndex = this.currentTextIndex
      this.currentTextIndex = this.currentText.length
    },
    typeStep() {
      if (!this.isDoneTyping) {
        this.shiftCursor(1)
      }

      if (this.isDoneTyping) {
        this.cancelCurrentAction()
        // Ensure the last typed character is rendered before proceeding
        // Note that $nextTick is not required after typing the previous characters due to setInterval
        this.$nextTick(this.onTyped)
      }
    },
    eraseStep() {
      if (!this.isDoneErasing) {
        if (this.isEraseAllStyle) {
          this.moveCursorToStart()
        } else {
          this.shiftCursor(-1)
        }
      }

      if (this.isDoneErasing) {
        this.cancelCurrentAction()
        // Ensure every last character is 'erased' in the DOM before proceeding
        this.$nextTick(this.onErased)
      }
    },
    startTyping() {
      if (this.actionTimeout || this.actionInterval) {
        return
      }

      this.moveCursorToStart()

      this.state = STATE.IDLE
      this.actionTimeout = setTimeout(() => {
        this.state = STATE.TYPING
        this.typeStep()
        if (!this.isDoneTyping) {
          this.actionInterval = setInterval(this.typeStep, this.typeDelay)
        }
      }, this.preTypeDelay)
    },
    startErasing() {
      if (this.actionTimeout || this.actionInterval) {
        return
      }

      this.moveCursorToEnd()

      this.state = STATE.IDLE
      this.actionTimeout = setTimeout(() => {
        this.state = STATE.ERASING
        this.eraseStep()
        if (!this.isDoneErasing) {
          this.actionInterval = setInterval(this.eraseStep, this.eraseDelay)
        }
      }, this.preEraseDelay)
    },
    onTyped() {
      this.$emit('typed', this.currentText)

      if (this.onLastWord) {
        if (this.eraseFinalText || this.shouldRepeat) {
          this.startErasing()
        } else {
          this.onComplete()
        }
      } else {
        this.startErasing()
      }
    },
    onErased() {
      this.$emit('erased', this.currentText)

      if (this.onLastWord) {
        if (this.shouldRepeat) {
          this.repeatCounter++
          this.spoolIndex = 0
          this.startTyping()
        } else {
          this.onComplete()
        }
      } else {
        this.spoolIndex++
        this.startTyping()
      }
    },
    onComplete() {
      this.state = STATE.COMPLETE
      this.$emit('complete')
    }
  },
  watch: {
    text() {
      this.reset()
    },
    repeat() {
      this.reset()
    },
    shuffle() {
      this.reset()
    }
  }
}
</script>

<style scoped lang='scss'>
@import 'colors';

span.vue-typer {
  word-break: break-all;

  span.char {
    white-space: pre-wrap;
  }

  span.left, span.right {
    display: inline;
  }
}

/* Keep the following .custom.char styles as low-specificity as possible so they are more easily overridden */
span {
  display: inline-block;
}

.typed {
  color: $char-typed-color;
  background-color: $char-typed-background-color;
}

.selected {
  color: $char-selected-color;
  background-color: $char-selected-background-color;
}

.erased {
  display: none;
}
</style>


tempSelect使用4个Unicode字符实现，完成部分样式

<template>

{{planTittle}}

Done

Edit

Submit

              日期

                <input type="text" class="name-input" v-model="tempName">

              {{u.name}}

                {{item.month}} /{{item.day}}

                  ☑
                  ☐

✔

✖

✔

✖

</template>

<template>

{{planTittle}}

Edit

Done

Submit

              日期

                <input type="text" v-model="tempName">

              {{u.name}}

                {{item.month}} /{{item.day}}

√

×

√

×

</template>


Add some css classes to the pulse event

Add some css classes to the pulse event

<template>
  <div
    id="z-container"
    :class="[$zircle.getCurrentTheme(), $zircle.getCurrentColor()]"
    :style="[$zircle.getPreviousViewName() !== '' ? {cursor: 'zoom-out'} : {}]"
    @click.stop="goback" >
      <div id="z-point">
        <slot></slot>
      </div>
  </div>
</template>

<script>
/* eslint-disable no-new */
export default {
  methods: {
    goback () {
      if (this.$zircle.getPreviousViewName() !== '' && this.$zircle.getBackNavState() === false) {
        if (this.$zircle.getRouterState() === false) {
          this.$zircle.goBack()
        } else {
          this.$router.back()
        }
      }
    }
  },
  mounted () {
    var vue = this
    // Get window dimension to set the initial width of ui components such as z-panel
    this.$zircle.getDimensions()
    window.addEventListener('resize', function (event) {
      // On resize change the width of ui components
      vue.$zircle.getDimensions()
    })
  }
}
</script>
<style>
/* This is the style for zircle. To override it use !important
*/

.z-pulse {
  opacity: 0;
  transform: scale(0);
  background-color: rgba(255, 255, 255, 0.1);
  width: 100%;
  height: 100%;
  margin: -50% 0 0 -50%;
  position: absolute;
  z-index: 0;
  top: 50%;
  left: 50%;
  border-radius: 50%;
}
.pulse {
animation: pulse 110ms ease-out
}
@keyframes pulse {
  0% {
    opacity: 0;
    transform: scale(0)
  }

  100% {
    opacity: 1;
    transform: scale(1.4)
  }

}
:root {
  --light-blue: #5FC9F3;
  --black: #283237;
  --purple: #ee305a;
  --orange: #f7892f;
  --yellow: #ffca26;
  --blue: #3e78bb;
  --green: #69bf66;
  --red: #ef3c3b;
  --gray: #7c7e81;
}

* {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
  -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}

::-webkit-scrollbar {
  display: none;
}

.color--light-blue {
  --background-color: #F1F1F1;
  --primary-color: #5FC9F3;
  --accent-color: #0D7FAC;
  --success-color: #52B781;
  --warning-color: #CFA749;
  --danger-color: #C76B6F;
}

.color--black {
  --background-color: #859ba6;
  --primary-color: #283237;
  --accent-color: #000000;
  --success-color: #418a49;
  --warning-color: #bf7911;
  --danger-color: #b73e36;
}

.color--purple {
  --background-color: #fdecf0;
  --primary-color: #ee305a;
  --accent-color: #7b0a23;
  --success-color: #7d8953;
  --warning-color: #fa791b;
  --danger-color: #f23d41;
}

.color--orange {
  --background-color: #fff8f3;
  --primary-color: #f7892f;
  --accent-color: #884005;
  --success-color: #7fa446;
  --warning-color: #fd940e;
  --danger-color: #f55834;
}

.color--yellow {
  --background-color: #fffcf2;
  --primary-color: #ffca26;
  --accent-color: #8c6a00;
  --success-color: #82b743;
  --warning-color: #ffa70b;
  --danger-color: #f76c31;
}

.color--blue {
  --background-color: #d4e1f1;
  --primary-color: #3e78bb;
  --accent-color: #182e48;
  --success-color: #489f70;
  --warning-color: #c58e38;
  --danger-color: #bd535e;
}

.color--green {
  --background-color: #f6fbf6;
  --primary-color: #69bf66;
  --accent-color: #2b6329;
  --success-color: #55b457;
  --warning-color: #d2a41f;
  --danger-color: #ca6844;
}

.color--red {
  --background-color: #fef8f8;
  --primary-color: #ef3c3b;
  --accent-color: #860c0b;
  --success-color: #7d8d4a;
  --warning-color: #fa7c12;
  --danger-color: #f34138;
}

.color--gray {
  --background-color: #e4e4e5;
  --primary-color: #7c7e81;
  --accent-color: #313233;
  --success-color: #5aa05f;
  --warning-color: #d89027;
  --danger-color: #d0554d;
}

.theme--light {
  --background: var(--background-color);
  --primary: var(--primary-color);
  --accent: var(--accent-color);
  --background-card: var(--background-color);
  --border-card: var(--primary-color);
  --primary-text: var(--primary-color);
}

.theme--bold-light {
  --background: var(--background-color);
  --primary: var(--primary-color);
  --accent: var(--accent-color);
  --background-card: var(--primary-color);
  --border-card: var(--background-color);
  --primary-text: var(--background-color);
}

.theme--dark {
  --background: var(--primary-color);
  --primary: var(--background-color);
  --accent: var(--accent-color);
  --background-card: var(--primary-color);
  --border-card: var(--background-color);
  --primary-text: var(--background-color);
}

.theme--bold-dark {
  --background: var(--primary-color);
  --primary: var(--background-color);
  --accent: var(--accent-color);
  --background-card: var(--background-color);
  --border-card: var(--primary-color);
  --primary-text: var(--primary-color);
}
#z-container {
  position: fixed;
  width: 100%;
  height: 100%;
  font-family: 'Source Sans Pro', sans-serif;
  font-size: calc(14px + 1vmax);
  font-style: normal;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-rendering: optimizeLegibility;
  background-color: var(--background);
  color: var(--primary);
  transition: background-color 1s;
  overflow: hidden;
}
#z-point {
  position: absolute;
  top: 50%;
  left: 50%;
  text-decoration: none;
  will-change: transform;
  perspective: 1000px;
  perspective-origin: 50% 50%;
  transform-style: perserve-3d
}
#z-point::after {
    width: 100%;
    height: 100%;
    display: block;
    content:'';
  }
div[type="panel"],
div[type="scale"],
div[type="button"] {
  transition: background-color 1s;
}

.handlebar:hover {
  cursor: grab;
}

.handlebar:active {
  cursor: grabbing;
}

.zui {
  overflow: visible;
  position: absolute;
  display: block;
  border-radius: 50%;
  top: 50%;
  left: 50%;
  text-align: center;

}

.disc {
  z-index: 80;
  cursor: default;
}

.range {
  z-index: 80;
  cursor: grab;
}
.range:active, .range.focus {
 filter: '';
 cursor: grabbing;
}
.main {
  z-index: 40;
}

.zoom {
  cursor: zoom-in;
}

.pop {
  z-index: 500;
}

.scroll,
.slider {
  position: absolute;
  overflow: visible;
  display: block;
  z-index: 40;
  fill: none;
  stroke-linecap: round;
  stroke-opacity: 1;
  filter: brightness(1.5);
}

.scroll2 {
  position: absolute;
  border-radius: 50%;
  display: block;
  width: 40px;
  height: 40px;
  top: 50%;
  left: 50%;
  margin: -20px 0 0 -20px;
  z-index: 70;
}
.slider > circle {
  transition: all .5s linear;
}

.accent .slider,
.accent .scroll {
  stroke: var(--primary);
}

.primary .slider,
.primary .scroll {
  stroke: var(--accent);
}

.accent .scroll2 {
  fill: var(--primary);
}

.primary .scroll2 {
  fill: var(--accent);
}

.dashed {
  border: 2px solid var(--background-card);
}

.flow {
  overflow: visible;
}

.z-content {
  position: absolute;
  z-index: 0;
  top: 1%;
  left: 1%;
  width: 98%;
  height: 98%;
  display: flex;
  background: none;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  border-radius: 50%;
  user-select: none;
  border: 1px solid transparent;
}

.overflow {
  width: 100%;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.label {
  font-size: calc(10px + 1vmax);
  position: absolute;
  width: 100%;
  height: 20px;
  top: 100%
}

.maindisc {
  cursor: default;
  z-index: 50;
}

.longtext {
  overflow-y: scroll;
  display: block;
}
.ffoxScroll {
  overflow-y: hidden;
}

.longtext::before {
  display: block;
  padding-top: 50px;
  content:'';

}

.longtext::after {
  top: 0;
  display: block;
    content:'';

}

.bottom {
  position: relative;
  display: block;
  width: 1px;
  height: 1px
}

.longtext span.bottom {
  position: relative;
  display: block;
  width: 1px;
  height: 140px
}

.button {
  cursor: pointer;
}

.button:hover {
  filter: brightness(0.7);
}

.button:active {
  // filter: brightness(0.4) drop-shadow(0 0 0.55rem black);
}

.hidden {
  cursor: zoom-out;
}

.pastclass {
  pointer-events: none;
  cursor: zoom-out;
  filter: blur(2px) opacity(20%);
  overflow: hidden
}

.prevclass {
  pointer-events: none;
  cursor: zoom-out;
  filter: blur(2px) opacity(40%);
  overflow: hidden
}

.currclass {
  pointer-events: auto;
  will-change: opacity;
  animation: appear 800ms linear forwards
}
.disappear {
  animation: disappear 800ms linear forwards
}
@keyframes appear {
  0% {
    opacity: 0;
  }

  20% {
    opacity: 0;
  }

  80% {
    opacity: 1;
  }

  100% {
    opacity: 1;
  }

}

@keyframes disappear {
  0% {
    opacity: 1;
  }

  20% {
    opacity: 1;
  }

  80% {
    opacity: 0;
  }

  100% {
    opacity: 0;
  }

}

@keyframes hide {
  0% {
    opacity: 1;
  }

  100% {
    opacity: 0;
  }

}

.prevclass div {
  cursor: zoom-out;
}

.background {
  background-color: var(--background);
  border: 3px solid var(--primary);
  color: var(--primary-text);
}

.accent {
  border: 3px solid var(--accent);
  background-color: var(--accent);
  color: var(--primary-text);
}

.accent-secondary {
  filter: invert(100%);
}

.accent-secondary-border {
  filter: invert(100%);
  background-color: transparent;
  border-width: 1px;
}

.primary,
.default {
  border: 3px solid var(--border-card);
  background-color: var(--background-card);
  color: var(--primary-text);
}

.success {
  background-color: var(--success);
  border: 3px solid var(--success);
  color: var(--primary-text);
}

.warning {
  background-color: var(--warning);
  border: 3px solid var(--warning);
  color: var(--primary-text);
}

.danger {
  background-color: var(--danger);
  border: 3px solid var(--danger);
  color: var(--primary-text);
}

.light-blue {
  background-color: var(--light-blue);
  border: 3px solid var(--light-blue);
  color: var(--primary-text);
}

.black {
  background-color: var(--black);
  border: 3px solid var(--black);
  color: var(--primary-text);
}

.purple {
  background-color: var(--purple);
  border: 3px solid var(--purple);
  color: var(--primary-text);
}

.orange {
  background-color: var(--orange);
  border: 3px solid var(--orange);
  color: var(--primary-text);
}

.yellow {
  background-color: var(--yellow);
  border: 3px solid var(--yellow);
  color: var(--primary-text);
}

.blue {
  background-color: var(--blue);
  border: 3px solid var(--blue);
  color: var(--primary-text);
}

.green {
  background-color: var(--green);
  border: 3px solid var(--green);
  color: var(--primary-text);
}

.red {
  background-color: var(--red);
  border: 3px solid var(--red);
  color: var(--primary-text);
}

.gray {
  background-color: var(--gray);
  border: 3px solid var(--gray);
  color: var(--primary-text);
}

.plate,
.popup-plate {
  position: absolute;
  z-index: 0;
  top: 50%;
  left: 50%;
  border-radius: 50%;
  border: 1px dashed var(--primary);
  opacity: 0.3;
  overflow: hidden;
}

.navplate {
  position: absolute;
  z-index: 90;
  top: 50%;
  left: 50%;
  border-radius: 50%;
  opacity: 1;
  overflow: hidden;
}

.alert {
  z-index: 100;
  background: var(--background);
  border: 1px solid var(--primary);
  opacity: 0.3;
  overflow: hidden;
}

.border {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  border-radius: 50%;
  border: 3px solid var(--background);
}

div[title="z-item"],
div[title="z-list-pagination"] {
  cursor: zoom-in;
}

div[title="z-list-pagination"]:hover {
  cursor: grab;
}

div[title="z-list-pagination"]:active {
  cursor: grabbing;
}

.prevclass section[title="z-list"] {
  pointer-events: none;
}

::placeholder {
  color: inherit;
}

input {
  font: inherit;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-rendering: optimizeLegibility;
  margin-top: 20px;
  width: 100%;
  background: none;
  appearance: none;
  box-shadow: none;
  border: none;
  color: inherit;
  border-bottom-width: 1px;
  border-bottom-style: solid;
  border-bottom-color: inherit;
  background: inherit;
  outline: none;
  text-align: center;
}
.z-alert-enter-active, .z-alert-leave-active {
  transition: transform 0.3s;
  position: absolute;
  top: 50%; left: 50%;
  z-index: 500;
}
.z-alert-enter, .z-alert-leave-to {
  transform: scale(0);
}
</style>

<template>
  <div
    id="z-container"
    :class="[$zircle.getCurrentTheme(), $zircle.getCurrentColor()]"
    :style="[$zircle.getPreviousViewName() !== '' ? {cursor: 'zoom-out'} : {}]"
    @click.stop="goback" >
      <div id="z-point">
        <slot></slot>
      </div>
  </div>
</template>

<script>
/* eslint-disable no-new */
export default {
  methods: {
    goback () {
      if (this.$zircle.getPreviousViewName() !== '' && this.$zircle.getBackNavState() === false) {
        if (this.$zircle.getRouterState() === false) {
          this.$zircle.goBack()
        } else {
          this.$router.back()
        }
      }
    }
  },
  mounted () {
    var vue = this
    // Get window dimension to set the initial width of ui components such as z-panel
    this.$zircle.getDimensions()
    window.addEventListener('resize', function (event) {
      // On resize change the width of ui components
      vue.$zircle.getDimensions()
    })
  }
}
</script>
<style>
/* This is the style for zircle. To override it use !important
z-container {
    position: absolute;
    width: 400px;
    height: 400px;
    z-index: 999;
    /* box-shadow: inset 0 0 2px 0; */
    font-family: 'Source Sans Pro', sans-serif;
    font-size: calc(14px + 1vmax);
    font-style: normal;
    -webkit-font-smoothing: antialiased;
    text-rendering: optimizeLegibility;
    border-radius: 50%;
    /* background-color: var(--background); */
    color: var(--primary);
    transition: background-color 1s;
    overflow: hidden;
*/
.zuit-move {

}
:root {
  --light-blue: #5FC9F3;
  --black: #283237;
  --purple: #ee305a;
  --orange: #f7892f;
  --yellow: #ffca26;
  --blue: #3e78bb;
  --green: #69bf66;
  --red: #ef3c3b;
  --gray: #7c7e81;
}

* {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
  -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}

::-webkit-scrollbar {
  display: none;
}

.color--light-blue {
  --background-color: #F1F1F1;
  --primary-color: #5FC9F3;
  --accent-color: #0D7FAC;
  --success-color: #52B781;
  --warning-color: #CFA749;
  --danger-color: #C76B6F;
}

.color--black {
  --background-color: #859ba6;
  --primary-color: #283237;
  --accent-color: #000000;
  --success-color: #418a49;
  --warning-color: #bf7911;
  --danger-color: #b73e36;
}

.color--purple {
  --background-color: #fdecf0;
  --primary-color: #ee305a;
  --accent-color: #7b0a23;
  --success-color: #7d8953;
  --warning-color: #fa791b;
  --danger-color: #f23d41;
}

.color--orange {
  --background-color: #fff8f3;
  --primary-color: #f7892f;
  --accent-color: #884005;
  --success-color: #7fa446;
  --warning-color: #fd940e;
  --danger-color: #f55834;
}

.color--yellow {
  --background-color: #fffcf2;
  --primary-color: #ffca26;
  --accent-color: #8c6a00;
  --success-color: #82b743;
  --warning-color: #ffa70b;
  --danger-color: #f76c31;
}

.color--blue {
  --background-color: #d4e1f1;
  --primary-color: #3e78bb;
  --accent-color: #182e48;
  --success-color: #489f70;
  --warning-color: #c58e38;
  --danger-color: #bd535e;
}

.color--green {
  --background-color: #f6fbf6;
  --primary-color: #69bf66;
  --accent-color: #2b6329;
  --success-color: #55b457;
  --warning-color: #d2a41f;
  --danger-color: #ca6844;
}

.color--red {
  --background-color: #fef8f8;
  --primary-color: #ef3c3b;
  --accent-color: #860c0b;
  --success-color: #7d8d4a;
  --warning-color: #fa7c12;
  --danger-color: #f34138;
}

.color--gray {
  --background-color: #e4e4e5;
  --primary-color: #7c7e81;
  --accent-color: #313233;
  --success-color: #5aa05f;
  --warning-color: #d89027;
  --danger-color: #d0554d;
}

.theme--light {
  --background: var(--background-color);
  --primary: var(--primary-color);
  --accent: var(--accent-color);
  --background-card: var(--background-color);
  --border-card: var(--primary-color);
  --primary-text: var(--primary-color);
}

.theme--bold-light {
  --background: var(--background-color);
  --primary: var(--primary-color);
  --accent: var(--accent-color);
  --background-card: var(--primary-color);
  --border-card: var(--background-color);
  --primary-text: var(--background-color);
}

.theme--dark {
  --background: var(--primary-color);
  --primary: var(--background-color);
  --accent: var(--accent-color);
  --background-card: var(--primary-color);
  --border-card: var(--background-color);
  --primary-text: var(--background-color);
}

.theme--bold-dark {
  --background: var(--primary-color);
  --primary: var(--background-color);
  --accent: var(--accent-color);
  --background-card: var(--background-color);
  --border-card: var(--primary-color);
  --primary-text: var(--primary-color);
}
#z-container {
  position: fixed;
  width: 100%;
  height: 100%;
  font-family: 'Source Sans Pro', sans-serif;
  font-size: calc(14px + 1vmax);
  font-style: normal;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  background-color: var(--background);
  color: var(--primary);
  transition: background-color 1s;
  overflow: hidden;
}
#z-point {
  position: absolute;
  top: 50%;
  left: 50%;
  text-decoration: none;
  will-change: transform;
  perspective: 1000px;
  perspective-origin: 50% 50%;
  transform-style: perserve-3d
}
#z-point::after {
    width: 100%;
  height: 100%;
    display: block;
    content:'';
  }
div[type="panel"],
div[type="scale"],
div[type="button"] {
  transition: background-color 1s;
}

.handlebar:hover {
  cursor: grab;
}

.handlebar:active {
  cursor: grabbing;
}

.zui {
  overflow: visible;
  position: absolute;
  display: block;
  border-radius: 50%;
  top: 50%;
  left: 50%;
  text-align: center;

}

.disc:active, .disc:focus {
  filter: brightness(0.4) drop-shadow(0 0 0.55rem black);
}

.main {
  z-index: 40;
}

.disc {
  z-index: 80;
  cursor: default;
}

.zoom {
  cursor: zoom-in;
}

.pop {
  z-index: 500;
}

.scroll,
.slider {
  position: absolute;
  overflow: visible;
  display: block;
  z-index: 40;
  fill: none;
  stroke-opacity: 0.9;
}

.accent .slider,
.accent .scroll {
  stroke: var(--primary);
}

.primary .slider,
.primary .scroll {
  stroke: var(--accent);
}

.accent .scroll2 {
  fill: var(--primary);
}

.primary .scroll2 {
  fill: var(--accent);
}

.scroll2 {
  position: absolute;
  border-radius: 50%;
  display: block;
  width: 40px;
  height: 40px;
  top: 50%;
  left: 50%;
  margin: -20px 0 0 -20px;
  z-index: 70;
}

.dashed {
  border: 2px solid var(--background-card);
}

.flow {
  overflow: visible;
}

.label {
  font-size: calc(10px + 1vmax);
  overflow: visible
}

.z-contentbox {
  position: absolute;
  z-index: 50;
  top: 2%;
  left: 2%;
  width: 96%;
  height: 96%;
  display: block;
  border-radius: 50%;
  background: none;
  overflow: hidden;
}

.z-contentbox > img {
  border-radius: 50%;
  overflow: hidden;
}

.z-content {
  position: absolute;
  z-index: 0;
  top: 1%;
  left: 1%;
  width: 98%;
  height: 98%;
  display: flex;
  background: none;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  border-radius: 50%;
  user-select: none;
  border: 1px solid transparent;
}

.maindisc {
  cursor: default;
}

.longtext {
  overflow-y: scroll;
  padding-right: 20px;
  margin-right: -20px;
}

.bottom {
  position: relative;
  display: block;
  width: 1px;
  height: 1px
}

.longtext span.bottom {
  position: relative;
  display: block;
  width: 1px;
  height: 140px
}

.nodisplay {
  opacity: 0.1;
}

.button {
  cursor: pointer;
}

.button:hover {
  filter: brightness(0.7);
}

.button:active {
  filter: brightness(0.4) drop-shadow(0 0 0.55rem black);
}

.hidden {
  cursor: zoom-out;
}

.pastclass {
  pointer-events: none;
  cursor: zoom-out;
  filter: blur(2px) grayscale(100%) opacity(20%);
  overflow: hidden
}

.prevclass {
  pointer-events: none;
  cursor: zoom-out;
  filter: blur(2px) grayscale(100%) opacity(20%);
  overflow: hidden
}

.currclass {
  pointer-events: auto;
  will-change: opacity;
  animation: appear 800ms linear forwards
}
.disappear {
  animation: disappear 800ms linear forwards
}
@keyframes appear {
  0% {
    opacity: 0;
  }

  20% {
    opacity: 0;
  }

  80% {
    opacity: 1;
  }

  100% {
    opacity: 1;
  }

}
.show {
  opacity: 1;
}
.no-show{
  opacity: 1;
}

@keyframes disappear {
  0% {
    opacity: 1;
  }

  20% {
    opacity: 1;
  }

  80% {
    opacity: 0;
  }

  100% {
    opacity: 0;
  }

}

@keyframes hide {
  0% {
    opacity: 1;
  }

  100% {
    opacity: 0;
  }

}

.prevclass div {
  cursor: zoom-out;
}

.background {
  background-color: var(--background);
  border: 3px solid var(--primary);
  color: var(--primary-text);
}

.accent {
  border: 3px solid var(--accent);
  background-color: var(--accent);
  color: var(--primary-text);
}

.accent-secondary {
  filter: invert(100%);
}

.accent-secondary-border {
  filter: invert(100%);
  background-color: transparent;
  border-width: 1px;
}

.primary,
.default {
  border: 3px solid var(--border-card);
  background-color: var(--background-card);
  color: var(--primary-text);
}

.success {
  background-color: var(--success);
  border: 3px solid var(--success);
  color: var(--primary-text);
}

.warning {
  background-color: var(--warning);
  border: 3px solid var(--warning);
  color: var(--primary-text);
}

.danger {
  background-color: var(--danger);
  border: 3px solid var(--danger);
  color: var(--primary-text);
}

.light-blue {
  background-color: var(--light-blue);
  border: 3px solid var(--light-blue);
  color: var(--primary-text);
}

.black {
  background-color: var(--black);
  border: 3px solid var(--black);
  color: var(--primary-text);
}

.purple {
  background-color: var(--purple);
  border: 3px solid var(--purple);
  color: var(--primary-text);
}

.orange {
  background-color: var(--orange);
  border: 3px solid var(--orange);
  color: var(--primary-text);
}

.yellow {
  background-color: var(--yellow);
  border: 3px solid var(--yellow);
  color: var(--primary-text);
}

.blue {
  background-color: var(--blue);
  border: 3px solid var(--blue);
  color: var(--primary-text);
}

.green {
  background-color: var(--green);
  border: 3px solid var(--green);
  color: var(--primary-text);
}

.red {
  background-color: var(--red);
  border: 3px solid var(--red);
  color: var(--primary-text);
}

.gray {
  background-color: var(--gray);
  border: 3px solid var(--gray);
  color: var(--primary-text);
}

.plate,
.popup-plate {
  position: absolute;
  z-index: 0;
  top: 50%;
  left: 50%;
  border-radius: 50%;
  border: 1px dashed var(--primary);
  opacity: 0.3;
  overflow: hidden;
}

.navplate {
  position: absolute;
  z-index: 90;
  top: 50%;
  left: 50%;
  border-radius: 50%;
  opacity: 1;
  overflow: hidden;
}

.alert {
  z-index: 100;
  background: var(--background);
  border: 1px solid var(--primary);
  opacity: 0.3;
  overflow: hidden;
}

.border {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  border-radius: 50%;
  border: 3px solid var(--background);
}

div[title="z-item"],
div[title="z-dotnav"] {
  cursor: zoom-in;
}

div[title="z-dotnav"]:hover {
  cursor: grab;
}

div[title="z-dotnav"]:active {
  cursor: grabbing;
}

.prevclass section[title="z-list"] {
  pointer-events: none;
}

::placeholder {
  color: inherit;
}

input {
  font-family: 'Source Sans Pro', sans-serif;
  font-size: calc(14px + 1vmax);
  font-style: normal;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  margin-top: 20px;
  width: 100%;
  background: none;
  appearance: none;
  box-shadow: none;
  border: none;
  color: inherit;
  border-bottom-width: 1px;
  border-bottom-style: solid;
  border-bottom-color: inherit;
  background: inherit;
  outline: none;
  text-align: center;
}
.z-alert-enter-active, .z-alert-leave-active {
  transition: transform 0.3s;
  position: absolute;
  top: 50%; left: 50%;
  z-index: 500;
}
.z-alert-enter, .z-alert-leave-to {
  transform: scale(0);
}
</style>


修复跨域问题

修复跨域问题

<template>

    <el-form :model="formData" label-width="0" :rules="rules" ref="createForm">
      <el-form-item>

            <el-upload
              v-show="!uploaderStates.thumbnail.isUploading"
              class="resource-thumbnail-uploader"
              drag
              ref="thumbnailUploader"
              action="//qi.testfreelog.com/v1/resources/upoladPreviewImage"
              :with-credentials="true"
              :data="uploader.data"
              :headers="uploader.headers"
              :on-error="errorHandler"
              :on-change="previewImageChangeHandler"
              :on-success="imageUploadSuccessHandler"
              :before-upload="validateImageHandler"
              :on-progress="uploadProgressHandler"
              :show-file-list="false"
              :auto-upload="true">

              <template v-else>

资源预览图

800X600

              </template>
            </el-upload>

资源预览图

                <el-progress
                  style="margin-right: 20px;"
                  :stroke-width="10"
                  :percentage="uploaderStates.thumbnail.percentage"
                  color="#333333"></el-progress>

              <el-upload
                v-if="showCreatorInputItem"
                class="resource-file-uploader"
                drag
                ref="resourceUploader"
                action="//qi.testfreelog.com/v1/resources/uploadResourceFile"
                :with-credentials="true"
                :multiple="false"
                :data="{resourceType: formData.resourceType}"
                :headers="uploader.headers"
                :before-upload="beforeUploadHandler"
                :on-error="errorHandler"
                :on-change="fileChangeHandler"
                :on-success="successHandler"
                :show-file-list="false"
                :on-progress="uploadProgressHandler"
                :auto-upload="true">

资源文件

拖拽或点击上传，最大不超过50M

              </el-upload>
              <el-select
                v-if="!uploaderStates.resource.isUploading"
                :disabled="!showCreatorInputItem"
                v-model="formData.resourceType"
                allow-create
                filterable
                @change="resourceTypeChange"
                class="resource-type"
                placeholder="资源类型">
                <el-option
                  v-for="item in options"
                  :key="item.value"
                  :label="item.label"
                  :value="item.value">
                </el-option>
              </el-select>

资源文件类型：{{formData.resourceType}}

                <el-progress
                  v-if="!shouldShowResourceUploader"
                  class="resource-file-progress"
                  :stroke-width="10"
                  :percentage="uploaderStates.resource.percentage"
                  color="#333333"></el-progress>

      </el-form-item>
      <el-form-item prop="resourceName">
        <input type="text" v-model="formData.resourceName" class="input-item resource-name" placeholder="资源标题">
      </el-form-item>
      <el-form-item prop="widgetName" v-show="formData.resourceType === ResourceTypes.widget">
        <input type="text" v-model="formData.widgetName"
               class="input-item widget-name"
               :disabled="!showCreatorInputItem"
               placeholder="widget名称">
      </el-form-item>
      <el-form-item prop="description">
        <rich-editor class="res-desc-editor"
                     ref="editor"
                     width="100%"
                     v-model="formData.description"
                     :config="editorConfig"
                     @load="imgUploadSuccessHandler"
                     placeholder="请输入资源描述"></rich-editor>
      </el-form-item>
      <el-form-item>

资源meta信息

        <resource-meta-info v-model="meta" @validate="checkMetaValid" placeholder="资源meta信息"></resource-meta-info>
      </el-form-item>
    </el-form>

</template>

<template>

    <el-form :model="formData" label-width="0" :rules="rules" ref="createForm">
      <el-form-item>

            <el-upload
              v-show="!uploaderStates.thumbnail.isUploading"
              class="resource-thumbnail-uploader"
              drag
              ref="thumbnailUploader"
              action="/api/v1/resources/upoladPreviewImage"
              :data="uploader.data"
              :headers="uploader.headers"
              :on-error="errorHandler"
              :on-change="previewImageChangeHandler"
              :on-success="imageUploadSuccessHandler"
              :before-upload="validateImageHandler"
              :on-progress="uploadProgressHandler"
              :show-file-list="false"
              :auto-upload="true">

              <template v-else>

资源预览图

800X600

              </template>
            </el-upload>

资源预览图

                <el-progress
                  style="margin-right: 20px;"
                  :stroke-width="10"
                  :percentage="uploaderStates.thumbnail.percentage"
                  color="#333333"></el-progress>

              <el-upload
                v-if="showCreatorInputItem"
                class="resource-file-uploader"
                drag
                ref="resourceUploader"
                action="/api/v1/resources/uploadResourceFile"
                :multiple="false"
                :data="{resourceType: formData.resourceType}"
                :headers="uploader.headers"
                :before-upload="beforeUploadHandler"
                :on-error="errorHandler"
                :on-change="fileChangeHandler"
                :on-success="successHandler"
                :show-file-list="false"
                :on-progress="uploadProgressHandler"
                :auto-upload="true">

资源文件

拖拽或点击上传，最大不超过50M

              </el-upload>
              <el-select
                v-if="!uploaderStates.resource.isUploading"
                :disabled="!showCreatorInputItem"
                v-model="formData.resourceType"
                allow-create
                filterable
                @change="resourceTypeChange"
                class="resource-type"
                placeholder="资源类型">
                <el-option
                  v-for="item in options"
                  :key="item.value"
                  :label="item.label"
                  :value="item.value">
                </el-option>
              </el-select>

资源文件类型：{{formData.resourceType}}

                <el-progress
                  v-if="!shouldShowResourceUploader"
                  class="resource-file-progress"
                  :stroke-width="10"
                  :percentage="uploaderStates.resource.percentage"
                  color="#333333"></el-progress>

      </el-form-item>
      <el-form-item prop="resourceName">
        <input type="text" v-model="formData.resourceName" class="input-item resource-name" placeholder="资源标题">
      </el-form-item>
      <el-form-item prop="widgetName" v-show="formData.resourceType === ResourceTypes.widget">
        <input type="text" v-model="formData.widgetName"
               class="input-item widget-name"
               :disabled="!showCreatorInputItem"
               placeholder="widget名称">
      </el-form-item>
      <el-form-item prop="description">
        <rich-editor class="res-desc-editor"
                     ref="editor"
                     width="100%"
                     v-model="formData.description"
                     :config="editorConfig"
                     @load="imgUploadSuccessHandler"
                     placeholder="请输入资源描述"></rich-editor>
      </el-form-item>
      <el-form-item>

资源meta信息

        <resource-meta-info v-model="meta" @validate="checkMetaValid" placeholder="资源meta信息"></resource-meta-info>
      </el-form-item>
    </el-form>

</template>


use history replace-state when changing sort order

<template>

      {{ text }}

        <svg class="icon" :class="{ sorted: isDescending }" src="./down.svg"></svg>
        <svg class="icon" :class="{ sorted: isAscending }" src="./up.svg"></svg>

      {{ $tr('ascending') }}
      {{ $tr('descending') }}

{{ text }}

</template>


User user-table in user-page

User user-table in user-page

<template>

{{ $tr('allUsers') }}

       ( {{ $formatNumber(visibleUsers.length) }} )

        <k-button
          @click="displayModal(Modals.CREATE_USER)"
          :text="$tr('addNew')"
          :primary="true"
        />

      <k-select
        class="kind-select"
        :label="$tr('filterUserType')"
        :options="userKinds"
        :inline="true"
        v-model="roleFilter"
      />

      <k-filter-textbox
        :placeholder="$tr('searchText')"
        v-model="searchFilter"
        class="user-filter"
      />

    <user-table
      :users="visibleUsers"
      :emptyMessage="emptyMessage"
    >
      <template slot="action" slot-scope="userRow">
        <k-dropdown-menu
          :text="$tr('manage')"
          :options="manageUserOptions(userRow.user.id)"
          :disabled="!userCanBeEdited(userRow.user)"
          appearance="flat-button"
          @select="handleManageUserSelection($event, userRow.user)"
        />
      </template>
    </user-table>

    <user-create-modal v-if="modalShown===Modals.CREATE_USER" />

    <edit-user-modal
      v-if="modalShown===Modals.EDIT_USER"
      :id="selectedUser.id"
      :name="selectedUser.full_name"
      :username="selectedUser.username"
      :kind="selectedUser.kind"
    />

    <reset-user-password-modal
      v-if="modalShown===Modals.RESET_USER_PASSWORD"
      :id="selectedUser.id"
      :name="selectedUser.full_name"
      :username="selectedUser.username"
    />

    <delete-user-modal
      v-if="modalShown===Modals.DELETE_USER"
      :id="selectedUser.id"
      :name="selectedUser.full_name"
      :username="selectedUser.username"
    />

</template>

<template>

{{ $tr('allUsers') }}

       ( {{ $formatNumber(visibleUsers.length) }} )

        <k-button
          @click="displayModal(Modals.CREATE_USER)"
          :text="$tr('addNew')"
          :primary="true"
        />

      <k-select
        class="kind-select"
        :label="$tr('filterUserType')"
        :options="userKinds"
        :inline="true"
        v-model="roleFilter"
      />

      <k-filter-textbox
        :placeholder="$tr('searchText')"
        v-model="searchFilter"
        class="user-filter"
      />

    <core-table>

{{ $tr('users') }}

          {{ $tr('fullName') }}

            {{ $tr('role') }}

          {{ $tr('username') }}

            {{ $tr('userActions') }}

            <ui-icon icon="person" />

          {{ user.full_name }}

            <user-role :role="user.kind" :omitLearner="true" />

          {{ user.username }}

            <k-dropdown-menu
              :text="$tr('manage')"
              :options="manageUserOptions(user.id)"
              :disabled="!userCanBeEdited(user)"
              appearance="flat-button"
              @select="handleManageUserSelection($event, user)"
            />

    </core-table>

{{ $tr('noUsersExist') }}

{{ $tr('allUsersFilteredOut') }}

    <user-create-modal v-if="modalShown===Modals.CREATE_USER" />

    <edit-user-modal
      v-if="modalShown===Modals.EDIT_USER"
      :id="selectedUser.id"
      :name="selectedUser.full_name"
      :username="selectedUser.username"
      :kind="selectedUser.kind"
    />

    <reset-user-password-modal
      v-if="modalShown===Modals.RESET_USER_PASSWORD"
      :id="selectedUser.id"
      :name="selectedUser.full_name"
      :username="selectedUser.username"
    />

    <delete-user-modal
      v-if="modalShown===Modals.DELETE_USER"
      :id="selectedUser.id"
      :name="selectedUser.full_name"
      :username="selectedUser.username"
    />

</template>


remove console

remove console

<template>

        <doc-header :category="category"></doc-header>

            <doc-submenu
                :category="category"
                :page="page"
            ></doc-submenu>

                <slot></slot>

        <doc-footer></doc-footer>

</template>


Autocomplete for topics added.

Autocomplete for topics added.

<template>
    <v-card id="dialog-create-topic-commitment">

            <slot name="header"></slot>

        <v-card-text id="dialog-create-topic-commitment-content">

                {{description}}

            <v-form v-model="valid" @keydown.enter.native="addTopic" ref="form" id="add-topic-container">

                    <v-select :label="$t('pages:commitment.createDialog.addTopicDescription')"
                              autocomplete
                              :loading="loading"
                              dense
                              :items="searchTopics"
                              :rules="[ruleToManyChars($t('validation:toManyChars'), 30),
                                       topicNotAlreadyUsed(),
                                       maxNumberOfTopics(),
                                       maxNumberOfSpaces()]"
                              :search-input.sync="newTopic"
                              v-model="newTopic"
                              @change="selectTopic">
                    </v-select>

                <v-btn color="primary" @click="addTopic" id="add-topic-button"
                       :disabled="!valid || (newTopic && newTopic.trim() === '') || !newTopic">
                    {{$t('pages:commitment.createDialog.addTopicButton')}}
                </v-btn>
            </v-form>

                    <v-chip v-model="topic.isActive" @input="onTopicRemove(topic)" close>
                        {{topic.name}}
                    </v-chip>

        </v-card-text>
        <v-divider></v-divider>
        <v-card-actions>
            <v-spacer></v-spacer>
            <v-btn color="primary" flat @click.native="$emit('close-dialog')">
                {{$t("common:button.close")}}
            </v-btn>
            <v-btn color="primary" @click.native="finish()" :loading="loading"
                   :disabled="(newTopic && newTopic.trim().length > 0) || !valid ||
                               topics.length === 0 || topicHasNotChanged || loading">
                {{actionButtonText}}
            </v-btn>
        </v-card-actions>
    </v-card>
</template>

<template>
    <v-card id="dialog-create-topic-commitment">

            <slot name="header"></slot>

        <v-card-text id="dialog-create-topic-commitment-content">

                {{description}}

            <v-form v-model="valid" @keydown.enter.native="addTopic" ref="form" id="add-topic-container">

                    <v-text-field v-model="newTopic"
                                  :label="$t('pages:commitment.createDialog.addTopicDescription')"
                                  :rules="[ruleToManyChars($t('validation:toManyChars'), 30),
                                           topicNotAlreadyUsed(),
                                           maxNumberOfTopics(),
                                           maxNumberOfSpaces()]">
                    </v-text-field>

                <v-btn color="primary" @click="addTopic" id="add-topic-button"
                       :disabled="!valid || newTopic.trim() === ''">
                    {{$t('pages:commitment.createDialog.addTopicButton')}}
                </v-btn>
            </v-form>

                    <v-chip v-model="topic.isActive" @input="onTopicRemove(topic)" close>
                        {{topic.name}}
                    </v-chip>

        </v-card-text>
        <v-divider></v-divider>
        <v-card-actions>
            <v-spacer></v-spacer>
            <v-btn color="primary" flat @click.native="$emit('close-dialog')">
                {{$t("common:button.close")}}
            </v-btn>
            <v-btn color="primary" @click.native="finish()" :loading="loading"
                   :disabled="newTopic.trim() !== '' || !valid || topics.length === 0 || topicHasNotChanged || loading">
                {{actionButtonText}}
            </v-btn>
        </v-card-actions>
    </v-card>
</template>


Close participant context menu after action

Signed-off-by: Vincent Petry <87e1f221a672a14a323e57bb65eaea19d3ed3804@nextcloud.com>

<template>

		<AvatarWrapper
			:id="computedId"
			:disable-tooltip="true"
			:disable-menu="isSearched"
			:size="44"
			:show-user-status="showUserStatus && !isSearched"
			:show-user-status-compact="false"
			:name="computedName"
			:source="participant.source || participant.actorType"
			:offline="isOffline" />

				{{ computedName }}
				({{ t('spreed', 'moderator') }})
				({{ t('spreed', 'guest') }})

				{{ statusMessage }}

			{{ callIconTooltip }}
			<Microphone
				v-if="callIcon === 'audio'"
				:size="24"
				title=""
				decorative />
			<Phone
				v-if="callIcon === 'phone'"
				:size="24"
				title=""
				decorative />
			<Video
				v-if="callIcon === 'video'"
				:size="24"
				title=""
				decorative />

		<Actions
			v-if="canModerate && !isSearched"
			:aria-label="participantSettingsAriaLabel"
			class="participant-row__actions">
			<ActionText
				v-if="attendeePin"
				:title="t('spreed', 'Dial-in PIN')"
				icon="icon-password">
				{{ attendeePin }}
			</ActionText>
			<ActionButton v-if="canBeDemoted"
				icon="icon-rename"
				:close-after-click="true"
				@click="demoteFromModerator">
				{{ t('spreed', 'Demote from moderator') }}
			</ActionButton>
			<ActionButton v-if="canBePromoted"
				icon="icon-rename"
				:close-after-click="true"
				@click="promoteToModerator">
				{{ t('spreed', 'Promote to moderator') }}
			</ActionButton>
			<ActionButton
				icon="icon-delete"
				:close-after-click="true"
				@click="removeParticipant">
				{{ t('spreed', 'Remove participant') }}
			</ActionButton>
		</Actions>

</template>

<template>

		<AvatarWrapper
			:id="computedId"
			:disable-tooltip="true"
			:disable-menu="isSearched"
			:size="44"
			:show-user-status="showUserStatus && !isSearched"
			:show-user-status-compact="false"
			:name="computedName"
			:source="participant.source || participant.actorType"
			:offline="isOffline" />

				{{ computedName }}
				({{ t('spreed', 'moderator') }})
				({{ t('spreed', 'guest') }})

				{{ statusMessage }}

			{{ callIconTooltip }}
			<Microphone
				v-if="callIcon === 'audio'"
				:size="24"
				title=""
				decorative />
			<Phone
				v-if="callIcon === 'phone'"
				:size="24"
				title=""
				decorative />
			<Video
				v-if="callIcon === 'video'"
				:size="24"
				title=""
				decorative />

		<Actions
			v-if="canModerate && !isSearched"
			:aria-label="participantSettingsAriaLabel"
			class="participant-row__actions">
			<ActionText
				v-if="attendeePin"
				:title="t('spreed', 'Dial-in PIN')"
				icon="icon-password">
				{{ attendeePin }}
			</ActionText>
			<ActionButton v-if="canBeDemoted"
				icon="icon-rename"
				@click="demoteFromModerator">
				{{ t('spreed', 'Demote from moderator') }}
			</ActionButton>
			<ActionButton v-if="canBePromoted"
				icon="icon-rename"
				@click="promoteToModerator">
				{{ t('spreed', 'Promote to moderator') }}
			</ActionButton>
			<ActionButton
				icon="icon-delete"
				@click="removeParticipant">
				{{ t('spreed', 'Remove participant') }}
			</ActionButton>
		</Actions>

</template>


refactor(breadcrumb): button aria-label fix

affects: da39a3ee5e6b4b0d3255bfef95601890afd80709@rei/cdr-breadcrumb

<template>
  <!-- disable lint errors on line length in template -->
  <!-- eslint-disable max-len -->
  <!-- eslint-disable vue/require-v-for-key -->

  <nav
    ref="container"
    :class="modifierClass"
    class="cdr-breadcrumb__container"
    aria-label="Breadcrumb">
    <ol
      ref="cdrBreadcrumbList"
      :class="$style['cdr-breadcrumb__list']">
      <li
        v-if="truncate"
        @click="shouldTruncate = false"
        :class="$style['cdr-breadcrumb__item']">
        <cdr-icon-only-button
          aria-expanded="false"
          aria-label="ellipsis"
          :class="[$style['cdr-breadcrumb__link'], $style['cdr-breadcrumb__ellipses']]">
          <icon-more-functions :class="$style['cdr-breadcrumb__ellipses-icon']" />
        </cdr-icon-only-button>
        <span
          :class="$style['cdr-breadcrumb__delimiter']"
          aria-hidden="true">
          /
        </span>
      </li>
      <li
        :class="$style['cdr-breadcrumb__item']"
        v-for="(item, index) in items"
        v-if="!truncate || (index >= items.length - 2)"
      >
        <a
          :class="$style['cdr-breadcrumb__link']"
          :href="item.url">
          {{ item.displayText }}
        </a>
        <span
          :class="$style['cdr-breadcrumb__delimiter']"
          aria-hidden="true"
          v-if="index < items.length - 1">
          /
        </span>
      </li>
    </ol>
  </nav>
</template>

<script>
/**
 * Cedar 2 component for breadcrumb
 *
 * @version 0.0.1
 * @author [REI Software Engineering](https://rei.github.io/rei-cedar/)
 */

import modifier from 'mixinsdir/modifier';
import { IconMoreFunctions } from '@rei/cdr-icon';
import CdrIconOnlyButton from 'componentsdir/iconOnlyButton/CdrIconOnlyButton';
import debounce from 'lodash/debounce';

export default {
  name: 'CdrBreadcrumb',
  components: {
    CdrIconOnlyButton,
    IconMoreFunctions,
  },
  mixins: [modifier],
  props: {
    /**
     * Required. List of source breadcrumb property objects
     *
     * {
     *   displayText: Breadcrumb Display Text
     *   url: location breadcrumb should navigate on click
     * }
     */
    items: {
      type: Array,
      default: () => [],
    },
    /**
     * Flag to track container width threshold exceeded
     */
    truncationEnabled: {
      type: Boolean,
      default: true,
    },
    /**
     * Value for max breadcrumb width to container width
     * ratio before truncation is performed
     */
    truncationThreshold: {
      type: Number,
      default: 0.8,
    },
  },
  data() {
    return {
      thresholdExceeded: false,
      shouldTruncate: this.truncationEnabled,
      breadcrumbWidth: 0,
    };
  },
  computed: {
    baseClass() {
      return 'cdr-breadcrumb';
    },
    truncate() {
      return this.shouldTruncate && this.thresholdExceeded;
    },
  },
  mounted() {
    this.breadcrumbWidth = this.getBreadcrumbWidth();
    this.thresholdExceeded = this.calculateTruncation();
    window.addEventListener('resize', debounce(() => {
      this.thresholdExceeded = this.calculateTruncation();
    }, 250));
  },
  methods: {
    getBreadcrumbWidth() {
      const breadcrumbsElements = Array.from(this.$refs.cdrBreadcrumbList.children);
      let totalWidth = 0;
      breadcrumbsElements.forEach((element) => {
        totalWidth += element.offsetWidth || 0;
      });
      return totalWidth;
    },
    calculateTruncation() {
      const containerWidth = this.$refs.container.offsetWidth || 0;
      const ratio = this.breadcrumbWidth / containerWidth || 0;
      return (ratio > this.truncationThreshold);
    },
  },
};
</script>

<style module>
  @import '../../css/settings/_index.pcss';
  @import './styles/CdrBreadcrumb.pcss';
</style>

<template>
  <!-- disable lint errors on line length in template -->
  <!-- eslint-disable max-len -->
  <!-- eslint-disable vue/require-v-for-key -->

  <nav
    ref="container"
    :class="modifierClass"
    class="cdr-breadcrumb__container"
    aria-label="Breadcrumb">
    <ol
      ref="cdrBreadcrumbList"
      :class="$style['cdr-breadcrumb__list']">
      <li
        v-if="truncate"
        @click="shouldTruncate = false"
        :class="$style['cdr-breadcrumb__item']">
        <cdr-icon-only-button
          aria-expanded="false"
          :class="[$style['cdr-breadcrumb__link'], $style['cdr-breadcrumb__ellipses']]">
          <icon-more-functions :class="$style['cdr-breadcrumb__ellipses-icon']" />
        </cdr-icon-only-button>
        <span
          :class="$style['cdr-breadcrumb__delimiter']"
          aria-hidden="true">
          /
        </span>
      </li>
      <li
        :class="$style['cdr-breadcrumb__item']"
        v-for="(item, index) in items"
        v-if="!truncate || (index >= items.length - 2)"
      >
        <a
          :class="$style['cdr-breadcrumb__link']"
          :href="item.url">
          {{ item.displayText }}
        </a>
        <span
          :class="$style['cdr-breadcrumb__delimiter']"
          aria-hidden="true"
          v-if="index < items.length - 1">
          /
        </span>
      </li>
    </ol>
  </nav>
</template>

<script>
/**
 * Cedar 2 component for breadcrumb
 *
 * @version 0.0.1
 * @author [REI Software Engineering](https://rei.github.io/rei-cedar/)
 */

import modifier from 'mixinsdir/modifier';
import { IconMoreFunctions } from '@rei/cdr-icon';
import CdrIconOnlyButton from 'componentsdir/iconOnlyButton/CdrIconOnlyButton';
import debounce from 'lodash/debounce';

export default {
  name: 'CdrBreadcrumb',
  components: {
    CdrIconOnlyButton,
    IconMoreFunctions,
  },
  mixins: [modifier],
  props: {
    /**
     * Required. List of source breadcrumb property objects
     *
     * {
     *   displayText: Breadcrumb Display Text
     *   url: location breadcrumb should navigate on click
     * }
     */
    items: {
      type: Array,
      default: () => [],
    },
    /**
     * Flag to track container width threshold exceeded
     */
    truncationEnabled: {
      type: Boolean,
      default: true,
    },
    /**
     * Value for max breadcrumb width to container width
     * ratio before truncation is performed
     */
    truncationThreshold: {
      type: Number,
      default: 0.8,
    },
  },
  data() {
    return {
      thresholdExceeded: false,
      shouldTruncate: this.truncationEnabled,
      breadcrumbWidth: 0,
    };
  },
  computed: {
    baseClass() {
      return 'cdr-breadcrumb';
    },
    truncate() {
      return this.shouldTruncate && this.thresholdExceeded;
    },
  },
  mounted() {
    this.breadcrumbWidth = this.getBreadcrumbWidth();
    this.thresholdExceeded = this.calculateTruncation();
    window.addEventListener('resize', debounce(() => {
      this.thresholdExceeded = this.calculateTruncation();
    }, 250));
  },
  methods: {
    getBreadcrumbWidth() {
      const breadcrumbsElements = Array.from(this.$refs.cdrBreadcrumbList.children);
      let totalWidth = 0;
      breadcrumbsElements.forEach((element) => {
        totalWidth += element.offsetWidth || 0;
      });
      return totalWidth;
    },
    calculateTruncation() {
      const containerWidth = this.$refs.container.offsetWidth || 0;
      const ratio = this.breadcrumbWidth / containerWidth || 0;
      return (ratio > this.truncationThreshold);
    },
  },
};
</script>

<style module>
  @import '../../css/settings/_index.pcss';
  @import './styles/CdrBreadcrumb.pcss';
</style>


Upgrade version of NEST UserDoc

Upgrade version of NEST UserDoc

<template>

    <v-toolbar dense flat>
      <v-toolbar-title>
        {{ modelView.state.doc.title || modelView.state.modelId }}
        <v-chip
          class="mx-4"
          small
          outlined
          v-if="modelView.state.defaults.element_type"
          v-text="modelView.state.defaults.element_type"
        />
        <v-chip
          class="mx-4"
          small
          outlined
          v-if="'synapse_model' in modelView.state.defaults"
          v-text="'synapse'"
        />
      </v-toolbar-title>
      <v-spacer />
      <v-btn
        :href="NESTDocURL()"
        :title="NESTDocURL()"
        class="mx-1"
        outlined
        small
        target="_blank"
        text
      >
        <v-icon left v-text="'mdi-open-in-new'" />
        More
      </v-btn>
    </v-toolbar>

    <v-card flat style="max-height: calc(100vh - 96px); overflow-y: auto" tile>
      <v-card-subtitle
        v-if="modelView.state.doc.subtitle"
        v-text="modelView.state.doc.subtitle"
      />
      <v-card
        :key="block.title"
        flat
        tile
        v-for="block of modelView.state.doc.blocks"
      >
        <v-card-title v-text="block.title" />
        <v-card-text>

        </v-card-text>
      </v-card>
    </v-card>

</template>


[ux] Fix display glitch in task type combo box

<template>
<div class="field">
  <label class="label" v-if="label.length > 0">
    {{ label }}
  </label>
  <div
    class="task-type-combo"
  >
    <div
      class="flexrow"
      @click="toggleTaskTypeList"
    >
      <div
        class="selected-task-type-line flexrow-item"
      >
        <task-type-name
          :task-type="currentTaskType"
          v-if="currentTaskType"
        />
      </div>
      <chevron-down-icon class="ml05 down-icon flexrow-item"/>
    </div>
    <div
      :class="{
        'select-input': true,
        'up': up
      }"
      ref="select"
      v-if="showTaskTypeList"
    >
      <div
        class="task-type-line"
        v-for="taskType in taskTypeList"
        @click="selectTaskType(taskType)"
        :key="taskType.id"
      >
        <task-type-name
          :task-type="taskType"
        />
      </div>
    </div>
  </div>
  <combobox-mask
    :displayed="showTaskTypeList"
    @click="toggleTaskTypeList"
  />
</div>

</template>

<script>
import { mapGetters } from 'vuex'
import { ChevronDownIcon } from 'vue-feather-icons'

import ComboboxMask from '@/components/widgets/ComboboxMask'
import TaskTypeName from '@/components/widgets/TaskTypeName'

export default {
  name: 'combobox-task-type',

  components: {
    ChevronDownIcon,
    ComboboxMask,
    TaskTypeName
  },

  data () {
    return {
      showTaskTypeList: false
    }
  },

  props: {
    label: {
      default: '',
      type: String
    },
    taskTypeList: {
      default: () => [],
      type: Array
    },
    value: {
      default: '',
      type: String
    },
    up: {
      default: false,
      type: Boolean
    },
    addPlaceholder: {
      default: false,
      type: Boolean
    }
  },

  mounted () {
  },

  computed: {
    ...mapGetters([
      'taskTypeMap'
    ]),

    currentTaskType () {
      if (this.value) {
        return this.taskTypeMap.get(this.value)
      } else if (this.addPlaceholder) {
        return { name: '+ Task Type', color: '#E5E5E5' }
      } else {
        return this.taskTypeList[0]
      }
    }
  },

  methods: {
    selectTaskType (taskType) {
      this.$emit('input', taskType.id)
      this.showTaskTypeList = false
    },

    toggleTaskTypeList () {
      this.showTaskTypeList = !this.showTaskTypeList
    }
  }
}
</script>

<style lang="scss" scoped>
.dark {
  .select-input,
  .selected-task-type-line,
  .task-type-line,
  .task-type-combo {
    background: $dark-grey-light;
    border-color: $dark-grey;
  }

  .task-type-line:hover {
    background: $dark-purple;
  }
}

.task-type-combo {
  background: $white;
  min-width: 200px;
  width: 200px;
  border: 1px solid $light-grey-light;
  user-select: none;
  cursor: pointer;
  border-radius: 3px;
  margin: 0;
  padding: 0.15em;
  position: relative;
}

.task-type-combo:hover {
  border: 1px solid $green;
}

.selected-task-type-line {
  background: $white;
  padding: 0.4em;
  text-transform: uppercase;
  flex: 1;
}

.task-type-line {
  background: $white;
  cursor: pointer;
  padding: 0.4em;
  margin: 0;
  text-transform: uppercase;

  &:hover {
    background: $purple;
  }
}

.down-icon {
  width: 15px;
  min-width: 15px;
  margin-right: 0.4em;
  color: $green;
  cursor: pointer;
}

.select-input {
  background: $white;
  width: 200px;
  position: absolute;
  border: 1px solid $light-grey-light;
  z-index: 300;
  margin-left: -1px;
  max-height: 200px;
  overflow-y: auto;

  &.up {
    height: 200px;
    top: -200px;
  }
}

.field .label {
  padding-top: 5px;
}
</style>

<template>
<div class="field">
  <label class="label" v-if="label.length > 0">
    {{ label }}
  </label>
  <div
    class="task-type-combo"
  >
    <div
      class="flexrow"
      @click="toggleTaskTypeList"
    >
      <div
        class="selected-task-type-line flexrow-item"
      >
        <task-type-name
          :task-type="currentTaskType"
          v-if="currentTaskType"
        />
      </div>
      <chevron-down-icon class="down-icon flexrow-item"/>
    </div>
    <div
      :class="{
        'select-input': true,
        'up': up
      }"
      ref="select"
      v-if="showTaskTypeList"
    >
      <div
        class="task-type-line"
        v-for="taskType in taskTypeList"
        @click="selectTaskType(taskType)"
        :key="taskType.id"
      >
        <task-type-name
          :task-type="taskType"
        />
      </div>
    </div>
  </div>
  <combobox-mask
    :displayed="showTaskTypeList"
    @click="toggleTaskTypeList"
  />
</div>

</template>

<script>
import { mapGetters } from 'vuex'
import { ChevronDownIcon } from 'vue-feather-icons'

import ComboboxMask from '@/components/widgets/ComboboxMask'
import TaskTypeName from '@/components/widgets/TaskTypeName'

export default {
  name: 'combobox-task-type',

  components: {
    ChevronDownIcon,
    ComboboxMask,
    TaskTypeName
  },

  data () {
    return {
      showTaskTypeList: false
    }
  },

  props: {
    label: {
      default: '',
      type: String
    },
    taskTypeList: {
      default: () => [],
      type: Array
    },
    value: {
      default: '',
      type: String
    },
    up: {
      default: false,
      type: Boolean
    },
    addPlaceholder: {
      default: false,
      type: Boolean
    }
  },

  mounted () {
  },

  computed: {
    ...mapGetters([
      'taskTypeMap'
    ]),

    currentTaskType () {
      if (this.value) {
        return this.taskTypeMap.get(this.value)
      } else if (this.addPlaceholder) {
        return { name: '+ Task Type', color: '#E5E5E5' }
      } else {
        return this.taskTypeList[0]
      }
    }
  },

  methods: {
    selectTaskType (taskType) {
      this.$emit('input', taskType.id)
      this.showTaskTypeList = false
    },

    toggleTaskTypeList () {
      this.showTaskTypeList = !this.showTaskTypeList
    }
  }
}
</script>

<style lang="scss" scoped>
.dark {
  .select-input,
  .selected-task-type-line,
  .task-type-line,
  .task-type-combo {
    background: $dark-grey-light;
    border-color: $dark-grey;
  }

  .task-type-line:hover {
    background: $dark-purple;
  }
}

.task-type-combo {
  background: $white;
  min-width: 200px;
  width: 200px;
  border: 1px solid $light-grey-light;
  user-select: none;
  cursor: pointer;
  border-radius: 3px;
  margin: 0;
  padding: 0.15em;
  position: relative;
}

.task-type-combo:hover {
  border: 1px solid $green;
}

.selected-task-type-line {
  background: $white;
  padding: 0.4em;
  text-transform: uppercase;
  flex: 1;
}

.task-type-line {
  background: $white;
  cursor: pointer;
  padding: 0.4em;
  margin: 0;
  text-transform: uppercase;

  &:hover {
    background: $purple;
  }
}

.down-icon {
  width: 15px;
  min-width: 15px;
  margin-right: 0.4em;
  color: $green;
  cursor: pointer;
}

.select-input {
  background: $white;
  width: 200px;
  position: absolute;
  border: 1px solid $light-grey-light;
  z-index: 300;
  margin-left: -1px;
  max-height: 200px;
  overflow-y: auto;

  &.up {
    height: 200px;
    top: -200px;
  }
}

.field .label {
  padding-top: 5px;
}
</style>


Updated style on group count

Updated style on group count

<template>

#

                    <slot></slot>

                            {{ groupingColumn.formatData(group) }}
                             1">{{ rows.length }}

                    No results

                        {{ groupIndex + rowIndex + 1 }}

                        <slot :name="column.id" :row="row" :column="column" :value="row[column.id]">
                            <input type="text" v-model="row[column.id]" v-if="editable">
                            {{ column.formatData(row[column.id]) }}
                        </slot>

                    Total

                    {{ calculateTotal(column) }}

            <select v-model="groupingId">
                <option :value="null">No grouping</option>
                <option v-for="column in columns" :value="column.id">{{ column.label }}</option>
            </select>
            <input type="text" placeholder="Filter this dataset" v-model="rowFilter">

</template>

<template>

#

                    <slot></slot>

                            {{ groupingColumn.formatData(group) }}
                             1" @click="rowFilter = group">{{ rows.length }}

                    No results

                        {{ groupIndex + rowIndex + 1 }}

                        <slot :name="column.id" :row="row" :column="column" :value="row[column.id]">
                            <input type="text" v-model="row[column.id]" v-if="editable">
                            {{ column.formatData(row[column.id]) }}
                        </slot>

                    Total

                    {{ calculateTotal(column) }}

            <select v-model="groupingId">
                <option :value="null">No grouping</option>
                <option v-for="column in columns" :value="column.id">{{ column.label }}</option>
            </select>
            <input type="text" placeholder="Filter this dataset" v-model="rowFilter">

</template>


Remove unused class

Remove unused class

<template>

  <CoreBase
    :immersivePage="false"
    :authorized="userIsAuthorized"
    authorizedRole="adminOrCoach"
    :showSubNav="true"
  >

    <TopNavbar slot="sub-nav" />

    <KPageContainer>

      <ReportsLessonExerciseHeader />

      <KCheckbox
        :label="coachStrings.$tr('viewByGroupsLabel')"
        :checked="viewByGroups"
        @change="toggleGroupsView"
      />

            {{ group.name }}

            <StatusSummary
              :tally="getGroupTally(group.id)"
              :verbose="false"
            />

          <ReportsExerciseLearners
            :entries="getTableEntries([group.id])"
            :showGroupsColumn="false"
          />

          <StatusSummary :tally="summaryTally" />

        <ReportsExerciseLearners :entries="getTableEntries()" />

    </KPageContainer>
  </CoreBase>

</template>


Link to github user page

Link to github user page

<template>

        © 2015-{{ currentYear }}

            Hiroto-K

</template>


trim textbox input

trim textbox input

<template>

  <ui-textbox
    ref="textbox"
    v-model.trim="currentText"
    :label="label"
    :disabled="disabled"
    :required="required"
    :invalid="invalid"
    :error="invalidText"
    :autofocus="autofocus"
    :maxlength="maxlength"
    :autocomplete="autocomplete"
    :type="type"
    :enforceMaxlength="true"
    :floatingLabel="true"
    @input="updateText"
    @keydown="emitKeydown"
    @focus="$emit('focus')"
    @blur="$emit('blur')"
  />

</template>

<script>

  import uiTextbox from 'keen-ui/src/UiTextbox';

  /**
   * Handles user input.
   */
  export default {
    name: 'k-textbox',
    components: { uiTextbox },
    props: {
      /**
       * v-model
       */
      value: {
        type: [String, Number],
      },
      /**
       * Label
       */
      label: {
        type: String,
        required: true,
      },
      /**
       * Whether or not disabled
       */
      disabled: {
        type: Boolean,
        default: false,
      },
      /**
       * Whether or not required
       */
      required: {
        type: Boolean,
        default: false,
      },
      /**
       * Whether or not input is invalid
       */
      invalid: {
        type: Boolean,
        default: false,
      },
      /**
       * Text displayed if input is invalid
       */
      invalidText: {
        type: String,
        required: false,
      },
      /**
       * Whether or not to autofocus
       */
      autofocus: {
        type: Boolean,
        default: false,
      },
      /**
       * Max allowed length of input
       */
      maxlength: {
        type: Number,
        required: false,
      },
      /**
       * HTML5 autocomplete attribute (off, on, name, username, current-password, etc.)
       */
      autocomplete: {
        type: String,
        required: false,
      },
      /**
       * HTML5 type of input (text, password, number, etc.)
       */
      type: {
        type: String,
        default: 'text',
      },
    },
    data() {
      return { currentText: this.value };
    },
    watch: {
      value(val) {
        this.currentText = val;
      },
    },
    methods: {
      updateText() {
        // v-model is just a :value + @input
        /**
         * Emits input event with new value
         */
        this.$emit('input', this.currentText);
      },
      reset() {
        this.$refs.textbox.reset();
      },
      emitKeydown(e) {
        /**
         * Emits keydown event
         */
        this.$emit('keydown', e);
      },
    },
  };

</script>

<style lang="stylus"></style>

<template>

  <ui-textbox
    ref="textbox"
    v-model="currentText"
    :label="label"
    :disabled="disabled"
    :required="required"
    :invalid="invalid"
    :error="invalidText"
    :autofocus="autofocus"
    :maxlength="maxlength"
    :autocomplete="autocomplete"
    :type="type"
    :enforceMaxlength="true"
    :floatingLabel="true"
    @input="updateText"
    @keydown="emitKeydown"
    @focus="$emit('focus')"
    @blur="$emit('blur')"
  />

</template>

<script>

  import uiTextbox from 'keen-ui/src/UiTextbox';

  /**
   * Handles user input.
   */
  export default {
    name: 'k-textbox',
    components: { uiTextbox },
    props: {
      /**
       * v-model
       */
      value: {
        type: [String, Number],
      },
      /**
       * Label
       */
      label: {
        type: String,
        required: true,
      },
      /**
       * Whether or not disabled
       */
      disabled: {
        type: Boolean,
        default: false,
      },
      /**
       * Whether or not required
       */
      required: {
        type: Boolean,
        default: false,
      },
      /**
       * Whether or not input is invalid
       */
      invalid: {
        type: Boolean,
        default: false,
      },
      /**
       * Text displayed if input is invalid
       */
      invalidText: {
        type: String,
        required: false,
      },
      /**
       * Whether or not to autofocus
       */
      autofocus: {
        type: Boolean,
        default: false,
      },
      /**
       * Max allowed length of input
       */
      maxlength: {
        type: Number,
        required: false,
      },
      /**
       * HTML5 autocomplete attribute (off, on, name, username, current-password, etc.)
       */
      autocomplete: {
        type: String,
        required: false,
      },
      /**
       * HTML5 type of input (text, password, number, etc.)
       */
      type: {
        type: String,
        default: 'text',
      },
    },
    data() {
      return { currentText: this.value };
    },
    watch: {
      value(val) {
        this.currentText = val;
      },
    },
    methods: {
      updateText() {
        // v-model is just a :value + @input
        /**
         * Emits input event with new value
         */
        this.$emit('input', this.currentText);
      },
      reset() {
        this.$refs.textbox.reset();
      },
      emitKeydown(e) {
        /**
         * Emits keydown event
         */
        this.$emit('keydown', e);
      },
    },
  };

</script>

<style lang="stylus"></style>


reset proposal form on save (#6112)

reset proposal form on save (#6112)

<script lang="coffee">
import Records from '@/shared/services/records'
import AnnouncementModalMixin from '@/mixins/announcement_modal'
import EventBus                 from '@/shared/services/event_bus'
import { submitPoll }    from '@/shared/helpers/form'
import { iconFor }                from '@/shared/helpers/poll'
import { applyPollStartSequence } from '@/shared/helpers/apply'
import { fieldFromTemplate } from '@/shared/helpers/poll'
import { map } from 'lodash'

export default
  mixins: [AnnouncementModalMixin]
  props:
    discussion: Object
    close: Function
  data: ->
    poll: null
  created: -> @init()
  methods:
    init: ->
      @poll = @newPoll()
      @submit = submitPoll @, @poll,
        successCallback: (data) =>
          @poll = @newPoll()
          pollKey = data.polls[0].key
          EventBus.$emit('pollSaved')
          Records.polls.findOrFetchById(pollKey, {}, true).then (poll) =>
            @openAnnouncementModal(Records.announcements.buildFromModel(poll))
            @init()

  computed:
    title_key: ->
      mode = if @poll.isNew()
        'start'
      else
        'edit'
      'poll_' + @poll.pollType + '_form.'+mode+'_header'

  methods:
    newPoll: ->
      Records.polls.build
        pollType:              'proposal'
        discussionId:          @discussion.id
        groupId:               @discussion.groupId
        pollOptionNames:       map fieldFromTemplate('proposal', 'poll_options_attributes'), 'name'

    icon: ->
      iconFor(@poll)

</script>
<template lang="pug">
.poll-proposal-complete-form.pa-2(@keyup.ctrl.enter="submit()" @keydown.meta.enter.stop.capture="submit()")
  submit-overlay(:value="poll.processing")
  v-card-title
    h1.headline(v-t="title_key")
    v-spacer
  v-card-text
    poll-common-directive(:poll='poll', name='form')
  v-card-actions.poll-common-form-actions
    v-spacer
    v-btn.poll-common-form__submit(color="primary" @click='submit()' v-t="'poll_common_form.start'")
</template>

<script lang="coffee">
import Records from '@/shared/services/records'
import AnnouncementModalMixin from '@/mixins/announcement_modal'
import EventBus                 from '@/shared/services/event_bus'
import { submitPoll }    from '@/shared/helpers/form'
import { iconFor }                from '@/shared/helpers/poll'
import { applyPollStartSequence } from '@/shared/helpers/apply'
import { fieldFromTemplate } from '@/shared/helpers/poll'
import { map } from 'lodash'

export default
  mixins: [AnnouncementModalMixin]
  props:
    discussion: Object
    close: Function
  data: ->
    poll: null
  created: ->
    @poll = @newPoll()
    @submit = submitPoll @, @poll,
      successCallback: (data) =>
        @poll = @newPoll()
        pollKey = data.polls[0].key
        EventBus.$emit('pollSaved')
        Records.polls.findOrFetchById(pollKey, {}, true).then (poll) =>
          @openAnnouncementModal(Records.announcements.buildFromModel(poll))

  computed:
    title_key: ->
      mode = if @poll.isNew()
        'start'
      else
        'edit'
      'poll_' + @poll.pollType + '_form.'+mode+'_header'

  methods:
    newPoll: ->
      Records.polls.build
        pollType:              'proposal'
        discussionId:          @discussion.id
        groupId:               @discussion.groupId
        pollOptionNames:       map fieldFromTemplate('proposal', 'poll_options_attributes'), 'name'

    icon: ->
      iconFor(@poll)

</script>
<template lang="pug">
.poll-proposal-complete-form.pa-2(@keyup.ctrl.enter="submit()" @keydown.meta.enter.stop.capture="submit()")
  submit-overlay(:value="poll.processing")
  v-card-title
    h1.headline(v-t="title_key")
    v-spacer
  v-card-text
    poll-common-directive(:poll='poll', name='form')
  v-card-actions.poll-common-form-actions
    v-spacer
    v-btn.poll-common-form__submit(color="primary" @click='submit()' v-t="'poll_common_form.start'")
</template>