src/plugin/markers/index.js
/**
* @typedef {Object} MarkerParams
* @desc The parameters used to describe a marker.
* @example wavesurfer.addMarker(regionParams);
* @property {number} time The time to set the marker at
* @property {?label} string An optional marker label
* @property {?tooltip} string An optional marker tooltip
* @property {?color} string Background color for marker
* @property {?position} string "top" or "bottom", defaults to "bottom"
* @property {?markerElement} element An HTML element to display instead of the default marker image
* @property {?draggable} boolean Set marker as draggable, defaults to false
* @property {?boolean} preventContextMenu Determines whether the context menu
* is prevented from being opened, defaults to false
*/
/**
* Markers are points in time in the audio that can be jumped to.
*
* @implements {PluginClass}
*
* @example
* import MarkersPlugin from 'wavesurfer.markers.js';
*
* // if you are using <script> tags
* var MarkerPlugin = window.WaveSurfer.markers;
*
* // ... initialising wavesurfer with the plugin
* var wavesurfer = WaveSurfer.create({
* // wavesurfer options ...
* plugins: [
* MarkersPlugin.create({
* // plugin options ...
* })
* ]
* });
*/
const DEFAULT_FILL_COLOR = "#D8D8D8";
const DEFAULT_POSITION = "bottom";
export default class MarkersPlugin {
/**
* @typedef {Object} MarkersPluginParams
* @property {?MarkerParams[]} markers Initial set of markers
* @fires MarkersPlugin#marker-click
* @fires MarkersPlugin#marker-drag
* @fires MarkersPlugin#marker-drop
*/
/**
* Markers plugin definition factory
*
* This function must be used to create a plugin definition which can be
* used by wavesurfer to correctly instantiate the plugin.
*
* @param {MarkersPluginParams} params parameters use to initialise the plugin
* @since 4.6.0
* @return {PluginDefinition} an object representing the plugin
*/
static create(params) {
return {
name: 'markers',
deferInit: params && params.deferInit ? params.deferInit : false,
params: params,
staticProps: {
addMarker(options) {
if (!this.initialisedPluginList.markers) {
this.initPlugin('markers');
}
return this.markers.add(options);
},
getMarkers() {
return this.markers;
},
clearMarkers() {
this.markers && this.markers.clear();
}
},
instance: MarkersPlugin
};
}
constructor(params, ws) {
this.params = params;
this.wavesurfer = ws;
this.util = ws.util;
this.style = this.util.style;
this.markerLineWidth = 1;
this.markerWidth = 11;
this.markerHeight = 22;
this.dragging = false;
this._onResize = () => {
this._updateMarkerPositions();
};
this._onBackendCreated = () => {
this.wrapper = this.wavesurfer.drawer.wrapper;
if (this.params.markers) {
this.params.markers.forEach(marker => this.add(marker));
}
window.addEventListener('resize', this._onResize, true);
window.addEventListener('orientationchange', this._onResize, true);
this.wavesurfer.on('zoom', this._onResize);
if (!this.markers.find(marker => marker.draggable)){
return;
}
this.onMouseMove = (e) => this._onMouseMove(e);
window.addEventListener('mousemove', this.onMouseMove);
this.onMouseUp = (e) => this._onMouseUp(e);
window.addEventListener("mouseup", this.onMouseUp);
};
this.markers = [];
this._onReady = () => {
this.wrapper = this.wavesurfer.drawer.wrapper;
this._updateMarkerPositions();
};
}
init() {
// Check if ws is ready
if (this.wavesurfer.isReady) {
this._onBackendCreated();
this._onReady();
} else {
this.wavesurfer.once('ready', this._onReady);
this.wavesurfer.once('backend-created', this._onBackendCreated);
}
}
destroy() {
this.wavesurfer.un('ready', this._onReady);
this.wavesurfer.un('backend-created', this._onBackendCreated);
this.wavesurfer.un('zoom', this._onResize);
window.removeEventListener('resize', this._onResize, true);
window.removeEventListener('orientationchange', this._onResize, true);
if (this.onMouseMove) {
window.removeEventListener('mousemove', this.onMouseMove);
}
if (this.onMouseUp) {
window.removeEventListener("mouseup", this.onMouseUp);
}
this.clear();
}
/**
* Add a marker
*
* @param {MarkerParams} params Marker definition
* @return {object} The created marker
*/
add(params) {
let marker = {
time: params.time,
label: params.label,
tooltip: params.tooltip,
color: params.color || DEFAULT_FILL_COLOR,
position: params.position || DEFAULT_POSITION,
draggable: !!params.draggable,
preventContextMenu: !!params.preventContextMenu
};
marker.el = this._createMarkerElement(marker, params.markerElement);
this.wrapper.appendChild(marker.el);
this.markers.push(marker);
this._updateMarkerPositions();
this._registerEvents();
return marker;
}
/**
* Remove a marker
*
* @param {number|Object} indexOrMarker Index of the marker to remove or the marker object itself
*/
remove(indexOrMarker) {
let index = indexOrMarker;
if (isNaN(index)) {
index = this.markers.findIndex(marker => marker === indexOrMarker);
}
let marker = this.markers[index];
if (!marker) {
return;
}
let label = marker.el.getElementsByClassName("marker-label")[0];
if (label) {
if (label._onContextMenu) {
label.removeEventListener("contextmenu", label._onContextMenu);
}
if (label._onClick) {
label.removeEventListener("click", label._onClick);
}
if (label._onMouseDown) {
label.removeEventListener("mousedown", label._onMouseDown);
}
}
this.wrapper.removeChild(marker.el);
this.markers.splice(index, 1);
this._unregisterEvents();
}
_createPointerSVG(color, position) {
const svgNS = "http://www.w3.org/2000/svg";
const el = document.createElementNS(svgNS, "svg");
const polygon = document.createElementNS(svgNS, "polygon");
el.setAttribute("viewBox", "0 0 40 80");
polygon.setAttribute("id", "polygon");
polygon.setAttribute("stroke", "#979797");
polygon.setAttribute("fill", color);
polygon.setAttribute("points", "20 0 40 30 40 80 0 80 0 30");
if ( position == "top" ) {
polygon.setAttribute("transform", "rotate(180, 20 40)");
}
el.appendChild(polygon);
this.style(el, {
width: this.markerWidth + "px",
height: this.markerHeight + "px",
"min-width": this.markerWidth + "px",
"margin-right": "5px",
"z-index": 4
});
return el;
}
_createMarkerElement(marker, markerElement) {
let label = marker.label;
let tooltip = marker.tooltip;
const el = document.createElement('marker');
el.className = "wavesurfer-marker";
this.style(el, {
position: "absolute",
height: "100%",
display: "flex",
overflow: "hidden",
"flex-direction": (marker.position == "top" ? "column-reverse" : "column")
});
const line = document.createElement('div');
const width = markerElement ? markerElement.width : this.markerWidth;
marker.offset = (width - this.markerLineWidth) / 2;
this.style(line, {
"flex-grow": 1,
"margin-left": marker.offset + "px",
background: "black",
width: this.markerLineWidth + "px",
opacity: 0.1
});
el.appendChild(line);
const labelDiv = document.createElement('div');
const point = markerElement || this._createPointerSVG(marker.color, marker.position);
if (marker.draggable){
point.draggable = false;
}
labelDiv.appendChild(point);
if ( label ) {
const labelEl = document.createElement('span');
labelEl.innerText = label;
labelEl.setAttribute('title', tooltip);
this.style(labelEl, {
"font-family": "inherit",
"font-size": "90%"
});
labelDiv.appendChild(labelEl);
}
this.style(labelDiv, {
display: "flex",
"align-items": "center",
cursor: "pointer"
});
labelDiv.classList.add("marker-label");
el.appendChild(labelDiv);
labelDiv._onClick = (e) => {
e.stopPropagation();
// Click event is caught when the marker-drop event was dispatched.
// Drop event was dispatched at this moment, but this.dragging
// is waiting for the next tick to set as false
if (this.dragging){
return;
}
this.wavesurfer.setCurrentTime(marker.time);
this.wavesurfer.fireEvent("marker-click", marker, e);
};
labelDiv.addEventListener("click", labelDiv._onClick);
labelDiv._onContextMenu = (e) => {
if (marker.preventContextMenu) {
e.preventDefault();
}
this.wavesurfer.fireEvent("marker-contextmenu", marker, e);
};
labelDiv.addEventListener("contextmenu", labelDiv._onContextMenu);
if (marker.draggable) {
labelDiv._onMouseDown = () => {
this.selectedMarker = marker;
};
labelDiv.addEventListener("mousedown", labelDiv._onMouseDown);
}
return el;
}
_updateMarkerPositions() {
for ( let i = 0 ; i < this.markers.length; i++ ) {
let marker = this.markers[i];
this._updateMarkerPosition(marker);
}
}
/**
* Update a marker position based on its time property.
*
* @private
* @param {MarkerParams} params The marker to update.
* @returns {void}
*/
_updateMarkerPosition(params) {
const duration = this.wavesurfer.getDuration();
const elementWidth =
this.wavesurfer.drawer.width /
this.wavesurfer.params.pixelRatio;
const positionPct = Math.min(params.time / duration, 1);
const leftPx = ((elementWidth * positionPct) - params.offset);
this.style(params.el, {
"left": leftPx + "px",
"max-width": (elementWidth - leftPx) + "px"
});
}
/**
* Fires `marker-drag` event, update the `time` property for the
* selected marker based on the mouse position, and calls to update
* its position.
*
* @private
* @param {MouseEvent} event The mouse event.
* @returns {void}
*/
_onMouseMove(event) {
if (!this.selectedMarker){
return;
}
if (!this.dragging){
this.dragging = true;
this.wavesurfer.fireEvent("marker-drag", this.selectedMarker, event);
}
this.selectedMarker.time = this.wavesurfer.drawer.handleEvent(event) * this.wavesurfer.getDuration();
this._updateMarkerPositions();
}
/**
* Fires `marker-drop` event and unselect the dragged marker.
*
* @private
* @param {MouseEvent} event The mouse event.
* @returns {void}
*/
_onMouseUp(event) {
if (this.selectedMarker) {
setTimeout(() => {
this.selectedMarker = false;
this.dragging = false;
}, 0);
}
if (!this.dragging) {
return;
}
event.stopPropagation();
const duration = this.wavesurfer.getDuration();
this.selectedMarker.time = this.wavesurfer.drawer.handleEvent(event) * duration;
this._updateMarkerPositions();
this.wavesurfer.fireEvent("marker-drop", this.selectedMarker, event);
}
_registerEvents() {
if (!this.markers.find(marker => marker.draggable)) {
return;
}
//we have some draggable markers, check for listeners
if (!this.onMouseMove) {
this.onMouseMove = (e) => this._onMouseMove(e);
window.addEventListener('mousemove', this.onMouseMove);
}
if (!this.onMouseUp) {
this.onMouseUp = (e) => this._onMouseUp(e);
window.addEventListener("mouseup", this.onMouseUp);
}
}
_unregisterEvents() {
if (this.markers.find(marker => marker.draggable)) {
return;
}
//we don't have any draggable markers, unregister listeners
if (this.onMouseMove) {
window.removeEventListener('mousemove', this.onMouseMove);
this.onMouseMove = null;
}
if (this.onMouseUp) {
window.removeEventListener("mouseup", this.onMouseUp);
this.onMouseUp = null;
}
}
/**
* Remove all markers
*/
clear() {
while ( this.markers.length > 0 ) {
this.remove(0);
}
}
}