Add toggle to show/hide resource based on source type

<template>

Resource Tracking

                  {{{ annotations.icon(t) }}}

                    {{{ annotations.icon(t) }}} {{ t.label }}

                    <input class="switch-input" id="resourcesInViewport" type="checkbox" v-bind:checked="viewportOnly" @change="toggleViewportOnly" />

                      Viewport resources only

                  Restrict to viewport ({{extentFeaturesSize}}/{{featurelistSize}})

                 Toggles

                <input class="switch-input" id="toggleDBCAResourceLabels" type="checkbox" v-bind:checked="showDBCAResource" @change="showDBCAResource = !showDBCAResource" />

                  Show DBCA Resources

              Show DBCA Resources

                <input class="switch-input" id="toggleDFESResourceLabels" type="checkbox" v-bind:checked="showDFESResource" @change="showDFESResource = !showDFESResource" />

                  Show DFES Resources

              Show DFES Resources

                <input class="switch-input" id="toggleOtherExternalResourceLabels" type="checkbox" v-bind:checked="showOtherExternalResource" @change="showOtherExternalResource = !showOtherExternalResource" />

                  Show Other External Resources

              Show Other External Resources

                <input class="switch-input" id="toggleResourceLabels" type="checkbox" v-bind:checked="resourceLabels" @change="toggleResourceLabels" />

                  Display resource labels

              Display resource labels

                <input class="switch-input" id="toggleResourceDirections" type="checkbox" v-bind:checked="resourceDirections" @change="toggleResourceDirections" />

                  Display resource directions

              Display resource directions

                <input class="switch-input" id="toggleResourceInfo" type="checkbox" v-bind:disabled="!systemsetting.hoverInfoSwitchable" v-bind:checked="systemsetting.hoverInfo" @change="systemsetting.toggleHoverInfo" />

                  Display hovering resource info

              Display hovering resource info

                      <input class="switch-input" id="clippedResourceOnly" v-bind:disabled="clippedFeatures.length === 0" type="checkbox" v-model="clippedOnly"  @change="updateCQLFilter()"/>

                        Show saved selection

                    Show saved selection

                        Save selection ({{selectedFeaturesSize}})

                    ({{clippedFeatures.length}})

                <select name="select" v-model="groupFilter" @change="updateCQLFilter()"  v-bind:disabled="clippedOnly">
                  <option value="" selected>All resources</option>
                  <option value="symbolid LIKE '%aircraft'">Aircraft</option>
                  <option value="symbolid LIKE '%comms_bus'">Communications Bus</option>
                  <option value="symbolid LIKE '%gang_truck'">Gang Truck</option>
                  <option value="symbolid LIKE '%heavy_duty'">Heavy Duty</option>
                  <option value="(symbolid LIKE '%heavy_duty' OR symbolid LIKE '%gang_truck')">Gang Truck and Heavy Duty</option>
                  <option value="symbolid LIKE '%light_unit'">Light Unit</option>
                  <option value="(symbolid LIKE '%dozer' OR symbolid LIKE '%grader' OR symbolid LIKE '%loader')">Machinery</option>
                  <option value="(symbolid LIKE '%2_wheel_drive' OR symbolid LIKE '%4_wheel_drive_passenger' OR symbolid LIKE '%4_wheel_drive_ute')">Other Light Fleet</option>
                </select>

                <input type="search" v-model="search" placeholder="Find a resource" @keyup="updateFeatureFilter(500)">

                      <input class="switch-input" id="resourceHistory" type="checkbox" v-model="toggleHistory" @change="clearHistory" />

                    Query history

                    Query history

                  From:

                  <input type="text" v-on:blur="utils.verifyDate($event,['YY-M-D','YYYY-M-D'],'YYYY-MM-DD')" v-model="historyFromDate" placeholder="yyyy-mm-dd"></input>

                  <input type="text" v-on:blur="utils.verifyDate($event,'H:m','HH:mm')" v-model="historyFromTime" placeholder="24:00"></input>

                  <select name="select" v-model="history" @change="historyRange = history">
                  <option value="" selected>Date range</option>

                  <option value="3600000">Last hour</option>
                  <option value="10800000">Last 3 hours</option>
                  <option value="86400000">Last day</option>
                  <option value="604800000">Last week</option>
                  <option value="2678400000">Last month</option>
                </select>

                  To:

                  <input type="text" v-on:blur="utils.verifyDate($event,['YY-M-D','YYYY-M-D'],'YYYY-MM-DD')" v-model="historyToDate" placeholder="yyyy-mm-dd"></input>

                  <input type="text" v-on:blur="utils.verifyDate($event,'H:m','HH:mm')" v-model="historyToTime" placeholder="24:00"></input>

                  <button v-bind:disabled="queryHistoryDisabled" class="button" style="float: right" @click="historyCQLFilter">Go</button>

              <template v-for="f in featurelist" track-by="get('id')">

 {{ f.get('label') }} ({{ ago(f.get('seen')) }})

              </template>

</template>

<template>

Resource Tracking

                  {{{ annotations.icon(t) }}}

                    {{{ annotations.icon(t) }}} {{ t.label }}

                    <input class="switch-input" id="resourcesInViewport" type="checkbox" v-bind:checked="viewportOnly" @change="toggleViewportOnly" />

                      Viewport resources only

                  Restrict to viewport ({{extentFeaturesSize}}/{{featurelistSize}})

                 Toggles

                <input class="switch-input" id="toggleResourceLabels" type="checkbox" v-bind:checked="resourceLabels" @change="toggleResourceLabels" />

                  Display resource labels

              Display resource labels

                <input class="switch-input" id="toggleResourceDirections" type="checkbox" v-bind:checked="resourceDirections" @change="toggleResourceDirections" />

                  Display resource directions

              Display resource directions

                <input class="switch-input" id="toggleResourceInfo" type="checkbox" v-bind:disabled="!systemsetting.hoverInfoSwitchable" v-bind:checked="systemsetting.hoverInfo" @change="systemsetting.toggleHoverInfo" />

                  Display hovering resource info

              Display hovering resource info

                      <input class="switch-input" id="clippedResourceOnly" v-bind:disabled="clippedFeatures.length === 0" type="checkbox" v-model="clippedOnly"  @change="updateCQLFilter()"/>

                        Show saved selection

                    Show saved selection

                        Save selection ({{selectedFeaturesSize}})

                    ({{clippedFeatures.length}})

                <select name="select" v-model="groupFilter" @change="updateCQLFilter()"  v-bind:disabled="clippedOnly">
                  <option value="" selected>All resources</option>
                  <option value="symbolid LIKE '%aircraft'">Aircraft</option>
                  <option value="symbolid LIKE '%comms_bus'">Communications Bus</option>
                  <option value="symbolid LIKE '%gang_truck'">Gang Truck</option>
                  <option value="symbolid LIKE '%heavy_duty'">Heavy Duty</option>
                  <option value="(symbolid LIKE '%heavy_duty' OR symbolid LIKE '%gang_truck')">Gang Truck and Heavy Duty</option>
                  <option value="symbolid LIKE '%light_unit'">Light Unit</option>
                  <option value="(symbolid LIKE '%dozer' OR symbolid LIKE '%grader' OR symbolid LIKE '%loader')">Machinery</option>
                  <option value="(symbolid LIKE '%2_wheel_drive' OR symbolid LIKE '%4_wheel_drive_passenger' OR symbolid LIKE '%4_wheel_drive_ute')">Other Light Fleet</option>
                </select>

                <input type="search" v-model="search" placeholder="Find a resource" @keyup="updateFeatureFilter(500)">

                      <input class="switch-input" id="resourceHistory" type="checkbox" v-model="toggleHistory" @change="clearHistory" />

                    Query history

                    Query history

                  From:

                  <input type="text" v-on:blur="utils.verifyDate($event,['YY-M-D','YYYY-M-D'],'YYYY-MM-DD')" v-model="historyFromDate" placeholder="yyyy-mm-dd"></input>

                  <input type="text" v-on:blur="utils.verifyDate($event,'H:m','HH:mm')" v-model="historyFromTime" placeholder="24:00"></input>

                  <select name="select" v-model="history" @change="historyRange = history">
                  <option value="" selected>Date range</option>

                  <option value="3600000">Last hour</option>
                  <option value="10800000">Last 3 hours</option>
                  <option value="86400000">Last day</option>
                  <option value="604800000">Last week</option>
                  <option value="2678400000">Last month</option>
                </select>

                  To:

                  <input type="text" v-on:blur="utils.verifyDate($event,['YY-M-D','YYYY-M-D'],'YYYY-MM-DD')" v-model="historyToDate" placeholder="yyyy-mm-dd"></input>

                  <input type="text" v-on:blur="utils.verifyDate($event,'H:m','HH:mm')" v-model="historyToTime" placeholder="24:00"></input>

                  <button v-bind:disabled="queryHistoryDisabled" class="button" style="float: right" @click="historyCQLFilter">Go</button>

              <template v-for="f in featurelist" track-by="get('id')">

 {{ f.get('label') }} ({{ ago(f.get('seen')) }})

              </template>

</template>


[update] bank bcp dolares actualizado

[update] bank bcp dolares actualizado


Changing function to arrow function

Changing function to arrow function

To avoid using _self as this. Also (not related), adding destructuring.

<template>
  <router-link
    :to="{ name: routingMember, params: {id: item.id } }"
    tag="tr"
    class="dashboards-home-main--tr"
  >
    <td
      v-for="{ field } in columns"
      class="dashboards-home-main--td"
    >
      <div>{{ formattedItem[field] }}</div>
    </td>
  </router-link>
</template>

<script>
export default {
  name: "TableRow",
  data(){
    return {
      formattedItem: {}
    }
  },
  props: {
    item: {
      type: Object,
      default: () => {}
    },
    columns: {
      type: Array,
      default: () => []
    },
    routingMember: {
      type: String,
      default: ''
    }
  },
  created(){
    this.initFormattedItem();
  },
  methods: {
    initFormattedItem(){
      this.columns.forEach(({format, field}) => {
        if (format === 'currency') {
          this.formattedItem[field] = parseFloat(this.item[field]).toLocaleString(I18n.locale, {
            style: 'currency',
            currency: 'EUR'
          });
        } else {
          this.formattedItem[field] = this.item[field]
        }
      });
    }
  }
};
</script>

<template>
  <router-link
    :to="{ name: routingMember, params: {id: item.id } }"
    tag="tr"
    class="dashboards-home-main--tr"
  >
    <td
      v-for="{ field } in columns"
      class="dashboards-home-main--td"
    >
      <div>{{ formattedItem[field] }}</div>
    </td>
  </router-link>
</template>

<script>
export default {
  name: "TableRow",
  data(){
    return {
      formattedItem: {}
    }
  },
  props: {
    item: {
      type: Object,
      default: () => {}
    },
    columns: {
      type: Array,
      default: () => []
    },
    routingMember: {
      type: String,
      default: ''
    }
  },
  created(){
    this.initFormattedItem();
  },
  methods: {
    initFormattedItem(){
      const _self = this;

      _self.columns.forEach(function(column){
        if (column.format === 'currency') {
          _self.formattedItem[column.field] = parseFloat(_self.item[column.field]).toLocaleString(I18n.locale, {
            style: 'currency',
            currency: 'EUR'
          });
        } else {
          _self.formattedItem[column.field] = _self.item[column.field]
        };
      });
    }
  }
};
</script>


修复模板的问题

修复模板的问题

<template>
    <v-secondary-nav :title="'购物篮'" :btn="''"></v-secondary-nav>
    <template v-if="cartItems.data.length > 0">

            <form action="" id="cart-form" method="post">

                                    <input checked="" :value="info.product_id" v-model="selected"
                                        type="checkbox" number/>

                                            {{info.title}}

                                                ¥{{info.unit_price | currency 1}}

                                                <input class="num_s" readonly="" type="text" value="{{info.qty}}">
                                                </input>

                                            库存不足

                                        9折特惠

            </form>

                    所有优惠

                                满300元减20

                    金额小计

                            诚食价

                            ¥{{total | currency}}

                            优惠

                            -¥ {{discount | currency}}

                                小计

                                ¥{{totalAfterDiscount | currency}}

                                可得积分

                                {{Math.floor(total)}}

                            <input type="checkbox" v-model="selectAll"/>
                            全选

                            共{{cartItems.num}}件商品

                            小计

                                ¥{{totalAfterDiscount | currency}}

                    去结算

                        <input type="checkbox" id="selAll" value=""
                            name="checkAllBox" checked="checked"/>
                        全选

                    添加至喜欢的
                    删除

    </template>

    <template v-if="!cartItems.data || cartItems.data.length == 0">

                    购物篮中空空的喔

                    去逛逛，不要忘记带上食欲

    </template>

                浏览记录

                    <v-good-slider></v-good-slider>

</template>


more api doc for validators

more api doc for validators

<template>

API

install()

TODO

validators object

        The validators object placed in your vue / component definition contains all the validation logic.

validators: {
  email: function(value) {
    return Validator.value(value).required().email();
  }
}

        The key of validator denotes the name of field in Vue model you want to observe and validate.

        When validated nested field, use dot syntax to write the field name.

        The key can also be a list of names separated by comma, which makes the library to watch multiple fields and provide multiple values to your validator function.
        Note that in case of multiple names, the first name is used as the error field name.

validators: {
  email: function(value) {...},
  'person.name': function(value) {...},
  'password, repeat': function(passwordValue, repeatValue) {...},
}

        The validator function should take the field value(s) as parameter(s),
        call Validator's methods to set validation rule,
        and finally and return it to the library.

        Additionally, you can pass the cache and debounce options to further control the validator's behavior.

validators: {
  'email': {
    cache: true, // or 'all', 'last'
    debounce: 500, // in milliseconds
    validator: function(value) {...},
  }
}

        The cache option is useful for async validation
        that the validation results for the same field values are cached.

        Beside setting it to true,
        you can also set it to 'all' which has the same effect as true,
        or 'last' which will only cache the last validation result, it's useful
        if you only want to prevent duplicate async validation in form submission.

        The debounce option can be set with a number which is the millisecond to wait before user stops typing,
        it's useful for saving ajax call so that it won't impact server performance.

Validator / rules

        The Validator class provides the entry point for setting validation rule,
        it has the following methods:

            Method
            Parmas
            Return
            Description

            create
            none
            the validator
            Creates an empty validator.

            value
            {String value}
            the validator
            Sets the value which will be used by all built-in rules.

            required
            none
            the validator
            Checks that field value is not null / empty.

            float
            none
            the validator
            Checks that field value is a float number.

            integer
            none
            the validator
            Checks that the field value is an integer.

            lessThan
            {Number num}
            the validator
            Checks that the field value is less than given num.

            lessThanOrEqualTo
            {Number num}
            the validator
            Checks that the field value is less than or equal to given num.

            greaterThan
            {Number num}
            the validator
            Checks that the field value is greater than given num.

            greaterThanOrEqualTo
            {Number num}
            the validator
            Checks that the field value is greater than or equal to given num.

            between
            {Number low}
{Number high}
            the validator
            Checks that the field value is between given bounds (inclusive).

            size
            {Number num}
            the validator
            Checks that if the field value is an array like object, its size is the same as given num.

            length
            {Number num}
            the validator
            Checks that the field value's string length equals to given num.

            minLength
            {Number num}
            the validator
            Checks that the field value's string length is greater or equals to given num.

            maxLength
            {Number num}
            the validator
            Checks that the field value's string length is less or equals to given num.

            lengthBetween
            {Number low}
{Number high}
            the validator
            Checks that the field value's string length between given bounds (inclusive).

            in
            {Array options}
            the validator
            Checks that the field value is in given options.

            notIn
            {Array options}
            the validator
            Checks that the field value is not in given options.

            regex
            {RegExp|String pattern}
{String message?}
            the validator
            Checks that the field value matches given pattern, optionally use given message as error message if provided.

            digit
            none
            the validator
            Checks that the field value contains only digit characters.

            email
            none
            the validator
            Checks that the field value is a valid e-mail.

            url
            none
            the validator
            Checks that the field value is a valid url.

            custom
            {Function callback}
{Object context?}
            the validator

              Execute custom validation logic in provide callback function, optional use given context for callback function invocation.

              The callback function is expected to return the error message if validation fails.

              Or in the case of async validation, the callback function can return a promise which would resolves to error message if validation fails.

            isEmpty
            {Object value}
            Boolean

              A utility method that check if given value is null / undefined / empty string (after trim) or empty array,
              it's used by required() method internally, and also exposed so to be utilized by custom code.

            format
            {String template}
{String... args}
            String

              A utility method to format given args using given template.

            extendTemplates
            {Object template}
            undefined

              Extend the built-in error text template, this method is mainly used for localization.

              See templates.js for all the text templates you can replace.

ValidationBag

        when you provides the validators object to your vue / component instance,
        the library adds a ValidationBag instance named validation to your vue / component instance.
        It contains various methods for you to display validation status / result in your template HTML.

          Method
          Parmas
          Return
          Description

            hasError
            {String field?}
            Boolean
            Returns if there's any error related to given field (or the whole validation instance if field is not provide).

            firstError
            {String field?}
            String
            Returns the first error related to given field (or the whole validation instance if field is not provide).

            allErrors
            {String field?}
            Array
            Returns all the errors related to given field (or the whole validation instance if field is not provide).

            countErrors
            {String field?}
            Array
            Returns the count of errors related to given field (or the whole validation instance if field is not provide).

            isValidating
            {String field?}
            Boolean

              Returns if the library is validating (async validation) given field
              (or if there's any field in the validation instance being validating).

            isPassed
            {String field}
            Boolean

              Returns if given field has passed the validation.

            isTouched
            {String field}
            Boolean

              Returns if given field has been touched by user.

$validate()

        when you provides the validators object to your vue / component instance,
        the library adds a $validate() method to your vue / component instance.

        Calling $validate() method would execute all the validators,
        and returns a promise which would resolve to true if validation successes.

</template>

<template>

API

install()

TODO

validators object

        The validators object placed in your vue / component definition contains all the validation logic.

validators: {
  email: function(value) {
    return Validator.value(value).required().email();
  }
}

        The key of validator denotes the name of field in Vue model you want to observe and validate.

        When validated nested field, use dot syntax to write the field name.

        The key can also be a list of names separated by comma, which makes the library to watch multiple fields and provide multiple values to your validator function.
        Note that in case of multiple names, the first name is used as the error field name.

validators: {
  email: function(value) {...},
  'person.name': function(value) {...},
  'password, repeat': function(passwordValue, repeatValue) {...},
}

        The validator function should take the field value(s) as parameter(s),
        call Validator's methods to set validation rule,
        and finally and return it to the library.

Validator / rules

        The Validator class provides the entry point for setting validation rule,
        it has the following methods:

            Method
            Parmas
            Return
            Description

            create
            none
            the validator
            Creates an empty validator.

            value
            {String value}
            the validator
            Sets the value which will be used by all built-in rules.

            required
            none
            the validator
            Checks that field value is not null / empty.

            float
            none
            the validator
            Checks that field value is a float number.

            integer
            none
            the validator
            Checks that the field value is an integer.

            lessThan
            {Number num}
            the validator
            Checks that the field value is less than given num.

            lessThanOrEqualTo
            {Number num}
            the validator
            Checks that the field value is less than or equal to given num.

            greaterThan
            {Number num}
            the validator
            Checks that the field value is greater than given num.

            greaterThanOrEqualTo
            {Number num}
            the validator
            Checks that the field value is greater than or equal to given num.

            between
            {Number low}
{Number high}
            the validator
            Checks that the field value is between given bounds (inclusive).

            size
            {Number num}
            the validator
            Checks that if the field value is an array like object, its size is the same as given num.

            length
            {Number num}
            the validator
            Checks that the field value's string length equals to given num.

            minLength
            {Number num}
            the validator
            Checks that the field value's string length is greater or equals to given num.

            maxLength
            {Number num}
            the validator
            Checks that the field value's string length is less or equals to given num.

            lengthBetween
            {Number low}
{Number high}
            the validator
            Checks that the field value's string length between given bounds (inclusive).

            in
            {Array options}
            the validator
            Checks that the field value is in given options.

            notIn
            {Array options}
            the validator
            Checks that the field value is not in given options.

            regex
            {RegExp|String pattern}
{String message?}
            the validator
            Checks that the field value matches given pattern, optionally use given message as error message if provided.

            digit
            none
            the validator
            Checks that the field value contains only digit characters.

            email
            none
            the validator
            Checks that the field value is a valid e-mail.

            url
            none
            the validator
            Checks that the field value is a valid url.

            custom
            {Function callback}
{Object context?}
            the validator

              Execute custom validation logic in provide callback function, optional use given context for callback function invocation.

              The callback function is expected to return the error message if validation fails.

              Or in the case of async validation, the callback function can return a promise which would resolves to error message if validation fails.

            isEmpty
            {Object value}
            Boolean

              A utility method that check if given value is null / undefined / empty string (after trim) or empty array,
              it's used by required() method internally, and also exposed so to be utilized by custom code.

            format
            {String template}
{String... args}
            String

              A utility method to format given args using given template.

            extendTemplates
            {Object template}
            undefined

              Extend the built-in error text template, this method is mainly used for localization.

              See templates.js for all the text templates you can replace.

ValidationBag

        when you provides the validators object to your vue / component instance,
        the library adds a ValidationBag instance named validation to your vue / component instance.
        It contains various methods for you to display validation status / result in your template HTML.

          Method
          Parmas
          Return
          Description

            hasError
            {String field?}
            Boolean
            Returns if there's any error related to given field (or the whole validation instance if field is not provide).

            firstError
            {String field?}
            String
            Returns the first error related to given field (or the whole validation instance if field is not provide).

            allErrors
            {String field?}
            Array
            Returns all the errors related to given field (or the whole validation instance if field is not provide).

            countErrors
            {String field?}
            Array
            Returns the count of errors related to given field (or the whole validation instance if field is not provide).

            isValidating
            {String field?}
            Boolean

              Returns if the library is validating (async validation) given field
              (or if there's any field in the validation instance being validating).

            isPassed
            {String field}
            Boolean

              Returns if given field has passed the validation.

            isTouched
            {String field}
            Boolean

              Returns if given field has been touched by user.

$validate()

        when you provides the validators object to your vue / component instance,
        the library adds a $validate() method to your vue / component instance.

        Calling $validate() method would execute all the validators,
        and returns a promise which would resolve to true if validation successes.

</template>


move content title to top of file, move download button below description

<template>

    <page-header>
      <breadcrumbs
        v-if="pageMode === $options.PageModes.EXPLORE"
        slot='extra-nav'
        :rootid='rootTopicId'
        :crumbs='breadcrumbs'>
      </breadcrumbs>

        ← Learn

    </page-header>

    <page-header :title='title'>
      <content-icon
        slot='icon'
        :ispageicon="true"
        :size="25"
        :kind="kind"
        :progress="progress">
      </content-icon>
    </page-header>

      <content-render
        :id="id"
        :kind="kind"
        :files="files"
        :content-id="contentId"
        :channel-id="channelId"
        :available="available"
        :extra-fields="extraFields">
      </content-render>

      {{ description }}

    <download-button
      :kind="kind"
      :files="files"
      :available="available"
      :title="title">
    </download-button>

    <expandable-content-grid class="recommendation-section"
      v-if="pageMode === $options.PageModes.LEARN"
      title="Recommended"
      :contents="recommended">
    </expandable-content-grid>

</template>

<template>

    <page-header>
      <breadcrumbs
        v-if="pageMode === $options.PageModes.EXPLORE"
        slot='extra-nav'
        :rootid='rootTopicId'
        :crumbs='breadcrumbs'>
      </breadcrumbs>

        ← Learn

    </page-header>

      <content-render
        :id="id"
        :kind="kind"
        :files="files"
        :content-id="contentId"
        :channel-id="channelId"
        :available="available"
        :extra-fields="extraFields">
      </content-render>

    <page-header :title='title'>
      <content-icon
        slot='icon'
        :ispageicon="true"
        :size="25"
        :kind="kind"
        :progress="progress">
      </content-icon>
    </page-header>

    <download-button
      :kind="kind"
      :files="files"
      :available="available"
      :title="title">
    </download-button>

      {{ description }}

    <expandable-content-grid class="recommendation-section"
      v-if="pageMode === $options.PageModes.LEARN"
      title="Recommended"
      :contents="recommended">
    </expandable-content-grid>

</template>


Single quote for wrapped strings.

Single quote for wrapped strings.

<template>

    <page-header :title="content.title"/>

    <content-renderer
      v-show="!searchOpen"
      class="content-renderer"
      :id="content.id"
      :kind="content.kind"
      :files="content.files"
      :contentId="content.content_id"
      :channelId="channelId"
      :available="content.available"
      :extraFields="content.extra_fields"/>

    <icon-button @click="nextContentClicked" v-if="progress >= 1 && showNextBtn" class="next-btn">
      {{ $tr('nextContent') }}
      <svg class="right-arrow" src="../icons/arrow_right.svg"/>
    </icon-button>

{{ content.description }}

    <download-button v-if="canDownload" :files="content.files" class="download-button-left-align"/>

        {{ $tr('author') }}:
        {{ content.author }}
        -

        {{ $tr('license') }}:
        {{ content.license }}
        -

        {{ $tr('copyrightHolder') }}:
        {{ content.license_owner }}
        -

    <expandable-content-grid
      class="recommendation-section"
      v-if="pageMode === Constants.PageModes.LEARN"
      :title="recommendedText"
      :contents="recommended"/>

</template>

<template>

    <page-header :title="content.title"/>

    <content-renderer
      v-show="!searchOpen"
      class="content-renderer"
      :id="content.id"
      :kind="content.kind"
      :files="content.files"
      :contentId="content.content_id"
      :channelId="channelId"
      :available="content.available"
      :extraFields="content.extra_fields"/>

    <icon-button @click="nextContentClicked" v-if="progress >= 1 && showNextBtn" class="next-btn">
      {{ $tr("nextContent") }}
      <svg class="right-arrow" src="../icons/arrow_right.svg"/>
    </icon-button>

{{ content.description }}

    <download-button v-if="canDownload" :files="content.files" class="download-button-left-align"/>

        {{ $tr('author') }}:
        {{ content.author }}
        -

        {{ $tr('license') }}:
        {{ content.license }}
        -

        {{ $tr('copyrightHolder') }}:
        {{ content.license_owner }}
        -

    <expandable-content-grid
      class="recommendation-section"
      v-if="pageMode === Constants.PageModes.LEARN"
      :title="recommendedText"
      :contents="recommended"/>

</template>


Uncategorized label for missing referer category

<template>

!

Page Loads by Traffic Source

            <button-switcher :options="[
                {text: 'Today', value: 'today'},
                {text: '7 days', value: '7days'},
                {text: '30 days', value: '30days'}]"
                             v-model="interval">
            </button-switcher>

                <svg class="pl-circular" viewBox="25 25 50 50">
                    <circle class="plc-path" cx="50" cy="50" r="20"></circle>
                </svg>

                <svg style="z-index: 10" ref="svg"></svg>

{{highlightedRow.startDate | formatDate(data.intervalMinutes)}}

                            Current
                            Week ago

                                ●
                                Uncategorized
                                {{tag}}

                            {{item.current}}
                            {{item.previous}}

                            Total
                            {{highlightedRow.currentSum}}
                            {{highlightedRow.previousSum}}

</template>

<template>

!

Page Loads by Traffic Source

            <button-switcher :options="[
                {text: 'Today', value: 'today'},
                {text: '7 days', value: '7days'},
                {text: '30 days', value: '30days'}]"
                             v-model="interval">
            </button-switcher>

                <svg class="pl-circular" viewBox="25 25 50 50">
                    <circle class="plc-path" cx="50" cy="50" r="20"></circle>
                </svg>

                <svg style="z-index: 10" ref="svg"></svg>

{{highlightedRow.startDate | formatDate(data.intervalMinutes)}}

                            Current
                            Week ago

                                ● {{tag}}

                            {{item.current}}
                            {{item.previous}}

                            Total
                            {{highlightedRow.currentSum}}
                            {{highlightedRow.previousSum}}

</template>


Wrong import name fix

Wrong import name fix

<template>

{{ $t('Find the right sporting clothes for all situations') }}

        <input ref="search" v-model="search" @input="makeSearch" type="text" class="m0 px25 inline-flex fs-medium" :placeholder="$t('What are you looking for?')">

      Top searches
      Shoes
      Bags
      Longsleeves
      Jackets

        <router-link :to="`/p/${product.sku}/${product.slug}/${product.sku}`">
          <product-tile :product="product" class="b bg-primary" :mobile-horizontal-mode="true"/>
        </router-link>

</template>


Set the correct placeholder avatar

Set the correct placeholder avatar

Signed-off-by: Joas Schilling <ab43a7c9cb5b2380afc4ddf8b3e2583169b39a02@schilljs.com>

<template>

		<audio v-if="!placeholderForPromoted" ref="audio" class="hidden" />
		<video v-if="!placeholderForPromoted" v-show="model.attributes.videoAvailable && sharedData.videoEnabled" ref="video" />

			<Avatar v-if="model.attributes.userId"
				:size="avatarSize"
				:disable-menu="true"
				:disable-tooltip="true"
				:user="model.attributes.userId"
				:display-name="model.attributes.name"
				:class="avatarClass" />
			<Avatar v-else
				:size="avatarSize"
				:disable-menu="true"
				:disable-tooltip="true"
				:display-name="firstLetterOfGuestName"
				:class="avatarClass" />

			{{ participantName }}

			<button v-show="!connectionStateFailedNoRestart"
				class="muteIndicator forced-white icon-audio-off"
				:class="audioButtonClass"
				disabled="true" />
			<button v-show="!connectionStateFailedNoRestart && model.attributes.videoAvailable"
				v-tooltip="videoButtonTooltip"
				class="hideRemoteVideo forced-white"
				:class="videoButtonClass"
				@click="toggleVideo" />
			<button v-show="!connectionStateFailedNoRestart"
				v-tooltip="t('spreed', 'Show screen')"
				class="screensharingIndicator forced-white icon-screen"
				:class="screenSharingButtonClass"
				@click="switchToScreen" />
			<button v-show="connectionStateFailedNoRestart"
				class="iceFailedIndicator forced-white icon-error"
				:class="{ 'not-failed': !connectionStateFailedNoRestart }"
				disabled="true" />

</template>

<template>

		<audio v-if="!placeholderForPromoted" ref="audio" class="hidden" />
		<video v-if="!placeholderForPromoted" v-show="model.attributes.videoAvailable && sharedData.videoEnabled" ref="video" />

			<Avatar v-if="model.attributes.userId"
				:size="avatarSize"
				:disable-menu="true"
				:disable-tooltip="true"
				:user="model.attributes.userId"
				:class="avatarClass" />
			<Avatar v-else
				:size="avatarSize"
				:disable-menu="true"
				:disable-tooltip="true"
				:display-name="firstLetterOfGuestName"
				:class="avatarClass" />

			{{ participantName }}

			<button v-show="!connectionStateFailedNoRestart"
				class="muteIndicator forced-white icon-audio-off"
				:class="audioButtonClass"
				disabled="true" />
			<button v-show="!connectionStateFailedNoRestart && model.attributes.videoAvailable"
				v-tooltip="videoButtonTooltip"
				class="hideRemoteVideo forced-white"
				:class="videoButtonClass"
				@click="toggleVideo" />
			<button v-show="!connectionStateFailedNoRestart"
				v-tooltip="t('spreed', 'Show screen')"
				class="screensharingIndicator forced-white icon-screen"
				:class="screenSharingButtonClass"
				@click="switchToScreen" />
			<button v-show="connectionStateFailedNoRestart"
				class="iceFailedIndicator forced-white icon-error"
				:class="{ 'not-failed': !connectionStateFailedNoRestart }"
				disabled="true" />

</template>


Change test filtering base to use watch

Change test filtering base to use watch

<template>

                <select2 v-model="selectedTag" :value="selectedTag" @change="$emit('change-tag')"
                         name="tagSelect" ref="tagSelect"
                         :option="{placeholder: i18n('perfTest.action.selectATag'), allowClear: true}">
                    <option value=""></option>
                    <option v-for="tag in userTags" v-text="tag" :value="tag"></option>
                </select2>
                <input type="search" name="search" class="search-query-without-radios form-control"
                       placeholder="Keywords" v-model="searchText" @keydown.enter="$emit('search')" v-focus>
                <button class="btn btn-info align-baseline" @click="$emit('search')">

                </button>

                    <input type="checkbox" class="align-middle" v-model="running">

                    <input type="checkbox" class="align-middle" v-model="scheduled">

            <button class="btn btn-primary align-baseline" @click="$emit('create')" data-position="left" data-step="1"
               :data-intro="i18n('intro.list.create')">

            </button>
            <button @click="$emit('delete-selected-tests')" class="btn btn-danger align-baseline" data-position="top"
               data-step="2" :data-intro="i18n('intro.list.delete')">

            </button>

</template>

<template>

                <select2 v-model="selectedTag" :value="selectedTag" @change="$emit('change-tag')"
                         name="tagSelect" ref="tagSelect"
                         :option="{placeholder: i18n('perfTest.action.selectATag'), allowClear: true}">
                    <option value=""></option>
                    <option v-for="tag in userTags" v-text="tag" :value="tag"></option>
                </select2>
                <input type="search" name="search" class="search-query-without-radios form-control"
                       placeholder="Keywords" v-model="searchText" @keydown.enter="$emit('search')" v-focus>
                <button class="btn btn-info align-baseline" @click="$emit('search')">

                </button>

                    <input type="checkbox" class="align-middle" @click="$emit('filter-running', {enable: !running, token: 'R'})" v-model="running">

                    <input type="checkbox" class="align-middle" @click="$emit('filter-schduled', {enable: !scheduled, token: 'S'})" v-model="scheduled">

            <button class="btn btn-primary align-baseline" @click="$emit('create')" data-position="left" data-step="1"
               :data-intro="i18n('intro.list.create')">

            </button>
            <button @click="$emit('delete-selected-tests')" class="btn btn-danger align-baseline" data-position="top"
               data-step="2" :data-intro="i18n('intro.list.delete')">

            </button>

</template>


法律法规插入

法律法规插入

<template>

    <el-form class="form-container" :model="postForm" ref="postForm">
      <sticky :className="'sub-navbar '">

              <router-link style="margin-right:15px;" v-show='isEdit' :to="{ path:'/event/editeventstep/:id'}">
                <el-button type="info">创建form</el-button>
              </router-link>

          <el-button v-loading="loading" style="margin-left: 10px;" type="primary" @click="submitForm()">保存
          </el-button>
      </sticky>

        <el-row>
          <el-col :span="21">
            <el-form-item style="margin-bottom: 40px;">
              <MDinput name="name" v-model="postForm.name" required :maxlength="100">
                步骤名称
              </MDinput>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row>
          <el-col :span="11">
            <el-form-item label-width="65px" label="部门:" prop="department_id" class="postInfo-container-item">
              <el-select clearable class="filter-item" style="width: 130px" filterable @change="changeDepRule" v-model="postForm.department_id" placeholder="请选择">
                <el-option v-for="item in  depArr" :key="item._id" :title="item.parentName" :label="item.name" :value="item._id">
                </el-option>
              </el-select>
            </el-form-item>
          </el-col>
          <el-col :span="11">
            <el-form-item label-width="85px" label="法律法规:" prop="department_id" class="postInfo-container-item">
              <el-cascader :options="myLawArr" :props="props" @change="handleItemChange2" @active-item-change="handleItemChange"></el-cascader>

              <el-button @click.prevent="insertRole_html()">插入</el-button>
            </el-form-item>
          </el-col>
        </el-row>

          <el-row>
            <el-col :span="11">
              <el-form-item label-width="65px" v-for="(para, index) in postForm.para_list" :label="'参数' + (index)" :key="index"  >
                <el-select style="width: 120px"  v-model="para.para_type"  placeholder="请选择">
                  <el-option v-for="item in  paraTypeArr" :key="item._id" :label="item.name" :value="item._id">
                  </el-option>
                </el-select>
                <el-select style="width: 130px" v-if="para.para_type === '6'"  v-model="para.para_name"  placeholder="请选择">
                  <el-option v-for="item in  lawArr" :key="item._id" :label="item.name" :value="item.content">
                  </el-option>
                </el-select>
                <el-input style="width: 130px" v-else placeholder="参数名" v-model="para.para_name"></el-input>
                <el-button v-if="index === 0" @click="addPare">新增</el-button>
                <el-button v-show="index !== 0" @click.prevent="removePare(para)">删除</el-button>

                <el-button v-if="para.para_type != '4'" @click.prevent="insertContent(para)">插入</el-button>
              </el-form-item>

提示：
              1.参数类型指的是用户以何种方式填写事件原由，插入参数后，不可直接在富文本框里面对参数进行修改.

              2.默认添加的表格无边框，添加边框需设置表格边框值

            </el-col>
            <el-col :span="11">
              <el-form-item label-width="80px" v-for="(role, index) in postForm.roleArr" :label="'审核'+ (index)" :key="role._id">
                <el-select  v-model="role._id"  placeholder="请选择">
                  <el-option v-for="item in  fetchArr" :key="item._id" :label="item.name" :value="item._id">
                  </el-option>
                </el-select>
                <el-button v-if="index === 0" @click="addRole">新增</el-button>
                <el-button v-show="index !== 0" @click.prevent="removeRole(role)">删除</el-button>
                <el-button @click.prevent="insertRole(role._id)">插入</el-button>
              </el-form-item>
            </el-col>
          </el-row>

          <tinymce id="tinymce" :height='500' v-model="postForm.content"></tinymce>

    </el-form>

</template>

<template>

    <el-form class="form-container" :model="postForm" ref="postForm">
      <sticky :className="'sub-navbar '">

              <router-link style="margin-right:15px;" v-show='isEdit' :to="{ path:'/event/editeventstep/:id'}">
                <el-button type="info">创建form</el-button>
              </router-link>

          <el-button v-loading="loading" style="margin-left: 10px;" type="primary" @click="submitForm()">保存
          </el-button>
      </sticky>

        <el-row>
          <el-col :span="21">
            <el-form-item style="margin-bottom: 40px;">
              <MDinput name="name" v-model="postForm.name" required :maxlength="100">
                步骤名称
              </MDinput>
            </el-form-item>
          </el-col>
        </el-row>
        <el-form-item label-width="65px" label="部门:" prop="department_id" class="postInfo-container-item">
          <el-select clearable class="filter-item" style="width: 130px" filterable @change="changeDepRule" v-model="postForm.department_id" placeholder="请选择">
            <el-option v-for="item in  depArr" :key="item._id" :title="item.parentName" :label="item.name" :value="item._id">
            </el-option>
          </el-select>
        </el-form-item>

          <el-row>
            <el-col :span="11">
              <el-form-item label-width="65px" v-for="(para, index) in postForm.para_list" :label="'参数' + (index)" :key="index"  >
                <el-select style="width: 120px"  v-model="para.para_type"  placeholder="请选择">
                  <el-option v-for="item in  paraTypeArr" :key="item._id" :label="item.name" :value="item._id">
                  </el-option>
                </el-select>
                <el-select style="width: 130px" v-if="para.para_type === '6'"  v-model="para.para_name"  placeholder="请选择">
                  <el-option v-for="item in  lawArr" :key="item._id" :label="item.name" :value="item.content">
                  </el-option>
                </el-select>
                <el-input style="width: 130px" v-else placeholder="参数名" v-model="para.para_name"></el-input>
                <el-button v-if="index === 0" @click="addPare">新增</el-button>
                <el-button v-show="index !== 0" @click.prevent="removePare(para)">删除</el-button>

                <el-button v-if="para.para_type != '4'" @click.prevent="insertContent(para)">插入</el-button>
              </el-form-item>

提示：
              1.参数类型指的是用户以何种方式填写事件原由，插入参数后，不可直接在富文本框里面对参数进行修改.

              2.默认添加的表格无边框，添加边框需设置表格边框值

            </el-col>
            <el-col :span="11">
              <el-form-item label-width="80px" v-for="(role, index) in postForm.roleArr" :label="'审核'+ (index)" :key="role._id">
                <el-select  v-model="role._id"  placeholder="请选择">
                  <el-option v-for="item in  fetchArr" :key="item._id" :label="item.name" :value="item._id">
                  </el-option>
                </el-select>
                <el-button v-if="index === 0" @click="addRole">新增</el-button>
                <el-button v-show="index !== 0" @click.prevent="removeRole(role)">删除</el-button>
                <el-button @click.prevent="insertRole(role._id)">插入</el-button>
              </el-form-item>
            </el-col>
          </el-row>

          <tinymce id="tinymce" :height='500' v-model="postForm.content"></tinymce>

    </el-form>

</template>


Deletes client from data

Deletes client from data

<template>

Loading...

            <clients-form :client-to-update="clientToEdit" @client-created="addClient" @client-updated="updateClient"></clients-form>

</template>


simplified expression

simplified expression

<template lang="pug">
  ul(
    class="dropdown"
    v-bind:class="classes"
    v-bind:data-right="right"
    v-bind:id="id"
    ref="dropdown"
  )
    v-dropdown-item(
      v-for="item in items"
      v-bind:item="item"
    )
    slot
</template>

<script>
  import Toggleable from '../../mixins/toggleable'

  export default {
    name: 'dropdown',

    mixins: [
      Toggleable
    ],

    props: {
      id: {
        type: String,
        required: true
      },

      items: {
        type: Array,
        default: () => []
      },

      right: {
        type: Boolean,
        default: false
      }
    },

    computed: {
      classes () {
        return {
          'dropdown--open': this.active,
          'dropdown--open-from-right': this.right
        }
      }
    },

    mounted () {
      this.$vuetify.bus.sub(`${this.$options.name}:opened`, this.opened)
    },

    methods: {
      opened (id) {
        this.active = id === this.id
      }
    }
  }
</script>

<template lang="pug">
  ul(
    class="dropdown"
    v-bind:class="classes"
    v-bind:data-right="right"
    v-bind:id="id"
    ref="dropdown"
  )
    v-dropdown-item(
      v-for="item in items"
      v-bind:item="item"
    )
    slot
</template>

<script>
  import Toggleable from '../../mixins/toggleable'

  export default {
    name: 'dropdown',

    mixins: [
      Toggleable
    ],

    props: {
      id: {
        type: String,
        required: true
      },

      items: {
        type: Array,
        default: () => []
      },

      right: {
        type: Boolean,
        default: false
      }
    },

    computed: {
      classes () {
        return {
          'dropdown--open': this.active,
          'dropdown--open-from-right': this.right
        }
      }
    },

    mounted () {
      this.$vuetify.bus.sub(`${this.$options.name}:opened`, this.opened)
    },

    methods: {
      opened (id) {
        if (id !== this.id) {
          this.active = false
        }
      }
    }
  }
</script>


revert onChange callback

revert onChange callback

<template>
  <cell :title="title" primary="content" is-link :inline-desc="inlineDesc" @click="onClick">
    {{value | array2string}}
    {{value | value2name data}}

  </cell>
  <popup :show.sync="show" class="vux-popup-picker" :id="'vux-popup-picker-'+uuid" @on-hide="onPopupHide" @on-show="$emit('on-show')">

        <flexbox>
          <flexbox-item style="text-align:left;padding-left:15px;line-height:44px;" @click="onHide(false)">取消</flexbox-item>
          <flexbox-item style="text-align:right;padding-right:15px;line-height:44px;" @click="onHide(true)">完成</flexbox-item>
        </flexbox>

      <picker :data="data" :value.sync="tempValue" :columns="columns" :container="'#vux-popup-picker-'+uuid"></picker>

  </popup>
</template>

<template>
  <cell :title="title" primary="content" is-link :inline-desc="inlineDesc" @click="onClick">
    {{value | array2string}}
    {{value | value2name data}}

  </cell>
  <popup :show.sync="show" class="vux-popup-picker" :id="'vux-popup-picker-'+uuid" @on-hide="onPopupHide" @on-show="$emit('on-show')">

        <flexbox>
          <flexbox-item style="text-align:left;padding-left:15px;line-height:44px;" @click="onHide(false)">取消</flexbox-item>
          <flexbox-item style="text-align:right;padding-right:15px;line-height:44px;" @click="onHide(true)">完成</flexbox-item>
        </flexbox>

      <picker :data="data" :value.sync="tempValue" :columns="columns" :container="'#vux-popup-picker-'+uuid" @on-change="onPickerChange"></picker>

  </popup>
</template>


fix: link foucs style

fix: link foucs style

<template>

      <time :datetime="date | dateToISOString" itemprop="datePublished">{{ date | formatDate }}</time>

      <time :datetime="modified | dateToISOString" itemprop="dateModified">{{ modified | formatDate }}</time>

      Edit

</template>


update text

update text

<template>

🌟📘🌟

          Slack Momentは、Slackのログをまとめて書き出すサービスです。

          まとめたログはMarkdownとして出てくるので、適当な場所に貼って残すといいと思います。

        <template v-if="needShowSignIn">

            Slackアカウントでログインしてください。

        </template>

      <template v-if="user.name">

              {{ user.name }} としてログイン中です。

            まとめたいログがあるチャンネルを探しましょう。

            次のボタンを押すと、あなたが参加しているチャンネル一覧が表示されます。

          <router-link to="channels" class="btn btn-secondary btn-lg btn-block">
            チャンネル一覧を見る
          </router-link>

      </template>

</template>

<template>

🌟📘🌟

          Slack Momentは、Slackのログをまとめて書き出すサービスです。

          まとめたログはMarkdownとして出てくるので、適当な場所に貼って残すといいと思います。

        <template v-if="needShowSignIn">

            まずはSlackアカウントでログインしてください。

            pxgridチームにのみ対応しています。

        </template>

      <template v-if="user.name">

              {{ user.name }} としてログイン中です。

            まとめたいログがあるチャンネルを探しましょう。

            次のボタンを押すと、あなたが参加しているチャンネル一覧が表示されます。

          <router-link to="channels" class="btn btn-secondary btn-lg btn-block">
            チャンネル一覧を見る
          </router-link>

      </template>

</template>


Add more items, so that We can see the previous and next icons. (#635)

In high resolution screens, 15 items is just not enough to get the previous and next items to show on this example. So by adding more elements, we'll be able to see them.

<template>
  <v-tabs
    color="cyan"
    dark
    next-icon="mdi-arrow-right-bold-box-outline"
    prev-icon="mdi-arrow-left-bold-box-outline"
    show-arrows
  >
    <v-tabs-slider color="yellow"></v-tabs-slider>
    <v-tab
      v-for="i in 30"
      :href="'#tab-' + i"
      :key="i"
    >
      Item {{ i }}
    </v-tab>

    <v-tabs-items>
      <v-tab-item
        v-for="i in 30"
        :id="'tab-' + i"
        :key="i"
      >
        <v-card flat>
          <v-card-text>{{ text }}</v-card-text>
        </v-card>
      </v-tab-item>
    </v-tabs-items>
  </v-tabs>
</template>

<script>
  export default {
    data () {
      return {
        text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.'
      }
    }
  }
</script>

<codepen-resources lang="json">
  {
    "css": ["https://cdn.materialdesignicons.com/2.5.94/css/materialdesignicons.min.css"]
  }
</codepen-resources>

<template>
  <v-tabs
    color="cyan"
    dark
    next-icon="mdi-arrow-right-bold-box-outline"
    prev-icon="mdi-arrow-left-bold-box-outline"
    show-arrows
  >
    <v-tabs-slider color="yellow"></v-tabs-slider>
    <v-tab
      v-for="i in 15"
      :href="'#tab-' + i"
      :key="i"
    >
      Item {{ i }}
    </v-tab>

    <v-tabs-items>
      <v-tab-item
        v-for="i in 15"
        :id="'tab-' + i"
        :key="i"
      >
        <v-card flat>
          <v-card-text>{{ text }}</v-card-text>
        </v-card>
      </v-tab-item>
    </v-tabs-items>
  </v-tabs>
</template>

<script>
  export default {
    data () {
      return {
        text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.'
      }
    }
  }
</script>

<codepen-resources lang="json">
  {
    "css": ["https://cdn.materialdesignicons.com/2.5.94/css/materialdesignicons.min.css"]
  }
</codepen-resources>


Updated vue-cli-plugin test app to use new form schema changes.

<template>

Welcome to Inkline!

This is your starter page. Make sure to read the documentation to learn about what Inkline has to offer.

                Read Documentation

Form

This is a simple Inkline forms test.

                            Football
                            Volleyball
                            Tennis

                            Accept
                            Decline

                        Submit

</template>


Rename of split prop to splitTo

Rename of split prop to splitTo

<template>
  <div
    class="pane-rs root"
    :class="classObject"
    @mousemove="onMouseMove"
    @mouseup="onMouseUp">
      <pane-comp
        ref="panel1"
        :class="{column: splitTo === 'columns', row: splitTo === 'rows'}"
        :style="iStyle('first')">
          <slot name='firstPane'></slot>
      </pane-comp>
      <resizer-comp
      @mousedown.native="onMouseDown"
        v-if="allowResize"
        :class="{
          rows: splitTo === 'rows',
          columns: splitTo === 'columns'}"></resizer-comp>
      <pane-comp
        ref="panel2"
        :class="{column: splitTo === 'columns', row: splitTo === 'rows'}"
        :style="iStyle('second')">
          <slot name='secondPane'></slot>
      </pane-comp>
  </div>
</template>

<script>
import Resizer from './Resizer'
import Pane from './Pane'

function unFocus(document, window) {
  if (document.selection) {
    document.selection.empty()
  } else {
    try {
      window.getSelection().removeAllRanges()
      // eslint-disable-next-line no-empty
    } catch (e) {
      console.log(e)
    }
  }
}

export default {
  name: 'pane-rs',
  components: {
    'resizer-comp': Resizer,
    'pane-comp': Pane,
  },
  props: {
    allowResize: { type: Boolean, default: false },
    splitTo: { type: String, default: 'columns' },
    primary: { type: String, default: 'first' },
    size: { type: Number, default: 100 },
    minSize: { type: Number, default: 16 },
    maxSize: { type: Number, default: 0 },
    step: { type: Number, default: 0 },
  },
  data() {
    return {
      active: false,
      position: 0,
      localSize: this.size,
    }
  },
  computed: {
    classObject() {
      return {
        columns: this.splitTo === 'columns',
        rows: this.splitTo === 'rows',
      }
    },
    iStyle: function() {
      return function(el) {
        let style = { flex: 1, position: 'relative', outline: 'none' }

        if (el === this.primary) {
          style.flex = '0 0 auto'
          this.splitTo === 'columns'
            ? (style.width = this.localSize + 'px')
            : (style.height = this.localSize + 'px')
        } else {
          style.flex = '1 1 0%'
        }
        return style
      }
    },
  },
  methods: {
    onMouseDown(event) {
      if (this.allowResize) {
        const eventWithTouches = Object.assign({}, event, {
          touches: [
            {
              clientX: event.clientX,
              clientY: event.clientY,
            },
          ],
        })
        this.onTouchStart(eventWithTouches)
      }
    },
    onTouchStart(event) {
      if (this.allowResize) {
        unFocus(document, window)
        const position =
          this.splitTo === 'columns'
            ? event.touches[0].clientX
            : event.touches[0].clientY

        if (typeof this.onDragStarted === 'function') {
          onDragStarted()
        }
        this.active = true
        this.position = position
      }
    },
    onMouseMove(event) {
      if (this.allowResize) {
        const eventWithTouches = Object.assign({}, event, {
          touches: [
            {
              clientX: event.clientX,
              clientY: event.clientY,
            },
          ],
        })
        this.onTouchMove(eventWithTouches)
      }
    },
    onTouchMove(event) {
      const { active, position } = this.$data
      const {
        maxSize,
        minSize,
        step,
        allowResize,
        splitTo,
        primary,
      } = this.$props
      if (allowResize && active) {
        unFocus(document, window)
        const isPrimaryFirst = primary === 'first'
        const ref = isPrimaryFirst ? 'panel1' : 'panel2'
        if (ref) {
          const node = this.$refs[ref].$vnode.elm
          if (node.getBoundingClientRect) {
            const width = node.getBoundingClientRect().width
            const height = node.getBoundingClientRect().height
            const current =
              splitTo === 'columns'
                ? event.touches[0].clientX
                : event.touches[0].clientY
            const size = splitTo === 'columns' ? width : height
            let positionDelta = position - current
            const sizeDelta = isPrimaryFirst ? positionDelta : -positionDelta
            let newSize = size - sizeDelta
            const newPosition = position - positionDelta

            if (this.step) {
              if (Math.abs(positionDelta) < this.step) {
                return
              }
              // Integer division
              // eslint-disable-next-line no-bitwise
              positionDelta = ~~(positionDelta / this.step) * this.step
            }

            if (minSize && newSize < minSize) {
              newSize = minSize
            }
            if (maxSize && newSize > maxSize) {
              newSize = maxSize
            }
            this.localSize = newSize
            this.position = newPosition
          }
        }
      }
    },
    onMouseUp() {
      // console.log(this)
      const { allowResize, onDragFinished } = {
        allowResize: this.allowResize,
        onDragFinished: this.onDragFinished,
      }
      const { active, draggedSize } = {
        active: this.active,
        draggedSize: this.draggedSize,
      }
      if (allowResize && active) {
        if (typeof onDragFinished === 'function') {
          onDragFinished(draggedSize)
        }
        this.active = false
      }
    },
  },
}
</script>

<style scoped>
*,
*:before,
*:after {
  -moz-box-sizing: border-box;
  -webkit-box-sizing: border-box;
 va      box-sizing: border-box;
  position: relative;
}

.root {
  height: 100%;
  width: 100%;
}

.pane-rs {
  display: flex;
  flex: 1;
  /*align-items: stretch;
  align-content: stretch;*/
  position: absolute;
  outline: none;
  overflow: hidden;
  user-select: text;
}
</style>

<template>
  <div
    class="pane-rs root"
    :class="classObject"
    @mousemove="onMouseMove"
    @mouseup="onMouseUp">
      <pane-comp
        ref="panel1"
        :class="{column: split === 'columns', row: split === 'rows'}"
        :style="iStyle('first')">
          <slot name='firstPane'></slot>
      </pane-comp>
      <resizer-comp
      @mousedown.native="onMouseDown"
        v-if="allowResize"
        :class="{
          rows: split === 'rows',
          columns: split === 'columns'}"></resizer-comp>
      <pane-comp
        ref="panel2"
        :class="{column: split === 'columns', row: split === 'rows'}"
        :style="iStyle('second')">
          <slot name='secondPane'></slot>
      </pane-comp>
  </div>
</template>

<script>
import Resizer from './Resizer'
import Pane from './Pane'

function unFocus(document, window) {
  if (document.selection) {
    document.selection.empty()
  } else {
    try {
      window.getSelection().removeAllRanges()
      // eslint-disable-next-line no-empty
    } catch (e) {
      console.log(e)
    }
  }
}

export default {
  name: 'pane-rs',
  components: {
    'resizer-comp': Resizer,
    'pane-comp': Pane,
  },
  props: {
    allowResize: { type: Boolean, default: false },
    splitTo: { type: String, default: 'columns' },
    primary: { type: String, default: 'first' },
    size: { type: Number, default: 100 },
    minSize: { type: Number, default: 16 },
    maxSize: { type: Number, default: 0 },
    step: { type: Number, default: 0 },
  },
  data() {
    return {
      active: false,
      position: 0,
      localSize: this.size,
    }
  },
  computed: {
    classObject() {
      return {
        columns: this.splitTo === 'columns',
        rows: this.splitTo === 'rows',
      }
    },
    iStyle: function() {
      return function(el) {
        let style = { flex: 1, position: 'relative', outline: 'none' }

        if (el === this.primary) {
          style.flex = '0 0 auto'
          this.splitTo === 'columns'
            ? (style.width = this.localSize + 'px')
            : (style.height = this.localSize + 'px')
        } else {
          style.flex = '1 1 0%'
        }
        return style
      }
    },
  },
  methods: {
    onMouseDown(event) {
      if (this.allowResize) {
        const eventWithTouches = Object.assign({}, event, {
          touches: [
            {
              clientX: event.clientX,
              clientY: event.clientY,
            },
          ],
        })
        this.onTouchStart(eventWithTouches)
      }
    },
    onTouchStart(event) {
      if (this.allowResize) {
        unFocus(document, window)
        const position =
          this.splitTo === 'columns'
            ? event.touches[0].clientX
            : event.touches[0].clientY

        if (typeof this.onDragStarted === 'function') {
          onDragStarted()
        }
        this.active = true
        this.position = position
      }
    },
    onMouseMove(event) {
      if (this.allowResize) {
        const eventWithTouches = Object.assign({}, event, {
          touches: [
            {
              clientX: event.clientX,
              clientY: event.clientY,
            },
          ],
        })
        this.onTouchMove(eventWithTouches)
      }
    },
    onTouchMove(event) {
      const { active, position } = this.$data
      const {
        maxSize,
        minSize,
        step,
        allowResize,
        splitTo,
        primary,
      } = this.$props
      if (allowResize && active) {
        unFocus(document, window)
        const isPrimaryFirst = primary === 'first'
        const ref = isPrimaryFirst ? 'panel1' : 'panel2'
        if (ref) {
          const node = this.$refs[ref].$vnode.elm
          if (node.getBoundingClientRect) {
            const width = node.getBoundingClientRect().width
            const height = node.getBoundingClientRect().height
            const current =
              splitTo === 'columns'
                ? event.touches[0].clientX
                : event.touches[0].clientY
            const size = splitTo === 'columns' ? width : height
            let positionDelta = position - current
            const sizeDelta = isPrimaryFirst ? positionDelta : -positionDelta
            let newSize = size - sizeDelta
            const newPosition = position - positionDelta

            if (this.step) {
              if (Math.abs(positionDelta) < this.step) {
                return
              }
              // Integer division
              // eslint-disable-next-line no-bitwise
              positionDelta = ~~(positionDelta / this.step) * this.step
            }

            if (minSize && newSize < minSize) {
              newSize = minSize
            }
            if (maxSize && newSize > maxSize) {
              newSize = maxSize
            }
            this.localSize = newSize
            this.position = newPosition
          }
        }
      }
    },
    onMouseUp() {
      // console.log(this)
      const { allowResize, onDragFinished } = {
        allowResize: this.allowResize,
        onDragFinished: this.onDragFinished,
      }
      const { active, draggedSize } = {
        active: this.active,
        draggedSize: this.draggedSize,
      }
      if (allowResize && active) {
        if (typeof onDragFinished === 'function') {
          onDragFinished(draggedSize)
        }
        this.active = false
      }
    },
  },
}
</script>

<style scoped>
*,
*:before,
*:after {
  -moz-box-sizing: border-box;
  -webkit-box-sizing: border-box;
 va      box-sizing: border-box;
  position: relative;
}

.root {
  height: 100%;
  width: 100%;
}

.pane-rs {
  display: flex;
  flex: 1;
  /*align-items: stretch;
  align-content: stretch;*/
  position: absolute;
  outline: none;
  overflow: hidden;
  user-select: text;
}
</style>


[UPD] to stylu

[UPD] to stylu

<template>

    <editable-input class="hex" label="hex" cid="hex"
    :val.sync="colors.hex"
    :style="{ borderColor: colors.hex }"
    :on-change="onChange">
    </editable-input>

        <editable-input label="r" cid="rgba"
        :val.sync="colors.rgba.r"
        :on-change="onChange">
        </editable-input>

        <editable-input label="g" cid="rgba"
        :val.sync="colors.rgba.g"
        :on-change="onChange">
        </editable-input>

        <editable-input label="b" cid="rgba"
        :val.sync="colors.rgba.b"
        :on-change="onChange">
        </editable-input>

</template>


special hot links

special hot links

<template lang="pug">
  footer.text-center(:class="$style.footer")
    div(:class="$style.links")
      router-link(to="/about")
        span About
      router-link(to="/terms")
        span Terms
      router-link(to="/privacy")
        span Privacy
      router-link(to="/contact")
        span Contact
    div(:class="[$style.links, $style.hot]")
      router-link(to="/about")
        span Hot Products
      router-link(to="/privacy")
        span Hot Products Price
      router-link(to="/contact")
        span Wholesale Hot Products
    div(:class="$style.copyright") Copyright © 2017 Focus Technology Co., Ltd. All Rights Reserved.
</template>
<script>

</script>
<style lang="stylus" module>
  .footer
    margin-top 25px
    padding-bottom 25px

  .links
    &.hot
      margin-top 10px

      a
        padding-left 0
        padding-right 0

    a
      padding-left 10px
      padding-right 10px
      color $primary-color

      span
        display inline-block
        scaleSize($smaller-size)

      + a
        border-left 1px solid $border-color

  .copyright
    relative()
    display block
    left 50%
    margin-top 2px
    scaleSize($smaller-size,, translate3d(-50%, 0, 0))
    width (14 / 12 * 100) %
    color $icon-color
    white-space nowrap
</style>

<template lang="pug">
  footer.text-center(:class="$style.footer")
    div(:class="$style.links")
      router-link(to="/about")
        span About
      router-link(to="/terms")
        span Terms
      router-link(to="/privacy")
        span Privacy
      router-link(to="/contact")
        span Contact
    div(:class="$style.links")
      router-link(to="/about")
        span Hot Products
      router-link(to="/privacy")
        span Hot Products Price
      router-link(to="/contact")
        span Wholesale Hot Products
    div(:class="$style.copyright") Copyright © 2017 Focus Technology Co., Ltd. All Rights Reserved.
</template>
<script>

</script>
<style lang="stylus" module>
  .footer
    margin-top 25px
    padding-bottom 25px

  .links
    + .links
      margin-top 10px

      a
        padding-left 0
        padding-right 0

    a
      padding-left 10px
      padding-right 10px
      color $primary-color

      span
        display inline-block
        scaleSize($smaller-size)

      + a
        border-left 1px solid $border-color

  .copyright
    relative()
    display block
    left 50%
    margin-top 2px
    scaleSize($smaller-size,, translate3d(-50%, 0, 0))
    width (14 / 12 * 100) %
    color $icon-color
    white-space nowrap
</style>


chore: 👷 remove skale

chore: 👷 remove skale

<template>
  <div>
    <the-default-header />
    <v-main>
      <router-view />
    </v-main>
    <the-default-footer />
  </div>
</template>

<script>
import TheDefaultHeader from './components-default/TheDefaultHeader';
import TheDefaultFooter from './components-default/TheDefaultFooter';
export default {
  name: 'TheDefaultView',
  components: {
    TheDefaultHeader,
    TheDefaultFooter
  }
};
</script>

<template>
  <div>
    <the-default-header />
    <v-main>
      <router-view />
    </v-main>
    <the-default-footer />
    <the-default-wallet-banner />
  </div>
</template>

<script>
import TheDefaultHeader from './components-default/TheDefaultHeader';
import TheDefaultFooter from './components-default/TheDefaultFooter';
import TheDefaultWalletBanner from './components-default/TheDefaultWalletBanner';
export default {
  name: 'TheDefaultView',
  components: {
    TheDefaultHeader,
    TheDefaultFooter,
    TheDefaultWalletBanner
  }
};
</script>


display node details nice

display node details nice

<template>

<button class="delete is-pulled-right" v-on:click="hideDetails()"></button>

        Selected Node:

              MAC
              {{ selectedNodeMAC }}

              Vendor
              {{ selectedNodeVendor }}

        Selected Association:

</template>

<template>

<button class="delete is-pulled-right" v-on:click="hideDetails()"></button>

display info about clicked nodes here

</template>


Clean up logic for deleting last character in selection vs direct-delete modes; add some documentation

<template lang='pug'>
span.vue-typer
  //- Ideally we'd just use 2 spans to contain the characters 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.char(v-for='l in numLeftChars') {{ currentTextArray[l-1] }}
  caret(v-if='showCaret')
  span.char(v-for='r in numRightChars', :class='characterClassObject') {{ 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'
}

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

export default {
  components: {
    Caret
  },
  props: {
    /* GENERAL */
    text: {
      type: [String, Array],
      required: true,
      validator(value) {
        if (typeof value === 'string') {
          return true
        }
        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: 75,
      validator: nonNegativeNumberValidator
    },
    startTypeDelay: {
      type: Number,
      default: 0,
      validator: nonNegativeNumberValidator
    },
    /* ERASE */
    eraseDelay: {
      type: Number,
      default: 250,
      validator: nonNegativeNumberValidator
    },
    startEraseDelay: {
      type: Number,
      default: 1750,
      validator: nonNegativeNumberValidator
    },
    eraseStyle: {
      type: String,
      default: ERASE_STYLE.BACKSPACE
    },
    eraseFinalText: {
      type: Boolean,
      default: false
    },
    /*
    CARET
    caretAnimation: String (see Sublime's styles)
    caretColor: String
    selectionBgColor: String
    selectionFgColor: String
    */
    showCaretType: {
      type: Boolean,
      default: true
    },
    showCaretSelect: {
      type: Boolean,
      default: false
    },
    showCaretErase: {
      type: Boolean,
      default: true
    },
    showCaretOnComplete: {
      type: Boolean,
      default: false
    }
  },
  data() {
    return {
      state: STATE.IDLE,
      repeatCounter: 0,
      actionTimeout: 0,
      actionInterval: 0,

      spool: [],
      spoolIndex: -1,
      currentTextIndex: -1,

      showCaret: true
    }
  },
  computed: {
    isSelectionBasedEraseStyle() {
      return this.eraseStyle.match(`^${ERASE_STYLE.SELECT_BACK}|${ERASE_STYLE.SELECT_ALL}$`)
    },
    isEraseAllStyle() {
      return this.eraseStyle.match(`^${ERASE_STYLE.DISAPPEAR}|${ERASE_STYLE.SELECT_ALL}$`)
    },
    onLastWord() {
      return this.spoolIndex === this.spool.length - 1
    },
    shouldRepeat() {
      return this.repeatCounter < this.repeat
    },
    characterClassObject() {
      return {
        delete: this.state === STATE.IDLE ||
                this.state === STATE.TYPING ||
                this.state === STATE.ERASING && !this.isSelectionBasedEraseStyle,
        select: this.state === STATE.ERASING && this.isSelectionBasedEraseStyle
      }
    },
    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, and shuffling the order if necessary
      if (typeof this.text === 'string') {
        this.spool = [this.text]
      } else {
        let textCopy = this.text.slice()
        if (this.shuffle && textCopy.length > 1) {
          shuffle(textCopy)
        }
        this.spool = textCopy
      }

      this.spoolIndex = 0
      this.repeatCounter = 0

      if (this.initialAction === STATE.TYPING) {
        this.transitionTo(STATE.TYPING)
      } 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.currentTextIndex = this.currentText.length
        this.onTyped()
      }
    },
    reset() {
      this.cancelCurrentAction()
      this.init()
    },
    transitionTo(state) {
      this.state = state
      switch (this.state) {
        case STATE.TYPING:
          this.startTyping()
          break
        case STATE.ERASING:
          this.startErasing()
          break
      }
    },
    startDelayedRepeatingAction(action, delay, repeatInterval) {
      if (this.actionTimeout || this.actionInterval) {
        return
      }
      this.actionTimeout = setTimeout(() => {
        this.actionInterval = setInterval(() => {
          action()
        }, repeatInterval)
      }, delay)
    },
    cancelCurrentAction() {
      if (this.actionInterval) {
        clearInterval(this.actionInterval)
        this.actionInterval = 0
      }
      if (this.actionTimeout) {
        clearTimeout(this.actionTimeout)
        this.actionTimeout = 0
      }
    },
    startTyping() {
      this.currentTextIndex = 0
      this.showCaret = this.isSelectionBasedEraseStyle ? this.showCaretSelect : this.showCaretErase

      this.startDelayedRepeatingAction(() => {
        this.currentTextIndex++
        this.showCaret = this.showCaretType

        if (this.currentTextIndex >= this.currentText.length) {
          this.cancelCurrentAction()
          this.onTyped()
        }
      }, this.startTypeDelay, this.typeDelay)
    },
    startErasing() {
      this.currentTextIndex = this.currentText.length
      this.showCaret = this.showCaretType

      this.startDelayedRepeatingAction(() => {
        const previousTextIndex = this.currentTextIndex
        if (this.currentTextIndex > 0) {
          if (this.isEraseAllStyle) {
            this.currentTextIndex = 0
          } else {
            this.currentTextIndex--
          }
        }
        this.showCaret = this.isSelectionBasedEraseStyle ? this.showCaretSelect : this.showCaretErase

        // Selection-based erase styles stop one iteration later because there's a highlight step before erasing happens.
        // We use previousTextIndex for checking simply because we shouldn't decrement currentTextIndex below 0.
        const finalPreviousTextIndex = this.isSelectionBasedEraseStyle ? 0 : 1
        if (previousTextIndex <= finalPreviousTextIndex) {
          this.cancelCurrentAction()
          this.onErased()
        }
      }, this.startEraseDelay, this.eraseDelay)
    },
    onTyped() {
      if (this.onLastWord) {
        if (this.eraseFinalText || this.shouldRepeat) {
          this.transitionTo(STATE.ERASING)
        } else {
          this.onComplete()
        }
      } else {
        this.transitionTo(STATE.ERASING)
      }
    },
    onErased() {
      if (this.onLastWord) {
        if (this.shouldRepeat) {
          this.repeatCounter++
          this.spoolIndex = 0
          this.transitionTo(STATE.TYPING)
        } else {
          this.onComplete()
        }
      } else {
        this.spoolIndex++
        this.transitionTo(STATE.TYPING)
      }
    },
    onComplete() {
      this.transitionTo(STATE.IDLE)
      this.showCaret = this.showCaretOnComplete
      this.$emit('complete')
    }
  },
  watch: {
    text() {
      this.reset()
    },
    repeat() {
      this.reset()
    },
    shuffle() {
      this.reset()
    }
  }
}
</script>

<style lang='scss'>
.vue-typer {
  word-break: break-all;

  .char {
    display: inline-block;
    white-space: pre-wrap;

    &.delete {
      display: none;
    }
    &.select {
      background-color: #ACCEF7;
    }
  }
}
</style>

<template lang='pug'>
span.vue-typer
  //- Ideally we'd just use 2 spans to contain the characters 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.char(v-for='l in numLeftChars') {{ currentTextArray[l-1] }}
  caret(v-if='showCaret')
  span.char(v-for='r in numRightChars', :class='characterClassObject') {{ 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'
}

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

export default {
  components: {
    Caret
  },
  props: {
    /* GENERAL */
    text: {
      type: [String, Array],
      required: true,
      validator(value) {
        if (typeof value === 'string') {
          return true
        }
        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: 75,
      validator: nonNegativeNumberValidator
    },
    startTypeDelay: {
      type: Number,
      default: 0,
      validator: nonNegativeNumberValidator
    },
    /* ERASE */
    eraseDelay: {
      type: Number,
      default: 250,
      validator: nonNegativeNumberValidator
    },
    startEraseDelay: {
      type: Number,
      default: 1750,
      validator: nonNegativeNumberValidator
    },
    eraseStyle: {
      type: String,
      default: ERASE_STYLE.BACKSPACE
    },
    eraseFinalText: {
      type: Boolean,
      default: false
    },
    /*
    CARET
    caretAnimation: String (see Sublime's styles)
    caretColor: String
    selectionBgColor: String
    selectionFgColor: String
    */
    showCaretType: {
      type: Boolean,
      default: true
    },
    showCaretSelect: {
      type: Boolean,
      default: false
    },
    showCaretErase: {
      type: Boolean,
      default: true
    },
    showCaretOnComplete: {
      type: Boolean,
      default: false
    }
  },
  data() {
    return {
      state: STATE.IDLE,
      repeatCounter: 0,
      actionTimeout: 0,
      actionInterval: 0,

      spool: [],
      spoolIndex: -1,
      currentTextIndex: -1,

      showCaret: true
    }
  },
  computed: {
    isSelectionBasedEraseStyle() {
      return this.eraseStyle.match(`^${ERASE_STYLE.SELECT_BACK}|${ERASE_STYLE.SELECT_ALL}$`)
    },
    isEraseAllStyle() {
      return this.eraseStyle.match(`^${ERASE_STYLE.DISAPPEAR}|${ERASE_STYLE.SELECT_ALL}$`)
    },
    onLastWord() {
      return this.spoolIndex === this.spool.length - 1
    },
    shouldRepeat() {
      return this.repeatCounter < this.repeat
    },
    characterClassObject() {
      return {
        delete: this.state === STATE.IDLE ||
                this.state === STATE.TYPING ||
                this.state === STATE.ERASING && !this.isSelectionBasedEraseStyle,
        select: this.state === STATE.ERASING && this.isSelectionBasedEraseStyle
      }
    },
    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() {
      if (typeof this.text === 'string') {
        this.spool = [this.text]
      } else {
        let textCopy = this.text.slice()
        if (this.shuffle && textCopy.length > 1) {
          shuffle(textCopy)
        }
        this.spool = textCopy
      }

      this.spoolIndex = 0
      this.repeatCounter = 0

      if (this.initialAction === STATE.TYPING) {
        this.transitionTo(STATE.TYPING)
      } else if (this.initialAction === STATE.ERASING) {
        this.currentTextIndex = this.currentText.length
        this.onTyped()
      }
    },
    reset() {
      this.cancelCurrentAction()
      this.init()
    },
    transitionTo(state) {
      this.state = state
      switch (this.state) {
        case STATE.TYPING:
          this.startTyping()
          break
        case STATE.ERASING:
          this.startErasing()
          break
      }
    },
    startDelayedRepeatingAction(action, delay, repeatInterval) {
      if (this.actionTimeout || this.actionInterval) {
        return
      }
      this.actionTimeout = setTimeout(() => {
        this.actionInterval = setInterval(() => {
          action()
        }, repeatInterval)
      }, delay)
    },
    cancelCurrentAction() {
      if (this.actionInterval) {
        clearInterval(this.actionInterval)
        this.actionInterval = 0
      }
      if (this.actionTimeout) {
        clearTimeout(this.actionTimeout)
        this.actionTimeout = 0
      }
    },
    startTyping() {
      this.currentTextIndex = 0
      this.showCaret = this.isSelectionBasedEraseStyle ? this.showCaretSelect : this.showCaretErase

      this.startDelayedRepeatingAction(() => {
        this.currentTextIndex++
        this.showCaret = this.showCaretType

        if (this.currentTextIndex >= this.currentText.length) {
          this.cancelCurrentAction()
          this.onTyped()
        }
      }, this.startTypeDelay, this.typeDelay)
    },
    startErasing() {
      this.currentTextIndex = this.currentText.length
      this.showCaret = this.showCaretType

      this.startDelayedRepeatingAction(() => {
        if (this.isEraseAllStyle && this.currentTextIndex > 0) {
          this.currentTextIndex = 0
        } else {
          this.currentTextIndex--
        }
        this.showCaret = this.isSelectionBasedEraseStyle ? this.showCaretSelect : this.showCaretErase

        // Non-selection-based erase styles should stop one iteration earlier because there's no highlight step
        const lastTextIndex = this.isSelectionBasedEraseStyle ? -1 : 0
        if (this.currentTextIndex <= lastTextIndex) {
          this.cancelCurrentAction()
          this.onErased()
        }
      }, this.startEraseDelay, this.eraseDelay)
    },
    onTyped() {
      if (this.onLastWord) {
        if (this.eraseFinalText || this.shouldRepeat) {
          this.transitionTo(STATE.ERASING)
        } else {
          this.onComplete()
        }
      } else {
        this.transitionTo(STATE.ERASING)
      }
    },
    onErased() {
      if (this.onLastWord) {
        if (this.shouldRepeat) {
          this.repeatCounter++
          this.spoolIndex = 0
          this.transitionTo(STATE.TYPING)
        } else {
          this.onComplete()
        }
      } else {
        this.spoolIndex++
        this.transitionTo(STATE.TYPING)
      }
    },
    onComplete() {
      this.transitionTo(STATE.IDLE)
      this.showCaret = this.showCaretOnComplete
      this.$emit('complete')
    }
  },
  watch: {
    text() {
      this.reset()
    },
    repeat() {
      this.reset()
    },
    shuffle() {
      this.reset()
    }
  }
}
</script>

<style lang='scss'>
.vue-typer {
  word-break: break-all;

  .char {
    display: inline-block;
    white-space: pre-wrap;

    &.delete {
      display: none;
    }
    &.select {
      background-color: #ACCEF7;
    }
  }
}
</style>