@itfin/components 1.0.21 → 1.0.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/assets/scss/_variables.scss +0 -2
- package/src/assets/scss/components/_spinner.scss +2 -2
- package/src/assets/scss/components/_states.scss +0 -1
- package/src/assets/scss/directives/loading.scss +1 -1
- package/src/components/button/Button.vue +7 -5
- package/src/components/datepicker/DatePickerInline.vue +3 -1
- package/src/components/datepicker/MonthPicker.vue +163 -0
- package/src/components/datepicker/index.stories.js +5 -0
- package/src/components/modal/Modal.vue +6 -2
- package/src/components/popover/Popover.vue +1 -1
- package/src/components/select/Select.vue +1 -1
- package/src/components/table/Table.vue +12 -9
- package/src/components/text-field/TextField.vue +10 -2
- package/src/directives/sticky.js +298 -0
package/package.json
CHANGED
|
@@ -36,8 +36,6 @@ $input-focus-border: rgb(11 49 79 / 25%);
|
|
|
36
36
|
|
|
37
37
|
$form-check-input-border: 1px solid rgba(#000, .08);
|
|
38
38
|
$form-switch-focus-color: tint-color($primary, 50%);
|
|
39
|
-
$dropdown-link-hover-bg: tint-color($primary, 50%);
|
|
40
|
-
$dropdown-link-active-bg: tint-color($primary, 70%);
|
|
41
39
|
|
|
42
40
|
$modal-backdrop-bg: #fff;
|
|
43
41
|
$modal-backdrop-opacity: .7;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/* Loading Spinner */
|
|
2
|
-
.itf-select .itf-
|
|
2
|
+
.itf-select .itf-select__loader {
|
|
3
3
|
opacity: 0;
|
|
4
4
|
transform: translateZ(0);
|
|
5
5
|
transition: opacity .1s;
|
|
@@ -11,6 +11,6 @@
|
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
/* Loading Spinner States */
|
|
14
|
-
.vs--loading .itf-
|
|
14
|
+
.vs--loading .itf-select__loader {
|
|
15
15
|
opacity: 1;
|
|
16
16
|
}
|
|
@@ -7,10 +7,11 @@
|
|
|
7
7
|
tabindex="0"
|
|
8
8
|
type="button"
|
|
9
9
|
:class="{
|
|
10
|
+
[`btn-${color}`]: color,
|
|
10
11
|
'labeled': labeled,
|
|
11
12
|
'disabled': disabled || loading,
|
|
12
13
|
'btn-primary': primary,
|
|
13
|
-
'btn-basic': !primary && !secondary,
|
|
14
|
+
'btn-basic': !primary && !secondary && !color,
|
|
14
15
|
'btn-secondary': secondary,
|
|
15
16
|
'btn-sm': small,
|
|
16
17
|
'btn-lg': large,
|
|
@@ -20,7 +21,7 @@
|
|
|
20
21
|
'btn-icon': icon,
|
|
21
22
|
'loading': loading
|
|
22
23
|
}">
|
|
23
|
-
<div class="itf-spinner" :class="{'
|
|
24
|
+
<div class="itf-spinner" :class="{'itf-spinner__white': primary}" v-if="loading"></div>
|
|
24
25
|
<div v-if="loading && loadingText">{{loadingText}}</div>
|
|
25
26
|
<slot v-else></slot>
|
|
26
27
|
</button>
|
|
@@ -31,11 +32,11 @@
|
|
|
31
32
|
@import '~bootstrap/scss/buttons';
|
|
32
33
|
|
|
33
34
|
.itf-button {
|
|
35
|
+
display: inline-flex;
|
|
36
|
+
align-items: center;
|
|
37
|
+
|
|
34
38
|
& > div {
|
|
35
|
-
display: flex;
|
|
36
39
|
pointer-events: none;
|
|
37
|
-
align-items: center;
|
|
38
|
-
text-align: left;
|
|
39
40
|
}
|
|
40
41
|
.itf-spinner {
|
|
41
42
|
mix-blend-mode: normal;
|
|
@@ -141,6 +142,7 @@ class itfButton extends Vue {
|
|
|
141
142
|
@Prop(Boolean) large;
|
|
142
143
|
@Prop(Boolean) icon;
|
|
143
144
|
@Prop(String) loadingText;
|
|
145
|
+
@Prop(String) color;
|
|
144
146
|
@Prop(Boolean) disabled;
|
|
145
147
|
}
|
|
146
148
|
</script>
|
|
@@ -74,6 +74,7 @@ class itfDatePickerInline extends Vue {
|
|
|
74
74
|
@Prop({ type: String, default: 'ISO' }) valueFormat;
|
|
75
75
|
@Prop({ type: String, default: ITFSettings.defaultDisplayDateFormat }) displayFormat;
|
|
76
76
|
@Prop({ type: String, default: 'days', validator: (value) => ['days', 'months', 'years'].includes(value) }) startView;
|
|
77
|
+
@Prop({ type: String, default: null, validator: (value) => ['days', 'months', 'years'].includes(value) }) minView;
|
|
77
78
|
@Prop({ type: Boolean, default: false }) onlyCalendar;
|
|
78
79
|
@Prop({ type: Boolean, default: false }) range;
|
|
79
80
|
@Prop({ type: Object, default: () => ({}) }) customDays;
|
|
@@ -108,7 +109,8 @@ class itfDatePickerInline extends Vue {
|
|
|
108
109
|
locale: localeEn,
|
|
109
110
|
firstDay: 1,
|
|
110
111
|
range: this.range,
|
|
111
|
-
view: this.valueAsLuxon ? 'days' : this.startView,
|
|
112
|
+
view: (this.valueAsLuxon && !this.minView) ? 'days' : this.startView,
|
|
113
|
+
minView: this.minView,
|
|
112
114
|
selectedDates: this.valueAsLuxon ? [this.valueAsLuxon.toJSDate()] : [],
|
|
113
115
|
onSelect: ({ date }) => {
|
|
114
116
|
if (this.range && !this.calendar.rangeDateTo) {
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="itf-monthpicker" :class="{'with-addon addon-start': prependIcon}">
|
|
3
|
+
<div class="addon" v-if="prependIcon">
|
|
4
|
+
<slot name="addon">
|
|
5
|
+
<itf-icon :name="prependIcon" />
|
|
6
|
+
</slot>
|
|
7
|
+
</div>
|
|
8
|
+
<input
|
|
9
|
+
ref="input"
|
|
10
|
+
readonly
|
|
11
|
+
class="form-control"
|
|
12
|
+
:class="{ 'is-invalid': isInvalid(), 'is-valid': isSuccess() }"
|
|
13
|
+
@focus="onFocus"
|
|
14
|
+
@blur="onBlur"
|
|
15
|
+
:value="displayValue"
|
|
16
|
+
:placeholder="placeholder"
|
|
17
|
+
/>
|
|
18
|
+
<div style="display: none">
|
|
19
|
+
<div ref="dropdown" class="itf-monthpicker__dropdown">
|
|
20
|
+
<itf-date-picker-inline
|
|
21
|
+
:value="value"
|
|
22
|
+
start-view="months"
|
|
23
|
+
min-view="months"
|
|
24
|
+
only-calendar
|
|
25
|
+
:display-format="displayFormat"
|
|
26
|
+
:value-format="valueFormat"
|
|
27
|
+
@input="selectInlineDate"
|
|
28
|
+
></itf-date-picker-inline>
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
</template>
|
|
33
|
+
<style lang="scss">
|
|
34
|
+
@import '../../assets/scss/variables';
|
|
35
|
+
@import '../../assets/scss/directives/tooltip';
|
|
36
|
+
@import '../../assets/scss/input-addon';
|
|
37
|
+
|
|
38
|
+
.itf-monthpicker {
|
|
39
|
+
&__dropdown {
|
|
40
|
+
width: max-content;
|
|
41
|
+
border: $input-border-width solid $input-border-color;
|
|
42
|
+
border-radius: $input-border-radius;
|
|
43
|
+
background-color: $body-bg;
|
|
44
|
+
overflow: hidden;
|
|
45
|
+
@if $enable-shadows {
|
|
46
|
+
@include box-shadow($input-box-shadow, $input-btn-focus-box-shadow);
|
|
47
|
+
} @else {
|
|
48
|
+
// Avoid using mixin so we can pass custom focus shadow properly
|
|
49
|
+
box-shadow: $input-btn-focus-box-shadow;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
@media (prefers-color-scheme: notdark) {
|
|
53
|
+
background-color: $dark-body-bg;
|
|
54
|
+
border-color: $dark-border-color;
|
|
55
|
+
box-shadow: 0 0 0 2px $dark-input-focus-border;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
</style>
|
|
60
|
+
<script>
|
|
61
|
+
import { Vue, Component, Prop, Inject } from 'vue-property-decorator';
|
|
62
|
+
import { DateTime } from 'luxon';
|
|
63
|
+
import tippy from 'tippy.js';
|
|
64
|
+
import itfIcon from '../icon/Icon';
|
|
65
|
+
import itfDatePickerInline from './DatePickerInline.vue';
|
|
66
|
+
|
|
67
|
+
export default @Component({
|
|
68
|
+
name: 'itfMonthPicker',
|
|
69
|
+
components: {
|
|
70
|
+
itfIcon,
|
|
71
|
+
itfDatePickerInline
|
|
72
|
+
},
|
|
73
|
+
})
|
|
74
|
+
class itfMonthPicker extends Vue {
|
|
75
|
+
@Inject({ default: null }) itemLabel;
|
|
76
|
+
@Inject({ default: null }) appendContext;
|
|
77
|
+
|
|
78
|
+
@Prop({ type: String }) value;
|
|
79
|
+
@Prop({ type: String, default: 'ISO' }) valueFormat;
|
|
80
|
+
@Prop({ type: String, default: 'MMMM yyyy' }) displayFormat;
|
|
81
|
+
@Prop({ type: String, default: '' }) placeholder;
|
|
82
|
+
@Prop({ type: String, default: '' }) prependIcon;
|
|
83
|
+
|
|
84
|
+
focused = false;
|
|
85
|
+
|
|
86
|
+
tooltip = null;
|
|
87
|
+
|
|
88
|
+
isInvalid() {
|
|
89
|
+
return this.itemLabel && this.itemLabel.isHasError();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
isSuccess() {
|
|
93
|
+
return this.itemLabel && this.itemLabel.isHasSuccess();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
mounted() {
|
|
97
|
+
// якщо в модалці, то контекст модалки, якщо ні, то аплікейшена
|
|
98
|
+
const context = (this.appendContext && this.appendContext()) || document.body;
|
|
99
|
+
this.tooltip = tippy(this.$refs.input, {
|
|
100
|
+
interactiveBorder: 30,
|
|
101
|
+
interactiveDebounce: 75,
|
|
102
|
+
animation: 'scale',
|
|
103
|
+
arrow: true,
|
|
104
|
+
content: this.$refs.dropdown,
|
|
105
|
+
allowHTML: true,
|
|
106
|
+
trigger: 'click',
|
|
107
|
+
interactive: true,
|
|
108
|
+
placement: 'bottom-start',
|
|
109
|
+
appendTo: context
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
get valueAsLuxon() {
|
|
114
|
+
if (!this.value) {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
if (this.valueFormat === 'ISO') {
|
|
118
|
+
return DateTime.fromISO(this.value);
|
|
119
|
+
}
|
|
120
|
+
return DateTime.fromFormat(this.value, this.valueFormat);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
get displayValue() {
|
|
124
|
+
if (!this.valueAsLuxon) {
|
|
125
|
+
return '';
|
|
126
|
+
}
|
|
127
|
+
return this.valueAsLuxon.toFormat(this.displayFormat);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
updateValue(value, emitEmpty = false) {
|
|
131
|
+
const val = value && DateTime.fromFormat(value, this.displayFormat);
|
|
132
|
+
if (!val || !val.isValid) {
|
|
133
|
+
if (emitEmpty) {
|
|
134
|
+
this.$emit('input', null);
|
|
135
|
+
}
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
this.$emit('input', this.valueFormat === 'ISO' ? val.toISO() : val.toFormat(this.valueFormat));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
format(date) {
|
|
142
|
+
return DateTime.fromJSDate(date).toFormat(this.displayFormat);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
parse(str) {
|
|
146
|
+
return DateTime.fromFormat(str, this.displayFormat).toJSDate();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
onFocus() {
|
|
150
|
+
this.focused = true;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
onBlur(e) {
|
|
154
|
+
this.focused = false;
|
|
155
|
+
this.updateValue(e.target.value, true);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
selectInlineDate(date) {
|
|
159
|
+
this.$emit('input', date);
|
|
160
|
+
this.tooltip.hide();
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
</script>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { storiesOf } from '@storybook/vue';
|
|
2
2
|
import itfDatePicker from './DatePicker.vue';
|
|
3
|
+
import itfMonthPicker from './MonthPicker.vue';
|
|
3
4
|
import itfDatePickerInline from './DatePickerInline.vue';
|
|
4
5
|
import itfDateRangePicker from './DateRangePicker.vue';
|
|
5
6
|
import itfDateRangePickerInline from './DateRangePickerInline.vue';
|
|
@@ -10,6 +11,7 @@ storiesOf('Common', module)
|
|
|
10
11
|
components: {
|
|
11
12
|
itfApp,
|
|
12
13
|
itfDatePicker,
|
|
14
|
+
itfMonthPicker,
|
|
13
15
|
itfDateRangePicker,
|
|
14
16
|
itfDatePickerInline,
|
|
15
17
|
itfDateRangePickerInline
|
|
@@ -67,6 +69,9 @@ storiesOf('Common', module)
|
|
|
67
69
|
|
|
68
70
|
<itf-date-range-picker-inline v-model="dateRange"></itf-date-range-picker-inline>
|
|
69
71
|
|
|
72
|
+
<h2>Month</h2>
|
|
73
|
+
|
|
74
|
+
<itf-month-picker :value="date" v-model.lazy="date"></itf-month-picker>
|
|
70
75
|
</itf-app>
|
|
71
76
|
</div>`,
|
|
72
77
|
}));
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
}
|
|
37
37
|
</style>
|
|
38
38
|
<script>
|
|
39
|
-
import { Vue, Component, Prop, Watch, PropSync,
|
|
39
|
+
import { Vue, Component, Prop, Watch, PropSync, Inject } from 'vue-property-decorator';
|
|
40
40
|
import itfButton from '../button/Button';
|
|
41
41
|
|
|
42
42
|
let globalModalIndex = 0; // base modal z-index
|
|
@@ -52,6 +52,8 @@ export default @Component({
|
|
|
52
52
|
}
|
|
53
53
|
})
|
|
54
54
|
class itfModal extends Vue {
|
|
55
|
+
@Inject({ default: null }) globalApp;
|
|
56
|
+
|
|
55
57
|
@PropSync('visible') value;
|
|
56
58
|
@Prop({ type: String }) title;
|
|
57
59
|
@Prop({ type: String, default: '', validator: (value) => ['', 'sm', 'lg', 'xl'].includes(value) }) size;
|
|
@@ -105,7 +107,9 @@ class itfModal extends Vue {
|
|
|
105
107
|
}
|
|
106
108
|
if (this.appendToBody && this.$el instanceof Node && this.$el.parentNode) {
|
|
107
109
|
this.$el.parentNode.removeChild(this.$el);
|
|
108
|
-
|
|
110
|
+
const elContext = (this.globalApp && this.globalApp.$el) || document.body;
|
|
111
|
+
console.info(elContext);
|
|
112
|
+
elContext.appendChild(this.$el);
|
|
109
113
|
}
|
|
110
114
|
const { default: Modal } = await import('bootstrap/js/src/modal.js');
|
|
111
115
|
this.modalEl = new Modal(this.$el);
|
|
@@ -60,7 +60,7 @@ class itfPopover extends Vue {
|
|
|
60
60
|
@PropSync('visible') value;
|
|
61
61
|
@Prop({ type: String, default: 'bottom', validator: (value) => ['bottom', 'left', 'right', 'top'].includes(value) }) placement;
|
|
62
62
|
@Prop({ type: String, default: 'click', validator: (value) => ['click', 'focus', 'hover', 'manual'].includes(value) }) trigger;
|
|
63
|
-
@Prop({ type: String }) customClass;
|
|
63
|
+
@Prop({ type: String, default: '' }) customClass;
|
|
64
64
|
|
|
65
65
|
modalId = '';
|
|
66
66
|
modalEl = null;
|
|
@@ -44,14 +44,13 @@
|
|
|
44
44
|
</div>
|
|
45
45
|
</div>
|
|
46
46
|
</div>
|
|
47
|
-
<!-- <v-progress-linear v-if="loading" :height="3" class="com-table-loading" indeterminate />-->
|
|
48
47
|
</div>
|
|
49
48
|
<div v-show="rows && rows.length" class="itf-table__rows" ref="scrollContainer2">
|
|
50
49
|
<div class="itf-table__row"
|
|
51
50
|
:class="{'itf-table__summary-row': row.summary}"
|
|
52
51
|
v-for="(row, r) in rows"
|
|
53
52
|
:key="r"
|
|
54
|
-
@click="$emit('row-click', { row, index: r })">
|
|
53
|
+
@click="!row.summary && $emit('row-click', { row, index: r })">
|
|
55
54
|
<div
|
|
56
55
|
class="itf-table__cell"
|
|
57
56
|
:class="{'itf-table__cell--sorted': sortedColumns[k]}"
|
|
@@ -137,7 +136,10 @@ html {
|
|
|
137
136
|
|
|
138
137
|
& > .itf-table__row {
|
|
139
138
|
display: contents;
|
|
140
|
-
|
|
139
|
+
|
|
140
|
+
&:not(.itf-table__summary-row) {
|
|
141
|
+
cursor: var(--itf-table-row-cursor);
|
|
142
|
+
}
|
|
141
143
|
}
|
|
142
144
|
}
|
|
143
145
|
|
|
@@ -145,9 +147,13 @@ html {
|
|
|
145
147
|
background-color: var(--itf-table-strip-color);
|
|
146
148
|
}
|
|
147
149
|
|
|
150
|
+
&__cell.itf-table__cell--sorted {
|
|
151
|
+
background-color: var(--itf-table-sorted-color);
|
|
152
|
+
}
|
|
148
153
|
&--hoverable &__rows{
|
|
149
|
-
& > .itf-table__row {
|
|
150
|
-
&:hover >
|
|
154
|
+
& > .itf-table__row:not(.itf-table__summary-row) {
|
|
155
|
+
&:hover > .itf-table__cell,
|
|
156
|
+
&:hover > .itf-table__cell--sorted {
|
|
151
157
|
background-color: var(--itf-table-hover-color);
|
|
152
158
|
}
|
|
153
159
|
}
|
|
@@ -171,9 +177,6 @@ html {
|
|
|
171
177
|
}
|
|
172
178
|
}
|
|
173
179
|
|
|
174
|
-
&__cell.itf-table__cell--sorted {
|
|
175
|
-
background-color: var(--itf-table-sorted-color) !important;
|
|
176
|
-
}
|
|
177
180
|
&__header {
|
|
178
181
|
background-color: var(--itf-table-background-color);
|
|
179
182
|
grid-row: header;
|
|
@@ -422,7 +425,7 @@ class itfTable extends Vue {
|
|
|
422
425
|
function initSyncScroll(el1, el2, stuck, unstuck) {
|
|
423
426
|
function isScrolledEnd(el) {
|
|
424
427
|
return el.scrollWidth === el.getBoundingClientRect().width // якщо контейнер менший ширини екрану
|
|
425
|
-
|
|
428
|
+
|| el.scrollLeft === el.scrollWidth - el.getBoundingClientRect().width; // якщо контейнер більший ширини екрану
|
|
426
429
|
}
|
|
427
430
|
|
|
428
431
|
function func1() {
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
<div class="itf-text-field input-group with-addon addon-start">
|
|
4
4
|
<div class="addon" v-if="prependIcon">
|
|
5
5
|
<slot name="addon">
|
|
6
|
-
<itf-icon :name="prependIcon"
|
|
6
|
+
<itf-icon :name="prependIcon"/>
|
|
7
7
|
</slot>
|
|
8
8
|
</div>
|
|
9
9
|
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
class="itf-text-field__input form-control"
|
|
14
14
|
type="text"
|
|
15
15
|
:value="value"
|
|
16
|
-
@input="
|
|
16
|
+
@input="onInput($event.target.value)"
|
|
17
17
|
/>
|
|
18
18
|
|
|
19
19
|
</div>
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
<script>
|
|
34
34
|
import { Vue, Component, Model, Inject, Prop } from 'vue-property-decorator';
|
|
35
35
|
import itfIcon from '../icon/Icon';
|
|
36
|
+
import { debounce } from '../../helpers/debounce';
|
|
36
37
|
|
|
37
38
|
export default @Component({
|
|
38
39
|
name: 'itfTextField',
|
|
@@ -45,6 +46,13 @@ class itfTextField extends Vue {
|
|
|
45
46
|
@Model('input') value;
|
|
46
47
|
@Prop(String) prependIcon;
|
|
47
48
|
@Prop(String) placeholder;
|
|
49
|
+
@Prop({ type: [Number, String], default: 0 }) delayInput;
|
|
50
|
+
|
|
51
|
+
onInput = null;
|
|
52
|
+
|
|
53
|
+
created() {
|
|
54
|
+
this.onInput = debounce((val) => this.$emit('input', val), Number(this.delayInput));
|
|
55
|
+
}
|
|
48
56
|
|
|
49
57
|
isInvalid() {
|
|
50
58
|
return this.itemLabel && this.itemLabel.isHasError();
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
const namespace = 'itf-sticky';
|
|
2
|
+
const events = [
|
|
3
|
+
'resize',
|
|
4
|
+
'scroll',
|
|
5
|
+
'touchstart',
|
|
6
|
+
'touchmove',
|
|
7
|
+
'touchend',
|
|
8
|
+
'pageshow',
|
|
9
|
+
'load',
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
const batchStyle = (el, style = {}, className = {}) => {
|
|
13
|
+
for (let k in style) {
|
|
14
|
+
el.style[k] = style[k];
|
|
15
|
+
}
|
|
16
|
+
for (let k in className) {
|
|
17
|
+
if (className[k] && !el.classList.contains(k)) {
|
|
18
|
+
el.classList.add(k);
|
|
19
|
+
} else if (!className[k] && el.classList.contains(k)) {
|
|
20
|
+
el.classList.remove(k);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
class Sticky {
|
|
26
|
+
constructor(el, vm) {
|
|
27
|
+
this.el = el;
|
|
28
|
+
this.vm = vm;
|
|
29
|
+
this.unsubscribers = [];
|
|
30
|
+
this.isPending = false;
|
|
31
|
+
this.state = {
|
|
32
|
+
isTopSticky: null,
|
|
33
|
+
isBottomSticky: null,
|
|
34
|
+
height: null,
|
|
35
|
+
width: null,
|
|
36
|
+
xOffset: null,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
this.lastState = {
|
|
40
|
+
top: null,
|
|
41
|
+
bottom: null,
|
|
42
|
+
sticked: false,
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const offset = this.getAttribute('sticky-offset') || {};
|
|
46
|
+
const side = this.getAttribute('sticky-side') || 'top';
|
|
47
|
+
const zIndex = this.getAttribute('sticky-z-index') || '10';
|
|
48
|
+
const onStick = this.getAttribute('on-stick') || null;
|
|
49
|
+
|
|
50
|
+
this.options = {
|
|
51
|
+
topOffset: Number(offset.top) || 0,
|
|
52
|
+
bottomOffset: Number(offset.bottom) || 0,
|
|
53
|
+
shouldTopSticky: side === 'top' || side === 'both',
|
|
54
|
+
shouldBottomSticky: side === 'bottom' || side === 'both',
|
|
55
|
+
zIndex: zIndex,
|
|
56
|
+
onStick: onStick,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
doBind() {
|
|
61
|
+
if (this.unsubscribers.length > 0) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const { el, vm } = this;
|
|
65
|
+
vm.$nextTick(() => {
|
|
66
|
+
this.placeholderEl = document.createElement('div');
|
|
67
|
+
this.containerEl = this.getContainerEl();
|
|
68
|
+
el.parentNode.insertBefore(this.placeholderEl, el);
|
|
69
|
+
events.forEach(event => {
|
|
70
|
+
const fn = this.update.bind(this);
|
|
71
|
+
this.unsubscribers.push(() => window.removeEventListener(event, fn));
|
|
72
|
+
this.unsubscribers.push(() =>
|
|
73
|
+
this.containerEl.removeEventListener(event, fn),
|
|
74
|
+
);
|
|
75
|
+
window.addEventListener(event, fn, { passive: true });
|
|
76
|
+
this.containerEl.addEventListener(event, fn, { passive: true });
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
doUnbind() {
|
|
82
|
+
this.unsubscribers.forEach(fn => fn());
|
|
83
|
+
this.unsubscribers = [];
|
|
84
|
+
this.resetElement();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
update() {
|
|
88
|
+
if (!this.isPending) {
|
|
89
|
+
requestAnimationFrame(() => {
|
|
90
|
+
this.isPending = false;
|
|
91
|
+
this.recomputeState();
|
|
92
|
+
this.updateElements();
|
|
93
|
+
});
|
|
94
|
+
this.isPending = true;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
isTopSticky() {
|
|
99
|
+
if (!this.options.shouldTopSticky) return false;
|
|
100
|
+
const fromTop = this.state.placeholderElRect.top;
|
|
101
|
+
const fromBottom = this.state.containerElRect.bottom;
|
|
102
|
+
|
|
103
|
+
const topBreakpoint = this.options.topOffset;
|
|
104
|
+
const bottomBreakpoint = this.options.bottomOffset;
|
|
105
|
+
|
|
106
|
+
return fromTop <= topBreakpoint && fromBottom >= bottomBreakpoint;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
isBottomSticky() {
|
|
110
|
+
if (!this.options.shouldBottomSticky) return false;
|
|
111
|
+
const fromBottom =
|
|
112
|
+
window.innerHeight - this.state.placeholderElRect.top - this.state.height;
|
|
113
|
+
const fromTop = window.innerHeight - this.state.containerElRect.top;
|
|
114
|
+
|
|
115
|
+
const topBreakpoint = this.options.topOffset;
|
|
116
|
+
const bottomBreakpoint = this.options.bottomOffset;
|
|
117
|
+
|
|
118
|
+
return fromBottom <= bottomBreakpoint && fromTop >= topBreakpoint;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
recomputeState() {
|
|
122
|
+
this.state = Object.assign({}, this.state, {
|
|
123
|
+
height: this.getHeight(),
|
|
124
|
+
width: this.getWidth(),
|
|
125
|
+
xOffset: this.getXOffset(),
|
|
126
|
+
placeholderElRect: this.getPlaceholderElRect(),
|
|
127
|
+
containerElRect: this.getContainerElRect(),
|
|
128
|
+
});
|
|
129
|
+
this.state.isTopSticky = this.isTopSticky();
|
|
130
|
+
this.state.isBottomSticky = this.isBottomSticky();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
fireEvents() {
|
|
134
|
+
if (
|
|
135
|
+
typeof this.options.onStick === 'function' &&
|
|
136
|
+
(this.lastState.top !== this.state.isTopSticky ||
|
|
137
|
+
this.lastState.bottom !== this.state.isBottomSticky ||
|
|
138
|
+
this.lastState.sticked !==
|
|
139
|
+
(this.state.isTopSticky || this.state.isBottomSticky))
|
|
140
|
+
) {
|
|
141
|
+
this.lastState = {
|
|
142
|
+
top: this.state.isTopSticky,
|
|
143
|
+
bottom: this.state.isBottomSticky,
|
|
144
|
+
sticked: this.state.isBottomSticky || this.state.isTopSticky,
|
|
145
|
+
};
|
|
146
|
+
this.options.onStick(this.lastState);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
updateElements() {
|
|
151
|
+
const placeholderStyle = { paddingTop: 0 };
|
|
152
|
+
const elStyle = {
|
|
153
|
+
position: 'static',
|
|
154
|
+
top: 'auto',
|
|
155
|
+
bottom: 'auto',
|
|
156
|
+
left: 'auto',
|
|
157
|
+
width: 'auto',
|
|
158
|
+
zIndex: this.options.zIndex,
|
|
159
|
+
};
|
|
160
|
+
const placeholderClassName = { 'vue-sticky-placeholder': true };
|
|
161
|
+
const elClassName = {
|
|
162
|
+
'vue-sticky-el': true,
|
|
163
|
+
'top-sticky': false,
|
|
164
|
+
'bottom-sticky': false,
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
if (this.state.isTopSticky) {
|
|
168
|
+
elStyle.position = 'fixed';
|
|
169
|
+
elStyle.top = this.options.topOffset + 'px';
|
|
170
|
+
elStyle.left = this.state.xOffset + 'px';
|
|
171
|
+
elStyle.width = this.state.width + 'px';
|
|
172
|
+
const bottomLimit =
|
|
173
|
+
this.state.containerElRect.bottom -
|
|
174
|
+
this.state.height -
|
|
175
|
+
this.options.bottomOffset -
|
|
176
|
+
this.options.topOffset;
|
|
177
|
+
if (bottomLimit < 0) {
|
|
178
|
+
elStyle.top = bottomLimit + this.options.topOffset + 'px';
|
|
179
|
+
}
|
|
180
|
+
placeholderStyle.paddingTop = this.state.height + 'px';
|
|
181
|
+
elClassName['top-sticky'] = true;
|
|
182
|
+
} else if (this.state.isBottomSticky) {
|
|
183
|
+
elStyle.position = 'fixed';
|
|
184
|
+
elStyle.bottom = this.options.bottomOffset + 'px';
|
|
185
|
+
elStyle.left = this.state.xOffset + 'px';
|
|
186
|
+
elStyle.width = this.state.width + 'px';
|
|
187
|
+
const topLimit =
|
|
188
|
+
window.innerHeight -
|
|
189
|
+
this.state.containerElRect.top -
|
|
190
|
+
this.state.height -
|
|
191
|
+
this.options.bottomOffset -
|
|
192
|
+
this.options.topOffset;
|
|
193
|
+
if (topLimit < 0) {
|
|
194
|
+
elStyle.bottom = topLimit + this.options.bottomOffset + 'px';
|
|
195
|
+
}
|
|
196
|
+
placeholderStyle.paddingTop = this.state.height + 'px';
|
|
197
|
+
elClassName['bottom-sticky'] = true;
|
|
198
|
+
} else {
|
|
199
|
+
placeholderStyle.paddingTop = 0;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
batchStyle(this.el, elStyle, elClassName);
|
|
203
|
+
batchStyle(this.placeholderEl, placeholderStyle, placeholderClassName);
|
|
204
|
+
|
|
205
|
+
this.fireEvents();
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
resetElement() {
|
|
209
|
+
['position', 'top', 'bottom', 'left', 'width', 'zIndex'].forEach(attr => {
|
|
210
|
+
this.el.style.removeProperty(attr);
|
|
211
|
+
});
|
|
212
|
+
this.el.classList.remove('bottom-sticky', 'top-sticky');
|
|
213
|
+
const { parentNode } = this.placeholderEl;
|
|
214
|
+
if (parentNode) {
|
|
215
|
+
parentNode.removeChild(this.placeholderEl);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
getContainerEl() {
|
|
220
|
+
let node = this.el.parentNode;
|
|
221
|
+
while (
|
|
222
|
+
node &&
|
|
223
|
+
node.tagName !== 'HTML' &&
|
|
224
|
+
node.tagName !== 'BODY' &&
|
|
225
|
+
node.nodeType === 1
|
|
226
|
+
) {
|
|
227
|
+
if (node.hasAttribute('sticky-container')) {
|
|
228
|
+
return node;
|
|
229
|
+
}
|
|
230
|
+
node = node.parentNode;
|
|
231
|
+
}
|
|
232
|
+
return this.el.parentNode;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
getXOffset() {
|
|
236
|
+
return this.placeholderEl.getBoundingClientRect().left;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
getWidth() {
|
|
240
|
+
return this.placeholderEl.getBoundingClientRect().width;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
getHeight() {
|
|
244
|
+
return this.el.getBoundingClientRect().height;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
getPlaceholderElRect() {
|
|
248
|
+
return this.placeholderEl.getBoundingClientRect();
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
getContainerElRect() {
|
|
252
|
+
return this.containerEl.getBoundingClientRect();
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
getAttribute(name) {
|
|
256
|
+
const expr = this.el.getAttribute(name);
|
|
257
|
+
let result = undefined;
|
|
258
|
+
if (expr) {
|
|
259
|
+
if (this.vm[expr]) {
|
|
260
|
+
result = this.vm[expr];
|
|
261
|
+
} else {
|
|
262
|
+
try {
|
|
263
|
+
result = eval(`(${expr})`);
|
|
264
|
+
} catch (error) {
|
|
265
|
+
result = expr;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return result;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export default {
|
|
274
|
+
inserted(el, bind, vnode) {
|
|
275
|
+
if (typeof bind.value === 'undefined' || bind.value) {
|
|
276
|
+
el[namespace] = new Sticky(el, vnode.context);
|
|
277
|
+
el[namespace].doBind();
|
|
278
|
+
}
|
|
279
|
+
},
|
|
280
|
+
unbind(el, bind, vnode) {
|
|
281
|
+
if (el[namespace]) {
|
|
282
|
+
el[namespace].doUnbind();
|
|
283
|
+
el[namespace] = undefined;
|
|
284
|
+
}
|
|
285
|
+
},
|
|
286
|
+
componentUpdated(el, bind, vnode) {
|
|
287
|
+
if (typeof bind.value === 'undefined' || bind.value) {
|
|
288
|
+
if (!el[namespace]) {
|
|
289
|
+
el[namespace] = new Sticky(el, vnode.context);
|
|
290
|
+
}
|
|
291
|
+
el[namespace].doBind();
|
|
292
|
+
} else {
|
|
293
|
+
if (el[namespace]) {
|
|
294
|
+
el[namespace].doUnbind();
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
},
|
|
298
|
+
};
|