Vue 组件:给Bootstrap Modal增加缩放功能

更新日期: 2019-12-03阅读: 2.3k标签: bootstrap

需求

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%,失去弹窗效果,所以也不能这样做。最终,我的方案是:

  1. 窗口完全展开后,获取宽高,写入 modal-content 的 style
  2. 然后去掉 modal-dialog 的 max-width,此时,因为子元素 modal-content 已经定宽,所以仍然是窗口样式
  3. 用 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 打开并完成布局后,高度和宽度被内容撑开导致记录不准,或者内容被异常遮盖。请读者自己想办法处理,就当练习题吧。


总结

本次组件开发非常符合我理想的组件模式:

  1. 充分利用浏览器原生机制
  2. 配合尽量少的 JS
  3. 需要什么功能就加什么功能,不需要大而全

在 MVVM 框架的配合下,这样的方案很容易实现。另一方面,每个项目都有独特的使用场景,通过长期在特定场景下工作,我们可以逐步整理出适用于这个场景的组件库,不断改进该项目的开发效率。我认为这才是组件化的正道。

原文:https://blog.meathill.com/tech/lecture/add-resize-to-bootstrap-modal.html


链接: https://fly63.com/article/detial/7139

bootstrap的响应式开发,如何使用bootstrap制作响应式布局网站

bootsrap响应式开发是为了让用户在不同尺寸的设备上,同一URL下浏览网页都可以获得良好的视觉效果。为了实现bootsrap响应式设计,我们需要创建适配各种设备尺寸的css,让页面加载的时候去匹配对应的css样式。

网站前端有必要学习bootstrap么?前端从bootstrap中可以学到什么

bootstrap出现的本意是让web开发更迅速、简单。那么作为前端开发的我们有必要学习bootstrap吗?作为前端开发的我们可以从bootstrap中可以学到什么?

bootstrap组件有哪些?bootstrap常用组件推荐

这篇文章主要整理bootstrap中常用的组件,比如:表格、表单验证、文件上传、复选下拉框、弹出框、时间组件、加载效果等

Bootstrap后台管理模板推荐_最值得拥有的免费Bootstrap后台管理模板

在网站开发项目中,后台管理因为面向群体相对比较固定,大部分以实现业务逻辑和功能。使用Bootstrap后台模板可以让后端开发很轻松的就展现给客户一个响应式的后台:AdminLTE、Gentelella Admin、Vali Admin、ModularAdmin、Metis、Ace

和 jQuery 说再见,Bootstrap 5 将移除对其依赖

Bootstrap 最新版本 4.3.1 已发布,作为 Bootstrap 4.3 发布的一部分,团队也公布了下一个主要版本 Bootstrap 5 的开发计划。将会在开发 Bootstrap 5 的过程中实现一些关键变化

响应式网站设计---Bootstrap

响应式布局可以帮助我们实现网站布局随屏幕大小自动调整的需求,实现不同屏幕分辨率的终端上浏览网页的不同展示方式,使得网页在PC端和手机端均可以完美的展现其内容,具有自适应性。

bootstrap-select使用过程中的一些问题

这里总结一下上次使用bootstrap-select的过程中遇到的一些问题。至于bootstrap-select的具体使用方法这里就不介绍了,网上有很多例子。点击没有反应,下拉框不出现

vue-cli项目引入jquery和bootstrap

webpack.ProvidePlugin插件里面可以配置全局引用,比如此处配置了jquery的使用,后面再vue里使用$,jQuery,windows.JQuery都等同于使用jquery,不需要再require或import

bootstrap-table树形结构(展开和折叠)

今天在工作的时候,遇到了一个需求,就是需要一键展开或者关闭树形结构。关于树形结构的不是很熟悉,然后去百度,结果也不是很准确。最后经过Google才找到。下面分享给大家

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!