<commit_before>// TODO: resolve how loading will work
var Framework = window.Mast;
////////////////////////////////////////////////////////////////////////////

// Supported "first-class" DOM events
var DOMEvents = [

	// Localized browser events
	// (works on individual elements)
	'error',
	'scroll',

	// Mouse events
	'click',
	'dblclick',
	'mousedown',
	'mouseup',
	'hover',
	'mouseenter',
	'mouseleave',
	'mouseover',
	'mouseout',
	'mousemove',

	// Keyboard events
	'keydown',
	'keyup',
	'keypress',

	// Form events
	'blur',
	'change',
	'focus',
	'focusin',
	'focusout',
	'select',
	'submit',

	// Raw touch events
	'touchstart',
	'touchend',
	'touchmove',
	'touchcancel',

	// Manufactured touch event
	'touch'
];

// Special Framework events
// (not implemented yet)
var FrameworkEvents = [

	// e.g. 'window resize', 'window scroll', 'window mousemove'...
	'window .+',

	'rightclick',

	'clickoutside'
];

/*
// Handle global window events
// (which are not covered by backbone natively)
var matchedSpecialDOMEvent = eventName.match(/window (.+)/);
if (matchedSpecialDOMEvent && matchedSpecialDOMEvent[1]) {
	$(window).unbind(matchedSpecialDOMEvent[1], handler);
	$(window).bind(matchedSpecialDOMEvent[1], handler);
}
*/

Framework.Util = {

	// Grab id xor data-id from an HTML element
	el2id: function (el, required) {
		var id = $(el).attr('id');
		var dataId = $(el).attr('data-id');

		if (id && dataId) {
			throw new Error(id + ' :: Cannot set both `id` and `data-id`!  Please use one or the other.  (data-id is safest)');
		}
		if (required && !id && !dataId) {
			throw new Error('No id specified in element where it is required:\n' + el);
		}

		return id || dataId;
	},

	/**
	 * Regexp to match "first class" DOM events
	 * (these are allowed in the top level of a component definition as method keys)
	 *		i.e. /^(click|hover|blur|focus)( (.+))/
	 *			[1] => event name
	 *			[3] => selector
	 */

	'/DOMEvent/': new RegExp('^(' + _.reduce(DOMEvents,
		function buildRegexp (memo, eventName, index) {

			// Omit `|` the first time
			if (index === 0) {
				return memo + eventName;
			}

			return memo + '|' + eventName;
		}, '') +
		')( (.+))?$'),

	/**
	 * Returns whether the specified event key matches a DOM event
	 *
	 * if no match is found
	 * @returns {Boolean} `false`
	 *
	 * otherwise
	 * @returns {Object} {
	 *		name		: name of DOM event
	 *		selector	: optionally, DOM selector(s) for delegation, or `undefined`
	 *	}
	 *
	 * NOTE: this function is cached using _.memoize
	 */

	parseDOMEvent: _.memoize(function (key) {

		var matches = key.match(Framework.Util['/DOMEvent/']);

		if (!matches || !matches[1]) {
			return false;
		}

		return {
			name		: matches[1],
			selector	: matches[3]
		};
	}),

	/**
	 * Translate **right-hand-side abbreviations** into functions that perform
	 * the proper behaviors, e.g.
	 *		#about_me
	 *		%mainMenu:open
	 */

	translateShorthand: function translateShorthand (value, key) {

		var matches, fn;

		// If this is an important, Framework-specific data key,
		// and a function was specified, run it to get its value
		// (this is to keep parity with Backbone's similar functionality)
		if (_.isFunction(value) && (key === 'collection' || key === 'model')) {
			return value();
		}

		// Ignore other non-strings
		if ( !_.isString(value) ) {
			return value;
		}

		// Redirects user to client-side URL, w/o affecting browser history
		// Like calling `Backbone.history.navigate('/foo', { replace: true })`
		if ( ( matches = value.match(/^##(.*[^.\s])/) ) && matches[1] ) {
			fn = function redirectAndCoverTracks () {
				var url = matches[1];
				Framework.history.navigate(url, { trigger: true, replace: true });
			};
		}
		// Method to redirect user to a client-side URL, then call the handler
		else if ( ( matches = value.match(/^#(.*[^.\s]+)/) ) && matches[1] ) {
			fn = function changeUrlFragment () {
				var url = matches[1];
				Framework.history.navigate(url, { trigger: true });
			};
		}
		// Method to trigger global event
		else if ( ( matches = value.match(/^(%.*[^.\s])/) ) && matches[1] ) {
			fn = function triggerEvent () {
				var trigger = matches[1];
				Framework.verbose(this.id + ' :: Triggering event (' + trigger + ')...');
				Framework.trigger(trigger);
			};
		}
		// Method to fire a test alert
		// (use message, if specified)
		else if ( ( matches = value.match(/^!!!\s*(.*[^.\s])?/) ) ) {
			fn = function bangAlert (e) {

				// If specified, message is used, otherwise 'Alert triggered!'
				var msg = (matches && matches[1]) || 'Debug alert (!!!) triggered!';

				// Other diagnostic information
				msg += '\n\nDiagnostics\n========================\n';
				if (e && e.currentTarget) {
					msg += 'e.currentTarget :: ' + e.currentTarget;
				}
				if (this.id) {
					msg +='this.id :: ' + this.id;
				}

				alert(msg);
			};
		}

		// Method to log a message to the console
		// (use message, if specified)
		else if ( ( matches = value.match(/^>>>\s*(.*[^.\s])?/) ) ) {
			fn = function logMessage (e) {

				// If specified, message is used, otherwise use default
				var msg = (matches && matches[1]) || 'Log message (>>>) triggered!';
				Framework.log(msg);
			};
		}

		// Method to attach the specified component/template to a region
		else if ( (matches = value.match(/^(.+)@(.*[^.\s])/)) && matches[1] && matches[2] ) {
			fn = function attachTemplate () {
				Framework.verbose(this.id + ' :: Attaching `' + matches[2] + '` to `' + matches[1] + '`...');

				var region = matches[1];
				var template = matches[2];
				this[region].attach(template);
			};
		}

		// Method to add the specified class
		else if ( (matches = value.match(/^\+\s*\.(.*[^.\s])/)) && matches[1] ) {
			fn = function addClass () {
				Framework.verbose(this.id + ' :: Adding class (' + matches[1] + ')...');
				this.$el.addClass(matches[1]);
			};
		}
		// Method to remove the specified class
		else if ( (matches = value.match(/^\-\s*\.(.*[^.\s])/)) && matches[1] ) {
			fn = function removeClass () {
				Framework.verbose(this.id + ' :: Removing class (' + matches[1] + ')...');
				this.$el.removeClass(matches[1]);
			};
		}
		// Method to toggle the specified class
		else if ( (matches = value.match(/^\!\s*\.(.*[^.\s])/)) && matches[1] ) {
			fn = function toggleClass () {
				Framework.verbose(this.id + ' :: Toggling class (' + matches[1] + ')...');
				this.$el.toggleClass(matches[1]);
			};
		}

		// TODO:	allow descendants to be controlled via shorthand
		//			e.g. : 'li.row -.highlighted'
		//			would remove the `highlighted` class from this.$('li.row')

		// If short-hand matched, return the dereferenced function
		if (fn) {

			Framework.verbose('Interpreting meaning from shorthand :: `' + value + '`...');

			// Curry the result function with any suffix matches
			var curriedFn = fn;

			// Trailing `.` indicates an e.stopPropagation()
			if (value.match(/\.\s*$/)) {
				curriedFn = function andStopPropagation (e) {

					// Bind (so it inherits component context) and call interior function
					fn.apply(this);

					// then immediately stop event bubbling/propagation
					if (e && e.stopPropagation) {
						e.stopPropagation();
					}
					else {
						Framework.warn(
							this.id + ' :: Trailing `.` shorthand was used to invoke an ' +
							'e.stopPropagation(), but "' + value + '" was not triggered by a DOM event!\n' +
							'Probably best to double-check this was what you meant to do.');
					}
				};
			}

			return curriedFn;
		}

		// Otherwise, if no short-hand matched, pass the original value
		// straight through
		return value;
	},

	// Map an object's values, but return a valid object
	// (this function is handy because underscore.map() returns a list, not an object)
	objMap: function objMap (obj, transformFn) {
		return _.object(_.keys(obj), _.map(obj, transformFn));
	}
};

/**
 * Event toolkit
 */

Framework.Util.Events = {

	/**
	 * Parse a dictionary of events
	 * (optionally filter by event type)
	 *
	 * @param {Object} events
	 *		`events` is a Backbone.View event object
	 *
	 * @param {Object} options
	 *	{
	 *		only: return the events explicitly laid out in this list
	 *	}
	 *
	 * @returns list of matching event keys
	 */

	parse: function (events, options) {

		var eventKeys = _.keys(events || {}),
			limitEvents,
			parsedEvents = [];

		// Optionally filter using set of acceptable event types
		limitEvents = options.only;
		eventKeys = _.filter(eventKeys, function checkEventName (eventKey) {

			// Parse event string into semantic representation
			var event = Framework.Util.parseDOMEvent(eventKey);
			parsedEvents.push(event);

			// Optional filter
			if (limitEvents) {
				return _.contains(limitEvents, event.name);
			}
			return true;
		});

		return parsedEvents;
	},

	/**
	 * Given a list of event expressions
	 *
	 * @param {Array} semanticEvents
	 *		List of event objs from `Framwork.Util.Events.parse`
	 *
	 * @param {Component} context
	 *		Instance of a component to use as a starting point for the DOM queries
	 *		(optional-- defaults to global context)
	 *
	 * @returns jQuery set of matched elements
	 */

	getElements: function (semanticEvents, context) {

		// Context optional
		context = context || { $: $ };

		// Iteratively build a set of affected elements
		var $affected = $();
		_.each(semanticEvents, function lookupElementsForEvent (event) {

			// Determine matched elements
			// Use delegate selector if specified
			// Otherwise, grab the element for this component
			var $matched =	event.selector ?
							context.$(event.selector) :
							context.$el;

			// Add matched elements to set
			$affected = $affected.add( $matched );
		}, this);

		return $affected;
	}
};
<commit_msg>Fixed bug that caused events not in options.only from being added to the parsedEvents array.<commit_after>// TODO: resolve how loading will work
var Framework = window.Mast;
////////////////////////////////////////////////////////////////////////////

// Supported "first-class" DOM events
var DOMEvents = [

	// Localized browser events
	// (works on individual elements)
	'error',
	'scroll',

	// Mouse events
	'click',
	'dblclick',
	'mousedown',
	'mouseup',
	'hover',
	'mouseenter',
	'mouseleave',
	'mouseover',
	'mouseout',
	'mousemove',

	// Keyboard events
	'keydown',
	'keyup',
	'keypress',

	// Form events
	'blur',
	'change',
	'focus',
	'focusin',
	'focusout',
	'select',
	'submit',

	// Raw touch events
	'touchstart',
	'touchend',
	'touchmove',
	'touchcancel',

	// Manufactured touch event
	'touch'
];

// Special Framework events
// (not implemented yet)
var FrameworkEvents = [

	// e.g. 'window resize', 'window scroll', 'window mousemove'...
	'window .+',

	'rightclick',

	'clickoutside'
];

/*
// Handle global window events
// (which are not covered by backbone natively)
var matchedSpecialDOMEvent = eventName.match(/window (.+)/);
if (matchedSpecialDOMEvent && matchedSpecialDOMEvent[1]) {
	$(window).unbind(matchedSpecialDOMEvent[1], handler);
	$(window).bind(matchedSpecialDOMEvent[1], handler);
}
*/

Framework.Util = {

	// Grab id xor data-id from an HTML element
	el2id: function (el, required) {
		var id = $(el).attr('id');
		var dataId = $(el).attr('data-id');

		if (id && dataId) {
			throw new Error(id + ' :: Cannot set both `id` and `data-id`!  Please use one or the other.  (data-id is safest)');
		}
		if (required && !id && !dataId) {
			throw new Error('No id specified in element where it is required:\n' + el);
		}

		return id || dataId;
	},

	/**
	 * Regexp to match "first class" DOM events
	 * (these are allowed in the top level of a component definition as method keys)
	 *		i.e. /^(click|hover|blur|focus)( (.+))/
	 *			[1] => event name
	 *			[3] => selector
	 */

	'/DOMEvent/': new RegExp('^(' + _.reduce(DOMEvents,
		function buildRegexp (memo, eventName, index) {

			// Omit `|` the first time
			if (index === 0) {
				return memo + eventName;
			}

			return memo + '|' + eventName;
		}, '') +
		')( (.+))?$'),

	/**
	 * Returns whether the specified event key matches a DOM event
	 *
	 * if no match is found
	 * @returns {Boolean} `false`
	 *
	 * otherwise
	 * @returns {Object} {
	 *		name		: name of DOM event
	 *		selector	: optionally, DOM selector(s) for delegation, or `undefined`
	 *	}
	 *
	 * NOTE: this function is cached using _.memoize
	 */

	parseDOMEvent: _.memoize(function (key) {

		var matches = key.match(Framework.Util['/DOMEvent/']);

		if (!matches || !matches[1]) {
			return false;
		}

		return {
			name		: matches[1],
			selector	: matches[3]
		};
	}),

	/**
	 * Translate **right-hand-side abbreviations** into functions that perform
	 * the proper behaviors, e.g.
	 *		#about_me
	 *		%mainMenu:open
	 */

	translateShorthand: function translateShorthand (value, key) {

		var matches, fn;

		// If this is an important, Framework-specific data key,
		// and a function was specified, run it to get its value
		// (this is to keep parity with Backbone's similar functionality)
		if (_.isFunction(value) && (key === 'collection' || key === 'model')) {
			return value();
		}

		// Ignore other non-strings
		if ( !_.isString(value) ) {
			return value;
		}

		// Redirects user to client-side URL, w/o affecting browser history
		// Like calling `Backbone.history.navigate('/foo', { replace: true })`
		if ( ( matches = value.match(/^##(.*[^.\s])/) ) && matches[1] ) {
			fn = function redirectAndCoverTracks () {
				var url = matches[1];
				Framework.history.navigate(url, { trigger: true, replace: true });
			};
		}
		// Method to redirect user to a client-side URL, then call the handler
		else if ( ( matches = value.match(/^#(.*[^.\s]+)/) ) && matches[1] ) {
			fn = function changeUrlFragment () {
				var url = matches[1];
				Framework.history.navigate(url, { trigger: true });
			};
		}
		// Method to trigger global event
		else if ( ( matches = value.match(/^(%.*[^.\s])/) ) && matches[1] ) {
			fn = function triggerEvent () {
				var trigger = matches[1];
				Framework.verbose(this.id + ' :: Triggering event (' + trigger + ')...');
				Framework.trigger(trigger);
			};
		}
		// Method to fire a test alert
		// (use message, if specified)
		else if ( ( matches = value.match(/^!!!\s*(.*[^.\s])?/) ) ) {
			fn = function bangAlert (e) {

				// If specified, message is used, otherwise 'Alert triggered!'
				var msg = (matches && matches[1]) || 'Debug alert (!!!) triggered!';

				// Other diagnostic information
				msg += '\n\nDiagnostics\n========================\n';
				if (e && e.currentTarget) {
					msg += 'e.currentTarget :: ' + e.currentTarget;
				}
				if (this.id) {
					msg +='this.id :: ' + this.id;
				}

				alert(msg);
			};
		}

		// Method to log a message to the console
		// (use message, if specified)
		else if ( ( matches = value.match(/^>>>\s*(.*[^.\s])?/) ) ) {
			fn = function logMessage (e) {

				// If specified, message is used, otherwise use default
				var msg = (matches && matches[1]) || 'Log message (>>>) triggered!';
				Framework.log(msg);
			};
		}

		// Method to attach the specified component/template to a region
		else if ( (matches = value.match(/^(.+)@(.*[^.\s])/)) && matches[1] && matches[2] ) {
			fn = function attachTemplate () {
				Framework.verbose(this.id + ' :: Attaching `' + matches[2] + '` to `' + matches[1] + '`...');

				var region = matches[1];
				var template = matches[2];
				this[region].attach(template);
			};
		}

		// Method to add the specified class
		else if ( (matches = value.match(/^\+\s*\.(.*[^.\s])/)) && matches[1] ) {
			fn = function addClass () {
				Framework.verbose(this.id + ' :: Adding class (' + matches[1] + ')...');
				this.$el.addClass(matches[1]);
			};
		}
		// Method to remove the specified class
		else if ( (matches = value.match(/^\-\s*\.(.*[^.\s])/)) && matches[1] ) {
			fn = function removeClass () {
				Framework.verbose(this.id + ' :: Removing class (' + matches[1] + ')...');
				this.$el.removeClass(matches[1]);
			};
		}
		// Method to toggle the specified class
		else if ( (matches = value.match(/^\!\s*\.(.*[^.\s])/)) && matches[1] ) {
			fn = function toggleClass () {
				Framework.verbose(this.id + ' :: Toggling class (' + matches[1] + ')...');
				this.$el.toggleClass(matches[1]);
			};
		}

		// TODO:	allow descendants to be controlled via shorthand
		//			e.g. : 'li.row -.highlighted'
		//			would remove the `highlighted` class from this.$('li.row')

		// If short-hand matched, return the dereferenced function
		if (fn) {

			Framework.verbose('Interpreting meaning from shorthand :: `' + value + '`...');

			// Curry the result function with any suffix matches
			var curriedFn = fn;

			// Trailing `.` indicates an e.stopPropagation()
			if (value.match(/\.\s*$/)) {
				curriedFn = function andStopPropagation (e) {

					// Bind (so it inherits component context) and call interior function
					fn.apply(this);

					// then immediately stop event bubbling/propagation
					if (e && e.stopPropagation) {
						e.stopPropagation();
					}
					else {
						Framework.warn(
							this.id + ' :: Trailing `.` shorthand was used to invoke an ' +
							'e.stopPropagation(), but "' + value + '" was not triggered by a DOM event!\n' +
							'Probably best to double-check this was what you meant to do.');
					}
				};
			}

			return curriedFn;
		}

		// Otherwise, if no short-hand matched, pass the original value
		// straight through
		return value;
	},

	// Map an object's values, but return a valid object
	// (this function is handy because underscore.map() returns a list, not an object)
	objMap: function objMap (obj, transformFn) {
		return _.object(_.keys(obj), _.map(obj, transformFn));
	}
};

/**
 * Event toolkit
 */

Framework.Util.Events = {

	/**
	 * Parse a dictionary of events
	 * (optionally filter by event type)
	 *
	 * @param {Object} events
	 *		`events` is a Backbone.View event object
	 *
	 * @param {Object} options
	 *	{
	 *		only: return the events explicitly laid out in this list
	 *	}
	 *
	 * @returns list of matching event keys
	 */

	parse: function (events, options) {

		var eventKeys = _.keys(events || {}),
			limitEvents,
			parsedEvents = [];

		// Optionally filter using set of acceptable event types
		limitEvents = options.only;
		eventKeys = _.filter(eventKeys, function checkEventName (eventKey) {

			// Parse event string into semantic representation
			var event = Framework.Util.parseDOMEvent(eventKey);

			// Optional filter
			if (limitEvents) {
				return _.contains(limitEvents, event.name);
			}

			parsedEvents.push(event);
			return true;
		});

		return parsedEvents;
	},

	/**
	 * Given a list of event expressions
	 *
	 * @param {Array} semanticEvents
	 *		List of event objs from `Framwork.Util.Events.parse`
	 *
	 * @param {Component} context
	 *		Instance of a component to use as a starting point for the DOM queries
	 *		(optional-- defaults to global context)
	 *
	 * @returns jQuery set of matched elements
	 */

	getElements: function (semanticEvents, context) {

		// Context optional
		context = context || { $: $ };

		// Iteratively build a set of affected elements
		var $affected = $();
		_.each(semanticEvents, function lookupElementsForEvent (event) {

			// Determine matched elements
			// Use delegate selector if specified
			// Otherwise, grab the element for this component
			var $matched =	event.selector ?
							context.$(event.selector) :
							context.$el;

			// Add matched elements to set
			$affected = $affected.add( $matched );
		}, this);

		return $affected;
	}
};
<|endoftext|>


<commit_before>/*
 * MelonJS Game Engine
 * Copyright (C) 2011 - 2018 Olivier Biot
 * http://www.melonjs.org
 *
 * Font / Bitmap font
 *
 * -> first char " " 32d (0x20);
 */
(function () {

    /**
     * Measures the width of a single line of text, does not account for \n
     * @ignore
     */
    var measureTextWidth = function(font, text) {
        var characters = text.split("");
        var width = 0;
        var lastGlyph = null;
        for (var i = 0; i < characters.length; i++) {
            var ch = characters[i].charCodeAt(0);
            var glyph = font.fontData.glyphs[ch];
            var kerning = (lastGlyph && lastGlyph.kerning) ? lastGlyph.getKerning(ch) : 0;
            width += (glyph.xadvance + kerning) * font.fontScale.x;
            lastGlyph = glyph;
        }

        return width;
    };

    /**
     * Measures the height of a single line of text, does not account for \n
     * @ignore
     */
    var measureTextHeight = function(font) {
        return font.fontData.capHeight * font.lineHeight * font.fontScale.y;
    };

    /**
     * a bitmap font object
     * @class
     * @extends me.Renderable
     * @memberOf me
     * @constructor
     * @param {Number} [scale=1.0]
     * @param {Object} settings the text configuration
     * @param {String|Image} settings.font a font name to identify the corresponing source image
     * @param {String} [settings.fontData=settings.font] the bitmap font data corresponding name, or the bitmap font data itself
     * @param {Number} [settings.size] size a scaling ratio
     * @param {Number} [settings.lineWidth=1] line width, in pixels, when drawing stroke
     * @param {String} [settings.textAlign="left"] horizontal text alignment
     * @param {String} [settings.textBaseline="top"] the text baseline
     * @param {Number} [settings.lineHeight=1.0] line spacing height
     * @param {(String|String[])} [settings.text] a string, or an array of strings
     * @example
     * // Use me.loader.preload or me.loader.load to load assets
     * me.loader.preload([
     * { name: "arial", type: "binary" src: "data/font/arial.fnt" },
     * { name: "arial", type: "image" src: "data/font/arial.png" },
     * ])
     * // Then create an instance of your bitmap font:
     * var myFont = new me.BitmapText(x, y, {font:"arial", text:"Hello"});
     * // two possibilities for using "myFont"
     * // either call the draw function from your Renderable draw function
     * myFont.draw(renderer, "Hello!", 0, 0);
     * // or just add it to the word container
     * me.game.world.addChild(myFont);
     */
    me.BitmapText = me.Renderable.extend(
    /** @scope me.BitmapText.prototype */ {
        /** @ignore */
        init : function (x, y, settings) {
            // call the parent constructor
            this._super(me.Renderable, "init", [x, y, settings.width || 0, settings.height || 0]);

            /**
             * Set the default text alignment (or justification),

             * possible values are "left", "right", and "center".
             * @public
             * @type String
             * @default "left"
             * @name textAlign
             * @memberOf me.BitmapText
             */
            this.textAlign = settings.textAlign || "left";

            /**
             * Set the text baseline (e.g. the Y-coordinate for the draw operation),

             * possible values are "top", "hanging, "middle, "alphabetic, "ideographic, "bottom"

             * @public
             * @type String
             * @default "top"
             * @name textBaseline
             * @memberOf me.BitmapText
             */
            this.textBaseline = settings.textBaseline || "top";

            /**
             * Set the line spacing height (when displaying multi-line strings).

             * Current font height will be multiplied with this value to set the line height.
             * @public
             * @type Number
             * @default 1.0
             * @name lineHeight
             * @memberOf me.BitmapText
             */
            this.lineHeight = settings.lineHeight || 1;

            /** @ignore */
            // scaled font size;
            this.fontScale = me.pool.pull("me.Vector2d", 1, 1);

            // get the corresponding image
            this.fontImage = (typeof settings.font === "object") ? settings.font : me.loader.getImage(settings.font);

            if (typeof settings.fontData !== "string") {
                // use settings.font to retreive the data from the loader
                this.fontData = new me.BitmapTextData(me.loader.getBinary(settings.font));
            } else {
                this.fontData = new me.BitmapTextData(
                    // if starting/includes "info face" the whole data string was passed as parameter
                    (settings.fontData.includes("info face")) ? settings.fontData : me.loader.getBinary(settings.fontData)
                );
            };

            // if floating was specified through settings
            if (typeof settings.floating !== "undefined") {
                this.floating = !!settings.floating;
            }

            // resize if necessary
            if (typeof settings.size === "number" && settings.size !== 1.0) {
                this.resize(settings.size);
            }

            // set the text
            this.setText(settings.text);
        },

        /**
         * change the font settings
         * @name set
         * @memberOf me.BitmapText
         * @function
         * @param {String} textAlign ("left", "center", "right")
         * @param {Number} [scale]
         * @return this object for chaining
         */
        set : function (textAlign, scale) {
            this.textAlign = textAlign;
            // updated scaled Size
            if (scale) {
                this.resize(scale);
            }
            this.isDirty = true;

            return this;
        },

        /**
         * change the text to be displayed
         * @name setText
         * @memberOf me.BitmapText
         * @function
         * @param {(Number|String|String[])} value a string, or an array of strings
         * @return this object for chaining
         */
        setText : function (value) {
            value = "" + value;
            if (this._text !== value) {
                if (Array.isArray(value)) {
                    this._text = value.join("\n");
                } else {
                    this._text = value;
                }
                this.isDirty = true;
            }
            return this;
        },

        /**
         * change the font display size
         * @name resize
         * @memberOf me.BitmapText
         * @function
         * @param {Number} scale ratio
         * @return this object for chaining
         */
        resize : function (scale) {
            this.fontScale.set(scale, scale);
            // clear the cache text to recalculate bounds
            this.isDirty = true;

            return this;
        },

        /**
         * measure the given text size in pixels
         * @name measureText
         * @memberOf me.BitmapText
         * @function
         * @param {String} [text]
         * @param {me.Rect} [ret] a object in which to store the text metrics
         * @returns {TextMetrics} a TextMetrics object with two properties: `width` and `height`, defining the output dimensions
         */
        measureText : function (text, ret) {
            text = text || this._text;

            var strings = ("" + text).split("\n");
            var stringHeight = measureTextHeight(this);
            var textMetrics  = ret || this.getBounds();

            textMetrics.height = textMetrics.width = 0;

            for (var i = 0; i < strings.length; i++) {
                textMetrics.width = Math.max(measureTextWidth(this, strings[i]), textMetrics.width);
                textMetrics.height += stringHeight;
            }
            return textMetrics;
        },

        /**
         * @ignore
         */
        update : function (/* dt */) {
            if (this.isDirty === true) {
                this.measureText();
            }
            return this.isDirty;
        },

        /**
         * draw the bitmap font
         * @name draw
         * @memberOf me.BitmapText
         * @function
         * @param {me.CanvasRenderer|me.WebGLRenderer} renderer Reference to the destination renderer instance
         * @param {String} [text]
         * @param {Number} [x]
         * @param {Number} [y]
         */
        draw : function (renderer, text, x, y) {
            // allows to provide backward compatibility when
            // adding Bitmap Font to an object container
            if (typeof this.ancestor === "undefined") {
                // update cache
                this.setText(text);
                // force update bounds
                this.update(0);
                // save the previous global alpha value
                var _alpha = renderer.globalAlpha();
                renderer.setGlobalAlpha(_alpha * this.getOpacity());
            } else {
                // added directly to an object container
                x = this.pos.x;
                y = this.pos.y;
            }

            var strings = ("" + this._text).split("\n");
            var lX = x;
            var stringHeight = measureTextHeight(this);
            var maxWidth = 0;

            for (var i = 0; i < strings.length; i++) {
                x = lX;
                var string = me.utils.string.trimRight(strings[i]);
                // adjust x pos based on alignment value
                var stringWidth = measureTextWidth(this, string);
                switch (this.textAlign) {
                    case "right":
                        x -= stringWidth;
                        break;

                    case "center":
                        x -= stringWidth * 0.5;
                        break;

                    default :
                        break;
                }

                // adjust y pos based on alignment value
                switch (this.textBaseline) {
                    case "middle":
                        y -= stringHeight * 0.5;
                        break;

                    case "ideographic":
                    case "alphabetic":
                    case "bottom":
                        y -= stringHeight;
                        break;

                    default :
                        break;
                }

                // update initial position if required
                if (this.isDirty === true && typeof this.ancestor === "undefined") {
                    if (i === 0) {
                        this.pos.y = y;
                    }
                    if (maxWidth < stringWidth) {
                        maxWidth = stringWidth;
                        this.pos.x = x;
                    }
                }

                // draw the string
                var lastGlyph = null;
                for (var c = 0, len = string.length; c < len; c++) {
                    // calculate the char index
                    var ch = string.charCodeAt(c);
                    var glyph = this.fontData.glyphs[ch];
                    var glyphWidth = glyph.width;
                    var glyphHeight = glyph.height;
                    var kerning = (lastGlyph && lastGlyph.kerning) ? lastGlyph.getKerning(ch) : 0;

                    // draw it
                    if (glyphWidth !== 0 && glyphHeight !== 0) {
                        // some browser throw an exception when drawing a 0 width or height image
                        renderer.drawImage(this.fontImage,
                            glyph.x, glyph.y,
                            glyphWidth, glyphHeight,
                            x + glyph.xoffset,
                            y + glyph.yoffset * this.fontScale.y,
                            glyphWidth * this.fontScale.x, glyphHeight * this.fontScale.y
                        );
                    }

                    // increment position
                    x += (glyph.xadvance + kerning) * this.fontScale.x;
                    lastGlyph = glyph;
                }
                // increment line
                y += stringHeight;
            }

            if (typeof this.ancestor === "undefined") {
                // restore the previous global alpha value
                renderer.setGlobalAlpha(_alpha);
            }

            // clear the dirty flag
            this.isDirty = false;
        },

        /**
         * Destroy function
         * @ignore
         */
        destroy : function () {
            me.pool.push(this.fontScale);
            this.fontScale = undefined;
            this._super(me.Renderable, "destroy");
        }
    });
})();
<commit_msg>set bitmap text anchor point in super<commit_after>/*
 * MelonJS Game Engine
 * Copyright (C) 2011 - 2018 Olivier Biot
 * http://www.melonjs.org
 *
 * Font / Bitmap font
 *
 * -> first char " " 32d (0x20);
 */
(function () {

    /**
     * Measures the width of a single line of text, does not account for \n
     * @ignore
     */
    var measureTextWidth = function(font, text) {
        var characters = text.split("");
        var width = 0;
        var lastGlyph = null;
        for (var i = 0; i < characters.length; i++) {
            var ch = characters[i].charCodeAt(0);
            var glyph = font.fontData.glyphs[ch];
            var kerning = (lastGlyph && lastGlyph.kerning) ? lastGlyph.getKerning(ch) : 0;
            width += (glyph.xadvance + kerning) * font.fontScale.x;
            lastGlyph = glyph;
        }

        return width;
    };

    /**
     * Measures the height of a single line of text, does not account for \n
     * @ignore
     */
    var measureTextHeight = function(font) {
        return font.fontData.capHeight * font.lineHeight * font.fontScale.y;
    };

    /**
     * a bitmap font object
     * @class
     * @extends me.Renderable
     * @memberOf me
     * @constructor
     * @param {Number} [scale=1.0]
     * @param {Object} settings the text configuration
     * @param {String|Image} settings.font a font name to identify the corresponing source image
     * @param {String} [settings.fontData=settings.font] the bitmap font data corresponding name, or the bitmap font data itself
     * @param {Number} [settings.size] size a scaling ratio
     * @param {Number} [settings.lineWidth=1] line width, in pixels, when drawing stroke
     * @param {String} [settings.textAlign="left"] horizontal text alignment
     * @param {String} [settings.textBaseline="top"] the text baseline
     * @param {Number} [settings.lineHeight=1.0] line spacing height
     * @param {(String|String[])} [settings.text] a string, or an array of strings
     * @example
     * // Use me.loader.preload or me.loader.load to load assets
     * me.loader.preload([
     * { name: "arial", type: "binary" src: "data/font/arial.fnt" },
     * { name: "arial", type: "image" src: "data/font/arial.png" },
     * ])
     * // Then create an instance of your bitmap font:
     * var myFont = new me.BitmapText(x, y, {font:"arial", text:"Hello"});
     * // two possibilities for using "myFont"
     * // either call the draw function from your Renderable draw function
     * myFont.draw(renderer, "Hello!", 0, 0);
     * // or just add it to the word container
     * me.game.world.addChild(myFont);
     */
    me.BitmapText = me.Renderable.extend(
    /** @scope me.BitmapText.prototype */ {
        /** @ignore */
        init : function (x, y, settings) {
            // call the parent constructor
            this._super(me.Renderable, "init", [x, y, settings.width || 0, settings.height || 0]);

            /**
             * Set the default text alignment (or justification),

             * possible values are "left", "right", and "center".
             * @public
             * @type String
             * @default "left"
             * @name textAlign
             * @memberOf me.BitmapText
             */
            this.textAlign = settings.textAlign || "left";

            /**
             * Set the text baseline (e.g. the Y-coordinate for the draw operation),

             * possible values are "top", "hanging, "middle, "alphabetic, "ideographic, "bottom"

             * @public
             * @type String
             * @default "top"
             * @name textBaseline
             * @memberOf me.BitmapText
             */
            this.textBaseline = settings.textBaseline || "top";

            /**
             * Set the line spacing height (when displaying multi-line strings).

             * Current font height will be multiplied with this value to set the line height.
             * @public
             * @type Number
             * @default 1.0
             * @name lineHeight
             * @memberOf me.BitmapText
             */
            this.lineHeight = settings.lineHeight || 1;

            /** @ignore */
            // scaled font size;
            this.fontScale = me.pool.pull("me.Vector2d", 1, 1);

            // get the corresponding image
            this.fontImage = (typeof settings.font === "object") ? settings.font : me.loader.getImage(settings.font);

            if (typeof settings.fontData !== "string") {
                // use settings.font to retreive the data from the loader
                this.fontData = new me.BitmapTextData(me.loader.getBinary(settings.font));
            } else {
                this.fontData = new me.BitmapTextData(
                    // if starting/includes "info face" the whole data string was passed as parameter
                    (settings.fontData.includes("info face")) ? settings.fontData : me.loader.getBinary(settings.fontData)
                );
            };

            // if floating was specified through settings
            if (typeof settings.floating !== "undefined") {
                this.floating = !!settings.floating;
            }

            // resize if necessary
            if (typeof settings.size === "number" && settings.size !== 1.0) {
                this.resize(settings.size);
            }

            // update anchorPoint
            if (settings.anchorPoint) {
                this.anchorPoint.set(settings.anchorPoint.x, settings.anchorPoint.y);
            }

            // set the text
            this.setText(settings.text);
        },

        /**
         * change the font settings
         * @name set
         * @memberOf me.BitmapText
         * @function
         * @param {String} textAlign ("left", "center", "right")
         * @param {Number} [scale]
         * @return this object for chaining
         */
        set : function (textAlign, scale) {
            this.textAlign = textAlign;
            // updated scaled Size
            if (scale) {
                this.resize(scale);
            }
            this.isDirty = true;

            return this;
        },

        /**
         * change the text to be displayed
         * @name setText
         * @memberOf me.BitmapText
         * @function
         * @param {(Number|String|String[])} value a string, or an array of strings
         * @return this object for chaining
         */
        setText : function (value) {
            value = "" + value;
            if (this._text !== value) {
                if (Array.isArray(value)) {
                    this._text = value.join("\n");
                } else {
                    this._text = value;
                }
                this.isDirty = true;
            }
            return this;
        },

        /**
         * change the font display size
         * @name resize
         * @memberOf me.BitmapText
         * @function
         * @param {Number} scale ratio
         * @return this object for chaining
         */
        resize : function (scale) {
            this.fontScale.set(scale, scale);
            // clear the cache text to recalculate bounds
            this.isDirty = true;

            return this;
        },

        /**
         * measure the given text size in pixels
         * @name measureText
         * @memberOf me.BitmapText
         * @function
         * @param {String} [text]
         * @param {me.Rect} [ret] a object in which to store the text metrics
         * @returns {TextMetrics} a TextMetrics object with two properties: `width` and `height`, defining the output dimensions
         */
        measureText : function (text, ret) {
            text = text || this._text;

            var strings = ("" + text).split("\n");
            var stringHeight = measureTextHeight(this);
            var textMetrics  = ret || this.getBounds();

            textMetrics.height = textMetrics.width = 0;

            for (var i = 0; i < strings.length; i++) {
                textMetrics.width = Math.max(measureTextWidth(this, strings[i]), textMetrics.width);
                textMetrics.height += stringHeight;
            }
            return textMetrics;
        },

        /**
         * @ignore
         */
        update : function (/* dt */) {
            if (this.isDirty === true) {
                this.measureText();
            }
            return this.isDirty;
        },

        /**
         * draw the bitmap font
         * @name draw
         * @memberOf me.BitmapText
         * @function
         * @param {me.CanvasRenderer|me.WebGLRenderer} renderer Reference to the destination renderer instance
         * @param {String} [text]
         * @param {Number} [x]
         * @param {Number} [y]
         */
        draw : function (renderer, text, x, y) {
            // allows to provide backward compatibility when
            // adding Bitmap Font to an object container
            if (typeof this.ancestor === "undefined") {
                // update cache
                this.setText(text);
                // force update bounds
                this.update(0);
                // save the previous global alpha value
                var _alpha = renderer.globalAlpha();
                renderer.setGlobalAlpha(_alpha * this.getOpacity());
            } else {
                // added directly to an object container
                x = this.pos.x;
                y = this.pos.y;
            }

            var strings = ("" + this._text).split("\n");
            var lX = x;
            var stringHeight = measureTextHeight(this);
            var maxWidth = 0;

            for (var i = 0; i < strings.length; i++) {
                x = lX;
                var string = me.utils.string.trimRight(strings[i]);
                // adjust x pos based on alignment value
                var stringWidth = measureTextWidth(this, string);
                switch (this.textAlign) {
                    case "right":
                        x -= stringWidth;
                        break;

                    case "center":
                        x -= stringWidth * 0.5;
                        break;

                    default :
                        break;
                }

                // adjust y pos based on alignment value
                switch (this.textBaseline) {
                    case "middle":
                        y -= stringHeight * 0.5;
                        break;

                    case "ideographic":
                    case "alphabetic":
                    case "bottom":
                        y -= stringHeight;
                        break;

                    default :
                        break;
                }

                // update initial position if required
                if (this.isDirty === true && typeof this.ancestor === "undefined") {
                    if (i === 0) {
                        this.pos.y = y;
                    }
                    if (maxWidth < stringWidth) {
                        maxWidth = stringWidth;
                        this.pos.x = x;
                    }
                }

                // draw the string
                var lastGlyph = null;
                for (var c = 0, len = string.length; c < len; c++) {
                    // calculate the char index
                    var ch = string.charCodeAt(c);
                    var glyph = this.fontData.glyphs[ch];
                    var glyphWidth = glyph.width;
                    var glyphHeight = glyph.height;
                    var kerning = (lastGlyph && lastGlyph.kerning) ? lastGlyph.getKerning(ch) : 0;

                    // draw it
                    if (glyphWidth !== 0 && glyphHeight !== 0) {
                        // some browser throw an exception when drawing a 0 width or height image
                        renderer.drawImage(this.fontImage,
                            glyph.x, glyph.y,
                            glyphWidth, glyphHeight,
                            x + glyph.xoffset,
                            y + glyph.yoffset * this.fontScale.y,
                            glyphWidth * this.fontScale.x, glyphHeight * this.fontScale.y
                        );
                    }

                    // increment position
                    x += (glyph.xadvance + kerning) * this.fontScale.x;
                    lastGlyph = glyph;
                }
                // increment line
                y += stringHeight;
            }

            if (typeof this.ancestor === "undefined") {
                // restore the previous global alpha value
                renderer.setGlobalAlpha(_alpha);
            }

            // clear the dirty flag
            this.isDirty = false;
        },

        /**
         * Destroy function
         * @ignore
         */
        destroy : function () {
            me.pool.push(this.fontScale);
            this.fontScale = undefined;
            this._super(me.Renderable, "destroy");
        }
    });
})();
<|endoftext|>


<commit_before>// Amara, universalsubtitles.org
//
// Copyright (C) 2012 Participatory Culture Foundation
//
// This program 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.
//
// This program 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 this program.  If not, see
// http://www.gnu.org/licenses/agpl-3.0.html.

goog.provide('unisubs.translate.Dialog');

/**
 * @constructor
 *
 */
unisubs.translate.Dialog = function(opener, serverModel, videoSource, subtitleState, standardSubState, reviewOrApprovalType) {
    unisubs.Dialog.call(this, videoSource);
    unisubs.SubTracker.getInstance().start(true);
    this.opener_ = opener;
    this.subtitleState_ = subtitleState;
    this.standardSubState_ = standardSubState;
    this.serverModel_ = serverModel;
    this.serverModel_.init();

    this.captionManager_ =
        new unisubs.CaptionManager(
            this.getVideoPlayerInternal(),
            new unisubs.subtitle.EditableCaptionSet(this.standardSubState_.SUBTITLES));

    this.saved_ = false;
    this.rightPanelListener_ = new goog.events.EventHandler(this);
    this.reviewOrApprovalType_ = reviewOrApprovalType;
    // if this is a review approve dialog, we must fetch saved notes for this task if available
    // but we should only do it when the dialog laods the first time
    // (else as users move back and forward between panels we might
    // overwrite their notes). This is what this flag is for
    this.notesFectched_ = false;
    if ( !Boolean(this.reviewOrApprovalType_)){
       this.notesFectched_ = true;
    }

    this.alreadySaving_ = false;
};

goog.inherits(unisubs.translate.Dialog, unisubs.subtitle.Dialog);

unisubs.translate.Dialog.State_ = {
    TRANSLATE: 0,
    EDIT_METADATA: 1
};
unisubs.translate.Dialog.prototype.createDom = function() {
    unisubs.translate.Dialog.superClass_.createDom.call(this);
    this.setDraggable(false);
    this.showGuidelines_();
    this.enterState_(unisubs.translate.Dialog.State_.TRANSLATE);
};
unisubs.translate.Dialog.prototype.showGuidelines_ = function() {
    var skipGuidelines = unisubs.UserSettings.getBooleanValue(
                            unisubs.UserSettings.Settings.ALWAYS_SKIP_GUIDELINES);

    if(skipGuidelines){
        return;
    }

    if (!unisubs.guidelines['translate']) var s = unisubs.subtitle.Dialog.State_;
    // the same dialog can be used in transcribing or review approval, which guidelines should we use?
    var guideline = this.reviewOrApprovalType_ ? this.getTeamGuidelineForReview() : unisubs.guidelines['translate'];

    if (!guideline) {
        return;
    }
    var guidelinesPanel = new unisubs.GuidelinesPanel(guideline);
    this.showTemporaryPanel(guidelinesPanel);
    this.displayingGuidelines_ = true;

    var that = this;
    this.getHandler().listenOnce(guidelinesPanel, unisubs.GuidelinesPanel.CONTINUE, function(e) {
        goog.Timer.callOnce(function() {
            that.displayingGuidelines_ = false;
            that.hideTemporaryPanel();
        });
    });
};
unisubs.translate.Dialog.prototype.handleSaveAndExitKeyPress_ = function(e) {
    e.preventDefault();
    var that = this;
    setTimeout(function(){
        that.saveWork(false, true);
    }, 1500);
};
unisubs.translate.Dialog.prototype.handleDoneKeyPress_ = function(event) {
    event.preventDefault();
    if (this.state_ == unisubs.translate.Dialog.State_.EDIT_METADATA) {

        // Make sure each new caption has text.
        var halt = false;

        goog.array.forEach(this.translationPanel_.captionSet_.captions_, function(c) {
            if (c.getText() === '') {
                halt = true;
            }
        });

        if (halt === true) {
            alert('You have untranslated captions. You must translate all captions in order to submit.');
            return false;
        }

        this.saveWork(true, false);

    }
    else {
        this.enterState_(unisubs.translate.Dialog.State_.EDIT_METADATA);
    }
};
unisubs.translate.Dialog.prototype.isWorkSaved = function() {
    if (this.reviewOrApprovalType_) {
        if (this.getNotesContent_(this.currentSubtitlePanel_) !== '') {
            return this.saved_;
        }
    }
    return this.saved_ || !this.serverModel_.anySubtitlingWorkDone();
};
unisubs.translate.Dialog.prototype.enterDocument = function() {
    // this is where we listen to the dialog close button
    unisubs.subtitle.Dialog.superClass_.enterDocument.call(this);
    unisubs.Dialog.translationDialogOpen = true;

    this.getHandler().
        listen(
            this.captionManager_,
            unisubs.CaptionManager.CAPTION,
            this.captionReached_);

    if (this.reviewOrApprovalType_ && !this.notesFectched_){
        var func  = this.serverModel_.fetchReviewData ;
        var that = this;
        if (this.reviewOrApprovalType_ == unisubs.Dialog.REVIEW_OR_APPROVAL.APPROVAL){
            func = this.serverModel_.fetchApproveData;
        }

        // make sure we retain the correct scope
        func.call(this.serverModel_, unisubs.task_id, function(body) {
            that.onNotesFetched_(body);
        });
    }
};
unisubs.translate.Dialog.prototype.onNotesFetched_ = function(body) {
    if( this.currentSubtitlePanel_ && this.currentSubtitlePanel_.setNotesContent_){
        this.currentSubtitlePanel_.setNotesContent_(body);
    }
};
unisubs.translate.Dialog.prototype.saveWorkInternal = function(closeAfterSave, saveForLater) {
    if(this.alreadySaving_){
        return;
    }

    this.alreadySaving_ = true;

    var notes = this.getNotesContent_(this.currentSubtitlePanel_);
    if (notes !== '') {
        this.serverModel_.setTaskNotes(notes);
    }

    if (goog.array.isEmpty(
        this.serverModel_.captionSet_.nonblankSubtitles()) && !this.forceSave_){
        this.alreadySaving_ = false;
        if((this.captionSet_.hasTitleChanged() && this.captionSet_.originalTitle == "") ||
             (this.captionSet_.hasDescriptionChanged() && this.captionSet_.originalDescription == "")){
            this.showTitleDescriptionChangedDialog(closeAfterSave, saveForLater);
        } else {
            // there are no subs here, close dialog or back to subtitling
            this.showEmptySubsDialog();
        }
        return;
    }

    var that = this;
    this.getRightPanelInternal().showLoading(true);
    this.serverModel_.finish(
        function(serverMsg){
            unisubs.subtitle.OnSavedDialog.show(serverMsg, function(){
                that.onWorkSaved(closeAfterSave);
            });
        },
        function(opt_status) {
            if (that.finishFailDialog_)
                that.finishFailDialog_.failedAgain(opt_status);
            else
                that.finishFailDialog_ = unisubs.finishfaildialog.Dialog.show(
                    that.serverModel_.getCaptionSet(), opt_status,
                    goog.bind(that.saveWorkInternal, that, closeAfterSave));
        },
        null,
        saveForLater);
};
unisubs.translate.Dialog.prototype.showGuidelinesForState_ = function(state) {
    this.setState_(state);
};
unisubs.Dialog.prototype.setVisible = function(visible) {
    if (visible) {
        unisubs.Dialog.superClass_.setVisible.call(this, true);
        goog.dom.getDocumentScrollElement().scrollTop = 0;
    }
    else {
        if (this.isWorkSaved()) {
            this.hideDialogImpl_();
        }
        else {
            this.showSaveWorkDialog_();
        }
    }
};
unisubs.translate.Dialog.prototype.onWorkSaved = function() {
    if (this.finishFailDialog_) {
        this.finishFailDialog_.setVisible(false);
        this.finishFailDialog_ = null;
    }
    unisubs.widget.ResumeEditingRecord.clear();
    this.getRightPanelInternal().showLoading(false);
    this.saved_ = true;
    this.setVisible(false);
};
unisubs.translate.Dialog.prototype.disposeInternal = function() {
    unisubs.translate.Dialog.superClass_.disposeInternal.call(this);
    this.serverModel_.dispose();
};
unisubs.translate.Dialog.prototype.enterState_ = function(state) {
    this.showGuidelinesForState_(state);
};
unisubs.translate.Dialog.prototype.suspendKeyEvents_ = function(suspended) {
    this.keyEventsSuspended_ = suspended;
    if (this.currentSubtitlePanel_ && this.currentSubtitlePanel_.suspendKeyEvents)
        this.currentSubtitlePanel_.suspendKeyEvents(suspended);
};
unisubs.translate.Dialog.prototype.setState_ = function(state) {
    this.state_ = state;

    this.suspendKeyEvents_(false);

    var s = unisubs.translate.Dialog.State_;

    this.setExtraClass_(' the-translate-panel');

    var nextSubPanel = this.makeCurrentStateSubtitlePanel_();
    var captionPanel = this.getCaptioningAreaInternal();
    captionPanel.removeChildren(true);
    captionPanel.addChild(nextSubPanel, true);

    var rightPanel = nextSubPanel.getRightPanel();
    this.setRightPanelInternal(rightPanel);

    this.getTimelinePanelInternal().removeChildren(true);

    if (this.currentSubtitlePanel_ && this.currentSubtitlePanel_.getNotesContent_){
       var currentNoteContent =  this.currentSubtitlePanel_.getNotesContent_();
       if (nextSubPanel  && nextSubPanel.setNotesContent_){
           nextSubPanel.setNotesContent_(currentNoteContent);
       }
    }

    this.disposeCurrentPanels_();
    this.currentSubtitlePanel_ = nextSubPanel;

    var et = unisubs.RightPanel.EventType;
    this.rightPanelListener_.
        listen(
            rightPanel, et.LEGENDKEY, this.handleLegendKeyPress_).
        listen(
            rightPanel, et.DONE, this.handleDoneKeyPress_).
        listen(
            rightPanel, et.SAVEANDEXIT, this.handleSaveAndExitKeyPress_).
        listen(
            rightPanel, et.GOTOSTEP, this.handleGoToStep_);
    var backButtonText = null;
    if (state == s.EDIT_METADATA ){
        backButtonText = "Back to Translation";
    }
    if (backButtonText){
        rightPanel.showBackLink(backButtonText);
        this.rightPanelListener_.listen(
            rightPanel, et.BACK, this.handleBackKeyPress_);

    }

    var videoPlayer = this.getVideoPlayerInternal();
    if (this.isInDocument()) {
        if (!videoPlayer.isPaused()){
            videoPlayer.pause();
        }
        if(videoPlayer.getPlayheadTime()){
            videoPlayer.setPlayheadTime(0);
            videoPlayer.showCaptionText('');
            videoPlayer.pause();
        }
    }

    var that = this;
    this.getRightPanelInternal().showDownloadLink(
        function() {
            return that.makeJsonSubs();
        });
};
unisubs.translate.Dialog.prototype.makeCurrentStateSubtitlePanel_ = function() {
    var s = unisubs.translate.Dialog.State_;

    if (this.state_ == s.TRANSLATE){
        this.translationPanel_ =  new unisubs.translate.TranslationPanel(
            this.serverModel_.getCaptionSet(),
            this.standardSubState_,
            this,
            this.reviewOrApprovalType_,
            this.serverModel_);
        return this.translationPanel_;
    }
    else if (this.state_ == s.EDIT_METADATA)
        return new unisubs.editmetadata.Panel(
            this.serverModel_.getCaptionSet(),
            this.getVideoPlayerInternal(),
            this.serverModel_,
            this.captionManager_,
            this.standardSubState_ ,
            false,
            this.reviewOrApprovalType_,
            this);
};

/**
 * Tries translate subtitles with BingTranslator
 */
unisubs.translate.Dialog.prototype.translateViaBing = function(){
    //I don't know how better call this. I think it should be incapsulated in translationList_,
    //but have chain of function calls can confuse.
    this.translationPanel_.getTranslationList().translateViaBing(
        this.standardSubState_.LANGUAGE, this.subtitleState_.LANGUAGE);
};
unisubs.translate.Dialog.prototype.getStandardLanguage = function(){
    return this.standardSubState_.LANGUAGE;
};
unisubs.translate.Dialog.prototype.getSubtitleLanguage = function(){
    return this.subtitleState_.LANGUAGE;
};
unisubs.translate.Dialog.prototype.getServerModel = function(){
    return this.serverModel_;
};
unisubs.translate.Dialog.prototype.makeJsonSubs =  function (){
    return this.serverModel_.getCaptionSet().makeJsonSubs();
};
unisubs.translate.Dialog.prototype.forkAndClose = function() {
    var dialog = new unisubs.translate.ForkDialog(
        goog.bind(this.forkImpl_, this));
    dialog.setVisible(true);
};
unisubs.translate.Dialog.prototype.forkImpl_ = function() {
    this.subtitleState_.fork();
    this.serverModel_.fork(this.standardSubState_);
    this.hideToFork();
    var dialog = this.opener_.openSubtitlingDialog(
        this.serverModel_,
        this.subtitleState_);
    //this.opener_.onDialogOpened_(dialog);
    window.location.reload();
};
unisubs.translate.Dialog.prototype.disposeCurrentPanels_ = function() {
    if (this.currentSubtitlePanel_) {
        this.currentSubtitlePanel_.dispose();
        this.currentSubtitlePanel_ = null;
    }
    this.rightPanelListener_.removeAll();
    if (this.timelineSubtitleSet_) {
        this.timelineSubtitleSet_.dispose();
        this.timelineSubtitleSet_ = null;
    }
};
unisubs.translate.Dialog.prototype.captionReached_ = function(event) {
    var c = event.caption;
    var caption;

    if (c) {

        var captionID = c.getCaptionID();

        var translatedCaptionSet = this.translationPanel_.getTranslationList().captionSet_;
        var translatedCaption = translatedCaptionSet.captionByID(captionID);

        if (translatedCaption.getText() !== '') {
            caption = translatedCaption.getText();
        } else {
            caption = c.getText();
        }

    } else {
        caption = '';
    }

    this.getVideoPlayerInternal().showCaptionText(caption);
};
<commit_msg>Revert "Attempting a different technique for reverting to timed subtitles from a translation UI. Simply refresh the page after the subs have been forked instead of trying to rebuild everything from scratch."<commit_after>// Amara, universalsubtitles.org
//
// Copyright (C) 2012 Participatory Culture Foundation
//
// This program 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.
//
// This program 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 this program.  If not, see
// http://www.gnu.org/licenses/agpl-3.0.html.

goog.provide('unisubs.translate.Dialog');

/**
 * @constructor
 *
 */
unisubs.translate.Dialog = function(opener, serverModel, videoSource, subtitleState, standardSubState, reviewOrApprovalType) {
    unisubs.Dialog.call(this, videoSource);
    unisubs.SubTracker.getInstance().start(true);
    this.opener_ = opener;
    this.subtitleState_ = subtitleState;
    this.standardSubState_ = standardSubState;
    this.serverModel_ = serverModel;
    this.serverModel_.init();

    this.captionManager_ =
        new unisubs.CaptionManager(
            this.getVideoPlayerInternal(),
            new unisubs.subtitle.EditableCaptionSet(this.standardSubState_.SUBTITLES));

    this.saved_ = false;
    this.rightPanelListener_ = new goog.events.EventHandler(this);
    this.reviewOrApprovalType_ = reviewOrApprovalType;
    // if this is a review approve dialog, we must fetch saved notes for this task if available
    // but we should only do it when the dialog laods the first time
    // (else as users move back and forward between panels we might
    // overwrite their notes). This is what this flag is for
    this.notesFectched_ = false;
    if ( !Boolean(this.reviewOrApprovalType_)){
       this.notesFectched_ = true;
    }

    this.alreadySaving_ = false;
};

goog.inherits(unisubs.translate.Dialog, unisubs.subtitle.Dialog);

unisubs.translate.Dialog.State_ = {
    TRANSLATE: 0,
    EDIT_METADATA: 1
};
unisubs.translate.Dialog.prototype.createDom = function() {
    unisubs.translate.Dialog.superClass_.createDom.call(this);
    this.setDraggable(false);
    this.showGuidelines_();
    this.enterState_(unisubs.translate.Dialog.State_.TRANSLATE);
};
unisubs.translate.Dialog.prototype.showGuidelines_ = function() {
    var skipGuidelines = unisubs.UserSettings.getBooleanValue(
                            unisubs.UserSettings.Settings.ALWAYS_SKIP_GUIDELINES);

    if(skipGuidelines){
        return;
    }

    if (!unisubs.guidelines['translate']) var s = unisubs.subtitle.Dialog.State_;
    // the same dialog can be used in transcribing or review approval, which guidelines should we use?
    var guideline = this.reviewOrApprovalType_ ? this.getTeamGuidelineForReview() : unisubs.guidelines['translate'];

    if (!guideline) {
        return;
    }
    var guidelinesPanel = new unisubs.GuidelinesPanel(guideline);
    this.showTemporaryPanel(guidelinesPanel);
    this.displayingGuidelines_ = true;

    var that = this;
    this.getHandler().listenOnce(guidelinesPanel, unisubs.GuidelinesPanel.CONTINUE, function(e) {
        goog.Timer.callOnce(function() {
            that.displayingGuidelines_ = false;
            that.hideTemporaryPanel();
        });
    });
};
unisubs.translate.Dialog.prototype.handleSaveAndExitKeyPress_ = function(e) {
    e.preventDefault();
    var that = this;
    setTimeout(function(){
        that.saveWork(false, true);
    }, 1500);
};
unisubs.translate.Dialog.prototype.handleDoneKeyPress_ = function(event) {
    event.preventDefault();
    if (this.state_ == unisubs.translate.Dialog.State_.EDIT_METADATA) {

        // Make sure each new caption has text.
        var halt = false;

        goog.array.forEach(this.translationPanel_.captionSet_.captions_, function(c) {
            if (c.getText() === '') {
                halt = true;
            }
        });

        if (halt === true) {
            alert('You have untranslated captions. You must translate all captions in order to submit.');
            return false;
        }

        this.saveWork(true, false);

    }
    else {
        this.enterState_(unisubs.translate.Dialog.State_.EDIT_METADATA);
    }
};
unisubs.translate.Dialog.prototype.isWorkSaved = function() {
    if (this.reviewOrApprovalType_) {
        if (this.getNotesContent_(this.currentSubtitlePanel_) !== '') {
            return this.saved_;
        }
    }
    return this.saved_ || !this.serverModel_.anySubtitlingWorkDone();
};
unisubs.translate.Dialog.prototype.enterDocument = function() {
    // this is where we listen to the dialog close button
    unisubs.subtitle.Dialog.superClass_.enterDocument.call(this);
    unisubs.Dialog.translationDialogOpen = true;

    this.getHandler().
        listen(
            this.captionManager_,
            unisubs.CaptionManager.CAPTION,
            this.captionReached_);

    if (this.reviewOrApprovalType_ && !this.notesFectched_){
        var func  = this.serverModel_.fetchReviewData ;
        var that = this;
        if (this.reviewOrApprovalType_ == unisubs.Dialog.REVIEW_OR_APPROVAL.APPROVAL){
            func = this.serverModel_.fetchApproveData;
        }

        // make sure we retain the correct scope
        func.call(this.serverModel_, unisubs.task_id, function(body) {
            that.onNotesFetched_(body);
        });
    }
};
unisubs.translate.Dialog.prototype.onNotesFetched_ = function(body) {
    if( this.currentSubtitlePanel_ && this.currentSubtitlePanel_.setNotesContent_){
        this.currentSubtitlePanel_.setNotesContent_(body);
    }
};
unisubs.translate.Dialog.prototype.saveWorkInternal = function(closeAfterSave, saveForLater) {
    if(this.alreadySaving_){
        return;
    }

    this.alreadySaving_ = true;

    var notes = this.getNotesContent_(this.currentSubtitlePanel_);
    if (notes !== '') {
        this.serverModel_.setTaskNotes(notes);
    }

    if (goog.array.isEmpty(
        this.serverModel_.captionSet_.nonblankSubtitles()) && !this.forceSave_){
        this.alreadySaving_ = false;
        if((this.captionSet_.hasTitleChanged() && this.captionSet_.originalTitle == "") ||
             (this.captionSet_.hasDescriptionChanged() && this.captionSet_.originalDescription == "")){
            this.showTitleDescriptionChangedDialog(closeAfterSave, saveForLater);
        } else {
            // there are no subs here, close dialog or back to subtitling
            this.showEmptySubsDialog();
        }
        return;
    }

    var that = this;
    this.getRightPanelInternal().showLoading(true);
    this.serverModel_.finish(
        function(serverMsg){
            unisubs.subtitle.OnSavedDialog.show(serverMsg, function(){
                that.onWorkSaved(closeAfterSave);
            });
        },
        function(opt_status) {
            if (that.finishFailDialog_)
                that.finishFailDialog_.failedAgain(opt_status);
            else
                that.finishFailDialog_ = unisubs.finishfaildialog.Dialog.show(
                    that.serverModel_.getCaptionSet(), opt_status,
                    goog.bind(that.saveWorkInternal, that, closeAfterSave));
        },
        null,
        saveForLater);
};
unisubs.translate.Dialog.prototype.showGuidelinesForState_ = function(state) {
    this.setState_(state);
};
unisubs.Dialog.prototype.setVisible = function(visible) {
    if (visible) {
        unisubs.Dialog.superClass_.setVisible.call(this, true);
        goog.dom.getDocumentScrollElement().scrollTop = 0;
    }
    else {
        if (this.isWorkSaved()) {
            this.hideDialogImpl_();
        }
        else {
            this.showSaveWorkDialog_();
        }
    }
};
unisubs.translate.Dialog.prototype.onWorkSaved = function() {
    if (this.finishFailDialog_) {
        this.finishFailDialog_.setVisible(false);
        this.finishFailDialog_ = null;
    }
    unisubs.widget.ResumeEditingRecord.clear();
    this.getRightPanelInternal().showLoading(false);
    this.saved_ = true;
    this.setVisible(false);
};
unisubs.translate.Dialog.prototype.disposeInternal = function() {
    unisubs.translate.Dialog.superClass_.disposeInternal.call(this);
    this.serverModel_.dispose();
};
unisubs.translate.Dialog.prototype.enterState_ = function(state) {
    this.showGuidelinesForState_(state);
};
unisubs.translate.Dialog.prototype.suspendKeyEvents_ = function(suspended) {
    this.keyEventsSuspended_ = suspended;
    if (this.currentSubtitlePanel_ && this.currentSubtitlePanel_.suspendKeyEvents)
        this.currentSubtitlePanel_.suspendKeyEvents(suspended);
};
unisubs.translate.Dialog.prototype.setState_ = function(state) {
    this.state_ = state;

    this.suspendKeyEvents_(false);

    var s = unisubs.translate.Dialog.State_;

    this.setExtraClass_(' the-translate-panel');

    var nextSubPanel = this.makeCurrentStateSubtitlePanel_();
    var captionPanel = this.getCaptioningAreaInternal();
    captionPanel.removeChildren(true);
    captionPanel.addChild(nextSubPanel, true);

    var rightPanel = nextSubPanel.getRightPanel();
    this.setRightPanelInternal(rightPanel);

    this.getTimelinePanelInternal().removeChildren(true);

    if (this.currentSubtitlePanel_ && this.currentSubtitlePanel_.getNotesContent_){
       var currentNoteContent =  this.currentSubtitlePanel_.getNotesContent_();
       if (nextSubPanel  && nextSubPanel.setNotesContent_){
           nextSubPanel.setNotesContent_(currentNoteContent);
       }
    }

    this.disposeCurrentPanels_();
    this.currentSubtitlePanel_ = nextSubPanel;

    var et = unisubs.RightPanel.EventType;
    this.rightPanelListener_.
        listen(
            rightPanel, et.LEGENDKEY, this.handleLegendKeyPress_).
        listen(
            rightPanel, et.DONE, this.handleDoneKeyPress_).
        listen(
            rightPanel, et.SAVEANDEXIT, this.handleSaveAndExitKeyPress_).
        listen(
            rightPanel, et.GOTOSTEP, this.handleGoToStep_);
    var backButtonText = null;
    if (state == s.EDIT_METADATA ){
        backButtonText = "Back to Translation";
    }
    if (backButtonText){
        rightPanel.showBackLink(backButtonText);
        this.rightPanelListener_.listen(
            rightPanel, et.BACK, this.handleBackKeyPress_);

    }

    var videoPlayer = this.getVideoPlayerInternal();
    if (this.isInDocument()) {
        if (!videoPlayer.isPaused()){
            videoPlayer.pause();
        }
        if(videoPlayer.getPlayheadTime()){
            videoPlayer.setPlayheadTime(0);
            videoPlayer.showCaptionText('');
            videoPlayer.pause();
        }
    }

    var that = this;
    this.getRightPanelInternal().showDownloadLink(
        function() {
            return that.makeJsonSubs();
        });
};
unisubs.translate.Dialog.prototype.makeCurrentStateSubtitlePanel_ = function() {
    var s = unisubs.translate.Dialog.State_;

    if (this.state_ == s.TRANSLATE){
        this.translationPanel_ =  new unisubs.translate.TranslationPanel(
            this.serverModel_.getCaptionSet(),
            this.standardSubState_,
            this,
            this.reviewOrApprovalType_,
            this.serverModel_);
        return this.translationPanel_;
    }
    else if (this.state_ == s.EDIT_METADATA)
        return new unisubs.editmetadata.Panel(
            this.serverModel_.getCaptionSet(),
            this.getVideoPlayerInternal(),
            this.serverModel_,
            this.captionManager_,
            this.standardSubState_ ,
            false,
            this.reviewOrApprovalType_,
            this);
};

/**
 * Tries translate subtitles with BingTranslator
 */
unisubs.translate.Dialog.prototype.translateViaBing = function(){
    //I don't know how better call this. I think it should be incapsulated in translationList_,
    //but have chain of function calls can confuse.
    this.translationPanel_.getTranslationList().translateViaBing(
        this.standardSubState_.LANGUAGE, this.subtitleState_.LANGUAGE);
};
unisubs.translate.Dialog.prototype.getStandardLanguage = function(){
    return this.standardSubState_.LANGUAGE;
};
unisubs.translate.Dialog.prototype.getSubtitleLanguage = function(){
    return this.subtitleState_.LANGUAGE;
};
unisubs.translate.Dialog.prototype.getServerModel = function(){
    return this.serverModel_;
};
unisubs.translate.Dialog.prototype.makeJsonSubs =  function (){
    return this.serverModel_.getCaptionSet().makeJsonSubs();
};
unisubs.translate.Dialog.prototype.forkAndClose = function() {
    var dialog = new unisubs.translate.ForkDialog(
        goog.bind(this.forkImpl_, this));
    dialog.setVisible(true);
};
unisubs.translate.Dialog.prototype.forkImpl_ = function() {
    this.subtitleState_.fork();
    this.serverModel_.fork(this.standardSubState_);
    this.hideToFork();
    var dialog = this.opener_.openSubtitlingDialog(
        this.serverModel_,
        this.subtitleState_);
    this.opener_.onDialogOpened_(dialog);
};
unisubs.translate.Dialog.prototype.disposeCurrentPanels_ = function() {
    if (this.currentSubtitlePanel_) {
        this.currentSubtitlePanel_.dispose();
        this.currentSubtitlePanel_ = null;
    }
    this.rightPanelListener_.removeAll();
    if (this.timelineSubtitleSet_) {
        this.timelineSubtitleSet_.dispose();
        this.timelineSubtitleSet_ = null;
    }
};
unisubs.translate.Dialog.prototype.captionReached_ = function(event) {
    var c = event.caption;
    var caption;

    if (c) {

        var captionID = c.getCaptionID();

        var translatedCaptionSet = this.translationPanel_.getTranslationList().captionSet_;
        var translatedCaption = translatedCaptionSet.captionByID(captionID);

        if (translatedCaption.getText() !== '') {
            caption = translatedCaption.getText();
        } else {
            caption = c.getText();
        }

    } else {
        caption = '';
    }

    this.getVideoPlayerInternal().showCaptionText(caption);
};
<|endoftext|>


<commit_before>// TODO: resolve how loading will work
var Framework = window.FRAMEWORK;
////////////////////////////////////////////////////////////////////////////

// Supported "first-class" DOM events
var DOMEvents = [

	// Localized browser events
	// (works on individual elements)
	'error',
	'scroll',

	// Mouse events
	'click',
	'dblclick',
	'mousedown',
	'mouseup',
	'hover',
	'mouseenter',
	'mouseleave',
	'mouseover',
	'mouseout',
	'mousemove',

	// Keyboard events
	'keydown',
	'keyup',
	'keypress',

	// Form events
	'blur',
	'change',
	'focus',
	'focusin',
	'focusout',
	'select',
	'submit',

	// Raw touch events
	'touchstart',
	'touchend',
	'touchmove',
	'touchcancel',

	// Manufactured events
	'touch',

	// TODO:
	'rightclick',
	'clickoutside'
];

/*
// TODO
// Handle global window events
// (which are not covered by backbone event delegator natively)
var matchedSpecialDOMEvent = eventName.match(/window (.+)/);
if (matchedSpecialDOMEvent && matchedSpecialDOMEvent[1]) {
	$(window).unbind(matchedSpecialDOMEvent[1], handler);
	$(window).bind(matchedSpecialDOMEvent[1], handler);
}
*/

Framework.Util = {

	// Grab id xor data-id from an HTML element
	el2id: function (el, required) {
		var id = $(el).attr('id');
		var dataId = $(el).attr('data-id');

		if (id && dataId) {
			throw new Error(id + ' :: Cannot set both `id` and `data-id`!  Please use one or the other.  (data-id is safest)');
		}
		if (required && !id && !dataId) {
			throw new Error('No id specified in element where it is required:\n' + el);
		}

		return id || dataId;
	},

	/**
	 * Regexp to match "first class" DOM events
	 * (these are allowed in the top level of a component definition as method keys)
	 *		i.e. /^(click|hover|blur|focus)( (.+))/
	 *			[1] => event name
	 *			[3] => selector
	 */

	'/DOMEvent/': new RegExp('^(' + _.reduce(DOMEvents,
		function buildRegexp (memo, eventName, index) {

			// Omit `|` the first time
			if (index === 0) {
				return memo + eventName;
			}

			return memo + '|' + eventName;
		}, '') +
		')( (.+))?$'),

	/**
	 * Returns whether the specified event key matches a DOM event
	 *
	 * if no match is found
	 * @returns {Boolean} `false`
	 *
	 * otherwise
	 * @returns {Object} {
	 *		name		: name of DOM event
	 *		selector	: optionally, DOM selector(s) for delegation, or `undefined`
	 *	}
	 *
	 * NOTE: this function is cached using _.memoize
	 */

	parseDOMEvent: _.memoize(function (key) {

		var matches = key.match(Framework.Util['/DOMEvent/']);

		if (!matches || !matches[1]) {
			return false;
		}

		return {
			name		: matches[1],
			selector	: matches[3]
		};
	}),

	/**
	 * Translate **right-hand-side abbreviations** into functions that perform
	 * the proper behaviors, e.g.
	 *		#about_me
	 *		%mainMenu:open
	 */

	translateShorthand: function translateShorthand (value, key) {

		var matches, fn;

		// If this is an important, Framework-specific data key,
		// and a function was specified, run it to get its value
		// (this is to keep parity with Backbone's similar functionality)
		if (_.isFunction(value) && (key === 'collection' || key === 'model')) {
			return value();
		}

		// Ignore other non-strings
		if ( !_.isString(value) ) {
			return value;
		}

		// Redirects user to client-side URL, w/o affecting browser history
		// Like calling `Backbone.history.navigate('/foo', { replace: true })`
		if ( ( matches = value.match(/^##(.*[^.\s])/) ) && matches[1] ) {
			fn = function redirectAndCoverTracks () {
				var url = matches[1];
				Framework.history.navigate(url, { trigger: true, replace: true });
			};
		}
		// Method to redirect user to a client-side URL, then call the handler
		else if ( ( matches = value.match(/^#(.*[^.\s]+)/) ) && matches[1] ) {
			fn = function changeUrlFragment () {
				var url = matches[1];
				Framework.history.navigate(url, { trigger: true });
			};
		}
		// Method to trigger global event
		else if ( ( matches = value.match(/^(%.*[^.\s])/) ) && matches[1] ) {
			fn = function triggerEvent () {
				var trigger = matches[1];
				Framework.verbose(this.id + ' :: Triggering event (' + trigger + ')...');
				Framework.trigger(trigger);
			};
		}
		// Method to fire a test alert
		// (use message, if specified)
		else if ( ( matches = value.match(/^!!!\s*(.*[^.\s])?/) ) ) {
			fn = function bangAlert (e) {

				// If specified, message is used, otherwise 'Alert triggered!'
				var msg = (matches && matches[1]) || 'Debug alert (!!!) triggered!';

				// Other diagnostic information
				msg += '\n\nDiagnostics\n========================\n';
				if (e && e.currentTarget) {
					msg += 'e.currentTarget :: ' + e.currentTarget;
				}
				if (this.id) {
					msg +='this.id :: ' + this.id;
				}

				alert(msg);
			};
		}

		// Method to log a message to the console
		// (use message, if specified)
		else if ( ( matches = value.match(/^>>>\s*(.*[^.\s])?/) ) ) {
			fn = function logMessage (e) {

				// If specified, message is used, otherwise use default
				var msg = (matches && matches[1]) || 'Log message (>>>) triggered!';
				Framework.log(msg);
			};
		}

		// Method to attach the specified component/template to a region
		else if ( (matches = value.match(/^(.+)@(.*[^.\s])/)) && matches[1] && matches[2] ) {
			fn = function attachTemplate () {
				Framework.verbose(this.id + ' :: Attaching `' + matches[2] + '` to `' + matches[1] + '`...');

				var region = matches[1];
				var template = matches[2];
				this[region].attach(template);
			};
		}

		// Method to add the specified class
		else if ( (matches = value.match(/^\+\s*\.(.*[^.\s])/)) && matches[1] ) {
			fn = function addClass () {
				Framework.verbose(this.id + ' :: Adding class (' + matches[1] + ')...');
				this.$el.addClass(matches[1]);
			};
		}
		// Method to remove the specified class
		else if ( (matches = value.match(/^\-\s*\.(.*[^.\s])/)) && matches[1] ) {
			fn = function removeClass () {
				Framework.verbose(this.id + ' :: Removing class (' + matches[1] + ')...');
				this.$el.removeClass(matches[1]);
			};
		}
		// Method to toggle the specified class
		else if ( (matches = value.match(/^\!\s*\.(.*[^.\s])/)) && matches[1] ) {
			fn = function toggleClass () {
				Framework.verbose(this.id + ' :: Toggling class (' + matches[1] + ')...');
				this.$el.toggleClass(matches[1]);
			};
		}

		// TODO:	allow descendants to be controlled via shorthand
		//			e.g. : 'li.row -.highlighted'
		//			would remove the `highlighted` class from this.$('li.row')

		// If short-hand matched, return the dereferenced function
		if (fn) {

			Framework.verbose('Interpreting meaning from shorthand :: `' + value + '`...');

			// Curry the result function with any suffix matches
			var curriedFn = fn;

			// Trailing `.` indicates an e.stopPropagation()
			if (value.match(/\.\s*$/)) {
				curriedFn = function andStopPropagation (e) {

					// Bind (so it inherits component context) and call interior function
					fn.apply(this);

					// then immediately stop event bubbling/propagation
					if (e && e.stopPropagation) {
						e.stopPropagation();
					}
					else {
						Framework.warn(
							this.id + ' :: Trailing `.` shorthand was used to invoke an ' +
							'e.stopPropagation(), but "' + value + '" was not triggered by a DOM event!\n' +
							'Probably best to double-check this was what you meant to do.');
					}
				};
			}

			return curriedFn;
		}

		// Otherwise, if no short-hand matched, pass the original value
		// straight through
		return value;
	},

	// Map an object's values, but return a valid object
	// (this function is handy because underscore.map() returns a list, not an object)
	objMap: function objMap (obj, transformFn) {
		return _.object(_.keys(obj), _.map(obj, transformFn));
	}
};

/**
 * Event toolkit
 */

Framework.Util.Events = {

	/**
	 * Parse a dictionary of events
	 * (optionally filter by event type)
	 *
	 * @param {Object} events
	 *		`events` is a Backbone.View event object
	 *
	 * @param {Object} options
	 *	{
	 *		only: return the events explicitly laid out in this list
	 *				(if `only` is omitted, all events will be returned)
	 *	}
	 *
	 * @returns list of matching event keys
	 */

	parse: function (events, options) {

		var eventKeys = _.keys(events || {}),
			limitEvents,
			parsedEvents = [];

		// Optionally filter using set of acceptable event types
		limitEvents = options.only;
		eventKeys = _.filter(eventKeys, function checkEventName (eventKey) {

			// Parse event string into semantic representation
			var event = Framework.Util.parseDOMEvent(eventKey);
			parsedEvents.push(event);

			// Optional filter
			if (limitEvents) {
				return _.contains(limitEvents, event.name);
			}

			return true;
		});

		return parsedEvents;
	},

	/**
	 * Given a list of event expressions
	 *
	 * @param {Array} semanticEvents
	 *		List of event objs from `Framwork.Util.Events.parse`
	 *
	 * @param {Component} context
	 *		Instance of a component to use as a starting point for the DOM queries
	 *		(optional-- defaults to global context)
	 *
	 * @returns jQuery set of matched elements
	 */

	getElements: function (semanticEvents, context) {

		// Context optional
		context = context || { $: $ };

		// Iteratively build a set of affected elements
		var $affected = $();
		_.each(semanticEvents, function lookupElementsForEvent (event) {

			// Determine matched elements
			// Use delegate selector if specified
			// Otherwise, grab the element for this component
			var $matched =	event.selector ?
							context.$(event.selector) :
							context.$el;

			// Add matched elements to set
			$affected = $affected.add( $matched );
		}, this);

		return $affected;
	}
};

/**
 * Bare-bones DOM/UI utilities
 */

Framework.Util.DOM = {

	/**
	 * Expose the "DOMmy-eventedness" of elements so that it's selectable via CSS
	 * You can use this to apply a few choice DOM modifications out the gate--
	 * (e.g. tweaks targeting common issues that typically get forgotten, like disabling text selection)
	 *
	 * @param {Component} component
	 */

	flagComponent: function ( component ) {

		// Build subset of just the click/touch events
		var clickOrTouchEvents = Framework.Util.Events.parse(
			component.events,
			{ only: ['click', 'touch', 'touchstart', 'touchend'] }
		);

		// If no click/touch events found, bail out
		if ( clickOrTouchEvents.length < 1 ) return;

		// Query affected elements from DOM
		var $affected = Framework.Util.Events.getElements(clickOrTouchEvents, component);

		// Set `data-FRAMEWORK-clickable` custom attribute
		// (ui logic should be extended in CSS)
		$affected.attr('data-' + Framework.id + '-clickable', 'true');

		Framework.verbose(
			component.id + ' :: ' +
			'Disabled user text selection on elements w/ click/touch events:',
			clickOrTouchEvents,
			$affected
		);
	}

};
<commit_msg>Update data-attr to be more extensible for later when we add other event flags<commit_after>// TODO: resolve how loading will work
var Framework = window.FRAMEWORK;
////////////////////////////////////////////////////////////////////////////

// Supported "first-class" DOM events
var DOMEvents = [

	// Localized browser events
	// (works on individual elements)
	'error',
	'scroll',

	// Mouse events
	'click',
	'dblclick',
	'mousedown',
	'mouseup',
	'hover',
	'mouseenter',
	'mouseleave',
	'mouseover',
	'mouseout',
	'mousemove',

	// Keyboard events
	'keydown',
	'keyup',
	'keypress',

	// Form events
	'blur',
	'change',
	'focus',
	'focusin',
	'focusout',
	'select',
	'submit',

	// Raw touch events
	'touchstart',
	'touchend',
	'touchmove',
	'touchcancel',

	// Manufactured events
	'touch',

	// TODO:
	'rightclick',
	'clickoutside'
];

/*
// TODO
// Handle global window events
// (which are not covered by backbone event delegator natively)
var matchedSpecialDOMEvent = eventName.match(/window (.+)/);
if (matchedSpecialDOMEvent && matchedSpecialDOMEvent[1]) {
	$(window).unbind(matchedSpecialDOMEvent[1], handler);
	$(window).bind(matchedSpecialDOMEvent[1], handler);
}
*/

Framework.Util = {

	// Grab id xor data-id from an HTML element
	el2id: function (el, required) {
		var id = $(el).attr('id');
		var dataId = $(el).attr('data-id');

		if (id && dataId) {
			throw new Error(id + ' :: Cannot set both `id` and `data-id`!  Please use one or the other.  (data-id is safest)');
		}
		if (required && !id && !dataId) {
			throw new Error('No id specified in element where it is required:\n' + el);
		}

		return id || dataId;
	},

	/**
	 * Regexp to match "first class" DOM events
	 * (these are allowed in the top level of a component definition as method keys)
	 *		i.e. /^(click|hover|blur|focus)( (.+))/
	 *			[1] => event name
	 *			[3] => selector
	 */

	'/DOMEvent/': new RegExp('^(' + _.reduce(DOMEvents,
		function buildRegexp (memo, eventName, index) {

			// Omit `|` the first time
			if (index === 0) {
				return memo + eventName;
			}

			return memo + '|' + eventName;
		}, '') +
		')( (.+))?$'),

	/**
	 * Returns whether the specified event key matches a DOM event
	 *
	 * if no match is found
	 * @returns {Boolean} `false`
	 *
	 * otherwise
	 * @returns {Object} {
	 *		name		: name of DOM event
	 *		selector	: optionally, DOM selector(s) for delegation, or `undefined`
	 *	}
	 *
	 * NOTE: this function is cached using _.memoize
	 */

	parseDOMEvent: _.memoize(function (key) {

		var matches = key.match(Framework.Util['/DOMEvent/']);

		if (!matches || !matches[1]) {
			return false;
		}

		return {
			name		: matches[1],
			selector	: matches[3]
		};
	}),

	/**
	 * Translate **right-hand-side abbreviations** into functions that perform
	 * the proper behaviors, e.g.
	 *		#about_me
	 *		%mainMenu:open
	 */

	translateShorthand: function translateShorthand (value, key) {

		var matches, fn;

		// If this is an important, Framework-specific data key,
		// and a function was specified, run it to get its value
		// (this is to keep parity with Backbone's similar functionality)
		if (_.isFunction(value) && (key === 'collection' || key === 'model')) {
			return value();
		}

		// Ignore other non-strings
		if ( !_.isString(value) ) {
			return value;
		}

		// Redirects user to client-side URL, w/o affecting browser history
		// Like calling `Backbone.history.navigate('/foo', { replace: true })`
		if ( ( matches = value.match(/^##(.*[^.\s])/) ) && matches[1] ) {
			fn = function redirectAndCoverTracks () {
				var url = matches[1];
				Framework.history.navigate(url, { trigger: true, replace: true });
			};
		}
		// Method to redirect user to a client-side URL, then call the handler
		else if ( ( matches = value.match(/^#(.*[^.\s]+)/) ) && matches[1] ) {
			fn = function changeUrlFragment () {
				var url = matches[1];
				Framework.history.navigate(url, { trigger: true });
			};
		}
		// Method to trigger global event
		else if ( ( matches = value.match(/^(%.*[^.\s])/) ) && matches[1] ) {
			fn = function triggerEvent () {
				var trigger = matches[1];
				Framework.verbose(this.id + ' :: Triggering event (' + trigger + ')...');
				Framework.trigger(trigger);
			};
		}
		// Method to fire a test alert
		// (use message, if specified)
		else if ( ( matches = value.match(/^!!!\s*(.*[^.\s])?/) ) ) {
			fn = function bangAlert (e) {

				// If specified, message is used, otherwise 'Alert triggered!'
				var msg = (matches && matches[1]) || 'Debug alert (!!!) triggered!';

				// Other diagnostic information
				msg += '\n\nDiagnostics\n========================\n';
				if (e && e.currentTarget) {
					msg += 'e.currentTarget :: ' + e.currentTarget;
				}
				if (this.id) {
					msg +='this.id :: ' + this.id;
				}

				alert(msg);
			};
		}

		// Method to log a message to the console
		// (use message, if specified)
		else if ( ( matches = value.match(/^>>>\s*(.*[^.\s])?/) ) ) {
			fn = function logMessage (e) {

				// If specified, message is used, otherwise use default
				var msg = (matches && matches[1]) || 'Log message (>>>) triggered!';
				Framework.log(msg);
			};
		}

		// Method to attach the specified component/template to a region
		else if ( (matches = value.match(/^(.+)@(.*[^.\s])/)) && matches[1] && matches[2] ) {
			fn = function attachTemplate () {
				Framework.verbose(this.id + ' :: Attaching `' + matches[2] + '` to `' + matches[1] + '`...');

				var region = matches[1];
				var template = matches[2];
				this[region].attach(template);
			};
		}

		// Method to add the specified class
		else if ( (matches = value.match(/^\+\s*\.(.*[^.\s])/)) && matches[1] ) {
			fn = function addClass () {
				Framework.verbose(this.id + ' :: Adding class (' + matches[1] + ')...');
				this.$el.addClass(matches[1]);
			};
		}
		// Method to remove the specified class
		else if ( (matches = value.match(/^\-\s*\.(.*[^.\s])/)) && matches[1] ) {
			fn = function removeClass () {
				Framework.verbose(this.id + ' :: Removing class (' + matches[1] + ')...');
				this.$el.removeClass(matches[1]);
			};
		}
		// Method to toggle the specified class
		else if ( (matches = value.match(/^\!\s*\.(.*[^.\s])/)) && matches[1] ) {
			fn = function toggleClass () {
				Framework.verbose(this.id + ' :: Toggling class (' + matches[1] + ')...');
				this.$el.toggleClass(matches[1]);
			};
		}

		// TODO:	allow descendants to be controlled via shorthand
		//			e.g. : 'li.row -.highlighted'
		//			would remove the `highlighted` class from this.$('li.row')

		// If short-hand matched, return the dereferenced function
		if (fn) {

			Framework.verbose('Interpreting meaning from shorthand :: `' + value + '`...');

			// Curry the result function with any suffix matches
			var curriedFn = fn;

			// Trailing `.` indicates an e.stopPropagation()
			if (value.match(/\.\s*$/)) {
				curriedFn = function andStopPropagation (e) {

					// Bind (so it inherits component context) and call interior function
					fn.apply(this);

					// then immediately stop event bubbling/propagation
					if (e && e.stopPropagation) {
						e.stopPropagation();
					}
					else {
						Framework.warn(
							this.id + ' :: Trailing `.` shorthand was used to invoke an ' +
							'e.stopPropagation(), but "' + value + '" was not triggered by a DOM event!\n' +
							'Probably best to double-check this was what you meant to do.');
					}
				};
			}

			return curriedFn;
		}

		// Otherwise, if no short-hand matched, pass the original value
		// straight through
		return value;
	},

	// Map an object's values, but return a valid object
	// (this function is handy because underscore.map() returns a list, not an object)
	objMap: function objMap (obj, transformFn) {
		return _.object(_.keys(obj), _.map(obj, transformFn));
	}
};

/**
 * Event toolkit
 */

Framework.Util.Events = {

	/**
	 * Parse a dictionary of events
	 * (optionally filter by event type)
	 *
	 * @param {Object} events
	 *		`events` is a Backbone.View event object
	 *
	 * @param {Object} options
	 *	{
	 *		only: return the events explicitly laid out in this list
	 *				(if `only` is omitted, all events will be returned)
	 *	}
	 *
	 * @returns list of matching event keys
	 */

	parse: function (events, options) {

		var eventKeys = _.keys(events || {}),
			limitEvents,
			parsedEvents = [];

		// Optionally filter using set of acceptable event types
		limitEvents = options.only;
		eventKeys = _.filter(eventKeys, function checkEventName (eventKey) {

			// Parse event string into semantic representation
			var event = Framework.Util.parseDOMEvent(eventKey);
			parsedEvents.push(event);

			// Optional filter
			if (limitEvents) {
				return _.contains(limitEvents, event.name);
			}

			return true;
		});

		return parsedEvents;
	},

	/**
	 * Given a list of event expressions
	 *
	 * @param {Array} semanticEvents
	 *		List of event objs from `Framwork.Util.Events.parse`
	 *
	 * @param {Component} context
	 *		Instance of a component to use as a starting point for the DOM queries
	 *		(optional-- defaults to global context)
	 *
	 * @returns jQuery set of matched elements
	 */

	getElements: function (semanticEvents, context) {

		// Context optional
		context = context || { $: $ };

		// Iteratively build a set of affected elements
		var $affected = $();
		_.each(semanticEvents, function lookupElementsForEvent (event) {

			// Determine matched elements
			// Use delegate selector if specified
			// Otherwise, grab the element for this component
			var $matched =	event.selector ?
							context.$(event.selector) :
							context.$el;

			// Add matched elements to set
			$affected = $affected.add( $matched );
		}, this);

		return $affected;
	}
};

/**
 * Bare-bones DOM/UI utilities
 */

Framework.Util.DOM = {

	/**
	 * Expose the "DOMmy-eventedness" of elements so that it's selectable via CSS
	 * You can use this to apply a few choice DOM modifications out the gate--
	 * (e.g. tweaks targeting common issues that typically get forgotten, like disabling text selection)
	 *
	 * @param {Component} component
	 */

	flagComponent: function ( component ) {

		// TODO: provide access to bound global events (%) and routes (#) as well
		// TODO: flag all DOM events, not just click and touch

		// Build subset of just the click/touch events
		var clickOrTouchEvents = Framework.Util.Events.parse(
			component.events,
			{ only: ['click', 'touch', 'touchstart', 'touchend'] }
		);

		// If no click/touch events found, bail out
		if ( clickOrTouchEvents.length < 1 ) return;

		// Query affected elements from DOM
		var $affected = Framework.Util.Events.getElements(clickOrTouchEvents, component);

		// NOTE: For now, this is always just 'click'
		var boundEventsString = 'click';

		// Set `data-FRAMEWORK-clickable` custom attribute
		// (ui logic should be extended in CSS)
		$affected.attr('data-' + Framework.id + '-events', boundEventsString);

		Framework.verbose(
			component.id + ' :: ' +
			'Disabled user text selection on elements w/ click/touch events:',
			clickOrTouchEvents,
			$affected
		);
	}

};
<|endoftext|>