Browse Source

'批量绑定'

zhangyu 4 years ago
parent
commit
ef29f1dc8b

BIN
src/assets/images/iconBlackBottom.png


BIN
src/assets/images/iconBlackTop.png


BIN
src/assets/images/iconLightBottom.png


BIN
src/assets/images/iconLightTop.png


+ 280 - 22
src/components/edit/edit-dialog.vue

@@ -118,7 +118,7 @@
                             <div v-show="data.floor.FloorName.length">
                                 <el-checkbox v-model="data.floor.Checked" :ref="`${key}check`" @change="handleCheckChange(key, data.floor.Checked)"></el-checkbox>
                                 {{`${data.floor.Name}`}}
-                                <el-link :underline="false" type="primary" style="vertical-align: baseline;margin-left: 5px;">查看</el-link>
+                                <el-link :underline="false" type="primary" @click="handleShowMap(data.floor, 'floor')" style="vertical-align: baseline;margin-left: 5px;">查看</el-link>
                             </div>
                             <div class="bind-equip-list">
                                 <el-tag
@@ -136,7 +136,7 @@
                                 <div class="bind-equip-title">
                                     <el-checkbox v-model="equip.Checked" :ref="`${key}check`" @change="handleCheckChange(key, equip.Checked)"></el-checkbox>
                                     {{`设备设施:${equip.Name}`}}
-                                    <el-link :underline="false" type="primary" style="vertical-align: baseline;margin-left: 5px;">查看</el-link>
+                                    <el-link :underline="false" type="primary" @click="handleShowMap(equip, 'equip')" style="vertical-align: baseline;margin-left: 5px;">查看</el-link>
                                 </div>
                                 <div class="bind-equip-list">
                                     <el-tag
@@ -155,7 +155,7 @@
                                 <div class="bind-equip-title">
                                     <el-checkbox v-model="system.Checked" :ref="`${key}check`" @change="handleCheckChange(key, system.Checked)"></el-checkbox>
                                     {{`系统原理图:${system.Name}`}}
-                                    <el-link :underline="false" type="primary" style="vertical-align: baseline;margin-left: 5px;">查看</el-link>
+                                    <el-link :underline="false" type="primary" @click="handleShowMap(system, 'system')" style="vertical-align: baseline;margin-left: 5px;">查看</el-link>
                                 </div>
                                 <div class="bind-equip-list">
                                     <el-tag
@@ -174,12 +174,17 @@
             </div>
         </a-modal>
 
-        <a-modal v-model="visibleMap" title="标题" width="1200px"></a-modal>
+        <a-modal v-model="visibleMap" :title="mapName" :footer="null" width="1200px" class="bind-map-dialog">
+            <div class="bind-map-content" v-loading="canvasLoading">
+                <canvas id="bind-canvas" :width="1200" :height="591" ref="bind-canvas" tabindex="0"></canvas>
+                <floor-list v-if='floorsArr.length>0' :floorsArr='floorsArr' @emitFloor='emitFloor' id='system'></floor-list>
+            </div>
+        </a-modal>
     </div>
 </template>
 
 <script>
-    import searchTree from "@/components/edit/search_tree";
+    import floorList from "@/components/edit/floorList.vue";
     import {
         queryGlsmsasset,
         getQuerySmsxt,
@@ -190,15 +195,22 @@
         queryGlsmsLocation,
         graphElementOrderInfoLocal,
         queryFloorByNode,
-        bindAttachObject
+        bindAttachObject,
+        readGroup
     } from "@/api/editer";
     import bus from "@/bus";
     import {getUrlMsg} from '@/components/urlMsg.js';
     import { mapGetters } from 'vuex';
+    import { FloorView } from "@/lib/FloorView";
+    import { EditScence } from "@/components/mapClass/EditScence";
+    import { ProjectRf } from "@saga-web/feng-map";
+    import { SImageItem, SImageShowType } from "@saga-web/graph/lib";
+    import { STopologyParser } from "@/lib/parsers/STopologyParser";
+    import { SFloorParser, SBoardItem } from "@saga-web/big";
 
     export default {
         name: "edit-dialog",
-        components: {searchTree},
+        components: {floorList},
         props: {
             sysNum: Number,//设备总数
             attrCards: Array,
@@ -303,7 +315,6 @@
                     },
                 ],
                 tableData: [],
-                loading: false,
                 page: 1,
                 size: 10,
                 treeData: [],
@@ -330,7 +341,15 @@
                     HasPub: {},
                     NotPub: {}
                 },
-                GraphsIdList: []
+                GraphsIdList: [],
+                visibleMap: false,
+                mapName: "",
+                floorsArr: [],
+                view: '',
+                scene: '',
+                mapServerURL: "http://map.wanda.cn/editor",
+                activeFloor: null,
+                canvasLoading: false
             }
         },
         watch: {
@@ -469,10 +488,9 @@
                                         const floor = this.bindData[key].floor;
                                         if (item.Graphs.length) {
                                             item.Graphs.forEach(graph => {
-                                                floor.FloorName.push({
-                                                    Id: graph.Id,
-                                                    Name: graph.Floor.Code
-                                                });
+                                                graph.Name = graph.Floor.Code;
+                                                graph.Type = key;
+                                                floor.FloorName.push(graph);
                                                 // this.GraphsIdList.push(graph.Id);
                                             })
                                         }
@@ -495,10 +513,9 @@
                                         const systemLast = this.bindData[key].system[this.bindData[key].system.length - 1];
                                         if (item.Graphs.length) {
                                             item.Graphs.forEach(graph => {
-                                                systemLast.FloorName.push({
-                                                    Id: graph.Id,
-                                                    Name: graph.Floor.Code
-                                                });
+                                                graph.Name = graph.Floor.Code;
+                                                graph.Type = key;
+                                                systemLast.FloorName.push(graph);
                                                 // this.GraphsIdList.push(graph.Id);
                                             })
                                         }
@@ -521,10 +538,9 @@
                                         const equipLast = this.bindData[key].equip[this.bindData[key].equip.length - 1];
                                         if (item.Graphs.length) {
                                             item.Graphs.forEach(graph => {
-                                                equipLast.FloorName.push({
-                                                    Id: graph.Id,
-                                                    Name: graph.Floor.Code
-                                                });
+                                                graph.Name = graph.Floor.Code;
+                                                graph.Type = key;
+                                                equipLast.FloorName.push(graph);
                                                 // this.GraphsIdList.push(graph.Id);
                                             })
                                         }
@@ -574,6 +590,29 @@
                 this.visibleBind = false;
                 this.visible = false;
             },
+            // 点击查看按钮
+            handleShowMap(data, type) {
+                this.floorsArr = [];
+                if (type == 'floor') {
+                    this.mapName = data.Name;
+                } else if (type == 'equip') {
+                    this.mapName = `设备设施 - ${data.Name}`;
+                } else if (type == 'system') {
+                    this.mapName = `系统原理图 - ${data.Name}`;
+                }
+                this.floorsArr = data.FloorName;
+                this.visibleMap = true;
+            },
+            // 切换楼层
+            emitFloor(floorObj) {
+                if (floorObj) {
+                    this.$nextTick(() => {
+                        this.canvasLoading = true;
+                        this.activeFloor = floorObj;
+                        this.initGraph(floorObj.FloorId);
+                    })
+                }
+            },
             // 点击批量绑定按钮
             handleClickBind(Marked) {
                 const lists = [];
@@ -1031,7 +1070,211 @@
                     })
                 }
 
-            }
+            },
+            // 清除底图
+            clearGraphy() {
+                if (this.view) {
+                    this.view.scene = null;
+                    return;
+                }
+                this.view = new FloorView("bind-canvas");
+                document.getElementById("bind-canvas").focus();
+            },
+            // 初始化
+            initGraph(floor){
+                this.clearGraphy();
+				this.scene = new EditScence();
+                this.scene.isEditStatus = false;
+				this.parserData(floor);
+            },
+            // 解析数据
+			parserData(floor) {
+				const obj = getUrlMsg()
+				this.floorMapList = window.FENGMAP.gnameToGid
+				if (floor == "g80") {
+					// 屋顶
+					if (window.FENGMAP.frImg) {
+					const pj = obj.fmapID.split('_')[0]
+					// 单张图片
+					if (!ProjectRf[pj]) {
+						const imgItem = new SImageItem(
+						null,
+						`${this.mapServerURL}/webtheme/${obj.fmapID}/${window.FENGMAP.frImg}`
+						);
+						imgItem.showType = SImageShowType.AutoFit;
+						imgItem.connect("imgLoadOver", this, () => {
+							this.readGraph();
+						});
+						this.scene.addItem(imgItem);
+						this.view.scene = this.scene;
+					} else {
+						// 多张图
+						try {
+							// 初始化0
+							this.count = 0;
+							ProjectRf[pj].forEach(t => {
+								const item = new SImageItem(
+								null,
+								`${this.mapServerURL}/webtheme/${obj.fmapID}/${t.name}`
+								);
+								item.width = t.width
+								item.height = t.height
+								item.moveTo(t.x, t.y)
+								item.connect("imgLoadOver", this, () => {
+									this.countRf(ProjectRf[pj].length);
+								});
+								this.scene.addItem(item);
+							})
+							this.view.scene = this.scene;
+						} catch(e) {
+							console.log(e);
+						}
+					}
+					} else {
+					// 屋顶图不为图片
+					this.readBaseMap(floor);
+					}
+				} else {
+					if (this.floorMapList[floor]) {
+						this.readBaseMap(floor);
+					} else {
+						console.log("楼层不正确");
+					}
+				}
+            },
+            // 顶楼为多张图时计数器
+			countRf(len) {
+				this.count++
+				console.log(len == this.count);
+				if (len == this.count) {
+					this.readGraph();
+				} else {
+					console.log('所有图片未加载完成');
+				}
+			},
+			// 读取绘制数据
+			readGraph() {
+                const pa = {
+                    categoryId: this.activeFloor.CategoryId,
+					projectId: this.activeFloor.ProjectId,
+					BuildingID: this.activeFloor.BuildingId, // 建筑ID
+                    FloorID: this.activeFloor.FloorId, // 楼层id
+                    Pub: this.activeFloor.Type == "HasPub", // false-草稿箱
+                }
+				readGroup(pa).then(data => {
+					if (data.Data) {
+					this.graphId = data.Data[0].ID;
+					const parserData = new STopologyParser(null);
+					const itemMap = {};
+					parserData.parseData(data.Data[0].Elements);
+					// 多边形(此item需在直线item添加之前添加)
+					parserData.zoneLegendList.forEach(t => {
+						this.scene.addItem(t);
+						this.scene.Nodes.push(t);
+						itemMap[t.id] = t;
+					});
+					// 增加文字(此item需在直线item添加之前添加)
+					parserData.textMarkerList.forEach(t => {
+						this.scene.addItem(t);
+						this.scene.Markers.push(t);
+						itemMap[t.id] = t;
+					});
+					// 增加图片(此item需在直线item添加之前添加)
+					parserData.imageMarkerList.forEach(t => {
+						this.scene.addItem(t);
+						this.scene.Markers.push(t);
+						itemMap[t.id] = t;
+					});
+					// 增加图标类图例(此item需在管线item添加之前添加)
+					parserData.imageLegendList.forEach(t => {
+						this.scene.addItem(t);
+						this.scene.Nodes.push(t);
+						if (t.anchorList && t.anchorList.length) {
+                            t.anchorList.forEach(anc => {
+                                itemMap[anc.id] = anc;
+                            });
+						}
+					});
+					// 增加直线
+					parserData.lineMarkerList.forEach(t => {
+						this.scene.addItem(t);
+						this.scene.Markers.push(t);
+						// 设置关联Item
+						if (t.data.Properties && t.data.Properties.StartItemId) {
+                            const startItem = itemMap[t.data.Properties.StartItemId];
+                            startItem?.connect("onMove", t, t.changePos);
+                            t.startItem = startItem || null;
+						}
+						if (t.data.Properties && t.data.Properties.EndItemId) {
+                            const endItem = itemMap[t.data.Properties.EndItemId];
+                            endItem?.connect("onMove", t, t.changePos);
+                            t.endItem = endItem || null;
+						}
+					});
+					// 增加管线类(需在图标类图例添加后添加)
+					parserData.relationList.forEach(t => {
+						this.scene.addItem(t);
+						this.scene.Relations.push(t);
+						// 设置锚点
+						if (t.anchor1ID) {
+						const startAnc = itemMap[t.anchor1ID];
+						if (startAnc) {
+							startAnc.isConnected = true;
+							startAnc.parent?.connect("changePos", t, t.changePos);
+							t.startAnchor = startAnc || null;
+						}
+						}
+						if (t.anchor2ID) {
+						const endAnc = itemMap[t.anchor2ID];
+						if (endAnc) {
+							endAnc.isConnected = true;
+							endAnc.parent?.connect("changePos", t, t.changePos);
+							t.endAnchor = endAnc || null;
+						}
+						}
+					});
+					}
+					this.view.fitSceneToView();
+                    this.canvasLoading = false;
+				});
+			},
+			// 解析底图
+			readBaseMap(floor){
+                this.loadBoard(this.floorMapList[floor]);
+                window.FENGMAP.parseData(this.floorMapList[floor], res => {
+                    if (res.err) {
+                        console.log(res.err);
+                        return;
+                    }
+                    this.fParser = new SFloorParser(null);
+                    this.fParser.parseData(res);
+                    this.scene.fidToItem = {};
+                    this.fParser.spaceList.forEach(t => {
+                        t.nameColor = "#2a2a2a";
+                        t.zOrder = t.zOrder + t.data.Height;
+                        t.selectable = true;
+                        this.scene.addItem(t);
+                    });
+                    this.scene.spaceList = this.fParser.spaceList;
+                    this.fParser.wallList.forEach(t => {
+                        this.scene.addItem(t)
+                    });
+                    this.fParser.virtualWallList.forEach(t => this.scene.addItem(t));
+                    this.fParser.doorList.forEach(t => this.scene.addItem(t));
+                    this.fParser.columnList.forEach(t => this.scene.addItem(t));
+                    this.fParser.casementList.forEach(t => this.scene.addItem(t));
+                    this.view.scene = this.scene;
+                    this.readGraph();
+                    console.log("success");
+                });
+			},
+            // 解析楼地板
+            loadBoard(floor){
+                window.FENGMAP.loadFloor(floor, res => {
+                    const zone = new SBoardItem(null, res)
+                    this.scene.addItem(zone);
+                })
+            },
         },
         created() {
             // 取gcname
@@ -1207,4 +1450,19 @@
             margin-left: 10px;
         }
     }
+
+    .bind-map-dialog {
+        .bind-map-content {
+            position: relative;
+            #system {
+                position: absolute;
+                top: 80px;
+                right: 36px;
+            }
+        }
+        /deep/ .ant-modal-body {
+            padding: 0;
+            background: #F2F5F7;
+        }
+    }
 </style>

+ 233 - 0
src/components/edit/floorList.vue

@@ -0,0 +1,233 @@
+<template>
+  <div class='floor-box'>
+    <div class='floor-list'>
+      <div class='icon-top' v-if='floorsArr.length>8'>
+        <!-- @click='changeFloor(1,currIndex)' -->
+        <img v-show='parseInt(marginTop) !== 0' v-repeat-click='increase' src='@/assets/images/iconBlackTop.png' alt />
+        <img class='disabled' v-show='parseInt(marginTop) === 0' src='@/assets/images/iconLightTop.png' alt />
+      </div>
+      <div class='floor-out' :style='{ height:conHeight + "px" }'>
+        <!--  放开marginTop样式  -->
+        <div class='floor-center' :style='{ marginTop : marginTop }'>
+          <div class='floor-item' :class='item.FloorId == currentFloorId?"isActive":""' @click='tabFloor(item,index)'
+            v-for='(item,index) in floorsArr' :key='index'>{{item.Name}}</div>
+        </div>
+      </div>
+      <div class='icon-bottom' v-if='floorsArr.length>8'>
+        <!-- v-repeat-click='decrease' -->
+        <img v-show='parseInt(marginTop) !== marginTopMax' v-repeat-click='decrease'
+          src='@/assets/images/iconBlackBottom.png' alt />
+        <img class='disabled' v-show='parseInt(marginTop) === marginTopMax' src='@/assets/images/iconLightBottom.png'
+          alt />
+      </div>
+    </div>
+  </div>
+</template>
+<script>
+import RepeatClick from '@/directives/repeat-click'
+
+export default {
+  directives: {
+    repeatClick: RepeatClick,
+  },
+  data() {
+    return {
+      currentFloorId: null,
+      marginTop: 0,
+      marginTopMax: 0,
+      showNumber: 8, //需要展示的楼层数   //TODO:
+      height: 39, //一个楼层的高度
+      currIndex: 0, //当前楼层在 楼层数组中的下标,上下箭头使用
+      conHeight: 0, // floor-out 的高度
+    }
+  },
+  props: {
+    floorsArr: {
+      type: Array,
+      default: () => {
+        return []
+      },
+    }
+  },
+  methods: {
+    /**
+     * @description 点击上箭头,marginTop<0时执行楼层滚动
+     */
+    increase() {
+      const marginTop = parseInt(this.marginTop)
+      marginTop < 0 && this.changeFloor(1, this.currIndex)
+    },
+    /**
+     * @description 点击下箭头,marginTop小于最大值marginTopMax时,执行楼层滚动
+     */
+    decrease() {
+      const marginTop = Math.abs(parseInt(this.marginTop)),
+      marginTopMax = Math.abs(parseInt(this.marginTopMax))
+      marginTop < marginTopMax && this.changeFloor(-1, this.currIndex)
+    },
+    init() {
+      // 修复在设备设施页面中,楼层组件不够 8个楼层时,出现的样式问题,
+      this.conHeight = this.floorsArr.length * 37.5
+      this.conHeight = this.conHeight >= 300 ? 300 : this.conHeight
+      this.showNumber = this.floorsArr.length > 8 ? 8 : this.floorsArr.length
+
+      this.marginTopMax = -(this.floorsArr.length - this.showNumber) * this.height
+      this.changeFloor(0, 0);
+      this.tabFloor(this.floorsArr[0], 0);
+    },
+    /**
+     * @name changeFloor
+     * @param {Number} flag 1:向上滚动楼层, -1向下滚动 , 0:进入页面初始化时,执行位置处理
+     * @description 点击图例下方的,上下切换按钮
+     */
+    changeFloor(flag, index) {
+      const len = this.floorsArr.length
+      this.currIndex = index
+      // 点击上箭头
+      if (flag === 1) {
+        index--
+        this.currIndex = index
+      } else if (flag === -1) {
+        //点击下箭头
+        index++
+        this.currIndex = index
+      }
+      this.handlePosition(flag, index, len)
+    },
+    /**
+     * @name tabFloor
+     * @param {Object} item 选中的楼层信息
+     * @param {Number} index 楼层信息在floorsArr数组中的位置
+     */
+    tabFloor(item, index) {
+      this.currentFloorId = item.FloorId;
+      this.$emit("emitFloor", item)
+      this.handlePosition(2, index, this.floorsArr.length)
+    },
+    /**
+     * @description 楼层位置动画处理
+     * @param flag 1:向上滚动楼层, -1向下滚动 , 0:进入页面初始化时,执行位置处理 2:直接点击楼层
+     * @param index 楼层 floorsArr
+     * @param len floorsArr
+     */
+    handlePosition(flag, index, len) {
+      // 取出当前 marginTop
+      let marginTop = parseInt(this.marginTop)
+      switch (flag) {
+        // 初始化进入页面,位置处理
+        case 0:
+        // 直接点击楼层,滚动楼层
+        case 2:
+          // 将 marginTop 设置为对应的index 应滚动的距离
+          marginTop = -index * this.height
+          // marginTop 过大时,取最大值marginTopMax
+          if (Math.abs(marginTop) >= Math.abs(this.marginTopMax)) {
+            marginTop = parseInt(this.marginTopMax)
+          }
+          // marginTop>0时,取0,防止楼层上边出现空白
+          marginTop = marginTop >= 0 ? 0 : marginTop
+          // index为0,marginTop设置为0
+          index == 0 && (marginTop = 0)
+          // index为最后一个,设置为最大marginTopMax
+          index == len - 1 && (marginTop = parseInt(this.marginTopMax))
+          this.marginTop = marginTop + 'px'
+          break
+        //  1:向上滚动楼层
+        case 1:
+          this.marginTop = marginTop + this.height + 'px'
+          break
+        // -1向下滚动楼层
+        case -1:
+          this.marginTop = marginTop + this.height * -1 + 'px'
+          break
+        default:
+          break
+      }
+    },
+  },
+  watch: {
+    floorsArr: {
+      handler() {
+        this.init()
+      },
+      immediate: true,
+      deep: true
+    }
+  }
+}
+</script>
+<style lang="less" scoped>
+.floor-box {
+  .floor-list {
+    width: 44px;
+    // height: 212px;
+    background: rgba(255, 255, 255, 1);
+    box-shadow: 0px 2px 15px 0px rgba(31, 36, 41, 0.08);
+    border-radius: 2px;
+    position: relative;
+    padding: 6px 4px;
+    text-align: center;
+    .floor-out {
+      // max-height: 300px; //TODO:
+      min-height: 38px;
+      overflow: hidden;
+      position: relative;
+      overflow-y: auto;
+      &::-webkit-scrollbar {
+        display: none;
+      }
+
+      .floor-center {
+        transition: all linear 0.5s;
+        .floor-item {
+          line-height: 28px;
+          height: 28px;
+          cursor: pointer;
+          position: relative;
+          &::after {
+            position: absolute;
+            left: 50%;
+            margin-left: -20%;
+            bottom: -6px;
+            content: "";
+            width: 14px;
+            height: 1px;
+            background: rgba(195, 199, 203, 1);
+            border: 0px solid rgba(228, 229, 231, 1);
+          }
+          & + .floor-item {
+            margin-top: 10px;
+          }
+        }
+      }
+    }
+
+    .icon-top {
+      cursor: pointer;
+      height: 18px;
+      img {
+        width: 18px;
+        height: 100%;
+        margin-top: -10px;
+      }
+    }
+    .icon-bottom {
+      cursor: pointer;
+      height: 18px;
+      img {
+        width: 18px;
+        height: 100%;
+        margin-top: -10px;
+      }
+    }
+    .isActive {
+      border-radius: 4px;
+      color: #025baa;
+      background: #e1f2ff;
+    }
+  }
+  .disabled {
+    cursor: not-allowed !important;
+  }
+}
+</style>

+ 230 - 0
src/directives/dom.js

@@ -0,0 +1,230 @@
+/* istanbul ignore next */
+
+import Vue from 'vue'
+
+const isServer = Vue.prototype.$isServer
+const SPECIAL_CHARS_REGEXP = /([\:\-\_]+(.))/g
+const MOZ_HACK_REGEXP = /^moz([A-Z])/
+const ieVersion = isServer ? 0 : Number(document.documentMode)
+
+/* istanbul ignore next */
+const trim = function(string) {
+    return (string || '').replace(/^[\s\uFEFF]+|[\s\uFEFF]+$/g, '')
+}
+/* istanbul ignore next */
+const camelCase = function(name) {
+    return name
+        .replace(SPECIAL_CHARS_REGEXP, function(_, separator, letter, offset) {
+            return offset ? letter.toUpperCase() : letter
+        })
+        .replace(MOZ_HACK_REGEXP, 'Moz$1')
+}
+
+/* istanbul ignore next */
+export const on = (function() {
+    if (!isServer && document.addEventListener) {
+        return function(element, event, handler) {
+            if (element && event && handler) {
+                element.addEventListener(event, handler, false)
+            }
+        }
+    } else {
+        return function(element, event, handler) {
+            if (element && event && handler) {
+                element.attachEvent('on' + event, handler)
+            }
+        }
+    }
+})()
+
+/* istanbul ignore next */
+export const off = (function() {
+    if (!isServer && document.removeEventListener) {
+        return function(element, event, handler) {
+            if (element && event) {
+                element.removeEventListener(event, handler, false)
+            }
+        }
+    } else {
+        return function(element, event, handler) {
+            if (element && event) {
+                element.detachEvent('on' + event, handler)
+            }
+        }
+    }
+})()
+
+/* istanbul ignore next */
+export const once = function(el, event, fn) {
+    var listener = function() {
+        if (fn) {
+            fn.apply(this, arguments)
+        }
+        off(el, event, listener)
+    }
+    on(el, event, listener)
+}
+
+/* istanbul ignore next */
+export function hasClass(el, cls) {
+    if (!el || !cls) return false
+    if (cls.indexOf(' ') !== -1) throw new Error('className should not contain space.')
+    if (el.classList) {
+        return el.classList.contains(cls)
+    } else {
+        return (' ' + el.className + ' ').indexOf(' ' + cls + ' ') > -1
+    }
+}
+
+/* istanbul ignore next */
+export function addClass(el, cls) {
+    if (!el) return
+    var curClass = el.className
+    var classes = (cls || '').split(' ')
+
+    for (var i = 0, j = classes.length; i < j; i++) {
+        var clsName = classes[i]
+        if (!clsName) continue
+
+        if (el.classList) {
+            el.classList.add(clsName)
+        } else if (!hasClass(el, clsName)) {
+            curClass += ' ' + clsName
+        }
+    }
+    if (!el.classList) {
+        el.className = curClass
+    }
+}
+
+/* istanbul ignore next */
+export function removeClass(el, cls) {
+    if (!el || !cls) return
+    var classes = cls.split(' ')
+    var curClass = ' ' + el.className + ' '
+
+    for (var i = 0, j = classes.length; i < j; i++) {
+        var clsName = classes[i]
+        if (!clsName) continue
+
+        if (el.classList) {
+            el.classList.remove(clsName)
+        } else if (hasClass(el, clsName)) {
+            curClass = curClass.replace(' ' + clsName + ' ', ' ')
+        }
+    }
+    if (!el.classList) {
+        el.className = trim(curClass)
+    }
+}
+
+/* istanbul ignore next */
+export const getStyle =
+    ieVersion < 9
+        ? function(element, styleName) {
+              if (isServer) return
+              if (!element || !styleName) return null
+              styleName = camelCase(styleName)
+              if (styleName === 'float') {
+                  styleName = 'styleFloat'
+              }
+              try {
+                  switch (styleName) {
+                      case 'opacity':
+                          try {
+                              return element.filters.item('alpha').opacity / 100
+                          } catch (e) {
+                              return 1.0
+                          }
+                      default:
+                          return element.style[styleName] || element.currentStyle ? element.currentStyle[styleName] : null
+                  }
+              } catch (e) {
+                  return element.style[styleName]
+              }
+          }
+        : function(element, styleName) {
+              if (isServer) return
+              if (!element || !styleName) return null
+              styleName = camelCase(styleName)
+              if (styleName === 'float') {
+                  styleName = 'cssFloat'
+              }
+              try {
+                  var computed = document.defaultView.getComputedStyle(element, '')
+                  return element.style[styleName] || computed ? computed[styleName] : null
+              } catch (e) {
+                  return element.style[styleName]
+              }
+          }
+
+/* istanbul ignore next */
+export function setStyle(element, styleName, value) {
+    if (!element || !styleName) return
+
+    if (typeof styleName === 'object') {
+        for (var prop in styleName) {
+            if (styleName.hasOwnProperty(prop)) {
+                setStyle(element, prop, styleName[prop])
+            }
+        }
+    } else {
+        styleName = camelCase(styleName)
+        if (styleName === 'opacity' && ieVersion < 9) {
+            element.style.filter = isNaN(value) ? '' : 'alpha(opacity=' + value * 100 + ')'
+        } else {
+            element.style[styleName] = value
+        }
+    }
+}
+
+export const isScroll = (el, vertical) => {
+    if (isServer) return
+
+    const determinedDirection = vertical !== null || vertical !== undefined
+    const overflow = determinedDirection ? (vertical ? getStyle(el, 'overflow-y') : getStyle(el, 'overflow-x')) : getStyle(el, 'overflow')
+
+    return overflow.match(/(scroll|auto)/)
+}
+
+export const getScrollContainer = (el, vertical) => {
+    if (isServer) return
+
+    let parent = el
+    while (parent) {
+        if ([window, document, document.documentElement].includes(parent)) {
+            return window
+        }
+        if (isScroll(parent, vertical)) {
+            return parent
+        }
+        parent = parent.parentNode
+    }
+
+    return parent
+}
+
+export const isInContainer = (el, container) => {
+    if (isServer || !el || !container) return false
+
+    const elRect = el.getBoundingClientRect()
+    let containerRect
+
+    if ([window, document, document.documentElement, null, undefined].includes(container)) {
+        containerRect = {
+            top: 0,
+            right: window.innerWidth,
+            bottom: window.innerHeight,
+            left: 0,
+        }
+    } else {
+        containerRect = container.getBoundingClientRect()
+    }
+
+    return (
+        elRect.top < containerRect.bottom &&
+        elRect.bottom > containerRect.top &&
+        elRect.right > containerRect.left &&
+        elRect.left < containerRect.right
+    )
+}

+ 24 - 0
src/directives/repeat-click.js

@@ -0,0 +1,24 @@
+import { once, on } from './dom'
+
+export default {
+    bind(el, binding, vnode) {
+        let interval = null
+        let startTime
+        const handler = () => vnode.context[binding.expression].apply()
+        const clear = () => {
+            if (Date.now() - startTime < 100) {
+                handler()
+            }
+            clearInterval(interval)
+            interval = null
+        }
+
+        on(el, 'mousedown', (e) => {
+            if (e.button !== 0) return
+            startTime = Date.now()
+            once(document, 'mouseup', clear)
+            clearInterval(interval)
+            interval = setInterval(handler, 100)
+        })
+    },
+}