File size: 13,922 Bytes
af6912c |
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 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 |
// window.ABCJS.Editor:
//
// constructor(editarea, params)
// if editarea is a string, then it is an HTML id of a textarea control.
// Otherwise, it should be an instantiation of an object that expresses the EditArea interface.
//
// params is a hash of:
// canvas_id: or paper_id: HTML id to draw in. If not present, then the drawing happens just below the editor.
// generate_midi: if present, then midi is generated.
// midi_id: if present, the HTML id to place the midi control. Otherwise it is placed in the same div as the paper.
// midi_download_id: if present, the HTML id to place the midi download link. Otherwise it is placed in the same div as the paper.
// generate_warnings: if present, then parser warnings are displayed on the page.
// warnings_id: if present, the HTML id to place the warnings. Otherwise they are placed in the same div as the paper.
// onchange: if present, the callback function to call whenever there has been a change.
// gui: if present, the paper can send changes back to the editor (presumably because the user changed something directly.)
// parser_options: options to send to the parser engine.
// midi_options: options to send to the midi engine.
// render_options: options to send to the render engine.
// indicate_changed: the dirty flag is set if this is true.
//
// - setReadOnly(bool)
// adds or removes the class abc_textarea_readonly, and adds or removes the attribute readonly=yes
// - setDirtyStyle(bool)
// adds or removes the class abc_textarea_dirty
// - modelChanged()
// Called when the model has been changed to trigger re-rendering
// - parseABC()
// Called internally by fireChanged()
// returns true if there has been a change since last call.
// - updateSelection()
// Called when the user has changed the selection. This calls the engraver to show the selection.
// - fireSelectionChanged()
// Called by the textarea object when the user has changed the selection.
// - paramChanged(engraverparams)
// Called to signal that the engraver params have changed, so re-rendering should occur.
// - fireChanged()
// Called by the textarea object when the user has changed something.
// - setNotDirty()
// Called by the client app to reset the dirty flag
// - isDirty()
// Returns true or false, whether the textarea contains the same text that it started with.
// - highlight(abcelem)
// Called by the engraver to highlight an area.
// - pause(bool)
// Stops the automatic rendering when the user is typing.
//
var parseCommon = require('../parse/abc_common');
var SynthController = require('../synth/synth-controller');
var supportsAudio = require('../synth/supports-audio');
var renderAbc = require('../api/abc_tunebook_svg');
var EditArea = require('./abc_editarea');
function gatherAbcParams(params) {
// There used to be a bunch of ways parameters can be passed in. This just simplifies it.
var abcjsParams = {};
var key;
if (params.abcjsParams) {
for (key in params.abcjsParams) {
if (params.abcjsParams.hasOwnProperty(key)) {
abcjsParams[key] = params.abcjsParams[key];
}
}
}
if (params.midi_options) {
for (key in params.midi_options) {
if (params.midi_options.hasOwnProperty(key)) {
abcjsParams[key] = params.midi_options[key];
}
}
}
if (params.parser_options) {
for (key in params.parser_options) {
if (params.parser_options.hasOwnProperty(key)) {
abcjsParams[key] = params.parser_options[key];
}
}
}
if (params.render_options) {
for (key in params.render_options) {
if (params.render_options.hasOwnProperty(key)) {
abcjsParams[key] = params.render_options[key];
}
}
}
/*
if (params.tablature_options) {
abcjsParams['tablatures'] = params.tablature_options;
}
*/
if (abcjsParams.tablature) {
if (params.warnings_id) {
// store for plugin error handling
abcjsParams.tablature.warnings_id = params.warnings_id;
}
}
return abcjsParams;
}
var Editor = function(editarea, params) {
// Copy all the options that will be passed through
this.abcjsParams = gatherAbcParams(params);
if (params.indicate_changed)
this.indicate_changed = true;
if (typeof editarea === "string") {
this.editarea = new EditArea(editarea);
} else {
this.editarea = editarea;
}
this.editarea.addSelectionListener(this);
this.editarea.addChangeListener(this);
if (params.canvas_id) {
this.div = params.canvas_id;
} else if (params.paper_id) {
this.div = params.paper_id;
} else {
this.div = document.createElement("DIV");
this.editarea.getElem().parentNode.insertBefore(this.div, this.editarea.getElem());
}
if (typeof this.div === 'string')
this.div = document.getElementById(this.div);
if (params.selectionChangeCallback) {
this.selectionChangeCallback = params.selectionChangeCallback;
}
this.clientClickListener = this.abcjsParams.clickListener;
this.abcjsParams.clickListener = this.highlight.bind(this);
if (params.synth) {
if (supportsAudio()) {
this.synth = {
el: params.synth.el,
cursorControl: params.synth.cursorControl,
options: params.synth.options
};
}
}
// If the user wants midi, then store the elements that it will be written to. The element could either be passed in as an id,
// an element, or nothing. If nothing is passed in, then just put the midi on top of the generated music.
if (params.generate_midi) {
this.generate_midi = params.generate_midi;
if (this.abcjsParams.generateDownload) {
if (typeof params.midi_download_id === 'string')
this.downloadMidi = document.getElementById(params.midi_download_id);
else if (params.midi_download_id) // assume, if the var is not a string it is an element. If not, it will crash soon enough.
this.downloadMidi = params.midi_download_id;
}
if (this.abcjsParams.generateInline !== false) { // The default for this is true, so undefined is also true.
if (typeof params.midi_id === 'string')
this.inlineMidi = document.getElementById(params.midi_id);
else if (params.midi_id) // assume, if the var is not a string it is an element. If not, it will crash soon enough.
this.inlineMidi = params.midi_id;
}
}
if (params.warnings_id) {
if (typeof(params.warnings_id) === "string")
this.warningsdiv = document.getElementById(params.warnings_id);
else
this.warningsdiv = params.warnings_id;
} else if (params.generate_warnings) {
this.warningsdiv = document.createElement("div");
this.div.parentNode.insertBefore(this.warningsdiv, this.div);
}
this.onchangeCallback = params.onchange;
this.currentAbc = "";
this.tunes = [];
this.bReentry = false;
this.parseABC();
this.modelChanged();
this.addClassName = function(element, className) {
var hasClassName = function(element, className) {
var elementClassName = element.className;
return (elementClassName.length > 0 && (elementClassName === className ||
new RegExp("(^|\\s)" + className + "(\\s|$)").test(elementClassName)));
};
if (!hasClassName(element, className))
element.className += (element.className ? ' ' : '') + className;
return element;
};
this.removeClassName = function(element, className) {
element.className = parseCommon.strip(element.className.replace(
new RegExp("(^|\\s+)" + className + "(\\s+|$)"), ' '));
return element;
};
this.setReadOnly = function(readOnly) {
var readonlyClass = 'abc_textarea_readonly';
var el = this.editarea.getElem();
if (readOnly) {
el.setAttribute('readonly', 'yes');
this.addClassName(el, readonlyClass);
} else {
el.removeAttribute('readonly');
this.removeClassName(el, readonlyClass);
}
};
};
Editor.prototype.redrawMidi = function() {
if (this.generate_midi && !this.midiPause) {
var event = new window.CustomEvent("generateMidi", {
detail: {
tunes: this.tunes,
abcjsParams: this.abcjsParams,
downloadMidiEl: this.downloadMidi,
inlineMidiEl: this.inlineMidi,
engravingEl: this.div
}
});
window.dispatchEvent(event);
}
if (this.synth) {
var userAction = this.synth.synthControl; // Can't really tell if there was a user action before drawing, but we assume that if the synthControl was created already there was a user action.
if (!this.synth.synthControl) {
this.synth.synthControl = new SynthController();
this.synth.synthControl.load(this.synth.el, this.synth.cursorControl, this.synth.options);
}
this.synth.synthControl.setTune(this.tunes[0], userAction, this.synth.options);
}
};
Editor.prototype.modelChanged = function() {
if (this.bReentry)
return; // TODO is this likely? maybe, if we rewrite abc immediately w/ abc2abc
this.bReentry = true;
try {
this.timerId = null;
if (this.synth && this.synth.synthControl)
this.synth.synthControl.disable(true);
this.tunes = renderAbc(this.div, this.currentAbc, this.abcjsParams);
if (this.tunes.length > 0) {
this.warnings = this.tunes[0].warnings;
}
this.redrawMidi();
} catch(error) {
console.error("ABCJS error: ", error);
if (!this.warnings)
this.warnings = [];
this.warnings.push(error.message);
}
if (this.warningsdiv) {
this.warningsdiv.innerHTML = (this.warnings) ? this.warnings.join("<br />") : "No errors";
}
this.updateSelection();
this.bReentry = false;
};
// Call this to reparse in response to the client changing the parameters on the fly
Editor.prototype.paramChanged = function(engraverParams) {
if (engraverParams) {
for (var key in engraverParams) {
if (engraverParams.hasOwnProperty(key)) {
this.abcjsParams[key] = engraverParams[key];
}
}
}
this.currentAbc = "";
this.fireChanged();
};
Editor.prototype.synthParamChanged = function(options) {
if (!this.synth)
return;
this.synth.options = {};
if (options) {
for (var key in options) {
if (options.hasOwnProperty(key)) {
this.synth.options[key] = options[key];
}
}
}
this.currentAbc = "";
this.fireChanged();
};
// return true if the model has changed
Editor.prototype.parseABC = function() {
var t = this.editarea.getString();
if (t===this.currentAbc) {
this.updateSelection();
return false;
}
this.currentAbc = t;
return true;
};
Editor.prototype.updateSelection = function() {
var selection = this.editarea.getSelection();
try {
if (this.tunes.length > 0 && this.tunes[0].engraver)
this.tunes[0].engraver.rangeHighlight(selection.start, selection.end);
} catch (e) {} // maybe printer isn't defined yet?
if (this.selectionChangeCallback)
this.selectionChangeCallback(selection.start, selection.end);
};
// Called when the textarea's selection is in the process of changing (after mouse down, dragging, or keyboard arrows)
Editor.prototype.fireSelectionChanged = function() {
this.updateSelection();
};
Editor.prototype.setDirtyStyle = function(isDirty) {
if (this.indicate_changed === undefined)
return;
var addClassName = function(element, className) {
var hasClassName = function(element, className) {
var elementClassName = element.className;
return (elementClassName.length > 0 && (elementClassName === className ||
new RegExp("(^|\\s)" + className + "(\\s|$)").test(elementClassName)));
};
if (!hasClassName(element, className))
element.className += (element.className ? ' ' : '') + className;
return element;
};
var removeClassName = function(element, className) {
element.className = parseCommon.strip(element.className.replace(
new RegExp("(^|\\s+)" + className + "(\\s+|$)"), ' '));
return element;
};
var readonlyClass = 'abc_textarea_dirty';
var el = this.editarea.getElem();
if (isDirty) {
addClassName(el, readonlyClass);
} else {
removeClassName(el, readonlyClass);
}
};
// call when the textarea alerts us that the abc text is changed and needs re-parsing
Editor.prototype.fireChanged = function() {
if (this.bIsPaused)
return;
if (this.parseABC()) {
var self = this;
if (this.timerId) // If the user is still typing, cancel the update
clearTimeout(this.timerId);
this.timerId = setTimeout(function () {
self.modelChanged();
}, 300); // Is this a good compromise between responsiveness and not redrawing too much?
var isDirty = this.isDirty();
if (this.wasDirty !== isDirty) {
this.wasDirty = isDirty;
this.setDirtyStyle(isDirty);
}
if (this.onchangeCallback)
this.onchangeCallback(this);
}
};
Editor.prototype.setNotDirty = function() {
this.editarea.initialText = this.editarea.getString();
this.wasDirty = false;
this.setDirtyStyle(false);
};
Editor.prototype.isDirty = function() {
if (this.indicate_changed === undefined)
return false;
return this.editarea.initialText !== this.editarea.getString();
};
Editor.prototype.highlight = function(abcelem, tuneNumber, classes, analysis, drag, mouseEvent) {
// TODO-PER: The marker appears to get off by one for each tune parsed. I'm not sure why, but adding the tuneNumber in corrects it for the time being.
// var offset = (tuneNumber !== undefined) ? this.startPos[tuneNumber] + tuneNumber : 0;
this.editarea.setSelection(abcelem.startChar, abcelem.endChar);
if (this.selectionChangeCallback)
this.selectionChangeCallback(abcelem.startChar, abcelem.endChar);
if (this.clientClickListener)
this.clientClickListener(abcelem, tuneNumber, classes, analysis, drag, mouseEvent);
};
Editor.prototype.pause = function(shouldPause) {
this.bIsPaused = shouldPause;
if (!shouldPause)
this.fireChanged();
};
Editor.prototype.millisecondsPerMeasure = function() {
if (!this.synth || !this.synth.synthControl || !this.synth.synthControl.visualObj)
return 0;
return this.synth.synthControl.visualObj.millisecondsPerMeasure();
};
Editor.prototype.pauseMidi = function(shouldPause) {
this.midiPause = shouldPause;
if (!shouldPause)
this.redrawMidi();
};
module.exports = Editor;
|