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;