From 5bae32c726b9002fb89f7668a3cb95e528c010bf Mon Sep 17 00:00:00 2001
From: cookfront <cookfront@gmail.com>
Date: Mon, 20 Feb 2017 11:49:37 +0800
Subject: [PATCH] picker component

---
 docs/examples/picker.md               |  22 ++++
 package.json                          |   1 -
 packages/picker/src/draggable.js      |  51 ++++++++++
 packages/picker/src/picker-column.vue | 141 ++++++++++++++++++++++++--
 packages/picker/src/picker.vue        |   6 +-
 packages/zanui-css/src/index.pcss     |   1 +
 packages/zanui-css/src/picker.pcss    |  73 +++++++++++++
 src/utils/transition.js               | 100 ++++++++++++++++++
 8 files changed, 385 insertions(+), 10 deletions(-)
 create mode 100644 packages/picker/src/draggable.js
 create mode 100644 packages/zanui-css/src/picker.pcss
 create mode 100644 src/utils/transition.js

diff --git a/docs/examples/picker.md b/docs/examples/picker.md
index d9c84223b..f4d8959f3 100644
--- a/docs/examples/picker.md
+++ b/docs/examples/picker.md
@@ -1,7 +1,29 @@
+<script>
+export default {
+  data() {
+    return {
+      pickerColumns: [
+        {
+          values: ['杭州', '宁波', '温州', '嘉兴', '湖州', '绍兴', '金华', '衢州', '舟山', '台州', '丽水']
+        }
+      ]
+    };
+  }
+};
+</script>
+
 ## Picker组件
 
 模仿iOS中的`UIPickerView`。
 
+### 基础用法
+
+:::demo 基础用法
+```html
+<z-picker :columns="pickerColumns"></z-picker>
+```
+:::
+
 ### API
 
 | 参数       | 说明      | 类型       | 默认值       | 可选值       |
diff --git a/package.json b/package.json
index 2090aa0d9..f10b62000 100644
--- a/package.json
+++ b/package.json
@@ -98,7 +98,6 @@
     "rimraf": "^2.5.4",
     "run-sequence": "^1.2.2",
     "saladcss-bem": "^0.0.1",
-    "sass-loader": "^3.2.3",
     "style-loader": "^0.13.1",
     "theaterjs": "^3.0.0",
     "transliteration": "^1.1.11",
diff --git a/packages/picker/src/draggable.js b/packages/picker/src/draggable.js
new file mode 100644
index 000000000..4f524c986
--- /dev/null
+++ b/packages/picker/src/draggable.js
@@ -0,0 +1,51 @@
+import Vue from 'vue';
+
+let isDragging = false;
+
+const supportTouch = !Vue.prototype.$isServer && 'ontouchstart' in window;
+
+export default function(element, options) {
+  const moveFn = function(event) {
+    if (options.drag) {
+      options.drag(supportTouch ? event.changedTouches[0] || event.touches[0] : event);
+    }
+  };
+
+  const endFn = function(event) {
+    if (!supportTouch) {
+      document.removeEventListener('mousemove', moveFn);
+      document.removeEventListener('mouseup', endFn);
+    }
+    document.onselectstart = null;
+    document.ondragstart = null;
+
+    isDragging = false;
+
+    if (options.end) {
+      options.end(supportTouch ? event.changedTouches[0] || event.touches[0] : event);
+    }
+  };
+
+  element.addEventListener(supportTouch ? 'touchstart' : 'mousedown', function(event) {
+    if (isDragging) return;
+    document.onselectstart = function() { return false; };
+    document.ondragstart = function() { return false; };
+
+    if (!supportTouch) {
+      document.addEventListener('mousemove', moveFn);
+      document.addEventListener('mouseup', endFn);
+    }
+    isDragging = true;
+
+    if (options.start) {
+      event.preventDefault();
+      options.start(supportTouch ? event.changedTouches[0] || event.touches[0] : event);
+    }
+  });
+
+  if (supportTouch) {
+    element.addEventListener('touchmove', moveFn);
+    element.addEventListener('touchend', endFn);
+    element.addEventListener('touchcancel', endFn);
+  }
+};
diff --git a/packages/picker/src/picker-column.vue b/packages/picker/src/picker-column.vue
index b07316837..f0ab04053 100644
--- a/packages/picker/src/picker-column.vue
+++ b/packages/picker/src/picker-column.vue
@@ -1,14 +1,21 @@
 <template>
   <div class="z-picker-column">
-    <div class="z-picker-column-wrapper">
-      <div class="z-picker-item">
-        
+    <div class="z-picker-column-wrapper" :class="{ dragging: isDragging }" ref="wrapper" :style="{ height: visibleContentHeight + 'px' }">
+      <div
+        v-for="item in currentValues"
+        class="z-picker-column__item"
+        :class="{ 'z-picker-column__item--selected': item === currentValue }"
+        :style="{ height: itemHeight + 'px', lineHeight: itemHeight + 'px' }">
+        {{item}}
       </div>
     </div>
   </div>
 </template>
 
 <script>
+import translateUtil from 'src/utils/transition';
+import draggable from './draggable';
+
 const DEFAULT_ITEM_HEIGHT = 44;
 
 export default {
@@ -40,7 +47,7 @@ export default {
     return {
       currentValue: this.value,
       currentValues: this.values,
-      dragging: false
+      isDragging: false
     };
   },
 
@@ -50,11 +57,16 @@ export default {
     },
 
     currentValues(val) {
-
+      if (this.valueIndex === -1) {
+        this.currentValue = (val || [])[0];
+      }
     },
 
     currentValue(val) {
+      this.doOnValueChange();
+
       this.$emit('change', this);
+      this.$emit('input', val);
     }
   },
 
@@ -64,11 +76,128 @@ export default {
      */
     visibleContentHeight() {
       return this.itemHeight * this.visibileColumnCount;
+    },
+
+    valueIndex() {
+      return this.currentValues.indexOf(this.currentValue);
+    },
+
+    dragRange() {
+      var values = this.currentValues;
+      var visibileColumnCount = this.visibileColumnCount;
+      var itemHeight = this.itemHeight;
+
+      return [ -itemHeight * (values.length - Math.ceil(visibileColumnCount / 2)), itemHeight * Math.floor(visibileColumnCount / 2) ];
     }
   },
 
-  methods: {
+  mounted() {
+    this.ready = true;
+    this.$emit('input', this.currentValue);
 
+    this.initEvents();
+    this.doOnValueChange();
+  },
+
+  methods: {
+    value2Translate(value) {
+      let values = this.currentValues;
+      let valueIndex = values.indexOf(value);
+      let offset = Math.floor(this.visibileColumnCount / 2);
+      let itemHeight = this.itemHeight;
+
+      if (valueIndex !== -1) {
+        return (valueIndex - offset) * -itemHeight;
+      }
+    },
+
+    translate2Value(translate) {
+      let itemHeight = this.itemHeight;
+      translate = Math.round(translate / itemHeight) * itemHeight;
+
+      let index = -(translate - Math.floor(this.visibileColumnCount / 2) * itemHeight) / itemHeight;
+
+      return this.currentValues[index];
+    },
+
+    initEvents() {
+      var el = this.$refs.wrapper;
+      var dragState = {};
+
+      var velocityTranslate, prevTranslate, pickerItems;
+
+      draggable(el, {
+        start: (event) => {
+          dragState = {
+            range: this.dragRange,
+            start: new Date(),
+            startLeft: event.pageX,
+            startTop: event.pageY,
+            startTranslateTop: translateUtil.getElementTranslate(el).top
+          };
+          pickerItems = el.querySelectorAll('.z-picker-item');
+        },
+
+        drag: (event) => {
+          this.isDragging = true;
+
+          dragState.left = event.pageX;
+          dragState.top = event.pageY;
+
+          let deltaY = dragState.top - dragState.startTop;
+          let translate = dragState.startTranslateTop + deltaY;
+
+          translateUtil.translateElement(el, null, translate);
+
+          velocityTranslate = translate - prevTranslate || translate;
+
+          prevTranslate = translate;
+        },
+
+        end: () => {
+          if (this.isDragging) {
+            this.isDragging = false;
+
+            var momentumRatio = 7;
+            var currentTranslate = translateUtil.getElementTranslate(el).top;
+            var duration = new Date() - dragState.start;
+
+            var momentumTranslate;
+            if (duration < 300) {
+              momentumTranslate = currentTranslate + velocityTranslate * momentumRatio;
+            }
+
+            var dragRange = dragState.range;
+
+            this.$nextTick(() => {
+              var translate;
+              var itemHeight = this.itemHeight;
+
+              if (momentumTranslate) {
+                translate = Math.round(momentumTranslate / itemHeight) * itemHeight;
+              } else {
+                translate = Math.round(currentTranslate / itemHeight) * itemHeight;
+              }
+
+              translate = Math.max(Math.min(translate, dragRange[1]), dragRange[0]);
+
+              translateUtil.translateElement(el, null, translate);
+
+              this.currentValue = this.translate2Value(translate);
+            });
+          }
+
+          dragState = {};
+        }
+      });
+    },
+
+    doOnValueChange() {
+      let value = this.currentValue;
+      let wrapper = this.$refs.wrapper;
+
+      translateUtil.translateElement(wrapper, null, this.value2Translate(value));
+    }
   }
 };
 </script>
diff --git a/packages/picker/src/picker.vue b/packages/picker/src/picker.vue
index 513e33c8b..90e2d6d7a 100644
--- a/packages/picker/src/picker.vue
+++ b/packages/picker/src/picker.vue
@@ -1,10 +1,10 @@
 <template>
   <div class="z-picker">
-    <div class="z-picker-toolbar">
+    <div class="z-picker__toolbar">
       <slot>
       </slot>
     </div>
-    <div class="z-picker-columns">
+    <div class="z-picker__columns" :class="['z-picker__columns--' + columns.length]">
       <picker-column
         v-for="(item, index) in columns"
         v-model="values[index]"
@@ -69,7 +69,7 @@ export default {
       let values = [];
 
       columns.forEach(column => {
-        values.push(column.value || column[column.defaultIndex || 0]);
+        values.push(column.value || column.values[column.defaultIndex || 0]);
       });
 
       return values;
diff --git a/packages/zanui-css/src/index.pcss b/packages/zanui-css/src/index.pcss
index 8a849223c..afe503ef7 100644
--- a/packages/zanui-css/src/index.pcss
+++ b/packages/zanui-css/src/index.pcss
@@ -8,4 +8,5 @@
 @import './field.pcss';
 @import './icon.pcss';
 @import './popup.pcss';
+@import './picker.pcss';
 @import './switch.pcss';
diff --git a/packages/zanui-css/src/picker.pcss b/packages/zanui-css/src/picker.pcss
new file mode 100644
index 000000000..59947341d
--- /dev/null
+++ b/packages/zanui-css/src/picker.pcss
@@ -0,0 +1,73 @@
+@component-namespace z {
+  @b picker {
+    overflow: hidden;
+
+    @e toolbar {
+      height: 40px;
+    }
+
+    @e columns {
+      position: relative;
+      overflow: hidden;
+
+      @m 1 {
+        .z-picker-column {
+          width: 100%;
+        }
+      }
+
+      @m 2 {
+        .z-picker-column {
+          width: 50%;
+        }
+      }
+
+      @m 2 {
+        .z-picker-column {
+          width: 33.333%;
+        }
+      }
+    }
+  }
+
+  @b picker-column {
+    font-size: 18px;
+    overflow: hidden;
+    position: relative;
+    max-height: 100%;
+    text-align: center;
+
+    @e item {
+      height: 44px;
+      line-height: 44px;
+      padding: 0 10px;
+      white-space: nowrap;
+      position: relative;
+      overflow: hidden;
+      text-overflow: ellipsis;
+      color: #707274;
+      left: 0;
+      top: 0;
+      width: 100%;
+      box-sizing: border-box;
+      transition-duration: .3s;
+      backface-visibility: hidden;
+
+      @m selected {
+        color: #000;
+        transform: translate3d(0, 0, 0) rotateX(0);
+      }
+    }
+  }
+
+  .picker-column-wrapper {
+    transition-duration: 0.3s;
+    transition-timing-function: ease-out;
+    backface-visibility: hidden;
+  }
+
+  .picker-column-wrapper.dragging,
+  .picker-column-wrapper.dragging .picker-item {
+    transition-duration: 0s;
+  }
+}
diff --git a/src/utils/transition.js b/src/utils/transition.js
new file mode 100644
index 000000000..215dc0d4f
--- /dev/null
+++ b/src/utils/transition.js
@@ -0,0 +1,100 @@
+import Vue from 'vue';
+
+var exportObj = {};
+
+if (!Vue.prototype.$isServer) {
+  var docStyle = document.documentElement.style;
+  var engine;
+  var translate3d = false;
+
+  if (window.opera && Object.prototype.toString.call(opera) === '[object Opera]') {
+    engine = 'presto';
+  } else if ('MozAppearance' in docStyle) {
+    engine = 'gecko';
+  } else if ('WebkitAppearance' in docStyle) {
+    engine = 'webkit';
+  } else if (typeof navigator.cpuClass === 'string') {
+    engine = 'trident';
+  }
+
+  var cssPrefix = {trident: '-ms-', gecko: '-moz-', webkit: '-webkit-', presto: '-o-'}[engine];
+
+  var vendorPrefix = {trident: 'ms', gecko: 'Moz', webkit: 'Webkit', presto: 'O'}[engine];
+
+  var helperElem = document.createElement('div');
+  var perspectiveProperty = vendorPrefix + 'Perspective';
+  var transformProperty = vendorPrefix + 'Transform';
+  var transformStyleName = cssPrefix + 'transform';
+  var transitionProperty = vendorPrefix + 'Transition';
+  var transitionStyleName = cssPrefix + 'transition';
+  var transitionEndProperty = vendorPrefix.toLowerCase() + 'TransitionEnd';
+
+  if (helperElem.style[perspectiveProperty] !== undefined) {
+    translate3d = true;
+  }
+
+  var getTranslate = function(element) {
+    var result = {left: 0, top: 0};
+    if (element === null || element.style === null) return result;
+
+    var transform = element.style[transformProperty];
+    var matches = /translate\(\s*(-?\d+(\.?\d+?)?)px,\s*(-?\d+(\.\d+)?)px\)\s*translateZ\(0px\)/ig.exec(transform);
+    if (matches) {
+      result.left = +matches[1];
+      result.top = +matches[3];
+    }
+
+    return result;
+  };
+
+  var translateElement = function(element, x, y) {
+    if (x === null && y === null) return;
+
+    if (element === null || element === undefined || element.style === null) return;
+
+    if (!element.style[transformProperty] && x === 0 && y === 0) return;
+
+    if (x === null || y === null) {
+      var translate = getTranslate(element);
+      if (x === null) {
+        x = translate.left;
+      }
+      if (y === null) {
+        y = translate.top;
+      }
+    }
+
+    cancelTranslateElement(element);
+
+    if (translate3d) {
+      element.style[transformProperty] += ' translate(' + (x ? (x + 'px') : '0px') + ',' + (y ? (y + 'px') : '0px') + ') translateZ(0px)';
+    } else {
+      element.style[transformProperty] += ' translate(' + (x ? (x + 'px') : '0px') + ',' + (y ? (y + 'px') : '0px') + ')';
+    }
+  };
+
+  var cancelTranslateElement = function(element) {
+    if (element === null || element.style === null) return;
+
+    var transformValue = element.style[transformProperty];
+    
+    if (transformValue) {
+      transformValue = transformValue.replace(/translate\(\s*(-?\d+(\.?\d+?)?)px,\s*(-?\d+(\.\d+)?)px\)\s*translateZ\(0px\)/g, '');
+      element.style[transformProperty] = transformValue;
+    }
+  };
+
+  exportObj = {
+    transformProperty: transformProperty,
+    transformStyleName: transformStyleName,
+    transitionProperty: transitionProperty,
+    transitionStyleName: transitionStyleName,
+    transitionEndProperty: transitionEndProperty,
+    getElementTranslate: getTranslate,
+    translateElement: translateElement,
+    cancelTranslateElement: cancelTranslateElement
+  };
+};
+
+export default exportObj;
+