├── README.md ├── contacts.css ├── contacts.js └── index.html /README.md: -------------------------------------------------------------------------------- 1 | # contacts 联系人/通讯录 插件 2 | 3 | javascript 实现微信通讯录,联系人按拼音首字母从A到Z分组排序/搜索过滤联系人 4 | 5 | ![预览](http://ww2.sinaimg.cn/mw690/60cdc5a5gw1faii44zp40g20aq0iuqv7.gif) 6 | 7 | ## 状态 8 | 9 | 开发中,进度看下面 10 | 11 | ## 进度 12 | - [x] 正确分类数据 13 | - [x] 点击索引跳转 14 | - [x] 顶栏显示 15 | - [x] 滚动流畅不卡 16 | - [x] 搜索联系人 17 | - [x] 做成原生插件 18 | - [x] 仿照微信联系人UI 19 | - [x] 对输入数据排序 20 | 21 | ## 用法 22 | 23 | 参照 index.html 24 | 25 | **license** 26 | 27 | MIT license 28 | -------------------------------------------------------------------------------- /contacts.css: -------------------------------------------------------------------------------- 1 | .hidden { 2 | display: none!important; 3 | } 4 | 5 | .wx-contacts-container { 6 | font-family: 'Microsoft YaHei'; 7 | } 8 | 9 | .wx-contacts-shortcuts-ctn { 10 | position: fixed; 11 | top: 50%; 12 | transform: translateY(-50%); 13 | right: 0; 14 | display: flex; 15 | flex-direction: column; 16 | text-align: center; 17 | font-family: 'Microsoft YaHei'; 18 | } 19 | .wx-contacts-shortcuts-ctn > a { 20 | width: 30px; 21 | height: 20px; 22 | text-align: center; 23 | text-decoration: none; 24 | color: #999; 25 | } 26 | .wx-contacts-shortcuts-ctn > a[href="#wx-contacts-hook-search"] { 27 | border: 2px solid #999; 28 | width: 7px; 29 | height: 7px; 30 | border-radius: 50%; 31 | transform: translate(80%,-50%); 32 | } 33 | 34 | .wx-contacts-shortcuts-ctn > a[href="#wx-contacts-hook-search"]::after { 35 | content: ''; 36 | display: block; 37 | width: 5px; 38 | height: 0px; 39 | border: 1px solid #999; 40 | transform: rotate(45deg) translate(140%,110%); 41 | } 42 | 43 | .wx-contacts-list { 44 | position: relative; 45 | padding: 0px; 46 | list-style-type: none; 47 | } 48 | .wx-contacts-list li { 49 | display: flex; 50 | align-items: center; 51 | height: 40px; 52 | padding-left: 10px; 53 | border-top: 1px solid #EFEFF4; 54 | } 55 | .wx-contacts-list li.wx-contacts-hooks { 56 | display: block; 57 | height: 20px; 58 | line-height: 20px; 59 | background-color: #EFEFF4; 60 | border-top: none; 61 | } 62 | .wx-contacts-list li.wx-contacts-hooks + li { 63 | border-top: none; 64 | } 65 | 66 | .wx-contacts-list li.wx-contacts-on-top { 67 | position: fixed; 68 | top: 0; 69 | left: 0; 70 | width: 100%; 71 | } 72 | .wx-contacts-list li.wx-contacts-on-top + li { 73 | padding-top: 20px; 74 | } 75 | 76 | input.wx-contacts-search { 77 | width: 100%; 78 | height: 40px; 79 | line-height: 40px; 80 | border: none; 81 | border-bottom: 1px solid #EFEFF4; 82 | font-size: 16px; 83 | outline: none; 84 | } 85 | .wx-contacts-search-result .wx-contacts-list li { 86 | border-top: none; 87 | border-bottom: 1px solid #EFEFF4; 88 | } 89 | 90 | .wx-contacts-badge { 91 | display: inline-block; 92 | width: 30px; 93 | height: 30px; 94 | margin-right: 10px; 95 | } 96 | 97 | .wx-contacts-search-bar { 98 | position: relative; 99 | display: flex; 100 | box-sizing: border-box; 101 | background-color: #EFEFF4; 102 | } 103 | 104 | .wx-contacts-search-bar:before { 105 | content: " "; 106 | position: absolute; 107 | left: 0; 108 | top: 0; 109 | right: 0; 110 | height: 1px; 111 | border-top: 1px solid #D7D6DC; 112 | color: #D7D6DC; 113 | transform: scaleY(0.5); 114 | } 115 | 116 | 117 | .wx-contacts-search-bar:after { 118 | content: " "; 119 | position: absolute; 120 | left: 0; 121 | bottom: 0; 122 | right: 0; 123 | height: 1px; 124 | border-bottom: 1px solid #D7D6DC; 125 | color: #D7D6DC; 126 | transform: scaleY(0.5); 127 | } 128 | 129 | .wx-contacts-search-inner { 130 | position: relative; 131 | box-sizing: border-box; 132 | margin: 7px 10px; 133 | padding-left: 30px; 134 | padding-right: 30px; 135 | height: 26px; 136 | width: 100%; 137 | border-radius: 3px; 138 | background: #FFFFFF; 139 | z-index: 1; 140 | } 141 | 142 | .wx-contacts-search-inner .wx-contacts-search-input { 143 | box-sizing: border-box; 144 | width: 100%; 145 | height: 26px; 146 | line-height: 26px; 147 | padding: 4px 0; 148 | border: 0; 149 | font-size: 14px; 150 | background: transparent; 151 | outline: none; 152 | } 153 | 154 | /* webkit 内核浏览器 type=search 的 input 自带 取消 按钮*/ 155 | .wx-contacts-search-input::-webkit-search-cancel-button { 156 | display: none; 157 | } 158 | 159 | .wx-contacts-icon-clear { 160 | position: absolute; 161 | right: 5px; 162 | top: 50%; 163 | transform: translateY(-50%); 164 | display: inline-block; 165 | width: 12px; 166 | height: 12px; 167 | border: 1px solid #999; 168 | border-radius: 50%; 169 | background: #999; 170 | z-index: 1; 171 | } 172 | 173 | .wx-contacts-icon-clear::before { 174 | content: ""; 175 | width: 1px; 176 | height: 80%; 177 | background-color: white; 178 | position: absolute; 179 | transform: translate(-50%, -50%) rotate(45deg); 180 | left: 50%; 181 | top: 50%; 182 | } 183 | 184 | .wx-contacts-icon-clear::after { 185 | content: ""; 186 | width: 1px; 187 | height: 80%; 188 | background-color: white; 189 | position: absolute; 190 | transform: translate(-50%, -50%) rotate(-45deg); 191 | left: 50%; 192 | top: 50%; 193 | } 194 | 195 | .wx-contacts-search-text { 196 | position: absolute; 197 | top: 1px; 198 | right: 1px; 199 | bottom: 1px; 200 | left: 1px; 201 | margin: 8px 10px; 202 | line-height: 22px; 203 | z-index: 2; 204 | border-radius: 3px; 205 | text-align: center; 206 | color: #9B9B9B; 207 | background: #FFFFFF; 208 | } 209 | 210 | .wx-contacts-search-focusing .wx-contacts-search-text { 211 | display: none; 212 | } -------------------------------------------------------------------------------- /contacts.js: -------------------------------------------------------------------------------- 1 | (function(global, undefined){ 2 | function Contacts(opts){ 3 | if(!(this instanceof Contacts)){ 4 | return new Contacts(opts); 5 | } 6 | this.merge(this.opts, opts); 7 | this.init(); 8 | } 9 | 10 | Contacts.prototype = { 11 | opts: { 12 | appendTo: '', 13 | generateListItem: null, 14 | data: [] 15 | }, 16 | merge: function(defaultOpts, userOpts){ 17 | if(userOpts){ 18 | Object.assign(defaultOpts, userOpts); 19 | } 20 | }, 21 | init: function(){ 22 | this.parseData(); 23 | this.generateShortcuts(); 24 | 25 | var list = this.generateList(); 26 | var ctn = this.generateCtn(); 27 | ctn.querySelector('.wx-contacts-all-result').appendChild(list); 28 | 29 | var appendTo = this.opts.appendTo || 'body'; 30 | document.querySelector(appendTo).appendChild(ctn); 31 | 32 | this.getAllAnchorPositions(); 33 | this.addListener(); 34 | }, 35 | parseData: function(){ 36 | var self = this; 37 | var data = this.opts.data; 38 | var map = {}; 39 | var c = 'A'.charCodeAt(); 40 | for(; c <= 'Z'.charCodeAt(); c++ ){ 41 | map[String.fromCharCode(c)] = []; 42 | } 43 | map['#'] = []; 44 | var firstCharUpper; 45 | data.forEach(function(item){ 46 | firstCharUpper = self.getFirstUpperChar(item.name); 47 | if (map.hasOwnProperty(firstCharUpper)) { 48 | map[firstCharUpper].push(item); 49 | } else { 50 | map['#'].push(item); 51 | } 52 | }); 53 | 54 | //排序 55 | for(var key in map) { 56 | if( map.hasOwnProperty( key ) && (map[key].length != 0)) { 57 | map[key].sort(function(a, b){ 58 | return a.name.localeCompare(b.name, 'zh-CN-u-co-pinyin'); 59 | }); 60 | } 61 | } 62 | 63 | this.dictMap = map; 64 | return map; 65 | }, 66 | generateShortcuts: function(){ 67 | var items = []; 68 | var map = this.dictMap; 69 | for(var key in map) { 70 | if( map.hasOwnProperty( key ) && (map[key].length != 0)) { 71 | items.push(key); 72 | } 73 | } 74 | var ctn = document.createElement('div'); 75 | ctn.classList.add('wx-contacts-shortcuts-ctn'); 76 | var test,a; 77 | items.forEach(function(item){ 78 | text = document.createTextNode(item); 79 | a = document.createElement('a'); 80 | a.setAttribute('href', '#wx-contacts-hook-' + item); 81 | a.setAttribute('rel', 'internal'); 82 | a.appendChild(text); 83 | ctn.appendChild(a); 84 | }); 85 | a = document.createElement('a'); 86 | a.setAttribute('href', '#wx-contacts-hook-search'); 87 | a.setAttribute('rel', 'internal'); 88 | ctn.insertBefore(a, ctn.firstChild); 89 | document.body.appendChild(ctn); 90 | }, 91 | generateList: function(){ 92 | var self = this; 93 | var map = self.dictMap; 94 | var formerKey = null; 95 | var list = document.createElement('ul'); 96 | list.classList.add('wx-contacts-list'); 97 | for(var key in map) { 98 | if( map.hasOwnProperty( key ) && (map[key].length != 0)) { 99 | var items = map[key]; 100 | items.forEach(function(item){ 101 | var text,li,a; 102 | if(key != formerKey){ 103 | a = document.createElement('a'); 104 | a.setAttribute('id', 'wx-contacts-hook-' + key); 105 | text = document.createTextNode(key); 106 | li = document.createElement('li'); 107 | li.classList.add('wx-contacts-hooks'); 108 | li.appendChild(a); 109 | li.appendChild(text); 110 | list.appendChild(li); 111 | formerKey = key; 112 | } 113 | list.appendChild(self.generateListItem(item)); 114 | }); 115 | } 116 | } 117 | return list; 118 | }, 119 | generateListItem: function(item){ 120 | if(this.opts.generateListItem && typeof this.opts.generateListItem == 'function'){ 121 | return this.opts.generateListItem(item); 122 | } 123 | var tpl = ''+ item.name + ''; 124 | var li = document.createElement('li'); 125 | li.innerHTML = tpl; 126 | return li; 127 | }, 128 | generateCtn: function(){ 129 | var ctn = document.createElement('div'); 130 | ctn.classList.add('wx-contacts-container'); 131 | ctn.appendChild(this.generateInputCtn()); 132 | var searchResult = document.createElement('div'); 133 | searchResult.classList.add('wx-contacts-search-result'); 134 | ctn.appendChild(searchResult); 135 | var allResult = document.createElement('div'); 136 | allResult.classList.add('wx-contacts-all-result'); 137 | ctn.appendChild(allResult); 138 | return ctn; 139 | }, 140 | generateInputCtn: function(){ 141 | var div = document.createElement('div'); 142 | div.classList.add('wx-contacts-search-bar'); 143 | div.setAttribute('id','wx-contacts-hook-search'); 144 | var innerHtml = 145 | '
' + 146 | '' + 147 | '' + 148 | '' + 149 | '
' + 150 | ''; 154 | 155 | div.innerHTML = innerHtml; 156 | 157 | return div; 158 | }, 159 | generateFilteredList: function(map, filter_str){ 160 | var list = document.createElement('ul'); 161 | list.classList.add('wx-contacts-list'); 162 | var li; 163 | for( var key in map){ 164 | if( map.hasOwnProperty( key ) && (map[key].length != 0)) { 165 | var items = map[key]; 166 | items.forEach(function(item){ 167 | if (String(item.name).match(filter_str)) { 168 | li = document.createElement('li') 169 | li.appendChild(document.createTextNode(item.name)); 170 | list.appendChild(li); 171 | } 172 | }); 173 | } 174 | } 175 | return list; 176 | }, 177 | getFirstUpperChar: function(str){ 178 | string = String(str); 179 | var c = string[0]; 180 | if (/[^\u4e00-\u9fa5]/.test(c)) { 181 | return c.toUpperCase(); 182 | } 183 | else { 184 | return this.chineseToEnglish(c); 185 | } 186 | }, 187 | // adopt from https://ruby-china.org/topics/29026 188 | chineseToEnglish: function(c){ 189 | var idx = -1; 190 | var MAP = 'ABCDEFGHJKLMNOPQRSTWXYZ'; 191 | var boundaryChar = '驁簿錯鵽樲鰒餜靃攟鬠纙鞪黁漚曝裠鶸蜶籜鶩鑂韻糳'; 192 | if (!String.prototype.localeCompare) { 193 | throw Error('String.prototype.localeCompare not supported.'); 194 | } 195 | if (/[^\u4e00-\u9fa5]/.test(c)) { 196 | return c; 197 | } 198 | for (var i = 0; i < boundaryChar.length; i++) { 199 | if (boundaryChar[i].localeCompare(c, 'zh-CN-u-co-pinyin') >= 0) { 200 | idx = i; 201 | break; 202 | } 203 | } 204 | return MAP[idx]; 205 | }, 206 | getAllAnchorPositions: function(){ 207 | var anchors = document.querySelectorAll('.wx-contacts-hooks'); 208 | var self = this; 209 | self.positions = []; 210 | anchors = [].slice.call(anchors); 211 | 212 | anchors.forEach(function(anchor){ 213 | self.positions.push({ 214 | anchor: anchor, 215 | pos: anchor.getBoundingClientRect().top 216 | }); 217 | }); 218 | }, 219 | getTopbarElement: function(scrollPosition){ 220 | var i = 0; 221 | var gutter = 20; 222 | while((i < this.positions.length) && scrollPosition + gutter >= this.positions[i].pos){ 223 | i ++; 224 | } 225 | 226 | 227 | if (i == 0) { 228 | return null; 229 | } 230 | else { 231 | return this.positions[i-1].anchor; 232 | } 233 | }, 234 | addListener: function(){ 235 | // scroll optimization 236 | // see https://developer.mozilla.org/en-US/docs/Web/Events/scroll 237 | var lastKnownScrollPosition = 0; 238 | var ticking = false; 239 | var topBarElement = null; 240 | var self = this; 241 | window.addEventListener('scroll', function(e){ 242 | lastKnownScrollPosition = window.scrollY; 243 | if(!ticking) { 244 | window.requestAnimationFrame(function(){ 245 | //do some thing here 246 | 247 | self.positions.forEach(function(item){ 248 | item.anchor.classList.remove('wx-contacts-on-top'); 249 | }); 250 | topBarElement = self.getTopbarElement(lastKnownScrollPosition); 251 | topBarElement && topBarElement.classList.add('wx-contacts-on-top'); 252 | ticking = false; 253 | }); 254 | } 255 | ticking = true; 256 | }); 257 | document.querySelector('#wx-contacts-search-input').addEventListener('change', function(e){ 258 | var searchStr = e.target.value.trim(); 259 | var list; 260 | if (searchStr.length != 0) { 261 | //hide list 262 | document.querySelector('.wx-contacts-all-result').classList.add('hidden'); 263 | //hide shortcuts 264 | document.querySelector('.wx-contacts-shortcuts-ctn').classList.add('hidden'); 265 | //filter list 266 | list = self.generateFilteredList(self.dictMap, searchStr); 267 | //set result list 268 | var searchResult = document.querySelector('.wx-contacts-search-result'); 269 | while(searchResult.lastChild){ 270 | searchResult.removeChild(searchResult.lastChild); 271 | } 272 | searchResult.appendChild(list); 273 | //show result list 274 | document.querySelector('.wx-contacts-search-result').classList.remove('hidden'); 275 | } 276 | else { 277 | document.querySelector('.wx-contacts-all-result').classList.remove('hidden'); 278 | document.querySelector('.wx-contacts-shortcuts-ctn').classList.remove('hidden'); 279 | document.querySelector('.wx-contacts-search-result').classList.add('hidden'); 280 | } 281 | }); 282 | 283 | document.querySelector('#wx-contacts-search-input').addEventListener('keyup', function(e){ 284 | var searchStr = e.target.value.trim(); 285 | if(searchStr.length != 0){ 286 | document.querySelector('#search-clear').classList.remove('hidden'); 287 | } 288 | else { 289 | document.querySelector('#search-clear').classList.add('hidden'); 290 | } 291 | }); 292 | 293 | document.querySelector('#wx-contacts-search-input').addEventListener('focus', function(){ 294 | document.querySelector('.wx-contacts-search-bar').classList.add('wx-contacts-search-focusing'); 295 | if(this.value){ 296 | document.querySelector('#search-clear').classList.remove('hidden'); 297 | } 298 | else { 299 | document.querySelector('#search-clear').classList.add('hidden'); 300 | } 301 | }); 302 | 303 | document.querySelector('#wx-contacts-search-input').addEventListener('blur', function(e){ 304 | document.querySelector('.wx-contacts-search-bar').classList.remove('wx-contacts-search-focusing'); 305 | if (this.value) { 306 | document.querySelector('.wx-contacts-search-text').classList.add('hidden'); 307 | } else { 308 | document.querySelector('.wx-contacts-search-text').classList.remove('hidden'); 309 | } 310 | }); 311 | 312 | document.querySelector('#search-clear').addEventListener('touchend', function(){ 313 | document.querySelector('#wx-contacts-search-input').value = ''; 314 | }); 315 | 316 | } 317 | }; 318 | 319 | global.Contacts = Contacts; 320 | })(window); 321 | 322 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Contacts 8 | 9 | 10 | 19 | 20 | 21 | 22 |
23 | 143 | 144 | --------------------------------------------------------------------------------