1 Star 0 Fork 34

堪培拉的风/outline.js

forked from Yaohaixiao/outline.js 
加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
文件
克隆/下载
chapters.js 15.74 KB
一键复制 编辑 原始数据 按行查看 历史
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743
import Base from './base'
import isString from './utils/types/isString'
import isFunction from './utils/types/isFunction'
import isElement from './utils/types/isElement'
import later from './utils/lang/later'
import at from './utils/event/at'
import on from './utils/event/on'
import off from './utils/event/off'
import stop from './utils/event/stop'
import createElement from './utils/dom/createElement'
import scrollTo from './utils/dom/scrollTo'
import addClass from './utils/dom/addClass'
import intersection from './utils/dom/intersection'
import removeClass from './utils/dom/removeClass'
import offsetTop from './utils/dom/offsetTop'
import getStyle from './utils/dom/getStyle'
import setProperty from './utils/dom/setProperty'
import _getScrollElement from './utils/dom/_getScrollElement'
import cloneDeep from './utils/lang/cloneDeep'
import _paintChapters from './_paintChapters'
import inBounding from './utils/dom/inBounding'
class Chapters extends Base {
constructor(options) {
super()
this._default()
this.scrollTimer = null
this.resizeTimer = null
this.observerTimer = null
this.Observer = null
if (options) {
this.initialize(options)
}
}
_default() {
this.attrs = cloneDeep(Chapters.DEFAULTS)
this.$el = null
this.$title = null
this.$main = null
this.$list = null
this.$placeholder = null
this.$parentElement = null
this.$scrollElement = null
this.$active = null
this.chapters = []
this.active = 0
this.offsetWidth = 0
this.offsetTop = 0
this.playing = false
this.closed = false
return this
}
initialize(options) {
let created
let parentElement
let scrollElement
let $parent
this.attr(options)
created = this.attr('created')
parentElement = this.attr('parentElement')
scrollElement = this.attr('scrollElement')
if (isString(parentElement)) {
$parent = document.querySelector(parentElement)
} else if (isElement(parentElement)) {
$parent = parentElement
}
this.$parentElement = $parent
this.$scrollElement = _getScrollElement(scrollElement)
this.chapters = this.attr('chapters')
this.closed = this.attr('closed')
this.active = this.attr('active')
if (isFunction(created)) {
created.call(this)
}
if (this.chapters.length < 1) {
return this
}
this.render().addListeners()
this.$active = document.querySelector(`#chapter-${this.active}`)
return this
}
isClosed() {
return this.closed
}
isSticky() {
const position = this.attr('position')
return position === 'sticky'
}
isFixed() {
const position = this.attr('position')
return position === 'fixed'
}
isInside() {
return this.isFixed() || this.isSticky()
}
isOutside() {
return !this.isInside()
}
count() {
return this.chapters.length
}
_paintEdge() {
const $fragment = document.createDocumentFragment()
const STICKY = 'outline-chapters_sticky'
const HIDDEN = 'outline-chapters_hidden'
const title = this.attr('title')
const animationCurrent = this.attr('animationCurrent')
const customClass = this.attr('customClass')
const $parentElement = this.$parentElement
const children = []
const contents = []
let $title = null
let $el
let $main
let $list
let $placeholder
if (!$parentElement) {
return this
}
if (this.isInside() && title) {
$title = createElement(
'h2',
{
className: 'outline-chapters__title'
},
title
)
this.$title = $title
contents.push($title)
}
$list = createElement('ul', {
// 为优化性能,添加了 _fixed 和 _hidden
// fixed 为了让 $list 脱离流布局
// hidden 让 $list 不可见
className: `outline-chapters__list`
})
this.$list = $list
children.push($list)
if (animationCurrent) {
$placeholder = createElement('div', {
className: 'outline-chapters__placeholder'
})
this.$placeholder = $placeholder
children.push($placeholder)
}
$main = createElement(
'div',
{
className: 'outline-chapters__main'
},
children
)
this.$main = $main
contents.push($main)
$el = createElement(
'nav',
{
id: 'outline-chapters',
className: `outline-chapters ${HIDDEN}`
},
contents
)
this.$el = $el
if (this.isSticky()) {
this.calculateStickyHeight()
addClass($el, STICKY)
}
if (customClass) {
addClass($el, customClass)
}
$fragment.appendChild($el)
$parentElement.appendChild($fragment)
return this
}
render() {
const mounted = this.attr('mounted')
const $parentElement = this.$parentElement
const chapters = this.chapters
const count = this.count()
let $el
if (!$parentElement || chapters.length < 1) {
return this
}
if (this.isInside()) {
addClass($parentElement, 'outline-chapters-parent')
}
this._paintEdge()
$el = this.$el
this._paint(chapters)
later(() => {
this.highlight(this.active)
}, 60)
this.offsetTop = offsetTop($el)
this.offsetWidth = $el.offsetWidth
if (this.isFixed()) {
this.sticky()
setProperty('--outline-chapters-width', `${this.offsetWidth}px`)
}
if (isFunction(mounted)) {
mounted.call(this)
}
if (count < 400) {
this.onObserver()
}
return this
}
erase() {
this.$list.innerHTML = ''
return this
}
_paint(chapters) {
const HIDDEN = 'outline-chapters_hidden'
const showCode = this.attr('showCode')
const $el = this.$el
const $list = this.$list
_paintChapters($list, chapters, showCode)
removeClass($el, HIDDEN)
return this
}
_remove() {
this.$parentElement.removeChild(this.$el)
return this
}
refresh(chapters) {
const HIDDEN = 'outline-chapters_hidden'
const $el = this.$el
removeClass($el, HIDDEN)
this.erase()._paint(chapters)
return this
}
_getPlaceholderOffset(index) {
const $main = this.$main
const $list = this.$list
const $anchor = $list.querySelector('.outline-chapters__anchor')
const animationCurrent = this.attr('animationCurrent')
const mainPaddingTop = parseInt(getStyle($main, 'padding-top'), 10)
const mainBorderTop = parseInt(getStyle($main, 'border-top-width'), 10)
const placeholderPaddingTop = parseInt(getStyle($list, 'padding-top'), 10)
const placeholderMarginTop = parseInt(getStyle($list, 'margin-top'), 10)
const placeholderBorderTop = parseInt(
getStyle($list, 'border-top-width'),
10
)
let height = $anchor.offsetHeight
let offsetTop = 0
let top
if (!animationCurrent) {
return this
}
if (mainPaddingTop) {
offsetTop += mainPaddingTop
}
if (placeholderPaddingTop) {
offsetTop += placeholderPaddingTop
}
if (placeholderMarginTop) {
offsetTop += placeholderMarginTop
}
if (mainBorderTop) {
offsetTop += mainBorderTop
}
if (placeholderBorderTop) {
offsetTop += placeholderBorderTop
}
top = height * index
return offsetTop + top
}
positionPlaceholder(index) {
const $list = this.$list
const $placeholder = this.$placeholder
const $anchor = $list.querySelector('.outline-chapters__anchor')
const animationCurrent = this.attr('animationCurrent')
const height = $anchor.offsetHeight
let offsetTop = 0
if (!animationCurrent) {
return this
}
offsetTop = this._getPlaceholderOffset(index)
$placeholder.style.cssText = `transform: translateY(${offsetTop}px);height:${height}px;`
return this
}
highlight(id) {
const $el = this.$el
const animationCurrent = this.attr('animationCurrent')
const ACTIVE = 'outline-chapters_active'
const HIGHLIGHT = 'outline-chapters_highlight'
let $anchor = null
let placeholderOffsetTop = 0
if (!$el) {
return this
}
$anchor = $el.querySelector(`#chapter__anchor-${id}`)
if (!$anchor) {
return this
}
this.active = parseInt($anchor.getAttribute('data-id'), 10)
if (this.$active) {
removeClass(this.$active, HIGHLIGHT)
removeClass(this.$active, ACTIVE)
}
this.$active = $anchor
addClass(this.$active, ACTIVE)
if (animationCurrent) {
this.positionPlaceholder(this.active)
later(() => {
if (!inBounding(this.$active, this.$parentElement)) {
placeholderOffsetTop = this._getPlaceholderOffset(this.active)
scrollTo(this.$main, placeholderOffsetTop)
}
})
} else {
addClass(this.$active, HIGHLIGHT)
}
return this
}
sticky() {
const afterSticky = this.attr('afterSticky')
const FIXED = 'outline-chapters_fixed'
const $el = this.$el
const top = this.offsetTop
const scrollTop = this.$scrollElement.scrollTop
let isStickying
if (!this.isFixed()) {
return this
}
isStickying = scrollTop >= top
if (isStickying) {
addClass($el, FIXED)
} else {
removeClass($el, FIXED)
}
if (isFunction(afterSticky)) {
afterSticky.call(this, this.isClosed(), isStickying)
}
return this
}
calculateStickyHeight() {
const documentElement = document.documentElement
const height = Math.max(
documentElement.clientHeight || 0,
window.innerHeight || 0
)
setProperty('--outline-sticky-height', `${height}px`)
return this
}
scrollTo(top, after) {
const el = this.$scrollElement
scrollTo(el, top, after)
return this
}
show() {
const FOLDED = 'outline-chapters_folded'
const HIDDEN = 'outline-chapters_hidden'
const opened = this.attr('afterOpened')
const count = this.count()
const $el = this.$el
const $parent = this.$parentElement
if (this.isInside()) {
if (count > 800) {
removeClass($parent, HIDDEN)
} else {
removeClass($parent, HIDDEN)
later(() => {
removeClass($parent, FOLDED)
}, 30)
}
} else {
removeClass($el, HIDDEN)
}
this.closed = false
if (isFunction(opened)) {
opened.call(this)
}
return this
}
hide() {
const FOLDED = 'outline-chapters_folded'
const HIDDEN = 'outline-chapters_hidden'
const closed = this.attr('afterClosed')
const count = this.count()
const $el = this.$el
const $parent = this.$parentElement
if (this.isInside()) {
if (count > 800) {
addClass($parent, HIDDEN)
} else {
addClass($parent, FOLDED)
later(() => {
addClass($parent, HIDDEN)
})
}
} else {
addClass($el, HIDDEN)
}
this.closed = true
if (isFunction(closed)) {
closed.call(this)
}
return this
}
toggle() {
const afterToggle = this.attr('afterToggle')
const top = this.offsetTop
const scrollTop = this.$scrollElement.scrollTop
let isStickying
if (this.isClosed()) {
this.show()
} else {
this.hide()
}
if (isFunction(afterToggle)) {
later(() => {
isStickying = scrollTop >= top
afterToggle.call(this, this.isClosed(), isStickying)
})
}
return this
}
destroy() {
const beforeDestroy = this.attr('beforeDestroy')
const afterDestroy = this.attr('afterDestroy')
if (isFunction(beforeDestroy)) {
beforeDestroy.call(this)
}
this.removeListeners()._remove()._default()
if (this.scrollTimer) {
clearTimeout(this.scrollTimer)
this.scrollTimer = null
}
if (this.resizeTimer) {
clearTimeout(this.resizeTimer)
this.resizeTimer = null
}
if (this.observerTimer) {
clearTimeout(this.observerTimer)
this.observerTimer = null
}
if (this.Observer) {
this.Observer = null
}
if (isFunction(afterDestroy)) {
afterDestroy.call(this)
}
return this
}
onObserver() {
const selector = this.attr('selector')
this.Observer = intersection(
($heading) => {
const id = $heading.getAttribute('data-id')
if (this.playing) {
return false
}
if (this.observerTimer) {
clearTimeout(this.observerTimer)
}
this.observerTimer = later(() => {
this.highlight(id)
}, 100)
},
{
selector,
context: this
}
)
return this
}
onSelect(evt) {
const stickyHeight = this.attr('stickyHeight')
const $anchor = evt.delegateTarget
const id = $anchor.getAttribute('data-id')
const headingId = $anchor.href.split('#')[1]
const $heading = document.querySelector(`#${headingId}`)
const top = offsetTop($heading) - (stickyHeight + 10)
const min = 0
const max = this.$scrollElement.scrollHeight
const afterScroll = this.attr('afterScroll')
const after = () => {
if (isFunction(afterScroll)) {
afterScroll.call(this, 'chapter')
}
later(() => {
this.playing = false
this.$emit('toolbar:update', {
top,
min,
max
})
})
}
this.playing = true
if (this.isFixed()) {
this.sticky()
later(() => {
this.scrollTo(top, after)
this.highlight(id)
}, 10)
} else {
this.scrollTo(top, after)
this.highlight(id)
}
stop(evt)
return this
}
onScroll() {
const $scrollElement = this.$scrollElement
if (this.scrollTimer) {
clearTimeout(this.scrollTimer)
}
this.scrollTimer = later(() => {
const top = $scrollElement.scrollTop
const min = 0
const max = $scrollElement.scrollHeight - $scrollElement.clientHeight
if (this.isFixed()) {
this.sticky()
}
this.$emit('toolbar:update', {
top,
min,
max
})
}, 100)
return this
}
onResize() {
if (this.resizeTimer) {
clearTimeout(this.resizeTimer)
}
this.resizeTimer = later(() => {
this.calculateStickyHeight()
})
return this
}
addListeners() {
const $el = this.$el
const $scrollElement = this.$scrollElement
const tagName = $scrollElement.tagName.toLowerCase()
let $element = $scrollElement
if (this.count() < 1) {
return this
}
if (tagName === 'html' || tagName === 'body') {
$element = window
}
on($el, '.outline-chapters__anchor', 'click', this.onSelect, this, true)
at($element, 'scroll', this.onScroll, this, true)
if (this.isSticky()) {
at(window, 'resize', this.onResize, this, true)
}
this.$on('anchors:all:paint', this.onObserver, this)
return this
}
removeListeners() {
const selector = this.attr('selector')
const $el = this.$el
const $scrollElement = this.$scrollElement
const tagName = $scrollElement.tagName.toLowerCase()
let $element = $scrollElement
if (this.count() < 1) {
return this
}
if (tagName === 'html' || tagName === 'body') {
$element = window
}
off($el, 'click', this.onSelect)
off($element, 'scroll', this.onScroll)
if (this.isSticky()) {
at(window, 'resize', this.onResize)
}
this.$off('anchors:all:paint')
if (this.Observer) {
document.querySelectorAll(selector).forEach((section) => {
this.Observer.unobserve(section)
})
}
return this
}
}
Chapters.DEFAULTS = {
parentElement: '',
scrollElement: '',
selector: '.outline-heading',
active: 0,
closed: false,
showCode: true,
animationCurrent: true,
position: 'relative',
stickyHeight: 0,
chapters: [],
created: null,
mounted: null,
afterClosed: null,
afterOpened: null,
afterScroll: null,
beforeDestroy: null,
afterDestroy: null,
afterSticky: null
}
export default Chapters
Loading...
马建仓 AI 助手
尝试更多
代码解读
代码找茬
代码优化
JavaScript
1
https://gitee.com/lelezr/outline.js.git
git@gitee.com:lelezr/outline.js.git
lelezr
outline.js
outline.js
master

搜索帮助

0d507c66 1850385 C8b1a773 1850385