satie
Version:
A sheet music renderer for the web
372 lines (371 loc) • 16.1 kB
JavaScript
/**
* This file is part of Satie music engraver <https://github.com/jnetterf/satie>.
* Copyright (C) Joshua Netterfield <joshua.ca> 2015 - present.
*
* Satie is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* Satie is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Satie. If not, see <http://www.gnu.org/licenses/>.
*/
"use strict";
var __extends = (this && this.__extends) || function (d, b) {
for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p];
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
var react_1 = require("react");
var lodash_1 = require("lodash");
var react_2 = require("react");
var operations_1 = require("musicxml-interfaces/operations");
var invariant = require("invariant");
var document_1 = require("./document");
var engine_createPatch_1 = require("./engine_createPatch");
var private_chordUtil_1 = require("./private_chordUtil");
var private_views_metadata_1 = require("./private_views_metadata");
var private_patchImpl_1 = require("./private_patchImpl");
var engine_import_1 = require("./engine_import");
var engine_export_1 = require("./engine_export");
var engine_applyOp_1 = require("./engine_applyOp");
var NOT_READY_ERROR = "The document is not yet initialized.";
var SATIE_ELEMENT_RX = /SATIE([0-9]*)_(\w*)_(\w*)_(\w*)_(\w*)_(\w*)/;
/**
* Represents a song as:
* - some MusicXML, as a string
* - a series of patches applied on top of the MusicXML.
*
* The class can be used:
* - as a React component, to render to the DOM.
* - as a simple class (in this case call song.run() to load or update the document) to
* export to MusicXML (toMusicXML()) or SVG (toSVG()).
*
* Note: toMusicXML and toSVG can also be used when Song is used as a React component
* (e.g., <Song ... ref={song => console.log(song.toMusicXML())} />)
*/
var SongImpl = (function (_super) {
__extends(SongImpl, _super);
function SongImpl() {
var _this = _super !== null && _super.apply(this, arguments) || this;
_this.state = {
document: null,
factory: null,
};
_this._docPatches = [];
_this._page1 = null;
/**
* Returns the document represented by the song. The document represents the
* current state of the song.
*
* - The returned document is constant (i.e., do not modify the document)
* - The returned document is NOT immutable (i.e., the document may change
* after patches are applied), or the song is re-rendered.
* - The song may load a new document without warning after any operation.
* As such, do not cache the document.
* - The document's API is not finalized. If you depend on this function call,
* expect breakages.
* - This API call will eventually be removed and replaced with higher-level
* functions.
*/
_this.getDocument = function (operations) {
if (!_this.state.document) {
throw new Error(NOT_READY_ERROR);
}
if (operations instanceof private_patchImpl_1.default) {
_this._rectify$(operations.content, operations.isPreview, function () { return operations.isPreview = false; });
return _this.state.document;
}
else if (!operations) {
_this._rectify$([], false, function () { return void 0; });
return _this.state.document;
}
throw new Error("Invalid operations element");
};
/**
* Given a set of OT diffs, returns something the "patches" prop can be set to.
*/
_this.createCanonicalPatch = function () {
var patchSpecs = [];
for (var _i = 0; _i < arguments.length; _i++) {
patchSpecs[_i] = arguments[_i];
}
return _this._createPatch(false, patchSpecs);
};
/**
* Given a set of operations, returns a set of operations that the "preview" prop can
* be set to.
*/
_this.createPreviewPatch = function () {
var patchSpecs = [];
for (var _i = 0; _i < arguments.length; _i++) {
patchSpecs[_i] = arguments[_i];
}
return _this._createPatch(true, patchSpecs);
};
_this.toSVG = function () {
var patches = _this.props.patches;
if (patches instanceof private_patchImpl_1.default) {
invariant(patches.isPreview === false, "Cannot render an SVG with a previewed patch");
_this._rectify$(patches.content, patches.isPreview, function () { patches.isPreview = false; });
}
else if (!patches) {
_this._rectify$([], false, function () { return void 0; });
}
else {
invariant(false, "Song.props.patches was not created through createPreviewPatch or createCanonicalPatch");
}
return _this.state.document.renderToStaticMarkup(0);
};
_this.toMusicXML = function () {
var patches = _this.props.patches;
if (patches instanceof private_patchImpl_1.default) {
invariant(patches.isPreview === false, "Cannot render MusicXML with a previewed patch");
_this._rectify$(patches.content, patches.isPreview, function () { return patches.preview = false; });
}
else if (!patches) {
_this._rectify$([], false, function () { return void 0; });
}
else {
invariant(false, "Song.props.patches was not created through createPreviewPatch or createCanonicalPatch");
}
return engine_export_1.exportXML(_this.state.document);
};
_this._rectifyAppendCanonical = function (ops) {
_this._rectify$(_this._docPatches.concat(ops), false, function () { return void 0; });
};
_this._rectifyAppendPreview = function (ops) {
_this._rectify$(_this._docPatches.concat(ops), true, function () { return void 0; });
};
_this._preRender = function (props) {
if (props === void 0) { props = _this.props; }
var patches = _this.props.patches;
if (patches instanceof private_patchImpl_1.default) {
_this._update$(patches.content, patches.isPreview, props);
}
else if (!patches) {
_this._update$([], false, props);
}
else {
invariant(false, "Internal error: preRender called, but the state is invalid.");
}
};
_this._syncSVG = function (svg) {
_this._svg = svg;
_this._pt = svg ? svg.createSVGPoint() : null;
};
_this._handleCursorPosition = lodash_1.throttle(function (p, handler) {
var match = private_views_metadata_1.get(p);
var path = match && match.key.match(SATIE_ELEMENT_RX);
if (!path) {
handler({
path: [],
pitch: null,
pos: p,
matchedOriginY: null,
_private: null,
});
return;
}
path = path.slice(1);
var measure = lodash_1.find(_this.state.document.measures, function (measure) { return 1 * measure.uuid === parseInt(path[0], 10); });
var el = measure[path[1]][path[2]][path[3]][path[4]][path[5]];
if (el) {
var originY = match.originY;
var clef = el._clef;
var pitch = void 0;
if (clef && originY) {
pitch = private_chordUtil_1.pitchForClef(originY - p.y, clef);
}
handler({
path: path,
pitch: pitch,
pos: p,
matchedOriginY: originY,
_private: match.obj,
});
}
}, 18);
_this._handleMouseMove = function (ev) {
var p = _this._getPos(ev);
if (p) {
_this._handleCursorPosition(p, _this.props.onMouseMove);
}
};
_this._handleClick = function (ev) {
var p = _this._getPos(ev);
if (p) {
_this._handleCursorPosition(p, _this.props.onMouseClick);
}
};
return _this;
}
SongImpl.prototype.render = function () {
// Note: we rectify/render before this is called. We assume shouldComponentUpdate
// stops temporary states from being rendered.
return react_2.createElement("div", {
onMouseMove: this.props.onMouseMove && this._handleMouseMove,
onClick: this.props.onMouseClick && this._handleClick,
}, this._page1);
};
SongImpl.prototype.shouldComponentUpdate = function (nextProps) {
return nextProps.baseSrc !== this.props.baseSrc ||
nextProps.patches !== this.props.patches ||
nextProps.pageClassName !== this.props.pageClassName ||
nextProps.singleLineMode !== this.props.singleLineMode ||
nextProps.fixedMeasureWidth !== this.props.fixedMeasureWidth;
};
SongImpl.prototype.componentWillReceiveProps = function (nextProps) {
if (nextProps.baseSrc !== this.props.baseSrc) {
this._loadXML(nextProps.baseSrc);
}
else if (nextProps.pageClassName !== this.props.pageClassName ||
nextProps.fixedMeasureWidth !== this.props.fixedMeasureWidth ||
nextProps.singleLineMode !== this.props.singleLineMode) {
this._preRender(nextProps);
}
else if (nextProps.patches !== this.props.patches) {
var patches = nextProps.patches;
if (patches instanceof private_patchImpl_1.default) {
this._update$(patches.content, patches.isPreview);
}
else if (!patches) {
this._update$([], false);
}
else {
throw new Error("Invalid patch.");
}
}
};
SongImpl.prototype.componentWillMount = function () {
this._loadXML(this.props.baseSrc);
};
Object.defineProperty(SongImpl.prototype, "header", {
get: function () {
if (this.state && this.state.document) {
return this.state.document.header;
}
return null;
},
set: function (header) {
if (this.state) {
throw new Error("Cannot set header. Use patches.");
}
// Do nothing -- makeExportsHot is running, probably.
},
enumerable: true,
configurable: true
});
SongImpl.prototype.run = function () {
var _this = this;
this.setState = function (state, cb) {
lodash_1.extend(_this.state, state);
if (cb) {
cb();
}
};
this.forceUpdate = function () {
// no-op
};
if (!this._isRunningWithoutDOM) {
this.componentWillMount();
}
this._isRunningWithoutDOM = true;
this.componentWillReceiveProps(this.props);
};
SongImpl.prototype._createPatch = function (isPreview, patchSpecs) {
var _this = this;
var patches = patchSpecs.reduce(function (array, spec) {
if (document_1.specIsRaw(spec)) {
return array.concat(spec.raw);
}
else if (document_1.specIsDocBuilder(spec)) {
return array.concat(engine_createPatch_1.default(isPreview, _this.state.document, spec.documentBuilder));
}
else if (document_1.specIsPartBuilder(spec)) {
return array.concat(engine_createPatch_1.default(isPreview, _this.state.document, spec.measure, spec.part, spec.partBuilder));
}
else if (spec instanceof private_patchImpl_1.default) {
return array.concat(spec.content);
}
else if (!spec) {
return array;
}
throw new Error("Invalid patch spec.");
}, []);
this._update$(patches, isPreview);
return new private_patchImpl_1.default(this._docPatches.slice(), isPreview);
};
SongImpl.prototype._rectify$ = function (newPatches, preview, notEligableForPreview) {
var _this = this;
var docPatches = this._docPatches;
var factory = this.state.factory;
var commonVersion = function () {
var maxPossibleCommonVersion = Math.min(docPatches.length, newPatches.length);
for (var i = 0; i < maxPossibleCommonVersion; ++i) {
if (!lodash_1.isEqual(docPatches[i], newPatches[i])) {
return i;
}
}
return maxPossibleCommonVersion;
};
var initialCommon = commonVersion();
// Undo actions not in common
lodash_1.forEach(operations_1.invert(docPatches.slice(initialCommon)), function (op) {
engine_applyOp_1.default(preview, _this.state.document.measures, factory, op, _this.state.document, notEligableForPreview);
docPatches.pop();
});
// Perform actions that are expected.
lodash_1.forEach(newPatches.slice(this._docPatches.length), function (op) {
engine_applyOp_1.default(preview, _this.state.document.measures, factory, op, _this.state.document, notEligableForPreview);
docPatches.push(op);
});
invariant(docPatches.length === newPatches.length, "Something went wrong in _rectify. The current state is now invalid.");
};
;
SongImpl.prototype._update$ = function (patches, isPreview, props) {
if (props === void 0) { props = this.props; }
this._rectify$(patches, isPreview, function () { return isPreview = false; });
this._page1 = this.state.document.__getPage(0, isPreview, "svg-web", props.pageClassName || "", props.singleLineMode, props.fixedMeasureWidth, isPreview ? this._rectifyAppendPreview : this._rectifyAppendCanonical, this._syncSVG, props.onPageHeightChanged);
this.forceUpdate();
};
SongImpl.prototype._getPos = function (ev) {
if (!this._svg.contains(ev.target)) {
return null;
}
// Get point in global SVG space
this._pt.x = ev.clientX;
this._pt.y = ev.clientY;
return this._pt.matrixTransform(this._svg.getScreenCTM().inverse());
};
SongImpl.prototype._loadXML = function (xml) {
var _this = this;
this.setState({
document: null,
factory: null,
});
engine_import_1.importXML(xml, function (error, loadedDocument, loadedFactory) {
if (error) {
_this.props.onError(error);
}
else {
_this.setState({
document: loadedDocument,
factory: loadedFactory,
}, _this._preRender);
}
invariant(!_this.props.patches, "Expected patches to be empty on document load.");
if (_this.props.onLoaded) {
_this.props.onLoaded();
}
});
};
return SongImpl;
}(react_1.Component));
Object.defineProperty(exports, "__esModule", { value: true });
exports.default = SongImpl;