Vue 元件:給Bootstrap Modal增加縮放功能
需求
Bootstrap 應該還是目前最流行的前端基礎框架之一。因為架構方面的優勢,它的侵入性很低,可以以各種方式整合到其它專案當中。在我廠的各種產品裡,都有它的用武之地。
前兩天,老闆抱怨,說Modal(彈窗)在他的螢幕上太小,浪費他的 5K 顯示器。
我看了一下,按照 Bootstrap 的設計,超過 1200px 就算是 XL,此時.modal-lg的寬度固定在 1140px。其實 Bootstrap 這麼設計也有它的道理,因為人眼聚焦後寬度有限,如果彈窗太寬的話,內容一眼看不全,也不好。不過在我廠的產品裡,彈窗要呈現火焰圖,所以寬一些也有好處。
技術方案
那麼,綜合來看,最合適的做法,就給 Modal 新增一個拖拽的功能:使用者覺得夠大了,就這麼著;使用者想看大一點,就自己拉大一些,然後我記錄使用者的選擇,以便複用。
看過我《用 `resize` 和 MutationObserver 實現縮放 DOM 並記錄尺寸》的同學,應該知道resize這個css屬性,使用它可以很方便的給元素新增縮放功能。參考caniuse上面的普及度,大部分新版本的瀏覽器都已經支援,可以放心使用。
使用它的時候要注意兩點:
首先,我們在縮放元素的同時,也會對它的子元素、父元素同時造成影響。因為在靜態文件流當中,塊級元素的寬度預設是父元素content-box的 100%,而高度由子元素決定。所以,對一個塊級元素的縮放,不可能寬過它的父元素(如果限制了寬度的話),也不可能矮於它的子元素。
其次,拖拽手柄的顯示優先順序很低,會被子元素蓋住,哪怕子元素沒有填充任何內容。換言之,一定要有padding的元素才適合新增resize縮放。
實施方案
總而言之,把這個屬性加在哪個元素上面,很有講究。具體到本次需求,Bootstrap Modal,最合適新增resize屬性的的是modal-content,因為它有1rem的內邊距。
但是限制寬度的是父元素,也就是modal-dialog,它是響應式的,會根據顯示器的寬度設定一個最大寬度。如果不修改它的max-width,modal-content的最大寬度就無法超過它,達不到預期效果。但是也不能改成width,這樣的話,彈窗會失去彈性,解析度低的時候表現不好。
所以還是要在max-width上做文章。如果直接去掉它,modal-dialog的寬度就會是 100%,失去彈窗效果,所以也不能這樣做。最終,我的方案是:
- 視窗完全展開後,獲取寬高,寫入modal-content的style
- 然後去掉modal-dialog的max-width,此時,因為子元素modal-content已經定寬,所以仍然是視窗樣式
- 用 MutationObserver 監測modal-content的寬高,儲存到 localStorage,以便在全域性使用
完整程式碼展示
我廠的產品基於vue開發,所以邏輯用vue元件實現。
效果演示
為方便在 Codepen 裡呈現,有部分修改。https://codepen.io/meathill/
程式碼及解釋
<template lang="pug">
.modal.simple-modal(
:style="{display: visibility ? 'block' : 'none'}",
@click="doCloseFromBackdrop",
)
.modal-dialog.modal-dialog-scrollable(
ref="dialog",
:class="dialogClass",
)
.modal-content(ref="content", :)
.modal-header.p-2
slot(name="header")
h4 {{title}}
span.close(v-if="canClose", @click="doClose") ×
.modal-body
slot(name="body")
</template>
<script>
import debounce from 'lodash/debounce';
const RESIZED_SIZE = 'resized_width_key';
let sharedSize = null;
export default {
props: {
canClose: {
type: Boolean,
default: true,
},
size: {
type: String,
default: null,
validator: function(value) {
return ['sm', 'lg', 'xl'].indexOf(value) !== -1;
},
},
resizable: {
type: Boolean,
default: false,
},
backdrop: {
type: Boolean,
default: true,
},
title: {
type: String,
default: 'Modal title',
},
},
computed: {
dialogClass() {
const classes = [];
if (this.size) {
classes.push(`modal-${this.size}`);
}
if (this.resizable) {
classes.push('modal-dialog-resizable');
}
if (this.resizedSize) {
classes.push('ready');
}
return classes.join(' ');
},
contentStyle() {
if (!this.resizable || !this.resizedSize) {
return null;
}
const {width, height} = this.resizedSize;
return {
width: `${width}px`,
height: `${height}px`,
};
},
},
data() {
return {
visibility: false,
resizedSize: null,
};
},
methods: {
async doOpen() {
this.visibility = true;
this.$emit('open');
if (this.resizable) {
// 通過 debounce 節流可以降低函式執行次數
const onResize = debounce(this.onEditorResize, 100);
// 這裡用 MutationObserver 監測元素尺寸
const observer = this.observer = new MutationObserver(onResize);
observer.observe(this.$refs.content, {
attributes: true,
});
if (sharedSize) {
this.resizedSize = sharedSize;
}
// 第一次執行的時候,記錄 Modal 尺寸,避免太大
if (!this.resizedSize) {
await this.$nextTick();
// 按照張鑫旭的說法,這裡用 `clientWidth` 有效能問題,不過暫時還沒有更好的解決方案
// https://weibo.com/1263362863/ImwIOmamC
const width = this.$refs.dialog.clientWidth;
this.resizedSize = {width};
// 這裡產生紀錄之後,上面的 computed 屬性就會把 `max-width` 去掉了
}
}
},
doClose() {
this.visibility = false;
this.$emit('close');
},
doCloseFromBackdrop({target}) {
if (!this.backdrop || target !== this.$el) {
return;
}
this.doClose();
},
onEditorResize([{target}]) {
const width = target.clientWidth;
const height = target.clientHeight;
if (width < 320 || height < 160) {
return;
}
sharedSize = {width, height};
localStorage.setItem(RESIZED_SIZE, jsON.stringify(sharedSize));
},
},
beforeMount() {
const size = localStorage.getItem(RESIZED_SIZE);
if (size) {
this.resizedSize = jsON.parse(size);
}
},
beforeDestroy() {
if (this.observer) {
this.observer.disconnect();
this.observer = null;
}
},
};
</script>
<style lang="stylus">
.simple-modal
background-color: rgba(0, 0, 0, 0.5)
.modal-content
padding 1em
.close
cursor pointer
.modal-dialog-resizable
&.ready
max-width unset !important
.modal-content
resize both
margin 0 auto
</style>
注意
因為瀏覽器的非同步載入機制,有可能在 modal 開啟並完成佈局後,高度和寬度被內容撐開導致記錄不準,或者內容被異常遮蓋。請讀者自己想辦法處理,就當練習題吧。
資源搜尋網站大全https://55wd.com 廣州品牌設計公司http://www.maiqicn.com
總結
本次元件開發非常符合我理想的元件模式:
- 充分利用瀏覽器原生機制
- 配合儘量少的 JS
- 需要什麼功能就加什麼功能,不需要大而全
在 MVVM框架的配合下,這樣的方案很容易實現。另一方面,每個專案都有獨特的使用場景,通過長期在特定場景下工作,我們可以逐步整理出適用於這個場景的元件庫,不斷改進該專案的開發效率。我認為這才是元件化的正道。