all files / packages/scroll-pane/ ScrollPane.js

0% Statements 0/73
0% Branches 0/34
0% Functions 0/19
0% Lines 0/73
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   
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) {
      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)
  }
 
  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(componentId, onlyIfNotVisible) {
    let scrollableEl = this.getScrollableElement()
    let targetNode = scrollableEl.find('*[data-id="'+componentId+'"]')
    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(componentId, 'not found 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