需求簡介
一個搜索頁面,上面輸入框,下面列表展示搜索到的結果。
重點是:產品要求搜索框默認顯示一行,當輸入的文字超過一行時,輸入框的高度會隨着改變,直到輸入完畢。
解決思路設想
本想利用textarea實現,但textarea不支持自適應高度,而是定好高度或者是行數之后,超出部分就會顯示滾動條。只能另想。
根據需求,首先想到了張鑫旭偽類匹配列表數目實現微信群頭像CSS布局的技巧一文提到的文字多字號自動變小的技巧,但仔細一琢磨,不行。這個是根據內容元素的個數,進行處理,而這兒是輸入框,沒有內容元素。
后面想到可以利用html屬性contenteditable="true"
,加在div上讓其可編輯來模擬自適應高度。可是需要在vue中雙向綁定實現,這個不是很好處理。
后面想到利用textarea的row屬性,根據輸入內容的長度控制row的值,為1-n行,但這個似乎不是很智能,因為多少個字體一行不一定,英文、中文、數字的寬度不一致,而且row屬性在每個瀏覽器中的表現不一致。
最后利用textarea,監聽change事件,讓其高度=其滾動條高度,來達到高度自適應。
沒想到最后還是利用了textarea。
實現
util.js
/**
* 文本框根據輸入內容自適應高度
* @param {HTMLElement} 輸入框元素
* @param {Number} 設置光標與輸入框保持的距離(默認0)
* @param {Number} 設置最大高度(可選)
* @callback {Function} 設置回調函數(可選)
*/
export const autoTextarea = function (elem, extra, maxHeight, callback) {
extra = extra || 0;
var isFirefox = !!document.getBoxObjectFor || 'mozInnerScreenX' in window,
isOpera = !!window.opera && !!window.opera.toString().indexOf('Opera'),
addEvent = function (type, callback) {
elem.addEventListener ?
elem.addEventListener(type, callback, false) :
elem.attachEvent('on' + type, callback);
},
getStyle = elem.currentStyle ? function (name) {
var val = elem.currentStyle[name];
if (name === 'height' && val.search(/px/i) !== 1) {
var rect = elem.getBoundingClientRect();
return rect.bottom - rect.top -
parseFloat(getStyle('paddingTop')) -
parseFloat(getStyle('paddingBottom')) + 'px';
};
return val;
} : function (name) {
return getComputedStyle(elem, null)[name];
},
minHeight = parseFloat(getStyle('height'));
elem.style.resize = 'none';
var change = function () {
var scrollTop, height,
padding = 0,
style = elem.style;
if (elem._length === elem.value.length) return;
elem._length = elem.value.length;
if (!isFirefox && !isOpera) {
padding = parseInt(getStyle('paddingTop')) + parseInt(getStyle('paddingBottom'));
};
scrollTop = document.body.scrollTop || document.documentElement.scrollTop;
elem.style.height = minHeight + 'px';
if (elem.scrollHeight > minHeight) {
if (maxHeight && elem.scrollHeight > maxHeight) {
height = maxHeight - padding;
style.overflowY = 'auto';
} else {
height = elem.scrollHeight - padding;
style.overflowY = 'hidden';
};
style.height = height + extra + 'px';
scrollTop += parseInt(style.height) - elem.currHeight;
document.body.scrollTop = scrollTop;
document.documentElement.scrollTop = scrollTop;
elem.currHeight = parseInt(style.height);
callback(parseInt(style.height));
};
};
addEvent('propertychange', change);
addEvent('input', change);
addEvent('focus', change);
change();
};
export const debounce = function (func, delay) {
let timer;
return function (...args) {
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(() => {
func.apply(this, args);
}, delay || 500);
}
}
說明:由於下面是列表,需要計算高度,為了避免重新再去獲取高度,所以加了個回調方法,把高度回調出去。
vue-search.vue
<template>
<div class="search-list">
<div>
<textarea ref="textarea" v-model="keywords" :maxlength="keywordsMax" @change="searchChange"></textarea>
<span class="clear" @click="clearKeywords">x</span>
</div>
<div class="list" ref="list">
<ul>
<li v-for="item in list" :key="item.id">
<span class="icon"></span>
<dl>
<dt>{{item.title}}</dt>
<dd>{{item.desc}}</dd>
</dl>
</li>
</ul>
</div>
</div>
</template>
<script>
import { debounce, autoTextarea } from '@/util.js';
let rootFontSize = parseFloat(document.documentElement.style.fontSize);
export default {
data () {
return {
keywordsMax: 128,
keywords: '',
list: []
}
},
mounted () {
this.$nextTick(() => {
let textarea = this.$refs.textarea;
textarea.focus();
let prevHeight = 65;
textarea && autoTextarea(textarea, 5, 0, (height) => {
height += 20;
if (height !== prevHeight) {
prevHeight = height;
let rem = height / rootFontSize;
this.$refs.list.style.height = `calc(100% - ${rem}rem)`;
}
});
})
},
methods: {
clearKeywords () {
this.keywords = '';
this.list = [];
let textarea = this.$refs.textarea;
let height = 40;
let rem = height / rootFontSize;
textarea.style.height = `${rem}rem`;
rem = (height + 20) / rootFontSize;
this.$refs.list.style.height = `calc(100% - ${rem}rem)`;
textarea.focus();
},
searchChange: debounce(function () {
let trim = this.keywords.trim();
if (!trim) {
this.list = [];
return;
}
const params = {
keywords: this.keywords
}
// 調api ...
})
}
}
</script>
補充div模擬textarea自適應
<style>
.textarea{
width: 400px;
min-height: 20px;
max-height: 300px;
_height: 120px;
margin-left: auto;
margin-right: auto;
padding: 3px;
outline: 0;
border: 1px solid #a0b3d6;
font-size: 12px;
line-height: 24px;
padding: 2px;
word-wrap: break-word;
overflow-x: hidden;
overflow-y: auto;
border-color: rgba(82, 168, 236, 0.8);
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1), 0 0 8px rgba(82, 168, 236, 0.6);
}
</style>
<div class="textarea" contenteditable="true"></div>