substance
Version:
Substance is a JavaScript library for web-based content editing. It provides building blocks for realizing custom text editors and web-based publishing systems.
247 lines (211 loc) • 6.82 kB
JavaScript
import { platform, getRelativeBoundingRect } from '../../util'
import { AbstractScrollPane } from '../../ui'
import Scrollbar from '../scrollbar/Scrollbar'
/**
Wraps content in a scroll pane.
NOTE: It is best practice to put all overlays as direct childs of the ScrollPane
to reduce the chance that positioning gets messed up (position: relative)
@prop {String} scrollbarType 'native' or 'substance' for a more advanced visual scrollbar. Defaults to 'native'
@prop {String} [scrollbarPosition] 'left' or 'right' only relevant when scrollBarType: 'substance'. Defaults to 'right'
@prop {ui/Highlights} [highlights] object that maintains highlights and can be manipulated from different sources
@prop {ui/TOCProvider} [tocProvider] object that maintains table of content entries
@example
```js
$$(ScrollPane, {
scrollbarType: 'substance', // defaults to native
scrollbarPosition: 'left', // defaults to right
onScroll: this.onScroll.bind(this),
highlights: this.contentHighlights,
tocProvider: this.tocProvider
})
```
*/
class ScrollPane extends AbstractScrollPane {
didMount() {
super.didMount()
if (this.refs.scrollbar && this.props.highlights) {
this.props.highlights.on('highlights:updated', this.onHighlightsUpdated, this)
}
if (this.refs.scrollbar) {
if (platform.inBrowser) {
this.domObserver = new window.MutationObserver(this._onContentChanged.bind(this))
this.domObserver.observe(this.el.getNativeElement(), {
subtree: true,
attributes: true,
characterData: true,
childList: true,
})
}
this.context.editorSession.onPosition(this._onPosition, this)
}
}
dispose() {
super.dispose()
if (this.props.highlights) {
this.props.highlights.off(this)
}
this.context.editorSession.off(this)
this.context.dragManager.off(this)
if (this.domObserver) {
this.domObserver.disconnect()
}
}
render($$) {
let el = $$('div')
.addClass('sc-scroll-pane')
if (platform.isFF) {
el.addClass('sm-firefox')
}
// When noStyle is provided we just use ScrollPane as a container, but without
// any absolute positioned containers, leaving the body scrollable.
if (!this.props.noStyle) {
el.addClass('sm-default-style')
}
// Initialize Substance scrollbar (if enabled)
if (this.props.scrollbarType === 'substance') {
el.addClass('sm-substance-scrollbar')
el.addClass('sm-scrollbar-position-' + this.props.scrollbarPosition)
el.append(
// TODO: is there a way to pass scrollbar highlights already
// via props? Currently the are initialized with a delay
$$(Scrollbar, {
scrollPane: this
}).ref('scrollbar')
.attr('id', 'content-scrollbar')
)
// Scanline is debugging purposes, display: none by default.
el.append(
$$('div').ref("scanline").addClass('se-scanline')
)
}
el.append(
$$('div').ref('scrollable').addClass('se-scrollable').append(
this.renderContent($$)
).on('scroll', this.onScroll)
)
return el
}
renderContent($$) {
let contentEl = $$('div').ref('content').addClass('se-content')
contentEl.append(this.props.children)
if (this.props.contextMenu === 'custom') {
contentEl.on('contextmenu', this._onContextMenu)
}
return contentEl
}
_onContentChanged() {
this._contentChanged = true
}
_onPosition() {
if (this.refs.scrollbar && this._contentChanged) {
this._contentChanged = false
this._updateScrollbar()
}
}
_updateScrollbar() {
if (this.refs.scrollbar) {
this.refs.scrollbar.updatePositions()
}
}
onHighlightsUpdated(highlights) {
this.refs.scrollbar.extendProps({
highlights: highlights
})
}
onScroll() {
let scrollPos = this.getScrollPosition()
let scrollable = this.refs.scrollable
if (this.props.onScroll) {
this.props.onScroll(scrollPos, scrollable)
}
// Update TOCProvider given
if (this.props.tocProvider) {
this.props.tocProvider.markActiveEntry(this)
}
this.emit('scroll', scrollPos, scrollable)
}
/**
Returns the height of scrollPane (inner content overflows)
*/
getHeight() {
let scrollableEl = this.getScrollableElement()
return scrollableEl.height
}
/**
Returns the cumulated height of a panel's content
*/
getContentHeight() {
let contentEl = this.refs.content.el.getNativeElement()
// Important to use scrollHeight here (e.g. to consider overflowing
// content, that stretches the content area, such as an overlay or
// a context menu)
return contentEl.scrollHeight
}
/**
Get the `.se-content` element
*/
getContentElement() {
return this.refs.content.el
}
/**
Get the `.se-scrollable` element
*/
getScrollableElement() {
return this.refs.scrollable.el
}
/**
Get current scroll position (scrollTop) of `.se-scrollable` element
*/
getScrollPosition() {
let scrollableEl = this.getScrollableElement()
return scrollableEl.getProperty('scrollTop')
}
setScrollPosition(scrollPos) {
let scrollableEl = this.getScrollableElement()
scrollableEl.setProperty('scrollTop', scrollPos)
}
/**
Get offset relative to `.se-content`.
@param {DOMNode} el DOM node that lives inside the
*/
getPanelOffsetForElement(el) {
let nativeEl = el.getNativeElement()
let contentContainerEl = this.refs.content.getNativeElement()
let rect = getRelativeBoundingRect(nativeEl, contentContainerEl)
return rect.top
}
/**
Scroll to a given sub component.
@param {String} componentId component id, must be present in data-id attribute
*/
scrollTo(selector, onlyIfNotVisible) {
let scrollableEl = this.getScrollableElement()
let targetNode = scrollableEl.find(selector)
if (targetNode) {
const offset = this.getPanelOffsetForElement(targetNode)
let shouldScroll = true
if (onlyIfNotVisible) {
const height = scrollableEl.height
const oldOffset = scrollableEl.getProperty('scrollTop')
shouldScroll = (offset < oldOffset || oldOffset+height<offset)
}
if (shouldScroll) {
this.setScrollPosition(offset)
}
} else {
console.warn(`No match found for selector '${selector}' in scrollable container`)
}
}
/*
Determines the selection bounding rectangle relative to the scrollpane's content.
*/
onSelectionPositioned(...args) {
super.onSelectionPositioned(...args)
this._updateScrollbar()
}
_onContextMenu(e) {
super._onContextMenu(e)
this._updateScrollbar()
}
}
export default ScrollPane