@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@itfin/components",
3
- "version": "1.0.21",
3
+ "version": "1.0.22",
4
4
  "main": "dist/itfin-components.umd.js",
5
5
  "unpkg": "dist/itfin-components.common.js",
6
6
  "author": "Vitalii Savchuk <esvit666@gmail.com>",
@@ -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-spinner {
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-spinner {
14
+ .vs--loading .itf-select__loader {
15
15
  opacity: 1;
16
16
  }
@@ -21,7 +21,6 @@ $disabled-cursor: $vs-state-disabled-cursor;
21
21
  .vs__open-indicator {
22
22
  cursor: $disabled-cursor;
23
23
  background-color: transparent;
24
- border-color: transparent;
25
24
  }
26
25
  }
27
26
 
@@ -23,7 +23,7 @@
23
23
  animation: spinner 0.7s linear infinite;
24
24
  mix-blend-mode: difference;
25
25
 
26
- &.white {
26
+ &.itf-spinner__white {
27
27
  border-color: rgba(158, 158, 158, 0.29);
28
28
  border-top-color: rgb(155, 155, 155);
29
29
  }
@@ -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="{'white': primary}" v-if="loading"></div>
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, Provide } from 'vue-property-decorator';
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
- document.body.appendChild(this.$el);
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;
@@ -82,7 +82,7 @@
82
82
  </slot>
83
83
 
84
84
  <slot name="spinner" v-bind="scope.spinner">
85
- <div v-show="mutableLoading" class="itf-spinner">Loading...</div>
85
+ <div v-show="mutableLoading" class="itf-spinner itf-select__loader">Loading...</div>
86
86
  </slot>
87
87
  </div>
88
88
  </div>
@@ -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
- cursor: var(--itf-table-row-cursor);
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 > div{
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
- || el.scrollLeft === el.scrollWidth - el.getBoundingClientRect().width; // якщо контейнер більший ширини екрану
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="$emit('input', $event.target.value)"
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
+ };