talos

Форк
0
312 строк · 8.0 Кб
1
/**
2
 * --------------------------------------------------------------------------
3
 * Bootstrap (v4.6.1): scrollspy.js
4
 * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
5
 * --------------------------------------------------------------------------
6
 */
7

8
import $ from 'jquery'
9
import Util from './util'
10

11
/**
12
 * Constants
13
 */
14

15
const NAME = 'scrollspy'
16
const VERSION = '4.6.1'
17
const DATA_KEY = 'bs.scrollspy'
18
const EVENT_KEY = `.${DATA_KEY}`
19
const DATA_API_KEY = '.data-api'
20
const JQUERY_NO_CONFLICT = $.fn[NAME]
21

22
const CLASS_NAME_DROPDOWN_ITEM = 'dropdown-item'
23
const CLASS_NAME_ACTIVE = 'active'
24

25
const EVENT_ACTIVATE = `activate${EVENT_KEY}`
26
const EVENT_SCROLL = `scroll${EVENT_KEY}`
27
const EVENT_LOAD_DATA_API = `load${EVENT_KEY}${DATA_API_KEY}`
28

29
const METHOD_OFFSET = 'offset'
30
const METHOD_POSITION = 'position'
31

32
const SELECTOR_DATA_SPY = '[data-spy="scroll"]'
33
const SELECTOR_NAV_LIST_GROUP = '.nav, .list-group'
34
const SELECTOR_NAV_LINKS = '.nav-link'
35
const SELECTOR_NAV_ITEMS = '.nav-item'
36
const SELECTOR_LIST_ITEMS = '.list-group-item'
37
const SELECTOR_DROPDOWN = '.dropdown'
38
const SELECTOR_DROPDOWN_ITEMS = '.dropdown-item'
39
const SELECTOR_DROPDOWN_TOGGLE = '.dropdown-toggle'
40

41
const Default = {
42
  offset: 10,
43
  method: 'auto',
44
  target: ''
45
}
46

47
const DefaultType = {
48
  offset: 'number',
49
  method: 'string',
50
  target: '(string|element)'
51
}
52

53
/**
54
 * Class definition
55
 */
56

57
class ScrollSpy {
58
  constructor(element, config) {
59
    this._element = element
60
    this._scrollElement = element.tagName === 'BODY' ? window : element
61
    this._config = this._getConfig(config)
62
    this._selector = `${this._config.target} ${SELECTOR_NAV_LINKS},` +
63
                          `${this._config.target} ${SELECTOR_LIST_ITEMS},` +
64
                          `${this._config.target} ${SELECTOR_DROPDOWN_ITEMS}`
65
    this._offsets = []
66
    this._targets = []
67
    this._activeTarget = null
68
    this._scrollHeight = 0
69

70
    $(this._scrollElement).on(EVENT_SCROLL, event => this._process(event))
71

72
    this.refresh()
73
    this._process()
74
  }
75

76
  // Getters
77
  static get VERSION() {
78
    return VERSION
79
  }
80

81
  static get Default() {
82
    return Default
83
  }
84

85
  // Public
86
  refresh() {
87
    const autoMethod = this._scrollElement === this._scrollElement.window ?
88
      METHOD_OFFSET : METHOD_POSITION
89

90
    const offsetMethod = this._config.method === 'auto' ?
91
      autoMethod : this._config.method
92

93
    const offsetBase = offsetMethod === METHOD_POSITION ?
94
      this._getScrollTop() : 0
95

96
    this._offsets = []
97
    this._targets = []
98

99
    this._scrollHeight = this._getScrollHeight()
100

101
    const targets = [].slice.call(document.querySelectorAll(this._selector))
102

103
    targets
104
      .map(element => {
105
        let target
106
        const targetSelector = Util.getSelectorFromElement(element)
107

108
        if (targetSelector) {
109
          target = document.querySelector(targetSelector)
110
        }
111

112
        if (target) {
113
          const targetBCR = target.getBoundingClientRect()
114
          if (targetBCR.width || targetBCR.height) {
115
            // TODO (fat): remove sketch reliance on jQuery position/offset
116
            return [
117
              $(target)[offsetMethod]().top + offsetBase,
118
              targetSelector
119
            ]
120
          }
121
        }
122

123
        return null
124
      })
125
      .filter(item => item)
126
      .sort((a, b) => a[0] - b[0])
127
      .forEach(item => {
128
        this._offsets.push(item[0])
129
        this._targets.push(item[1])
130
      })
131
  }
132

133
  dispose() {
134
    $.removeData(this._element, DATA_KEY)
135
    $(this._scrollElement).off(EVENT_KEY)
136

137
    this._element = null
138
    this._scrollElement = null
139
    this._config = null
140
    this._selector = null
141
    this._offsets = null
142
    this._targets = null
143
    this._activeTarget = null
144
    this._scrollHeight = null
145
  }
146

147
  // Private
148
  _getConfig(config) {
149
    config = {
150
      ...Default,
151
      ...(typeof config === 'object' && config ? config : {})
152
    }
153

154
    if (typeof config.target !== 'string' && Util.isElement(config.target)) {
155
      let id = $(config.target).attr('id')
156
      if (!id) {
157
        id = Util.getUID(NAME)
158
        $(config.target).attr('id', id)
159
      }
160

161
      config.target = `#${id}`
162
    }
163

164
    Util.typeCheckConfig(NAME, config, DefaultType)
165

166
    return config
167
  }
168

169
  _getScrollTop() {
170
    return this._scrollElement === window ?
171
      this._scrollElement.pageYOffset : this._scrollElement.scrollTop
172
  }
173

174
  _getScrollHeight() {
175
    return this._scrollElement.scrollHeight || Math.max(
176
      document.body.scrollHeight,
177
      document.documentElement.scrollHeight
178
    )
179
  }
180

181
  _getOffsetHeight() {
182
    return this._scrollElement === window ?
183
      window.innerHeight : this._scrollElement.getBoundingClientRect().height
184
  }
185

186
  _process() {
187
    const scrollTop = this._getScrollTop() + this._config.offset
188
    const scrollHeight = this._getScrollHeight()
189
    const maxScroll = this._config.offset + scrollHeight - this._getOffsetHeight()
190

191
    if (this._scrollHeight !== scrollHeight) {
192
      this.refresh()
193
    }
194

195
    if (scrollTop >= maxScroll) {
196
      const target = this._targets[this._targets.length - 1]
197

198
      if (this._activeTarget !== target) {
199
        this._activate(target)
200
      }
201

202
      return
203
    }
204

205
    if (this._activeTarget && scrollTop < this._offsets[0] && this._offsets[0] > 0) {
206
      this._activeTarget = null
207
      this._clear()
208
      return
209
    }
210

211
    for (let i = this._offsets.length; i--;) {
212
      const isActiveTarget = this._activeTarget !== this._targets[i] &&
213
          scrollTop >= this._offsets[i] &&
214
          (typeof this._offsets[i + 1] === 'undefined' ||
215
              scrollTop < this._offsets[i + 1])
216

217
      if (isActiveTarget) {
218
        this._activate(this._targets[i])
219
      }
220
    }
221
  }
222

223
  _activate(target) {
224
    this._activeTarget = target
225

226
    this._clear()
227

228
    const queries = this._selector
229
      .split(',')
230
      .map(selector => `${selector}[data-target="${target}"],${selector}[href="${target}"]`)
231

232
    const $link = $([].slice.call(document.querySelectorAll(queries.join(','))))
233

234
    if ($link.hasClass(CLASS_NAME_DROPDOWN_ITEM)) {
235
      $link.closest(SELECTOR_DROPDOWN)
236
        .find(SELECTOR_DROPDOWN_TOGGLE)
237
        .addClass(CLASS_NAME_ACTIVE)
238
      $link.addClass(CLASS_NAME_ACTIVE)
239
    } else {
240
      // Set triggered link as active
241
      $link.addClass(CLASS_NAME_ACTIVE)
242
      // Set triggered links parents as active
243
      // With both <ul> and <nav> markup a parent is the previous sibling of any nav ancestor
244
      $link.parents(SELECTOR_NAV_LIST_GROUP)
245
        .prev(`${SELECTOR_NAV_LINKS}, ${SELECTOR_LIST_ITEMS}`)
246
        .addClass(CLASS_NAME_ACTIVE)
247
      // Handle special case when .nav-link is inside .nav-item
248
      $link.parents(SELECTOR_NAV_LIST_GROUP)
249
        .prev(SELECTOR_NAV_ITEMS)
250
        .children(SELECTOR_NAV_LINKS)
251
        .addClass(CLASS_NAME_ACTIVE)
252
    }
253

254
    $(this._scrollElement).trigger(EVENT_ACTIVATE, {
255
      relatedTarget: target
256
    })
257
  }
258

259
  _clear() {
260
    [].slice.call(document.querySelectorAll(this._selector))
261
      .filter(node => node.classList.contains(CLASS_NAME_ACTIVE))
262
      .forEach(node => node.classList.remove(CLASS_NAME_ACTIVE))
263
  }
264

265
  // Static
266
  static _jQueryInterface(config) {
267
    return this.each(function () {
268
      let data = $(this).data(DATA_KEY)
269
      const _config = typeof config === 'object' && config
270

271
      if (!data) {
272
        data = new ScrollSpy(this, _config)
273
        $(this).data(DATA_KEY, data)
274
      }
275

276
      if (typeof config === 'string') {
277
        if (typeof data[config] === 'undefined') {
278
          throw new TypeError(`No method named "${config}"`)
279
        }
280

281
        data[config]()
282
      }
283
    })
284
  }
285
}
286

287
/**
288
 * Data API implementation
289
 */
290

291
$(window).on(EVENT_LOAD_DATA_API, () => {
292
  const scrollSpys = [].slice.call(document.querySelectorAll(SELECTOR_DATA_SPY))
293
  const scrollSpysLength = scrollSpys.length
294

295
  for (let i = scrollSpysLength; i--;) {
296
    const $spy = $(scrollSpys[i])
297
    ScrollSpy._jQueryInterface.call($spy, $spy.data())
298
  }
299
})
300

301
/**
302
 * jQuery
303
 */
304

305
$.fn[NAME] = ScrollSpy._jQueryInterface
306
$.fn[NAME].Constructor = ScrollSpy
307
$.fn[NAME].noConflict = () => {
308
  $.fn[NAME] = JQUERY_NO_CONFLICT
309
  return ScrollSpy._jQueryInterface
310
}
311

312
export default ScrollSpy
313

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.