searchbar/searchbar.vue

<template>
    <div class="ion-searchbar searchbar"
         :class="[
         modeClass,colorClass,
         {'searchbar-has-focus': sbHasFocus},
         {'searchbar-has-value':theValue},
         {'searchbar-animated':shouldAnimated},
         {'searchbar-active':sbHasFocus},
         {'searchbar-show-cancel':showCancelButton},
         {'searchbar-left-aligned':shouldAlignLeft}
         ]">
        <div class="searchbar-input-container" @touchstart="setFocus">
            <!--在md模式下,md的取消按钮是在这里的,当点击inputs输入时,返回按钮将覆盖search按钮-->
            <vm-button mode="md"
                       @click="cancelSearchbar($event)"
                       clear
                       color="dark"
                       class="searchbar-md-cancel"
                       role="button">
                <Icon mode="md" name="md-arrow-back"></Icon>
            </vm-button>

            <!--input左边的search按钮-->
            <div ref="searchbarIcon" class="searchbar-search-icon"></div>
            <input ref="searchbarInput" class="searchbar-input" id="searchbarInput"
                   @input="onInputHandler($event)"
                   @blur="onBlurHandler($event)"
                   @focus="onFocusHandler($event)"
                   :value="theValue"
                   :placeholder="placeholder"
                   :type="type"
                   :autocomplete="autocompleteValue"
                   :autocorrect="autocorrectValue"
                   :spellcheck="spellcheckValue">
            <!--input右边的关闭按钮-->
            <vm-button clear
                       class="searchbar-clear-icon"
                       :mode="mode"
                       @click="clearInput($event)"
                       role="button"></vm-button>
        </div>

        <!--取消按钮,点击input时出现,只对IOS,md在search icon位置显示,wp没有-->
        <vm-button ref="cancelButton"
                   mode="ios"
                   clear
                   @click="cancelSearchbar($event)"
                   class="searchbar-ios-cancel"
                   role="button">
            <span>{{cancelButtonText}}</span>
        </vm-button>
    </div>
</template>
<style lang="scss" src="./style.scss"></style>
<script type="text/javascript">
  /**
   * @component Searchbar
   * @description
   * 搜索条
   *
   * ## 表单组件 / SearchBar搜索条组件
   *
   * 搜索组件, 一般是放在Header组件的Toolbar组件中, 当然也可以放置在任何位置
   *
   *
   * ### 如何引入
   * ```
   * // 引入
   * import { Searchbar } from 'vimo'
   * // 安装
   * Vue.component(Searchbar.name, Searchbar)
   * // 或者
   * export default{
   *   components: {
   *    Searchbar
   *  }
   * }
   * ```
   *
   * @props {String} [color]                        - 颜色
   * @props {String} [mode='ios']                   - 模式
   * @props {String} [cancelButtonText='Cancel']    - 取消按钮的文本
   * @props {Boolean} [showCancelButton=false]      - 是否显示cancelButton(只在focus状态才显示cancelBtn, 且cancelBtn只对组件作用, 如果要赋予业务能力, 请在右侧自己实现cancelBtn)
   * @props {Number} [debounce=0]                   - 等待多久触发onInput事件
   * @props {String} [placeholder='Search']         - 设置placeholder的值.
   * @props {String} [autocomplete]                 - 自动完成
   * @props {String} [autocorrect]                  - 自动纠错
   * @props {String|Boolean} [autofocus]            - 如果是boolean类型, 则立即判断, 如果是Number, 则等待dom完毕定时后自动focus
   * @props {Boolean} [spellcheck]                  - 拼写检查
   * @props {String} [type='search']                - 设置input配型, 可以是: "text", "password", "email", "number", "search", "tel", "url".
   * @props {Boolean} [animated=false]              - 是否启动点击动画
   *
   *
   * @fires component:Searchbar#onInput
   * @fires component:Searchbar#onFocus
   * @fires component:Searchbar#onBlur
   * @fires component:Searchbar#onClear
   * @fires component:Searchbar#onCancel
   *
   *
   * @demo #/searchbar
   *
   * @usage
   * <template>
   *    <Page>
   *    <Header>
   *        <Navbar>
   *            <Title>Searchbar</Title>
   *        </Navbar>
   *        <Toolbar>
   *            <Searchbar :animated="true"
   *                placeholder="Search"
   *                :debounce="0"
   *                v-model="myInput"
   *                :showCancelButton="true"
   *                cancelButtonText="取消"
   *                @onInput="onInput"
   *                @onFocus="onFocus"
   *                @onBlur="onBlur"
   *                @onCancel="onCancel"
   *                @onClear="onClear"></Searchbar>
   *         </Toolbar>
   *     </Header>
   *    <Content padding>
   *        <p>Search debounce: 100</p>
   *        <p>Search Value: {{myInput}}</p>
   *    </Content>
   *    </Page>
   * </template>
   * */
  import Button from '../button'
  import Icon from '../icon'
  import { isNumber, isBoolean } from '../../util/type'

  // TODO: use padding-left to  animate is not prefect , hah
  export default {
    name: 'Searchbar',
    data () {
      return {
        isCancelVisible: false,
        sbHasFocus: false,
        shouldAlignLeft: true,
        shouldBlur: true,
        shouldAnimated: false,

        // 三个元素的id的document实例
        searchbarIconElement: '',
        searchbarInputElement: '',
        cancelButtonElement: '',

        // 外部的value映射
        theValue: this.value,
        timer: '',

        placeHolderTextWidth: null // number eg: 44

      }
    },
    props: {
      /**
       * The predefined color to use. For example: "primary", "secondary", "danger".
       * */
      color: String,
      /**
       * The mode to apply to this component. Mode can be ios, wp, or md.
       * */
      mode: {
        type: String,
        default () { return this.$config && this.$config.get('mode') || 'ios' }
      },
      /**
       * Set the the cancel button text. Default: "Cancel".
       * */
      cancelButtonText: {
        type: String,
        default: 'Cancel'
      },
      /**
       * Whether to show the cancel button or not. Default: "false".
       * */
      showCancelButton: [Boolean],
      /**
       * How long, in milliseconds, to wait to trigger the onInput event after each keystroke. Default 250.
       * */
      debounce: {
        type: Number,
        default: 0
      },
      /**
       * Set the input's placeholder. Default "Search".
       * */
      placeholder: {
        type: String,
        default: 'Search'
      },
      /**
       * Set the input's autocomplete property. Values: "on", "off". Default "off".
       * */
      autocomplete: {
        type: String,
        default: 'off'
      },
      /**
       * Set the input's autocorrect property. Values: "on", "off". Default "off".
       * */
      autocorrect: {
        type: String,
        default: 'off'
      },

      autofocus: [Boolean, Number],
      /**
       * Set the input's spellcheck property. Values: true, false. Default false.
       * */
      spellcheck: {
        type: [String, Boolean],
        default: false
      },
      /**
       * Set the type of the input. Values: "text", "password", "email", "number", "search", "tel", "url". Default "search".
       * */
      type: {
        type: String,
        default: 'search'
      },
      /**
       * Configures if the searchbar is animated or no. By default, animation is false.
       * */
      animated: {
        type: Boolean,
        default: false
      },
      /**
       * Set the input value.
       * */
      value: String
    },
    watch: {
      value (val) {
        this.theValue = val
        this.positionElements()
      }
    },
    computed: {
      // props处理
      autocompleteValue () {
        return (this.autocomplete === '' || this.autocomplete === 'on') ? 'on' : 'off'
      },
      autocorrectValue () {
        return (this.autocorrect === '' || this.autocorrect === 'on') ? 'on' : 'off'
      },
      spellcheckValue () {
        return this.spellcheck === '' || this.spellcheck === 'true' || this.spellcheck === true
      },
      // class处理
      modeClass () {
        return this.mode ? `searchbar-${this.mode}` : ''
      },
      colorClass () {
        return this.color ? `searchbar-${this.mode}-${this.color}` : ''
      }
    },
    methods: {

      // -------- public --------
      /**
       * @function setFocus
       * @description
       * 手动设置当前input的focus状态
       */
      setFocus () {
        this.searchbarInputElement.focus()
      },

      // -------- private --------
      /**
       * Update the Searchbar input value when the input changes
       * @private
       */
      onInputHandler ($event) {
        let _valueInner = $event.target ? $event.target.value : ''
        if (_valueInner) {
          this.theValue = _valueInner
        } else {
          this.theValue = null
        }

        if (this.debounce > 16) {
          window.clearTimeout(this.timer)
          this.timer = window.setTimeout(() => {
            // 通知父组件的v-model
            this.$emit('input', this.theValue)

            this.$emit('onInput', $event)
          }, this.debounce)
        } else {
          /**
           * @event component:Searchbar#onInput
           * @description input事件
           * @property {object} $event - 事件对象
           */
          this.$emit('input', this.theValue)
          this.$emit('onInput', $event)
        }
      },

      /**
       * Sets the Searchbar to focused and active on input focus.
       * @private
       */
      onFocusHandler ($event) {
        /**
         * @event component:Searchbar#onFocus
         * @description focus事件
         * @property {object} $event - 事件对象
         */
        this.$emit('onFocus', $event)
        this.sbHasFocus = true
        this.positionElements()
      },

      /**
       * Sets the Searchbar to not focused and checks if it should align left
       * based on whether there is a value in the searchbar or not.
       * @private
       */
      onBlurHandler ($event) {
        // shouldBlur: 是否真正的blur, 因为当点击clearBtn时, 需要再次focus, 所以等到16*4ms后, 判断是否blue
        // shouldBlur determines if it should blur
        // if we are clearing the input we still want to stay focused in the input
        // wait for DOM update, because of focus method
        window.setTimeout(() => {
          if (!this.shouldBlur) {
            this.sbHasFocus = true
            this.searchbarInputElement.focus()
          } else {
            /**
             * @event component:Searchbar#onBlur
             * @description blur事件
             * @property {object} $event - 事件对象
             */
            this.$emit('onBlur', $event)
            this.sbHasFocus = false
            this.positionElements()
          }
          this.shouldBlur = true
        }, 16 * 4)
      },
      /**
       * Clears the input field and triggers the control change.
       * @private
       */
      clearInput ($event) {
        this.searchbarInputElement.focus()
        /**
         * @event component:Searchbar#onClear
         * @description clear事件
         * @property {object} $event - 事件对象
         */
        this.$emit('onClear', $event)
        this.shouldBlur = false
        if (this.theValue) {
          this.theValue = null
          this.$emit('input', this.theValue)
          this.$emit('onInput', $event)
        }
      },
      /**
       * Clears the input field and tells the input to blur since
       * the clearInput function doesn't want the input to blur
       * then calls the custom cancel function if the user passed one in.
       * @private
       */
      cancelSearchbar ($event) {
        /**
         * @event component:Searchbar#onCancel
         * @description cancel事件
         * @property {object} $event - 事件对象
         */
        this.$emit('onCancel', $event)
        if (this.theValue) {
          this.theValue = null
          this.$emit('input', this.theValue)
          this.$emit('onInput', $event)
        }
        this.shouldBlur = true
      },

      /**
       * 当focus时, 设置搜索框的icon/placeholder/cancel button的位置 (ios only)
       * @private
       */
      positionElements () {
        let isAnimated = this.animated
        let prevAlignLeft = this.shouldAlignLeft
        let shouldAlignLeft = (!isAnimated || (this.theValue && this.theValue.toString().trim() !== '') || this.sbHasFocus === true)
        this.shouldAlignLeft = shouldAlignLeft

        if (this.mode !== 'ios') {
          return
        }

        if (prevAlignLeft !== shouldAlignLeft) {
          this.positionPlaceholder()
        }
        if (isAnimated) {
          this.positionCancelButton()
        }
        this.shouldAnimated = this.animated
      },

      positionPlaceholder () {
        let inputEle = this.searchbarInputElement
        let iconEle = this.searchbarIconElement
        console.assert(inputEle, 'The input element is undefined, please check!::<Function>positionPlaceholder():inputEle')
        console.assert(iconEle, 'The icon element is undefined, please check!::<Function>positionPlaceholder():iconEle')
        if (!inputEle || !iconEle) {
          return
        }

        if (this.shouldAlignLeft) {
          inputEle.removeAttribute('style')
          iconEle.removeAttribute('style')
        } else {
          if (this.sbHasFocus) {
            this.searchbarInputElement.blur()
          }

          if (this.placeHolderTextWidth === null) {
            // Create a dummy span to get the placeholder width
            if (!this.placeholder) {
              this.placeHolderTextWidth = 0
            } else {
              let tempSpan = document.createElement('span')
              tempSpan.innerHTML = this.placeholder
              tempSpan.style.fontSize = window.getComputedStyle(inputEle).fontSize
              tempSpan.style.display = 'inline'
              document.body.appendChild(tempSpan)

              // Get the width of the span then remove it
              this.placeHolderTextWidth = tempSpan.offsetWidth
              tempSpan.remove()
            }
          }

          // Set the input padding left
          let inputLeft = 'calc(50% - ' + (this.placeHolderTextWidth / 2) + 'px)'
          inputEle.style.paddingLeft = inputLeft

          let paddingLeft = this.placeHolderTextWidth === 0 ? 14 : 30
          // Set the icon margin left
          let iconLeft = 'calc(50% - ' + ((this.placeHolderTextWidth / 2) + paddingLeft) + 'px)'
          iconEle.style.marginLeft = iconLeft
        }
      },

      /**
       * Show the iOS Cancel button on focus, hide it offscreen otherwise
       * @private
       */
      positionCancelButton () {
        if (!this.cancelButtonElement) {
          return
        }
        let showShowCancel = this.sbHasFocus
        if (showShowCancel !== this.isCancelVisible) {
          let cancelStyleEle = this.cancelButtonElement
          let cancelStyle = cancelStyleEle.style
          this.isCancelVisible = showShowCancel
          if (showShowCancel) {
            cancelStyle.marginRight = '0'
          } else {
            let offset = cancelStyleEle.offsetWidth
            if (offset > 0) {
              cancelStyle.marginRight = -offset + 'px'
            }
          }
        }
      }
    },
    mounted () {
      this.searchbarIconElement = this.$refs.searchbarIcon
      this.searchbarInputElement = this.$refs.searchbarInput
      this.cancelButtonElement = this.$refs.cancelButton.$el
      this.positionElements()

      if (isBoolean(this.autofocus) && this.autofocus) {
        this.setFocus()
      }

      if (isNumber(this.autofocus) && this.autofocus > 0) {
        window.setTimeout(() => {
          this.setFocus()
        }, this.autofocus)
      }
    },
    components: {
      'vm-button': Button, Icon
    }
  }
</script>