vgapp 0.5.9 → 0.6.0

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/CHANGELOG.md CHANGED
@@ -1,3 +1,6 @@
1
+ # VEGAS-APP 0.6.0 (Август, 20, 2025)
2
+ * Исправлены ошибки в разных модулях
3
+
1
4
  # VEGAS-APP 0.5.6 - 0.5.9 (Август, 08, 2025)
2
5
  * Разделение JS и CSS
3
6
  * Исправлены ошибки в разных модулях
@@ -96,6 +96,16 @@ class BaseModule {
96
96
  new Animation(element, key, params);
97
97
  }
98
98
 
99
+ isMobileDevice() {
100
+ const userAgent = navigator.userAgent;
101
+ const isMobileUA = /Android|iPhone|iPad|iPod/i.test(userAgent);
102
+ const isTouchDevice = "ontouchstart" in window || navigator.maxTouchPoints > 0;
103
+ const isSmallScreen = window.innerWidth < 768;
104
+ const isHighDPI = window.devicePixelRatio >= 2;
105
+
106
+ return isMobileUA || (isTouchDevice && isSmallScreen && isHighDPI);
107
+ }
108
+
99
109
  static getInstance(element) {
100
110
  return Data.get(Selectors.find(element), this.NAME_KEY)
101
111
  }
@@ -209,8 +209,9 @@ class VGDropdown extends BaseModule {
209
209
  static init(element, params = {}) {
210
210
  const instance = VGDropdown.getOrCreateInstance(element, params);
211
211
 
212
- if (instance._params.hover) {
212
+ if (instance._params.hover && !instance.isMobileDevice()) {
213
213
  let currentElem = null;
214
+
214
215
  EventHandler.on(instance._parent, EVENT_MOUSEOVER_DATA_API, function (event) {
215
216
  if (currentElem) return;
216
217
  VGDropdown.hideOpenToggles(event);
@@ -236,16 +237,16 @@ class VGDropdown extends BaseModule {
236
237
  currentElem = null;
237
238
  instance._completeHide({relatedTarget: instance._element});
238
239
  })
239
- } else {
240
- EventHandler.on(document, EVENT_KEYUP_DATA_API, SELECTOR_DATA_TOGGLE, VGDropdown.keydownHandler);
241
- EventHandler.on(document, EVENT_KEYDOWN_DATA_API, '.' + TARGET_CONTAINER, VGDropdown.keydownHandler);
242
- EventHandler.on(document, EVENT_KEYUP_DATA_API, VGDropdown.clearDrops);
243
- EventHandler.on(document, EVENT_CLICK_DATA_API, VGDropdown.clearDrops);
244
- EventHandler.on(element, EVENT_CLICK_DATA_API, function (event) {
245
- event.preventDefault();
246
- instance.toggle();
247
- });
248
240
  }
241
+
242
+ EventHandler.on(document, EVENT_KEYUP_DATA_API, SELECTOR_DATA_TOGGLE, VGDropdown.keydownHandler);
243
+ EventHandler.on(document, EVENT_KEYDOWN_DATA_API, '.' + TARGET_CONTAINER, VGDropdown.keydownHandler);
244
+ EventHandler.on(document, EVENT_KEYUP_DATA_API, VGDropdown.clearDrops);
245
+ EventHandler.on(document, EVENT_CLICK_DATA_API, VGDropdown.clearDrops);
246
+ EventHandler.on(element, EVENT_CLICK_DATA_API, function (event) {
247
+ event.preventDefault();
248
+ instance.toggle();
249
+ });
249
250
  }
250
251
 
251
252
  static hideOpenToggles(event) {
@@ -0,0 +1,510 @@
1
+ import BaseModule from "../../base-module";
2
+ import Selectors from "../../../utils/js/dom/selectors";
3
+ import Responsive from "../../../utils/js/components/responsive";
4
+ import {getSVG} from "../../module-fn";
5
+ import {execute, isDisabled, isVisible, mergeDeepObject, noop, normalizeData} from "../../../utils/js/functions";
6
+ import EventHandler from "../../../utils/js/dom/event";
7
+ import {Manipulator} from "../../../utils/js/dom/manipulator";
8
+
9
+ /**
10
+ * Constants
11
+ */
12
+ const NAME = 'nav';
13
+ const NAME_KEY = 'vg.nav';
14
+
15
+ /**
16
+ * Constants Classes
17
+ */
18
+ const CLASS_NAME_SHOW = 'show';
19
+ const CLASS_NAME_FADE = 'fade';
20
+ const CLASS_NAME_ACTIVE = 'active';
21
+ const SELECTOR_DATA_TOGGLE = '.vg-nav a';
22
+
23
+ /**
24
+ * Constants Events
25
+ */
26
+ const EVENT_KEY_HIDE = `${NAME_KEY}.hide`;
27
+ const EVENT_KEY_HIDDEN = `${NAME_KEY}.hidden`;
28
+ const EVENT_KEY_SHOW = `${NAME_KEY}.show`;
29
+ const EVENT_KEY_SHOWN = `${NAME_KEY}.shown`;
30
+
31
+ const EVENT_MOUSEOVER_DATA_API = `mouseover.${NAME_KEY}.data.api`;
32
+ const EVENT_MOUSEOUT_DATA_API = `mouseout.${NAME_KEY}.data.api`;
33
+ const EVENT_CLICK_DATA_API = `click.${NAME_KEY}.data.api`;
34
+ const EVENT_KEYUP_DATA_API = `keyup.${NAME_KEY}.data.api`;
35
+ const EVENT_RESIZE_DATA_API = `resize.${NAME_KEY}.data.api`;
36
+
37
+ class VGNav extends BaseModule {
38
+ constructor(element, params = {}) {
39
+ super(element);
40
+
41
+ this._params = this._getParams(element, mergeDeepObject({
42
+ breakpoint: false,
43
+ placement: 'horizontal',
44
+ classes: {
45
+ hamburgerActive: 'vg-nav-hamburger-active',
46
+ hamburgerAlways: 'vg-nav-hamburger-always',
47
+ hamburger: 'vg-nav-hamburger',
48
+ container: 'vg-nav-container',
49
+ wrapper: 'vg-nav-wrapper',
50
+ active: 'vg-nav-active',
51
+ expand: 'vg-nav-expand',
52
+ cloned: 'vg-nav-cloned',
53
+ hover: 'vg-nav-hover',
54
+ flip: 'vg-nav-flip',
55
+ XXXL: 'vg-nav-xxxl',
56
+ XXL: 'vg-nav-xxl',
57
+ XL: 'vg-nav-xl',
58
+ LG: 'vg-nav-lg',
59
+ MD: 'vg-nav-md',
60
+ SM: 'vg-nav-sm',
61
+ XS: 'vg-nav-xs'
62
+ },
63
+ expand: true,
64
+ hover: false,
65
+ position: true,
66
+ collapse: true,
67
+ toggle: '<span class="default"></span>',
68
+ hamburger: {
69
+ enable: true,
70
+ always: false,
71
+ title: '',
72
+ body: null
73
+ },
74
+ callback: noop,
75
+ animation: true,
76
+ timeoutAnimation: 300,
77
+ ajax: {
78
+ route: '',
79
+ target: '',
80
+ method: 'get',
81
+ loader: false,
82
+ once: false,
83
+ output: true,
84
+ }
85
+ }, params));
86
+
87
+ this._navigation = null;
88
+ this.navigation = '.' + this._params.classes.wrapper;
89
+
90
+ this.movedLinks = [];
91
+ this.$links = Selectors.findAll('.' + this._params.classes.wrapper + ' > li', this.navigation)
92
+
93
+ if (this._params.animation === false) {
94
+ this._params.timeoutAnimation = 10
95
+ }
96
+ }
97
+
98
+ static get NAME() {
99
+ return NAME;
100
+ }
101
+
102
+ static get NAME_KEY() {
103
+ return NAME_KEY;
104
+ }
105
+
106
+ get navigation() {
107
+ return this._navigation;
108
+ }
109
+
110
+ set navigation(el) {
111
+ let elm = Selectors.find(el, this._element);
112
+ if (!elm) return;
113
+ this._navigation = elm;
114
+ }
115
+
116
+ build() {
117
+ if (!this.navigation) return;
118
+
119
+ let params = this._params;
120
+
121
+ // Вешаем основные классы
122
+ this._element.classList.add(params.classes.container);
123
+ this._element.classList.add('vg-nav-' + params.placement);
124
+
125
+ // Если нужно оставить список меню или установить медиа точку
126
+ if (!params.breakpoint) {
127
+ params.expand = false;
128
+ }
129
+
130
+ if (!params.hamburger.always) {
131
+ if (!params.breakpoint || !params.expand) {
132
+ this._element.classList.add(params.classes.expand);
133
+ } else if (params.breakpoint !== false) {
134
+ this._element.classList.add('vg-nav-' + params.breakpoint);
135
+ }
136
+ } else {
137
+ this._element.classList.add(params.classes.hamburgerAlways);
138
+ }
139
+
140
+ // Меню срабатывает при наведении, если это не мобильное устройство
141
+ if (params.hover) {
142
+ this._element.classList.add(params.classes.hover);
143
+
144
+ if (Responsive.checkMobileOrTablet()) {
145
+ this._element.classList.remove(params.classes.hover);
146
+ }
147
+ }
148
+
149
+ // Устанавливаем гамбургер, если его нет в разметке
150
+ if (params.expand && !params.hamburger.body && params.hamburger.enable) {
151
+ let isHamburger = Selectors.find('.' + params.classes.hamburger, this._element);
152
+
153
+ if (isHamburger === null) {
154
+ let mTitle = '',
155
+ hamburger = '<span class="' + params.classes.hamburger + '--lines"><span></span><span></span><span></span></span>';
156
+
157
+ if (params.hamburger.title) {
158
+ mTitle = '<span class="' + params.classes.hamburger + '--title">'+ params.hamburger.title +'</span>';
159
+ }
160
+
161
+ if (params.hamburger.body !== null) {
162
+ hamburger = params.hamburger.body;
163
+ }
164
+
165
+ this._element.insertAdjacentHTML('afterbegin','<a href="#sidebar-nav" class="' + params.classes.hamburger + '" data-vg-toggle="sidebar">' + mTitle + hamburger +'</a>');
166
+ }
167
+ }
168
+
169
+ // Устанавливаем указатель переключателя
170
+ if (params.toggle) {
171
+ let $dropdown_a = [...Selectors.findAll('.dropdown-mega > a, .dropdown > a', this._element)],
172
+ toggle = '<span class="toggle">' + params.toggle + '</span>';
173
+
174
+ if ($dropdown_a.length) {
175
+ $dropdown_a.forEach(function (elem) {
176
+ if (!elem.querySelector('.toggle') && !elem.closest('.dots')) {
177
+ elem.setAttribute('aria-expanded', 'false')
178
+ elem.insertAdjacentHTML('beforeend', toggle)
179
+ }
180
+ });
181
+ }
182
+ }
183
+
184
+ if (params.collapse && Responsive.check(this) && params.placement !== 'vertical') {
185
+ setCollapse(this);
186
+ }
187
+
188
+ if ('afterInit' in this._params.callback) {
189
+ execute(this._params.callback.afterInit, [this]);
190
+ }
191
+
192
+ /**
193
+ * Функция сворачивания
194
+ * TODO Придумать что то с мега меню, которое уходит в подменю
195
+ * TODO Так же есть косяки при ресайзе
196
+ */
197
+ function setCollapse(_this) {
198
+ let width_navigation_responsive = _this.navigation.clientWidth,
199
+ width_all_links_responsive = 0,
200
+ $dots = Selectors.find('.dots', _this.navigation),
201
+ _dots = getSVG('dots');
202
+
203
+ if (_this.$links.length) {
204
+ if ($dots) {
205
+ width_all_links_responsive = $dots.clientWidth
206
+ } else {
207
+ let $a = Selectors.find('a', _this.$links[0]),
208
+ $linkStyle = getComputedStyle($a),
209
+ paddingLeft = normalizeData($linkStyle.paddingLeft.slice(0, -2)),
210
+ paddingRight = normalizeData($linkStyle.paddingRight.slice(0, -2)),
211
+ padding = paddingLeft + paddingRight;
212
+
213
+ // TODO не совсем верно, но мы точно знаем ширину точек в svg - 16px
214
+ width_all_links_responsive = padding + 16;
215
+ }
216
+
217
+ for (let $link of _this.$links) {
218
+ let width = $link.getBoundingClientRect().width;
219
+ width_all_links_responsive = width_all_links_responsive + width;
220
+
221
+ if ((width_navigation_responsive) < width_all_links_responsive) {
222
+ _this.movedLinks.push($link);
223
+ $link.remove();
224
+ } else {
225
+ if (_this.movedLinks.length) {
226
+ if ($dots) {
227
+ _this.navigation.insertBefore(_this.movedLinks[0], $dots)
228
+ } else {
229
+ _this.navigation.appendChild(_this.movedLinks[0])
230
+ }
231
+ _this.movedLinks.splice(0, 1);
232
+ }
233
+ }
234
+ }
235
+
236
+ if (_this.movedLinks.length) {
237
+ if (!$dots) {
238
+ _this.navigation.insertAdjacentHTML('beforeend','<li class="dropdown dots">' + '<a href="#" aria-expanded="false">'+ _dots +'</a></li>');
239
+ }
240
+ } else {
241
+ if ($dots) {
242
+ $dots.remove();
243
+ }
244
+ }
245
+
246
+ let $d = _this.navigation.querySelector('.dots');
247
+ if ($d && _this.movedLinks.length) {
248
+ let $dropdown = $d.querySelector('ul');
249
+ if ($dropdown) {
250
+ for (let link of _this.movedLinks) {
251
+ $dropdown.prepend(link);
252
+ }
253
+ } else {
254
+ let $dropdown = document.createElement('ul');
255
+ $dropdown.classList.add('dropdown-content');
256
+ $dropdown.classList.add('right');
257
+
258
+ for (let link of _this.movedLinks) {
259
+ $dropdown.prepend(link);
260
+ }
261
+
262
+ $d.appendChild($dropdown);
263
+ }
264
+ }
265
+ }
266
+ }
267
+ }
268
+
269
+ show(relatedTarget) {
270
+ let target = relatedTarget.relatedTarget;
271
+
272
+ if (!target || isDisabled(target)) {
273
+ return;
274
+ }
275
+
276
+ if (!target.closest('.dropdown-content')) {
277
+ target.classList.add('first');
278
+ }
279
+
280
+ const showEvent = EventHandler.trigger(target, EVENT_KEY_SHOW, { relatedTarget });
281
+ if (showEvent.defaultPrevented) return;
282
+
283
+ let drop = Selectors.find('.dropdown-content', target),
284
+ link = target.firstElementChild;
285
+
286
+ if (link) link.setAttribute('aria-expanded', 'true');
287
+ drop.classList.add(CLASS_NAME_SHOW);
288
+ target.classList.add(CLASS_NAME_ACTIVE);
289
+
290
+ setDropPosition(drop)
291
+
292
+ const completeCallBack = () => {
293
+ drop.classList.add(CLASS_NAME_FADE);
294
+ EventHandler.trigger(target, EVENT_KEY_SHOWN, relatedTarget)
295
+ }
296
+ this._queueCallback(completeCallBack, drop, true, 50);
297
+
298
+ /**
299
+ *
300
+ * @param $drop
301
+ */
302
+ function setDropPosition($drop) {
303
+ let {width, right} = $drop.getBoundingClientRect(),
304
+ window_width = window.innerWidth;
305
+
306
+ let N_right = window_width - right - width;
307
+
308
+ $drop.classList.remove('right');
309
+ $drop.classList.remove('left');
310
+
311
+ let $parent = $drop.closest('li'),
312
+ $ul = $parent.querySelectorAll('ul');
313
+
314
+ if (N_right > width) {
315
+ for (const $el of $ul) {
316
+ $el.classList.add('left');
317
+ }
318
+ } else {
319
+ for (const $el of $ul) {
320
+ $el.classList.add('right');
321
+ }
322
+ }
323
+ }
324
+ }
325
+
326
+ hide(relatedTarget) {
327
+ const _this = this;
328
+ if ('ontouchstart' in document.documentElement) {
329
+ for (const element of [].concat(...document.body.children)) {
330
+ EventHandler.off(element, 'mouseover', noop);
331
+ }
332
+ }
333
+
334
+ let element = relatedTarget.relatedTarget;
335
+
336
+ if ('elm' in relatedTarget && relatedTarget.elm) {
337
+ element = relatedTarget.elm
338
+ }
339
+
340
+ if (element) {
341
+ const hideEvent = EventHandler.trigger(element, EVENT_KEY_HIDE);
342
+ if (hideEvent.defaultPrevented) return;
343
+
344
+ element.classList.remove(CLASS_NAME_ACTIVE);
345
+
346
+ if (element.classList.contains('first')) {
347
+ element.classList.remove('first');
348
+ }
349
+
350
+ [...Selectors.findAll('.' + CLASS_NAME_SHOW, element)].forEach(function (el, index) {
351
+ el.classList.remove(CLASS_NAME_FADE);
352
+
353
+ let parent = el.closest('.dropdown');
354
+ if (parent.classList.contains(CLASS_NAME_ACTIVE)) {
355
+ parent.classList.remove(CLASS_NAME_ACTIVE);
356
+ }
357
+
358
+ let link = el.previousElementSibling;
359
+ if (link) link.setAttribute('aria-expanded', 'false');
360
+
361
+ if (index === 0) {
362
+ const completeCallback = () => {
363
+ el.classList.remove(CLASS_NAME_SHOW);
364
+ EventHandler.trigger(el, EVENT_KEY_HIDDEN, relatedTarget)
365
+ }
366
+
367
+ _this._queueCallback(completeCallback, el, true, 500);
368
+ }
369
+ });
370
+ }
371
+ }
372
+
373
+ /**
374
+ * TODO если на странице несколько навигаций, то есть косяки
375
+ * @param element
376
+ * @param params
377
+ */
378
+ static init(element, params = {}) {
379
+ const instance = VGNav.getOrCreateInstance(element, params);
380
+ instance.build();
381
+
382
+ let drops = Selectors.findAll('.dropdown', instance._navigation)
383
+
384
+ if (instance._params.hover) {
385
+ [...drops].forEach(function (el) {
386
+ let currentElem = null;
387
+ EventHandler.on(el, EVENT_MOUSEOVER_DATA_API, function (event) {
388
+ if (currentElem) return;
389
+ VGNav.hideOpenDrops(event);
390
+
391
+ let target = event.target.closest('.dropdown');
392
+ if (!target) return;
393
+
394
+ if (!instance.navigation.contains(target)) return;
395
+ currentElem = target;
396
+
397
+ let relatedTarget = {
398
+ relatedTarget: target
399
+ }
400
+
401
+ instance.show(relatedTarget);
402
+ });
403
+ EventHandler.on(el, EVENT_MOUSEOUT_DATA_API, function (event) {
404
+ if (!currentElem) return;
405
+
406
+ let relatedTarget = event.relatedTarget.closest('.dropdown'),
407
+ elm = currentElem;
408
+
409
+ while (relatedTarget) {
410
+ if (relatedTarget === currentElem) return;
411
+ relatedTarget = relatedTarget.parentNode;
412
+ }
413
+
414
+ currentElem = null;
415
+ instance.hide({relatedTarget: relatedTarget, elm: elm});
416
+ })
417
+ })
418
+ } else {
419
+ EventHandler.on(document, EVENT_KEYUP_DATA_API, VGNav.clearDrops);
420
+ EventHandler.on(document, EVENT_CLICK_DATA_API, VGNav.clearDrops);
421
+ EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) {
422
+ if (!Manipulator.has(this, 'aria-expanded')) {
423
+ return;
424
+ }
425
+
426
+ if ('click' in instance._params.callback) {
427
+ execute(instance._params.callback.click, [this]);
428
+ }
429
+
430
+ event.preventDefault();
431
+
432
+ let self = this.closest('.vg-nav'),
433
+ isFirst = self.querySelector('.first');
434
+
435
+ let target = this.closest('.dropdown');
436
+ if (!target) return;
437
+
438
+ if (isDisabled(target) && !isVisible(target)) {
439
+ return;
440
+ }
441
+
442
+ if (isFirst && this.closest('.first')) {
443
+ if (target.classList.contains('active')) {
444
+ instance.hide({relatedTarget: target});
445
+ return;
446
+ }
447
+ } else {
448
+ [...Selectors.findAll('.active', self)].forEach(function (el) {
449
+ if (el && el !== target) {
450
+ instance.hide({relatedTarget: el})
451
+ }
452
+ });
453
+ }
454
+
455
+ instance.show({relatedTarget: target});
456
+ });
457
+ }
458
+
459
+ const vgNavSidebar = document.getElementById('sidebar-nav');
460
+ let hamburger = instance._element.querySelector('.' + instance._params.classes.hamburger);
461
+
462
+ if (vgNavSidebar && hamburger) {
463
+ vgNavSidebar.addEventListener('vg.sidebar.show', function () {
464
+ hamburger.classList.add(instance._params.classes.hamburgerActive);
465
+ });
466
+
467
+ vgNavSidebar.addEventListener('vg.sidebar.hide', function () {
468
+ hamburger.classList.remove(instance._params.classes.hamburgerActive);
469
+ });
470
+ }
471
+ }
472
+
473
+ static clearDrops(event) {
474
+ if (event.button === 2 || (event.type === 'keyup' && event.key !== 'Tab')) {
475
+ return
476
+ }
477
+
478
+ VGNav.hideOpenDrops(event)
479
+ }
480
+
481
+ static hideOpenDrops(event) {
482
+ const openToggles = Selectors.findAll('.dropdown:not(.disabled):not(:disabled).active');
483
+
484
+ for (const toggle of openToggles) {
485
+ const context = VGNav.getInstance(toggle.closest('.vg-nav'));
486
+ if (!context) continue;
487
+
488
+ if (event.target.closest('.first')) {
489
+ return;
490
+ }
491
+
492
+ const relatedTarget = { relatedTarget: toggle }
493
+
494
+ if (event.type === 'click') {
495
+ relatedTarget.clickEvent = event
496
+ }
497
+
498
+ context.hide(relatedTarget)
499
+ }
500
+ }
501
+ }
502
+
503
+ EventHandler.on(window, EVENT_RESIZE_DATA_API, function () {
504
+ if (Selectors.find('.vg-nav')) {
505
+ const instance = VGNav.getOrCreateInstance('.vg-nav', {});
506
+ instance.build();
507
+ }
508
+ })
509
+
510
+ export default VGNav;
@@ -0,0 +1,127 @@
1
+ // Breakpoint viewport sizes and media queries.
2
+ //
3
+ // Breakpoints are defined as a map of (name: minimum width), order from small to large:
4
+ //
5
+ // (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px, xxl: 1400px)
6
+ //
7
+ // The map defined in the `$nav-breakpoints` global variable is used as the `$breakpoints` argument by default.
8
+
9
+ // Name of the next breakpoint, or null for the last breakpoint.
10
+ //
11
+ // >> breakpoint-next(sm)
12
+ // md
13
+ // >> breakpoint-next(sm, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px, xxl: 1400px))
14
+ // md
15
+ // >> breakpoint-next(sm, $breakpoint-names: (xs sm md lg xl xxl))
16
+ // md
17
+ @function breakpoint-next($name, $breakpoints: $nav-breakpoints, $breakpoint-names: map-keys($breakpoints)) {
18
+ $n: index($breakpoint-names, $name);
19
+ @if not $n {
20
+ @error "breakpoint `#{$name}` not found in `#{$breakpoints}`";
21
+ }
22
+ @return if($n < length($breakpoint-names), nth($breakpoint-names, $n + 1), null);
23
+ }
24
+
25
+ // Minimum breakpoint width. Null for the smallest (first) breakpoint.
26
+ //
27
+ // >> breakpoint-min(sm, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px, xxl: 1400px))
28
+ // 576px
29
+ @function breakpoint-min($name, $breakpoints: $nav-breakpoints) {
30
+ $min: map-get($breakpoints, $name);
31
+ @return if($min != 0, $min, null);
32
+ }
33
+
34
+ // Maximum breakpoint width.
35
+ // The maximum value is reduced by 0.02px to work around the limitations of
36
+ // `min-` and `max-` prefixes and viewports with fractional widths.
37
+ // See https://www.w3.org/TR/mediaqueries-4/#mq-min-max
38
+ // Uses 0.02px rather than 0.01px to work around a current rounding bug in Safari.
39
+ // See https://bugs.webkit.org/show_bug.cgi?id=178261
40
+ //
41
+ // >> breakpoint-max(md, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px, xxl: 1400px))
42
+ // 767.98px
43
+ @function breakpoint-max($name, $breakpoints: $nav-breakpoints) {
44
+ $max: map-get($breakpoints, $name);
45
+ @return if($max and $max > 0, $max - .02, null);
46
+ }
47
+
48
+ // Returns a blank string if smallest breakpoint, otherwise returns the name with a dash in front.
49
+ // Useful for making responsive utilities.
50
+ //
51
+ // >> breakpoint-infix(xs, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px, xxl: 1400px))
52
+ // "" (Returns a blank string)
53
+ // >> breakpoint-infix(sm, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px, xxl: 1400px))
54
+ // "-sm"
55
+ @function breakpoint-infix($name, $breakpoints: $nav-breakpoints) {
56
+ @return if(breakpoint-min($name, $breakpoints) == null, "", "-#{$name}");
57
+ }
58
+
59
+ // Media of at least the minimum breakpoint width. No query for the smallest breakpoint.
60
+ // Makes the @content apply to the given breakpoint and wider.
61
+ @mixin media-breakpoint-up($name, $breakpoints: $nav-breakpoints) {
62
+ $min: breakpoint-min($name, $breakpoints);
63
+ @if $min {
64
+ @media (min-width: $min) {
65
+ @content;
66
+ }
67
+ } @else {
68
+ @content;
69
+ }
70
+ }
71
+
72
+ // Media of at most the maximum breakpoint width. No query for the largest breakpoint.
73
+ // Makes the @content apply to the given breakpoint and narrower.
74
+ @mixin media-breakpoint-down($name, $breakpoints: $nav-breakpoints) {
75
+ $max: breakpoint-max($name, $breakpoints);
76
+ @if $max {
77
+ @media (max-width: $max) {
78
+ @content;
79
+ }
80
+ } @else {
81
+ @content;
82
+ }
83
+ }
84
+
85
+ // Media that spans multiple breakpoint widths.
86
+ // Makes the @content apply between the min and max breakpoints
87
+ @mixin media-breakpoint-between($lower, $upper, $breakpoints: $nav-breakpoints) {
88
+ $min: breakpoint-min($lower, $breakpoints);
89
+ $max: breakpoint-max($upper, $breakpoints);
90
+
91
+ @if $min != null and $max != null {
92
+ @media (min-width: $min) and (max-width: $max) {
93
+ @content;
94
+ }
95
+ } @else if $max == null {
96
+ @include media-breakpoint-up($lower, $breakpoints) {
97
+ @content;
98
+ }
99
+ } @else if $min == null {
100
+ @include media-breakpoint-down($upper, $breakpoints) {
101
+ @content;
102
+ }
103
+ }
104
+ }
105
+
106
+ // Media between the breakpoint's minimum and maximum widths.
107
+ // No minimum for the smallest breakpoint, and no maximum for the largest one.
108
+ // Makes the @content apply only to the given breakpoint, not viewports any wider or narrower.
109
+ @mixin media-breakpoint-only($name, $breakpoints: $nav-breakpoints) {
110
+ $min: breakpoint-min($name, $breakpoints);
111
+ $next: breakpoint-next($name, $breakpoints);
112
+ $max: breakpoint-max($next, $breakpoints);
113
+
114
+ @if $min != null and $max != null {
115
+ @media (min-width: $min) and (max-width: $max) {
116
+ @content;
117
+ }
118
+ } @else if $max == null {
119
+ @include media-breakpoint-up($name, $breakpoints) {
120
+ @content;
121
+ }
122
+ } @else if $min == null {
123
+ @include media-breakpoint-down($next, $breakpoints) {
124
+ @content;
125
+ }
126
+ }
127
+ }