gjj
2025-02-21 efe41f68868a8926dfc1a6851a492805b56786db
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
<template>
  <transition
    :name="name"
    @before-appear="beforeAppear"
    @appear="appear"
    @after-appear="afterAppear"
    @appear-cancelled="appearCancelled"
    @before-enter="beforeEnter"
    @enter="enter"
    @after-enter="afterEnter"
    @enter-cancelled="enterCancelled"
    @before-leave="beforeLeave"
    @leave="leave"
    @after-leave="afterLeave"
    @leave-cancelled="leaveCancelled"
  >
    <slot></slot>
  </transition>
</template>
 
<script>
  export default {
    name: "CollapseTransition",
    props: {
      name: {
        type: String,
        required: false,
        default: "collapse",
      },
      dimension: {
        type: String,
        required: false,
        default: "height",
        validator: (value) => {
          return ["height", "width"].includes(value);
        },
      },
      duration: {
        type: Number,
        required: false,
        default: 300,
      },
      easing: {
        type: String,
        required: false,
        default: "ease-in-out",
      },
    },
    data() {
      return {
        cachedStyles: null,
      };
    },
    computed: {
      transition() {
        let transitions = [];
        Object.keys(this.cachedStyles).forEach((key) => {
          transitions.push(`${this.convertToCssProperty(key)} ${this.duration}ms ${this.easing}`);
        });
        return transitions.join(", ");
      },
    },
    watch: {
      dimension() {
        this.clearCachedDimensions();
      },
    },
    methods: {
      beforeAppear(el) {
        // Emit the event to the parent
        this.$emit("before-appear", el);
      },
      appear(el) {
        // Emit the event to the parent
        this.$emit("appear", el);
      },
      afterAppear(el) {
        // Emit the event to the parent
        this.$emit("after-appear", el);
      },
      appearCancelled(el) {
        // Emit the event to the parent
        this.$emit("appear-cancelled", el);
      },
      beforeEnter(el) {
        // Emit the event to the parent
        this.$emit("before-enter", el);
      },
      enter(el, done) {
        // Because width and height may be 'auto',
        // first detect and cache the dimensions
        this.detectAndCacheDimensions(el);
        // The order of applying styles is important:
        // - 1. Set styles for state before transition
        // - 2. Force repaint
        // - 3. Add transition style
        // - 4. Set styles for state after transition
        // If the order is not right and you open any 2nd level submenu
        // for the first time, the transition will not work.
        this.setClosedDimensions(el);
        this.hideOverflow(el);
        this.forceRepaint(el);
        this.setTransition(el);
        this.setOpenedDimensions(el);
        // Emit the event to the parent
        this.$emit("enter", el, done);
        // Call done() when the transition ends
        // to trigger the @after-enter event.
        setTimeout(done, this.duration);
      },
      afterEnter(el) {
        // Clean up inline styles
        this.unsetOverflow(el);
        this.unsetTransition(el);
        this.unsetDimensions(el);
        this.clearCachedDimensions();
        // Emit the event to the parent
        this.$emit("after-enter", el);
      },
      enterCancelled(el) {
        // Emit the event to the parent
        this.$emit("enter-cancelled", el);
      },
      beforeLeave(el) {
        // Emit the event to the parent
        this.$emit("before-leave", el);
      },
      leave(el, done) {
        // For some reason, @leave triggered when starting
        // from open state on page load. So for safety,
        // check if the dimensions have been cached.
        this.detectAndCacheDimensions(el);
        // The order of applying styles is less important
        // than in the enter phase, as long as we repaint
        // before setting the closed dimensions.
        // But it is probably best to use the same
        // order as the enter phase.
        this.setOpenedDimensions(el);
        this.hideOverflow(el);
        this.forceRepaint(el);
        this.setTransition(el);
        this.setClosedDimensions(el);
        // Emit the event to the parent
        this.$emit("leave", el, done);
        // Call done() when the transition ends
        // to trigger the @after-leave event.
        // This will also cause v-show
        // to reapply 'display: none'.
        setTimeout(done, this.duration);
      },
      afterLeave(el) {
        // Clean up inline styles
        this.unsetOverflow(el);
        this.unsetTransition(el);
        this.unsetDimensions(el);
        this.clearCachedDimensions();
        // Emit the event to the parent
        this.$emit("after-leave", el);
      },
      leaveCancelled(el) {
        // Emit the event to the parent
        this.$emit("leave-cancelled", el);
      },
      detectAndCacheDimensions(el) {
        // Cache actual dimensions
        // only once to void invalid values when
        // triggering during a transition
        if (this.cachedStyles) return;
        const visibility = el.style.visibility;
        const display = el.style.display;
        // Trick to get the width and
        // height of a hidden element
        el.style.visibility = "hidden";
        el.style.display = "";
        this.cachedStyles = this.detectRelevantDimensions(el);
        // Restore any original styling
        el.style.visibility = visibility;
        el.style.display = display;
      },
      clearCachedDimensions() {
        this.cachedStyles = null;
      },
      detectRelevantDimensions(el) {
        // These properties will be transitioned
        if (this.dimension === "height") {
          return {
            height: el.offsetHeight + "px",
            paddingTop: el.style.paddingTop || this.getCssValue(el, "padding-top"),
            paddingBottom: el.style.paddingBottom || this.getCssValue(el, "padding-bottom"),
          };
        }
        if (this.dimension === "width") {
          return {
            width: el.offsetWidth + "px",
            paddingLeft: el.style.paddingLeft || this.getCssValue(el, "padding-left"),
            paddingRight: el.style.paddingRight || this.getCssValue(el, "padding-right"),
          };
        }
        return {};
      },
      setTransition(el) {
        el.style.transition = this.transition;
      },
      unsetTransition(el) {
        el.style.transition = "";
      },
      hideOverflow(el) {
        el.style.overflow = "hidden";
      },
      unsetOverflow(el) {
        el.style.overflow = "";
      },
      setClosedDimensions(el) {
        Object.keys(this.cachedStyles).forEach((key) => {
          el.style[key] = "0";
        });
      },
      setOpenedDimensions(el) {
        Object.keys(this.cachedStyles).forEach((key) => {
          el.style[key] = this.cachedStyles[key];
        });
      },
      unsetDimensions(el) {
        Object.keys(this.cachedStyles).forEach((key) => {
          el.style[key] = "";
        });
      },
      forceRepaint(el) {
        // Force repaint to make sure the animation is triggered correctly.
        // Thanks: https://markus.oberlehner.net/blog/transition-to-height-auto-with-vue/
        getComputedStyle(el)[this.dimension];
      },
      getCssValue(el, style) {
        return getComputedStyle(el, null).getPropertyValue(style);
      },
      convertToCssProperty(style) {
        // Example: convert 'paddingTop' to 'padding-top'
        // Thanks: https://gist.github.com/tan-yuki/3450323
        const upperChars = style.match(/([A-Z])/g);
        if (!upperChars) {
          return style;
        }
        for (let i = 0, n = upperChars.length; i < n; i++) {
          style = style.replace(new RegExp(upperChars[i]), "-" + upperChars[i].toLowerCase());
        }
        if (style.slice(0, 1) === "-") {
          style = style.slice(1);
        }
        return style;
      },
    },
  };
</script>