lib/minimap-element.js
'use babel'
import {CompositeDisposable, Disposable} from 'atom'
import {EventsDelegation, AncestorsMethods} from 'atom-utils'
import include from './decorators/include'
import element from './decorators/element'
import DOMStylesReader from './mixins/dom-styles-reader'
import CanvasDrawer from './mixins/canvas-drawer'
import MinimapQuickSettingsElement from './minimap-quick-settings-element'
const SPEC_MODE = atom.inSpecMode()
/**
* Public: The MinimapElement is the view meant to render a {@link Minimap}
* instance in the DOM.
*
* You can retrieve the MinimapElement associated to a Minimap
* using the `atom.views.getView` method.
*
* Note that most interactions with the Minimap package is done through the
* Minimap model so you should never have to access MinimapElement
* instances.
*
* @example
* let minimapElement = atom.views.getView(minimap)
*/
@element('atom-text-editor-minimap')
@include(DOMStylesReader, CanvasDrawer, EventsDelegation, AncestorsMethods)
export default class MinimapElement {
/**
* The method that registers the MinimapElement factory in the
* `atom.views` registry with the Minimap model.
*/
static registerViewProvider () {
atom.views.addViewProvider(require('./minimap'), function (model) {
let element = new MinimapElement()
element.setModel(model)
return element
})
}
// ## ## ####### ####### ## ## ######
// ## ## ## ## ## ## ## ## ## ##
// ## ## ## ## ## ## ## ## ##
// ######### ## ## ## ## ##### ######
// ## ## ## ## ## ## ## ## ##
// ## ## ## ## ## ## ## ## ## ##
// ## ## ####### ####### ## ## ######
/**
* DOM callback invoked when a new MinimapElement is created.
*
* @access private
*/
createdCallback () {
// Core properties
/**
* @access private
*/
this.minimap = undefined
/**
* @access private
*/
this.editorElement = undefined
/**
* @access private
*/
this.width = undefined
/**
* @access private
*/
this.height = undefined
// Subscriptions
/**
* @access private
*/
this.subscriptions = new CompositeDisposable()
/**
* @access private
*/
this.visibleAreaSubscription = undefined
/**
* @access private
*/
this.quickSettingsSubscription = undefined
/**
* @access private
*/
this.dragSubscription = undefined
/**
* @access private
*/
this.openQuickSettingSubscription = undefined
// Configs
/**
* @access private
*/
this.displayMinimapOnLeft = false
/**
* @access private
*/
this.minimapScrollIndicator = undefined
/**
* @access private
*/
this.displayMinimapOnLeft = undefined
/**
* @access private
*/
this.displayPluginsControls = undefined
/**
* @access private
*/
this.textOpacity = undefined
/**
* @access private
*/
this.displayCodeHighlights = undefined
/**
* @access private
*/
this.adjustToSoftWrap = undefined
/**
* @access private
*/
this.useHardwareAcceleration = undefined
/**
* @access private
*/
this.absoluteMode = undefined
// Elements
/**
* @access private
*/
this.shadowRoot = undefined
/**
* @access private
*/
this.visibleArea = undefined
/**
* @access private
*/
this.controls = undefined
/**
* @access private
*/
this.scrollIndicator = undefined
/**
* @access private
*/
this.openQuickSettings = undefined
/**
* @access private
*/
this.quickSettingsElement = undefined
// States
/**
* @access private
*/
this.attached = undefined
/**
* @access private
*/
this.attachedToTextEditor = undefined
/**
* @access private
*/
this.standAlone = undefined
/**
* @access private
*/
this.wasVisible = undefined
// Other
/**
* @access private
*/
this.offscreenFirstRow = undefined
/**
* @access private
*/
this.offscreenLastRow = undefined
/**
* @access private
*/
this.frameRequested = undefined
/**
* @access private
*/
this.flexBasis = undefined
this.initializeContent()
return this.observeConfig({
'minimap.displayMinimapOnLeft': (displayMinimapOnLeft) => {
this.displayMinimapOnLeft = displayMinimapOnLeft
this.updateMinimapFlexPosition()
},
'minimap.minimapScrollIndicator': (minimapScrollIndicator) => {
this.minimapScrollIndicator = minimapScrollIndicator
if (this.minimapScrollIndicator && !(this.scrollIndicator != null) && !this.standAlone) {
this.initializeScrollIndicator()
} else if ((this.scrollIndicator != null)) {
this.disposeScrollIndicator()
}
if (this.attached) { this.requestUpdate() }
},
'minimap.displayPluginsControls': (displayPluginsControls) => {
this.displayPluginsControls = displayPluginsControls
if (this.displayPluginsControls && !(this.openQuickSettings != null) && !this.standAlone) {
this.initializeOpenQuickSettings()
} else if ((this.openQuickSettings != null)) {
this.disposeOpenQuickSettings()
}
},
'minimap.textOpacity': (textOpacity) => {
this.textOpacity = textOpacity
if (this.attached) { this.requestForcedUpdate() }
},
'minimap.displayCodeHighlights': (displayCodeHighlights) => {
this.displayCodeHighlights = displayCodeHighlights
if (this.attached) { this.requestForcedUpdate() }
},
'minimap.adjustMinimapWidthToSoftWrap': (adjustToSoftWrap) => {
this.adjustToSoftWrap = adjustToSoftWrap
if (this.attached) { this.measureHeightAndWidth() }
},
'minimap.useHardwareAcceleration': (useHardwareAcceleration) => {
this.useHardwareAcceleration = useHardwareAcceleration
if (this.attached) { this.requestUpdate() }
},
'minimap.absoluteMode': (absoluteMode) => {
this.absoluteMode = absoluteMode
return this.classList.toggle('absolute', this.absoluteMode)
},
'editor.preferredLineLength': () => {
if (this.attached) { this.measureHeightAndWidth() }
},
'editor.softWrap': () => {
if (this.attached) { this.requestUpdate() }
},
'editor.softWrapAtPreferredLineLength': () => {
if (this.attached) { this.requestUpdate() }
}
})
}
/**
* DOM callback invoked when a new MinimapElement is attached to the DOM.
*
* @access private
*/
attachedCallback () {
this.subscriptions.add(atom.views.pollDocument(() => { this.pollDOM() }))
this.measureHeightAndWidth()
this.updateMinimapFlexPosition()
this.attached = true
this.attachedToTextEditor = this.parentNode === this.getTextEditorElementRoot()
/*
We use `atom.styles.onDidAddStyleElement` instead of
`atom.themes.onDidChangeActiveThemes`.
Why? Currently, The style element will be removed first, and then re-added
and the `change` event has not be triggered in the process.
*/
return this.subscriptions.add(atom.styles.onDidAddStyleElement(() => {
this.invalidateDOMStylesCache()
this.requestForcedUpdate()
}))
}
/**
* DOM callback invoked when a new MinimapElement is detached from the DOM.
*
* @access private
*/
detachedCallback () {
this.attached = false
}
// ### ######## ######## ### ###### ## ##
// ## ## ## ## ## ## ## ## ## ##
// ## ## ## ## ## ## ## ## ##
// ## ## ## ## ## ## ## #########
// ######### ## ## ######### ## ## ##
// ## ## ## ## ## ## ## ## ## ##
// ## ## ## ## ## ## ###### ## ##
/**
* Returns whether the MinimapElement is currently visible on screen or not.
*
* The visibility of the minimap is defined by testing the size of the offset
* width and height of the element.
*
* @return {boolean} whether the MinimapElement is currently visible or not
*/
isVisible () { return this.offsetWidth > 0 || this.offsetHeight > 0 }
/**
* Attaches the MinimapElement to the DOM.
*
* The position at which the element is attached is defined by the
* `displayMinimapOnLeft` setting.
*
* @param {HTMLElement} [parent] the DOM node where attaching the minimap
* element
*/
attach (parent) {
if (this.attached) { return }
(parent || this.getTextEditorElementRoot()).appendChild(this)
}
/**
* Detaches the MinimapElement from the DOM.
*/
detach () {
if (!this.attached || this.parentNode == null) { return }
this.parentNode.removeChild(this)
}
/**
* Toggles the minimap left/right position based on the value of the
* `displayMinimapOnLeft` setting.
*
* @access private
*/
updateMinimapFlexPosition () {
this.classList.toggle('left', this.displayMinimapOnLeft)
}
/**
* Destroys this MinimapElement
*/
destroy () {
this.subscriptions.dispose()
this.detach()
this.minimap = null
}
// ###### ####### ## ## ######## ######## ## ## ########
// ## ## ## ## ### ## ## ## ### ## ##
// ## ## ## #### ## ## ## #### ## ##
// ## ## ## ## ## ## ## ###### ## ## ## ##
// ## ## ## ## #### ## ## ## #### ##
// ## ## ## ## ## ### ## ## ## ### ##
// ###### ####### ## ## ## ######## ## ## ##
/**
* Creates the content of the MinimapElement and attaches the mouse control
* event listeners.
*
* @access private
*/
initializeContent () {
this.initializeCanvas()
this.shadowRoot = this.createShadowRoot()
this.attachCanvases(this.shadowRoot)
this.createVisibleArea()
this.createControls()
this.subscriptions.add(this.subscribeTo(this, {
'mousewheel': (e) => {
if (!this.standAlone) { this.relayMousewheelEvent(e) }
}
}))
this.subscriptions.add(this.subscribeTo(this.getFrontCanvas(), {
'mousedown': (e) => { this.mousePressedOverCanvas(e) }
}))
}
/**
* Initializes the visible area div.
*
* @access private
*/
createVisibleArea () {
if (this.visibleArea) { return }
this.visibleArea = document.createElement('div')
this.visibleArea.classList.add('minimap-visible-area')
this.shadowRoot.appendChild(this.visibleArea)
this.visibleAreaSubscription = this.subscribeTo(this.visibleArea, {
'mousedown': (e) => { this.startDrag(e) },
'touchstart': (e) => { this.startDrag(e) }
})
this.subscriptions.add(this.visibleAreaSubscription)
}
/**
* Removes the visible area div.
*
* @access private
*/
removeVisibleArea () {
if (!this.visibleArea) { return }
this.subscriptions.remove(this.visibleAreaSubscription)
this.visibleAreaSubscription.dispose()
this.shadowRoot.removeChild(this.visibleArea)
delete this.visibleArea
}
/**
* Creates the controls container div.
*
* @access private
*/
createControls () {
if (this.controls || this.standAlone) { return }
this.controls = document.createElement('div')
this.controls.classList.add('minimap-controls')
this.shadowRoot.appendChild(this.controls)
}
/**
* Removes the controls container div.
*
* @access private
*/
removeControls () {
if (!this.controls) { return }
this.shadowRoot.removeChild(this.controls)
delete this.controls
}
/**
* Initializes the scroll indicator div when the `minimapScrollIndicator`
* settings is enabled.
*
* @access private
*/
initializeScrollIndicator () {
if (this.scrollIndicator || this.standAlone) { return }
this.scrollIndicator = document.createElement('div')
this.scrollIndicator.classList.add('minimap-scroll-indicator')
this.controls.appendChild(this.scrollIndicator)
}
/**
* Disposes the scroll indicator div when the `minimapScrollIndicator`
* settings is disabled.
*
* @access private
*/
disposeScrollIndicator () {
if (!this.scrollIndicator) { return }
this.controls.removeChild(this.scrollIndicator)
delete this.scrollIndicator
}
/**
* Initializes the quick settings openener div when the
* `displayPluginsControls` setting is enabled.
*
* @access private
*/
initializeOpenQuickSettings () {
if (this.openQuickSettings || this.standAlone) { return }
this.openQuickSettings = document.createElement('div')
this.openQuickSettings.classList.add('open-minimap-quick-settings')
this.controls.appendChild(this.openQuickSettings)
this.openQuickSettingSubscription = this.subscribeTo(this.openQuickSettings, {
'mousedown': (e) => {
e.preventDefault()
e.stopPropagation()
if ((this.quickSettingsElement != null)) {
this.quickSettingsElement.destroy()
this.quickSettingsSubscription.dispose()
} else {
this.quickSettingsElement = new MinimapQuickSettingsElement()
this.quickSettingsElement.setModel(this)
this.quickSettingsSubscription = this.quickSettingsElement.onDidDestroy(() => {
this.quickSettingsElement = null
})
let {top, left, right} = this.getFrontCanvas().getBoundingClientRect()
this.quickSettingsElement.style.top = top + 'px'
this.quickSettingsElement.attach()
if (this.displayMinimapOnLeft) {
this.quickSettingsElement.style.left = (right) + 'px'
} else {
this.quickSettingsElement.style.left = (left - this.quickSettingsElement.clientWidth) + 'px'
}
}
}
})
}
/**
* Disposes the quick settings openener div when the `displayPluginsControls`
* setting is disabled.
*
* @access private
*/
disposeOpenQuickSettings () {
if (!this.openQuickSettings) { return }
this.controls.removeChild(this.openQuickSettings)
this.openQuickSettingSubscription.dispose()
delete this.openQuickSettings
}
/**
* Returns the target `TextEditor` of the Minimap.
*
* @return {TextEditor} the minimap's text editor
*/
getTextEditor () { return this.minimap.getTextEditor() }
/**
* Returns the `TextEditorElement` for the Minimap's `TextEditor`.
*
* @return {TextEditorElement} the minimap's text editor element
*/
getTextEditorElement () {
if (this.editorElement) { return this.editorElement }
this.editorElement = atom.views.getView(this.getTextEditor())
return this.editorElement
}
/**
* Returns the root of the `TextEditorElement` content.
*
* This method is mostly used to ensure compatibility with the `shadowDom`
* setting.
*
* @return {HTMLElement} the root of the `TextEditorElement` content
*/
getTextEditorElementRoot () {
let editorElement = this.getTextEditorElement()
if (editorElement.shadowRoot) {
return editorElement.shadowRoot
} else {
return editorElement
}
}
/**
* Returns the root where to inject the dummy node used to read DOM styles.
*
* @param {boolean} shadowRoot whether to use the text editor shadow DOM
* or not
* @return {HTMLElement} the root node where appending the dummy node
* @access private
*/
getDummyDOMRoot (shadowRoot) {
if (shadowRoot) {
return this.getTextEditorElementRoot()
} else {
return this.getTextEditorElement()
}
}
// ## ## ####### ######## ######## ##
// ### ### ## ## ## ## ## ##
// #### #### ## ## ## ## ## ##
// ## ### ## ## ## ## ## ###### ##
// ## ## ## ## ## ## ## ##
// ## ## ## ## ## ## ## ##
// ## ## ####### ######## ######## ########
/**
* Returns the Minimap for which this MinimapElement was created.
*
* @return {Minimap} this element's Minimap
*/
getModel () { return this.minimap }
/**
* Defines the Minimap model for this MinimapElement instance.
*
* @param {Minimap} minimap the Minimap model for this instance.
* @return {Minimap} this element's Minimap
*/
setModel (minimap) {
this.minimap = minimap
this.subscriptions.add(this.minimap.onDidChangeScrollTop(() => {
this.requestUpdate()
}))
this.subscriptions.add(this.minimap.onDidChangeScrollLeft(() => {
this.requestUpdate()
}))
this.subscriptions.add(this.minimap.onDidDestroy(() => {
this.destroy()
}))
this.subscriptions.add(this.minimap.onDidChangeConfig(() => {
if (this.attached) { return this.requestForcedUpdate() }
}))
this.subscriptions.add(this.minimap.onDidChangeStandAlone(() => {
this.setStandAlone(this.minimap.isStandAlone())
this.requestUpdate()
}))
this.subscriptions.add(this.minimap.onDidChange((change) => {
this.pendingChanges.push(change)
this.requestUpdate()
}))
this.subscriptions.add(this.minimap.onDidChangeDecorationRange((change) => {
this.pendingDecorationChanges.push(change)
this.requestUpdate()
}))
this.setStandAlone(this.minimap.isStandAlone())
if (this.width != null && this.height != null) {
this.minimap.setScreenHeightAndWidth(this.height, this.width)
}
return this.minimap
}
/**
* Sets the stand-alone mode for this MinimapElement.
*
* @param {boolean} standAlone the new mode for this MinimapElement
*/
setStandAlone (standAlone) {
this.standAlone = standAlone
if (this.standAlone) {
this.setAttribute('stand-alone', true)
this.disposeScrollIndicator()
this.disposeOpenQuickSettings()
this.removeControls()
this.removeVisibleArea()
} else {
this.removeAttribute('stand-alone')
this.createVisibleArea()
this.createControls()
if (this.minimapScrollIndicator) { this.initializeScrollIndicator() }
if (this.displayPluginsControls) { this.initializeOpenQuickSettings() }
}
}
// ## ## ######## ######## ### ######## ########
// ## ## ## ## ## ## ## ## ## ##
// ## ## ## ## ## ## ## ## ## ##
// ## ## ######## ## ## ## ## ## ######
// ## ## ## ## ## ######### ## ##
// ## ## ## ## ## ## ## ## ##
// ####### ## ######## ## ## ## ########
/**
* Requests an update to be performed on the next frame.
*/
requestUpdate () {
if (this.frameRequested) { return }
this.frameRequested = true
requestAnimationFrame(() => {
this.update()
this.frameRequested = false
})
}
/**
* Requests an update to be performed on the next frame that will completely
* redraw the minimap.
*/
requestForcedUpdate () {
this.offscreenFirstRow = null
this.offscreenLastRow = null
this.requestUpdate()
}
/**
* Performs the actual MinimapElement update.
*
* @access private
*/
update () {
if (!(this.attached && this.isVisible() && this.minimap)) { return }
let minimap = this.minimap
minimap.enableCache()
let canvas = this.getFrontCanvas()
let visibleAreaLeft = minimap.getTextEditorScaledScrollLeft()
let visibleAreaTop = minimap.getTextEditorScaledScrollTop() - minimap.getScrollTop()
let visibleWidth = Math.min(canvas.width / devicePixelRatio, this.width)
if (this.adjustToSoftWrap && this.flexBasis) {
this.style.flexBasis = this.flexBasis + 'px'
} else {
this.style.flexBasis = null
}
if (SPEC_MODE) {
this.applyStyles(this.visibleArea, {
width: visibleWidth + 'px',
height: minimap.getTextEditorScaledHeight() + 'px',
top: visibleAreaTop + 'px',
left: visibleAreaLeft + 'px'
})
} else {
this.applyStyles(this.visibleArea, {
width: visibleWidth + 'px',
height: minimap.getTextEditorScaledHeight() + 'px',
transform: this.makeTranslate(visibleAreaLeft, visibleAreaTop)
})
}
this.applyStyles(this.controls, {width: visibleWidth + 'px'})
let canvasTop = minimap.getFirstVisibleScreenRow() * minimap.getLineHeight() - minimap.getScrollTop()
let canvasTransform = this.makeTranslate(0, canvasTop)
if (devicePixelRatio !== 1) {
canvasTransform += ' ' + this.makeScale(1 / devicePixelRatio)
}
if (SPEC_MODE) {
this.applyStyles(this.backLayer.canvas, {top: canvasTop + 'px'})
this.applyStyles(this.tokensLayer.canvas, {top: canvasTop + 'px'})
this.applyStyles(this.frontLayer.canvas, {top: canvasTop + 'px'})
} else {
this.applyStyles(this.backLayer.canvas, {transform: canvasTransform})
this.applyStyles(this.tokensLayer.canvas, {transform: canvasTransform})
this.applyStyles(this.frontLayer.canvas, {transform: canvasTransform})
}
if (this.minimapScrollIndicator && minimap.canScroll() && !this.scrollIndicator) {
this.initializeScrollIndicator()
}
if (this.scrollIndicator != null) {
let minimapScreenHeight = minimap.getScreenHeight()
let indicatorHeight = minimapScreenHeight * (minimapScreenHeight / minimap.getHeight())
let indicatorScroll = (minimapScreenHeight - indicatorHeight) * minimap.getCapedTextEditorScrollRatio()
if (SPEC_MODE) {
this.applyStyles(this.scrollIndicator, {
height: indicatorHeight + 'px',
top: indicatorScroll + 'px'
})
} else {
this.applyStyles(this.scrollIndicator, {
height: indicatorHeight + 'px',
transform: this.makeTranslate(0, indicatorScroll)
})
}
if (!minimap.canScroll()) { this.disposeScrollIndicator() }
}
this.updateCanvas()
minimap.clearCache()
}
/**
* Defines whether to render the code highlights or not.
*
* @param {Boolean} displayCodeHighlights whether to render the code
* highlights or not
*/
setDisplayCodeHighlights (displayCodeHighlights) {
this.displayCodeHighlights = displayCodeHighlights
if (this.attached) { this.requestForcedUpdate() }
}
/**
* Polling callback used to detect visibility and size changes.
*
* @access private
*/
pollDOM () {
let visibilityChanged = this.checkForVisibilityChange()
if (this.isVisible()) {
if (!this.wasVisible) { this.requestForcedUpdate() }
this.measureHeightAndWidth(visibilityChanged, false)
}
}
/**
* A method that checks for visibility changes in the MinimapElement.
* The method returns `true` when the visibility changed from visible to
* hidden or from hidden to visible.
*
* @return {boolean} whether the visibility changed or not since the last call
* @access private
*/
checkForVisibilityChange () {
if (this.isVisible()) {
if (this.wasVisible) {
return false
} else {
this.wasVisible = true
return this.wasVisible
}
} else {
if (this.wasVisible) {
this.wasVisible = false
return true
} else {
this.wasVisible = false
return this.wasVisible
}
}
}
/**
* A method used to measure the size of the MinimapElement and update internal
* components based on the new size.
*
* @param {boolean} visibilityChanged did the visibility changed since last
* measurement
* @param {[type]} [forceUpdate=true] forces the update even when no changes
* were detected
* @access private
*/
measureHeightAndWidth (visibilityChanged, forceUpdate = true) {
if (!this.minimap) { return }
let wasResized = this.width !== this.clientWidth || this.height !== this.clientHeight
this.height = this.clientHeight
this.width = this.clientWidth
let canvasWidth = this.width
if ((this.minimap != null)) { this.minimap.setScreenHeightAndWidth(this.height, this.width) }
if (wasResized || visibilityChanged || forceUpdate) { this.requestForcedUpdate() }
if (!this.isVisible()) { return }
if (wasResized || forceUpdate) {
if (this.adjustToSoftWrap) {
let lineLength = atom.config.get('editor.preferredLineLength')
let softWrap = atom.config.get('editor.softWrap')
let softWrapAtPreferredLineLength = atom.config.get('editor.softWrapAtPreferredLineLength')
let width = lineLength * this.minimap.getCharWidth()
if (softWrap && softWrapAtPreferredLineLength && lineLength && width <= this.width) {
this.flexBasis = width
canvasWidth = width
} else {
delete this.flexBasis
}
} else {
delete this.flexBasis
}
let canvas = this.getFrontCanvas()
if (canvasWidth !== canvas.width || this.height !== canvas.height) {
this.setCanvasesSize(
canvasWidth * devicePixelRatio,
(this.height + this.minimap.getLineHeight()) * devicePixelRatio
)
}
}
}
// ######## ## ## ######## ## ## ######## ######
// ## ## ## ## ### ## ## ## ##
// ## ## ## ## #### ## ## ##
// ###### ## ## ###### ## ## ## ## ######
// ## ## ## ## ## #### ## ##
// ## ## ## ## ## ### ## ## ##
// ######## ### ######## ## ## ## ######
/**
* Helper method to register config observers.
*
* @param {Object} configs={} an object mapping the config name to observe
* with the function to call back when a change
* occurs
* @access private
*/
observeConfig (configs = {}) {
for (let config in configs) {
this.subscriptions.add(atom.config.observe(config, configs[config]))
}
}
/**
* Callback triggered when the mouse is pressed on the MinimapElement canvas.
*
* @param {MouseEvent} e the mouse event object
* @access private
*/
mousePressedOverCanvas (e) {
if (this.minimap.isStandAlone()) { return }
if (e.which === 1) {
this.leftMousePressedOverCanvas(e)
} else if (e.which === 2) {
this.middleMousePressedOverCanvas(e)
let {top, height} = this.visibleArea.getBoundingClientRect()
this.startDrag({which: 2, pageY: top + height / 2}) // ugly hack
}
}
/**
* Callback triggered when the mouse left button is pressed on the
* MinimapElement canvas.
*
* @param {MouseEvent} e the mouse event object
* @param {number} e.pageY the mouse y position in page
* @param {HTMLElement} e.target the source of the event
* @access private
*/
leftMousePressedOverCanvas ({pageY, target}) {
let y = pageY - target.getBoundingClientRect().top
let row = Math.floor(y / this.minimap.getLineHeight()) + this.minimap.getFirstVisibleScreenRow()
let textEditor = this.minimap.getTextEditor()
let scrollTop = row * textEditor.getLineHeightInPixels() - this.minimap.getTextEditorHeight() / 2
if (atom.config.get('minimap.scrollAnimation')) {
let from = this.minimap.getTextEditorScrollTop()
let to = scrollTop
let step = (now) => this.minimap.setTextEditorScrollTop(now)
let duration = atom.config.get('minimap.scrollAnimationDuration')
this.animate({from: from, to: to, duration: duration, step: step})
} else {
this.minimap.setTextEditorScrollTop(scrollTop)
}
}
/**
* Callback triggered when the mouse middle button is pressed on the
* MinimapElement canvas.
*
* @param {MouseEvent} e the mouse event object
* @param {number} e.pageY the mouse y position in page
* @access private
*/
middleMousePressedOverCanvas ({pageY}) {
let {top: offsetTop} = this.getBoundingClientRect()
let y = pageY - offsetTop - this.minimap.getTextEditorScaledHeight() / 2
let ratio = y / (this.minimap.getVisibleHeight() - this.minimap.getTextEditorScaledHeight())
this.minimap.setTextEditorScrollTop(ratio * this.minimap.getTextEditorMaxScrollTop())
}
/**
* A method that relays the `mousewheel` events received by the MinimapElement
* to the `TextEditorElement`.
*
* @param {MouseEvent} e the mouse event object
* @access private
*/
relayMousewheelEvent (e) {
this.getTextEditorElement().component.onMouseWheel(e)
}
// ######## #### ########
// ## ## ## ## ## ##
// ## ## #### ## ##
// ## ## #### ## ##
// ## ## ## ## ## ## ##
// ## ## ## ## ## ##
// ######## #### ## ########
/**
* A method triggered when the mouse is pressed over the visible area that
* starts the dragging gesture.
*
* @param {MouseEvent} e the mouse event object
* @access private
*/
startDrag (e) {
let {which, pageY} = e
if (!this.minimap) { return }
if (which !== 1 && which !== 2 && !(e.touches != null)) { return }
let {top} = this.visibleArea.getBoundingClientRect()
let {top: offsetTop} = this.getBoundingClientRect()
let dragOffset = pageY - top
let initial = {dragOffset, offsetTop}
let mousemoveHandler = (e) => this.drag(e, initial)
let mouseupHandler = (e) => this.endDrag(e, initial)
document.body.addEventListener('mousemove', mousemoveHandler)
document.body.addEventListener('mouseup', mouseupHandler)
document.body.addEventListener('mouseleave', mouseupHandler)
document.body.addEventListener('touchmove', mousemoveHandler)
document.body.addEventListener('touchend', mouseupHandler)
this.dragSubscription = new Disposable(function () {
document.body.removeEventListener('mousemove', mousemoveHandler)
document.body.removeEventListener('mouseup', mouseupHandler)
document.body.removeEventListener('mouseleave', mouseupHandler)
document.body.removeEventListener('touchmove', mousemoveHandler)
document.body.removeEventListener('touchend', mouseupHandler)
})
}
/**
* The method called during the drag gesture.
*
* @param {MouseEvent} e the mouse event object
* @param {Object} initial
* @param {number} initial.dragOffset the mouse offset within the visible
* area
* @param {number} initial.offsetTop the MinimapElement offset at the moment
* of the drag start
* @access private
*/
drag (e, initial) {
if (!this.minimap) { return }
if (e.which !== 1 && e.which !== 2 && !(e.touches != null)) { return }
let y = e.pageY - initial.offsetTop - initial.dragOffset
let ratio = y / (this.minimap.getVisibleHeight() - this.minimap.getTextEditorScaledHeight())
this.minimap.setTextEditorScrollTop(ratio * this.minimap.getTextEditorMaxScrollTop())
}
/**
* The method that ends the drag gesture.
*
* @param {MouseEvent} e the mouse event object
* @param {Object} initial
* @param {number} initial.dragOffset the mouse offset within the visible
* area
* @param {number} initial.offsetTop the MinimapElement offset at the moment
* of the drag start
* @access private
*/
endDrag (e, initial) {
if (!this.minimap) { return }
this.dragSubscription.dispose()
}
// ###### ###### ######
// ## ## ## ## ## ##
// ## ## ##
// ## ###### ######
// ## ## ##
// ## ## ## ## ## ##
// ###### ###### ######
/**
* Applies the passed-in styles properties to the specified element
*
* @param {HTMLElement} element the element onto which apply the styles
* @param {Object} styles the styles to apply
* @access private
*/
applyStyles (element, styles) {
if (!element) { return }
let cssText = ''
for (let property in styles) {
cssText += `${property}: ${styles[property]}; `
}
element.style.cssText = cssText
}
/**
* Returns a string with a CSS translation tranform value.
*
* @param {number} [x = 0] the x offset of the translation
* @param {number} [y = 0] the y offset of the translation
* @return {string} the CSS translation string
* @access private
*/
makeTranslate (x = 0, y = 0) {
if (this.useHardwareAcceleration) {
return `translate3d(${x}px, ${y}px, 0)`
} else {
return `translate(${x}px, ${y}px)`
}
}
/**
* Returns a string with a CSS scaling tranform value.
*
* @param {number} [x = 0] the x scaling factor
* @param {number} [y = 0] the y scaling factor
* @return {string} the CSS scaling string
* @access private
*/
makeScale (x = 0, y = x) {
if (this.useHardwareAcceleration) {
return `scale3d(${x}, ${y}, 1)`
} else {
return `scale(${x}, ${y})`
}
}
/**
* A method that return the current time as a Date.
*
* That method exist so that we can mock it in tests.
*
* @return {Date} the current time as Date
* @access private
*/
getTime () { return new Date() }
/**
* A method that mimic the jQuery `animate` method and used to animate the
* scroll when clicking on the MinimapElement canvas.
*
* @param {Object} param the animation data object
* @param {[type]} param.from the start value
* @param {[type]} param.to the end value
* @param {[type]} param.duration the animation duration
* @param {[type]} param.step the easing function for the animation
* @access private
*/
animate ({from, to, duration, step}) {
let progress
let start = this.getTime()
let swing = function (progress) {
return 0.5 - Math.cos(progress * Math.PI) / 2
}
let update = () => {
let passed = this.getTime() - start
if (duration === 0) {
progress = 1
} else {
progress = passed / duration
}
if (progress > 1) { progress = 1 }
let delta = swing(progress)
step(from + (to - from) * delta)
if (progress < 1) { requestAnimationFrame(update) }
}
update()
}
}