Source: red-dingo-tag-preview-framework.js

/*! promise-polyfill 3.1.0 */
!function(a){function b(a,b){return function(){a.apply(b,arguments)}}function c(a){if("object"!=typeof this)throw new TypeError("Promises must be constructed via new");if("function"!=typeof a)throw new TypeError("not a function");this._state=null,this._value=null,this._deferreds=[],i(a,b(e,this),b(f,this))}function d(a){var b=this;return null===this._state?void this._deferreds.push(a):void k(function(){var c=b._state?a.onFulfilled:a.onRejected;if(null===c)return void(b._state?a.resolve:a.reject)(b._value);var d;try{d=c(b._value)}catch(e){return void a.reject(e)}a.resolve(d)})}function e(a){try{if(a===this)throw new TypeError("A promise cannot be resolved with itself.");if(a&&("object"==typeof a||"function"==typeof a)){var c=a.then;if("function"==typeof c)return void i(b(c,a),b(e,this),b(f,this))}this._state=!0,this._value=a,g.call(this)}catch(d){f.call(this,d)}}function f(a){this._state=!1,this._value=a,g.call(this)}function g(){for(var a=0,b=this._deferreds.length;b>a;a++)d.call(this,this._deferreds[a]);this._deferreds=null}function h(a,b,c,d){this.onFulfilled="function"==typeof a?a:null,this.onRejected="function"==typeof b?b:null,this.resolve=c,this.reject=d}function i(a,b,c){var d=!1;try{a(function(a){d||(d=!0,b(a))},function(a){d||(d=!0,c(a))})}catch(e){if(d)return;d=!0,c(e)}}var j=setTimeout,k="function"==typeof setImmediate&&setImmediate||function(a){j(a,1)},l=Array.isArray||function(a){return"[object Array]"===Object.prototype.toString.call(a)};c.prototype["catch"]=function(a){return this.then(null,a)},c.prototype.then=function(a,b){var e=this;return new c(function(c,f){d.call(e,new h(a,b,c,f))})},c.all=function(){var a=Array.prototype.slice.call(1===arguments.length&&l(arguments[0])?arguments[0]:arguments);return new c(function(b,c){function d(f,g){try{if(g&&("object"==typeof g||"function"==typeof g)){var h=g.then;if("function"==typeof h)return void h.call(g,function(a){d(f,a)},c)}a[f]=g,0===--e&&b(a)}catch(i){c(i)}}if(0===a.length)return b([]);for(var e=a.length,f=0;f<a.length;f++)d(f,a[f])})},c.resolve=function(a){return a&&"object"==typeof a&&a.constructor===c?a:new c(function(b){b(a)})},c.reject=function(a){return new c(function(b,c){c(a)})},c.race=function(a){return new c(function(b,c){for(var d=0,e=a.length;e>d;d++)a[d].then(b,c)})},c._setImmediateFn=function(a){k=a},"undefined"!=typeof module&&module.exports?module.exports=c:a.Promise||(a.Promise=c)}(this);
/**
 * The core JS file for the tag editor framework.
 */ 
(function (root, factory) {
    if (typeof define === "function" && define.amd) {
        // AMD. Register as a named module.
        define("red-dingo-tag-preview-framework", [], factory);
    } else {
        // Browser globals. Install a global object named TagPreview.
        root.TagPreview = factory();
    }
} (this, function () {
	
	// Red Dingo framework common definitions
	var rdCommon = { }

	rdCommon.buildNumber = "20241";

	// Red Dingo static resources CDN root
	rdCommon.cdnRoot = "https://cdn.tags.reddingo.com/";

	rdCommon.tagEditorDir = "https://cdn.tags.reddingo.com/tag-preview-framework/lib/";

	// Module global private variables
	rdCommon.gwtBridge = null;

	/**
	 * Checks if the given object is a browser DOM element object.
	 */
	rdCommon.isDomElement = function(obj) {
		return typeof obj == "object" && "nodeType" in obj && obj.nodeType === 1;
	}

	/**
	 * Unwraps the underlying DOM element from a jQuery selector. If the object is already a DOM element
	 * it is simply returned.
	 */
	rdCommon.unwrapJQueryElement = function(el) {
		if (rdCommon.isDomElement(el)) {
			return el;
		} else {
			return el[0];
		}
	}

	/**
	 * Copies own properties of the second and any additional arguments into the first argument in manner similar to
	 * jQuery's $.extend or Lodash's _.assign. Less clever than either of those but enough for our purposes.
	 */
	rdCommon.extend = function(obj) {
		for (var i = 1; i < arguments.length; i++) {
			var source = arguments[i];

			for (var key in source) {
				if (source.hasOwnProperty(key)) {
					obj[key] = source[key];
				}
			}
		}
	}

	/**
	 * Gets a URL to a static resource on the CDN.
	 */
	rdCommon.getStatic = function(path) {
		return rdCommon.cdnRoot + path + "?v=" + rdCommon.buildNumber;
	}

	/**
	 * Namespace for functions used by the GWT code.
	 */
	rdCommon.GwtExports = {
		getStatic: rdCommon.getStatic
	};

	/**
	 * Lazily loads the GWT module and instantiates the GWT bridge when first required.
	 */
	rdCommon.initGwtBridge = function() {
		if (rdCommon.gwtBridge == null) {
			return new Promise(function(resolve, reject) {
				// Need to initialize the GWT glue code first. Through it, the JS code will talk to the GWT module.
				window.__tagEditorGwtCallbacks = window.__tagEditorGwtCallbacks || [];

				window.__tagEditorGwtCallbacks.push(function(err, result) {
					if (err) {
						reject(err);
					} else {
						rdCommon.gwtBridge = result;
						rdCommon.gwtBridge.setJsExports(rdCommon.GwtExports);
						rdCommon.gwtBridge.setTagDataPath(rdCommon.tagDataPath);
						resolve(null);
					}
				});

				var tagEditorDir;

				// Add <script> tag for the GWT module.
				var gwtScriptFile = "red_dingo_tag_editor_framework.nocache.js";
				var scriptElement = document.createElement("script");
				scriptElement.setAttribute("src",
						rdCommon.tagEditorDir + gwtScriptFile + "?v=" + rdCommon.buildNumber);
				document.getElementsByTagName("head")[0].appendChild(scriptElement);
			});
		} else {
			// Already initialized
			return Promise.resolve();
		}
	}

	rdCommon.createSideView = function(options) {
		return rdCommon.initGwtBridge().then(function() {
			var canvas = rdCommon.unwrapJQueryElement(options.canvasElement);

			if (!canvas) {
				return Promise.reject(new TypeError("Invalid canvas element"), null);
			}

			return rdCommon.gwtBridge.loadEngraveFont().then(function(font) {
				var side = options.side || "SIDE1";
				var zoomer = options.zoomer || rdCommon.gwtBridge.createZoomer(rdCommon.minZoomLevel);
				var view = rdCommon.gwtBridge.createSideView(
						canvas, font, side.toUpperCase(), zoomer, rdCommon.imagePath);

				// Repaint side on zoom level change
				zoomer.addScaleChangeListener(function() {
					view.repaint();
				});

				// RD-1565: Repaint on resize
				var resizeHandler = rdCommon.gwtBridge.debounce(function() {
					if (!document.contains(canvas)) {
						// canvas no longer attached to DOM - stop listening to window resize
						window.removeEventListener("resize", resizeHandler);
					} else {
						// repaint on window resize, just in case
						view.repaint();
					}
				}, 50, false);

				window.addEventListener("resize", resizeHandler);

				// Install hook for replacing the default wheel handler if necessary
				var wheelHandler = null;

				view.setOnWheel = function(handler) {
					if (wheelHandler) {
						canvas.removeEventListener("wheel", wheelHandler);
						wheelHandler = null;
					}

					if (canvas.__tagEditorDefaultOnWheel) {
						canvas.removeEventListener(canvas.__tagEditorDefaultOnWheel);
						canvas.__tagEditorDefaultOnWheel = null;
					}

					if (handler) {
						wheelHandler = function(e) {
							handler.call(view, e);
						};

						canvas.addEventListener("wheel", wheelHandler);
					}
				};

				// Install arrow key handler
				canvas.addEventListener("keydown", function(e) {
					switch (e.keyCode) {
					case 37: // left
						view.handleEditCommand("MOVE_LEFT");
						break;
					case 38: // up
						view.handleEditCommand("MOVE_UP");
						break;
					case 39: // right
						view.handleEditCommand("MOVE_RIGHT");
						break;
					case 40: // down
						view.handleEditCommand("MOVE_DOWN");
						break;
					default:
						break;
					}
				});

				return view;
			});
		});
	}

	/**
	 * A two-side view without toolbars.
	 */
	rdCommon.TwoSideView = function(side1, side2, zoomer) {
		var changeListeners = [];
		var selectionChangeListeners = [];
		var outOfBoundsChangeListeners = [];
		var currentAutoHideSide2 = false;
		var isSide2Hidden = false;

		function fireChange(side, otherSide) {
			changeListeners.forEach(function(listener) {
				listener(side, otherSide);
			});
		}

		function fireSelectionChange(selectedLines, side, otherSide) {
			selectionChangeListeners.forEach(function(listener) {
				listener(selectedLines, side, otherSide);
			});
		}

		function fireOutOfBoundsChange(outOfBoundsLines, side, otherSide) {
			outOfBoundsChangeListeners.forEach(function(listener) {
				listener(outOfBoundsLines, side, otherSide);
			});
		}

		/**
		 * Gets the side 1 view used by this two-side view.
		 *
		 * @function TwoSideView#getSide1
		 * @return {SideView} the side 1 view
		 */
		this.getSide1 = function() {
			return side1;
		};

		/**
		 * Gets the side 2 view used by this two-side view.
		 *
		 * @function TwoSideView#getSide2
		 * @return {SideView} the side 1 view
		 */
		this.getSide2 = function() {
			return side2;
		};

		/**
		 * Gets the zoomer associated with this two-side view.
		 *
		 * @function TwoSideView#getZoomer
		 * @return {Zoomer} the zoomer
		 */
		this.getZoomer = function() {
			return zoomer;
		};

		/**
		 * Listener for data change events.
		 *
		 * @param side {SideView} the side the event was fired for
		 * @param otherSide {SideView} the side the event was <b>not</b> fired for
		 * @callback TwoSideView~changeListener
		 */

		/**
		 * Registers a data change listener with this two-side view.
		 *
		 * @function TwoSideView#addChangeListener
		 * @param listener {TwoSideView~changeListener} listener to register
		 */
		this.addChangeListener = function(listener) {
			changeListeners.push(listener);
		};

		/**
		 * Listener for selection change events.
		 *
		 * @param selectedLines {number[]} Indexes of selected lines (zero-based), empty if no selection
		 * @param side {SideView} the side the event was fired for
		 * @param otherSide {SideView} the side the event was <b>not</b> fired for
		 * @callback TwoSideView~selectionChangeListener
		 */

		/**
		 * Registers a selection change listener with this two-side view.
		 *
		 * @function TwoSideView#addSelectionChangeListener
		 * @param listener {TwoSideView~selectionChangeListener} listener to register
		 */
		this.addSelectionChangeListener = function(listener) {
			selectionChangeListeners.push(listener);
		};

		/**
		 * Listener for out of bounds change events.
		 *
		 * @callback TwoSideView~outOfBoundsChangeListener
		 * @param outOfBoundsLines {number[]} Indexes of lines that cross the tag bounds (zero-based), empty if none
		 * @param side {SideView} the side the event was fired for
		 * @param otherSide {SideView} the side the event was <b>not</b> fired for
		 */

		/**
		 * Registers an out of bounds change listener with this two-side view.
		 *
		 * @function TwoSideView#addOutOfBoundsChangeListener
		 * @param listener {TwoSideView~outOfBoundsChangeListener} listener to register
		 */
		this.addOutOfBoundsChangeListener = function(listener) {
			outOfBoundsChangeListeners.push(listener);
		};

		// Install data change, selection change and line intersection change events
		function onDataChanged(side, otherSide) {
			// Return the actual event handler
			return function() {
				// For now, do nothing, but in the future, we might need this. Fire the event, though.
				fireChange(side, otherSide);
			};
		}

		if (side1.addChangeListener) { side1.addChangeListener(onDataChanged(side1, side2)); }
		if (side2.addChangeListener) { side2.addChangeListener(onDataChanged(side2, side1)); }

		function onSelectionChanged(side, otherSide) {
			// Return the actual event handler
			return function(selectedLines) {
				// Reset selection on the other side if all lines are deselected
				if (selectedLines.length == 0) {
					otherSide.resetSelection();
				}

				fireSelectionChange(selectedLines, side, otherSide);
			};
		}

		side1.addSelectionChangeListener(onSelectionChanged(side1, side2));
		side2.addSelectionChangeListener(onSelectionChanged(side2, side1));

		function onOutOfBoundsChanged(side, otherSide) {
			// Return the actual event handler
			return function(outOfBoundsLines) {
				fireOutOfBoundsChange(outOfBoundsLines, side, otherSide);
			};
		}

		side1.addOutOfBoundsChangeListener(onOutOfBoundsChanged(side1, side2));
		side2.addOutOfBoundsChangeListener(onOutOfBoundsChanged(side2, side1));

		function setSide2Visible(visible) {
			if (visible && isSide2Hidden) {
				side2.getCanvasElement().style.display = "none";
				isSide2Hidden = false;
			} else if (!visible && !isSide2Hidden) {
				side2.getCanvasElement().style.display = "";
				isSide2Hidden = true;
			}
		}

		/**
		 * Returns the tag options from the last successful call to {@link TwoSideView#setTag}. If <code>setTag</code>
		 * was not previously called, or no call succeeded, returns <code>null</code>.
		 *
		 * @function TwoSideView#getTag
		 * @return {?TagOptions} tag options used previously by <code>setTag</code>
		 */
		this.getTag = function() {
			return this.getSide1().getTag();
		};

		/**
		 * Asynchronously sets the tag for the two-side view. Arguments are identical to single-side.
		 *
		 * If the side has text on it, and the default layout was not customized, the text is relaid out according to
		 * the default layout of the new tag. A custom layout is untouched, so the text will likely not fit the new tag.
		 * If desired, a custom layout can be manually reset afterwards using {@link TwoSideView#resetLayout}.
		 *
		 * @function TwoSideView#setTag
		 * @param options {TagOptions} tag options
		 * @return {Promise.<void>} promise resolved after the operation completes
		 */
		this.setTag = function(options) {
			var that = this;

			// Note that if we need to hide side 2, it is done BEFORE queuing tag update for side 2,
			// whereas if we need to show side 2, it is done AFTER tag update for side 2 finishes.
			// This is done to prevent flickering.
			if (currentAutoHideSide2 && !options.doubleSided) {
				setSide2Visible(false);
			}

			return Promise.all([
					side1.setTag(options),
					side2.setTag(options).then(function() {
						if (currentAutoHideSide2 && options.doubleSided) {
							setSide2Visible(true);
							side2.repaint();
						}
					})
			]);
		};

		/**
		 * Returns whether side 2 should be automatically hidden upon when this tag view displays a single-side tag.
		 *
		 * @function TwoSideView#isAutoHideSide2
		 * @return {boolean} whether to automatically hide side 2
		 */
		this.isAutoHideSide2 = function() {
			return autoHideSide2;
		}

		/**
		 * Sets a flag specifying whether side 2 should be automatically hidden upon when this tag view displays a
		 * single-side tag. If necessary, shows or hides side 2 depending on the current tag and the new setting.
		 * <p>
		 * By default, side 2 is not automatically hidden.
		 * </p>
		 *
		 * @function TwoSideView#setAutoHideSide2
		 * @param autoHideSide2 {boolean} whether to automatically hide side 2
		 */
		this.setAutoHideSide2 = function(autoHideSide2) {
			autoHideSide2 = !!autoHideSide2; // coerce to boolean

			if (!currentAutoHideSide2 && autoHideSide2) {
				var tag = this.getTag();
				setSide2Visible(tag ? tag.doubleSided : false);
			} else if (currentAutoHideSide2 && !autoHideSide2) {
				setSide2Visible(true);
			}

			currentAutoHideSide2 = autoHideSide2;
		}
	}

	/**
	 * A two-side view without toolbars.
	 *
	 * @interface TwoSideView
	 */
	rdCommon.TwoSideView.prototype = {
		/**
		 * Returns the font set by the last successful call to {@link TwoSideView#setFont}. When a <code>TwoSideView</code>
		 * is created, the font is set to <code>ENGRAVE</code> by default.
		 *
		 * @function TwoSideView#getFont
		 * @return {module:TagEditor.Font} the current font
		 */
		getFont: function() {
			return this.getSide1().getFont();
		},

		/**
		 * Asynchronously sets the font for the two-side view. Arguments are identical to single-side.
		 *
		 * @function TwoSideView#setFont
		 * @param font {module:TagEditor.Font} the font to set (e.g. "ENGRAVE" or "UNICODE")
		 * @return {Promise.<void>} promise resolved after the operation completes
		 */
		setFont: function(font) {
			var that = this;

			return Promise.all([ that.getSide1().setFont(font), that.getSide2().setFont(font) ]);
		},

		/**
		 * Repaints the two-side view.
		 *
		 * @function TwoSideView#repaint
		 */
		repaint: function() {
			this.getSide1().repaint();
			this.getSide2().repaint();
		},

		/**
		 * Returns the text lines set by the last successful call to {@link TwoSideView#setLines}. When a
		 * <code>TwoSideView</code> is created, it has no text lines set, and this method returns an empty array.
		 *
		 * @function TwoSideView#getLines
		 * @return {TwoSideLines} current text lines
		 */
		getLines: function() {
			return {
				side1: this.getSide1().getLines(),
				side2: this.getSide2().getLines()
			};
		},

		/**
		 * Asynchronously sets the text for the two-side view. Any custom layout changes are reset, and the text is
		 * relaid out according to the default layout rules.
		 *
		 * @function TwoSideView#setLines
		 * @param textLines {TwoSideLines} text lines to set
		 * @return {Promise.<void>} promise resolved after the operation completes
		 */
		setLines: function(textLines) {
			var that = this;

			return Promise.all([
					that.getSide1().setLines(textLines.side1 || []),
					that.getSide2().setLines(textLines.side2 || [])
			]);
		},

		/**
		 * Checks if preview is enabled for this two-side view. When preview is enabled, a translucent version of the
		 * tag image or (for side 1, if set) the blank tag image is displayed under the rendered tag outline and text.
		 *
		 * @function TwoSideView#isPreviewEnabled
		 * @return {boolean} whether preview is enabled
		 */
		isPreviewEnabled: function() {
			return this.getSide1().isPreviewEnabled() && this.getSide2().isPreviewEnabled();
		},

		/**
		 * Sets the preview flag for this two-side view. Repaints the view if necessary.
		 *
		 * @function TwoSideView#setPreviewEnabled
		 * @param previewEnabled true to enable preview, false to disable
		 */
		setPreviewEnabled: function(previewEnabled) {
			this.getSide1().setPreviewEnabled(previewEnabled);
			this.getSide2().setPreviewEnabled(previewEnabled);
		},

		/**
		 * Checks if the view has any selected lines.
		 *
		 * @function TwoSideView#hasSelection
		 * @return {boolean} Whether there are any selected lines on either side of the view
		 */
		hasSelection: function() {
			return this.getSide1().hasSelection() || this.getSide2().hasSelection();
		},

		/**
		 * Unselects all lines on both sides of the view. Does not fire a selection change event.
		 *
		 * @function TwoSideView#resetSelection
		 */
		resetSelection: function() {
			this.getSide1().resetSelection();
			this.getSide2().resetSelection();
		},

		/**
		 * Forces the two-side view to use metric units (cm) or imperial units (inches) for rulers on both sides,
		 * replacing the current user selection (which can be different for each side).
		 *
		 * @function TwoSideView#showImperialUnits
		 * @param imperial true to use inches, false to use centimeters
		 */
		showImperialUnits: function(imperial) {
			this.getSide1().showImperialUnits(imperial);
			this.getSide2().showImperialUnits(imperial);
		},

		/**
		 * Replaces the default mouse wheel handler with <code>handler</code>. The handler listens to the browser
		 * <code>wheel</code> event on the canvas element. Inside the handler, <code>this</code> refers to the
		 * two-side view.
		 *
		 * @function TwoSideView#setOnWheel
		 * @param handler {jQueryEventHandler} the new wheel event handler
		 */
		setOnWheel: function(handler) {
			var that = this;
			var handlerWrapper = function(event) {
				handler.call(that, event);
			};

			this.getSide1().setOnWheel(handlerWrapper);
			this.getSide2().setOnWheel(handlerWrapper);
		}
	};

	rdCommon.createTwoSideViewPrerequisites = function(options) {
		return rdCommon.initGwtBridge().then(function() {
			var zoomer = options.zoomer || rdCommon.gwtBridge.createZoomer(rdCommon.minZoomLevel);
			var side1CanvasEl = rdCommon.unwrapJQueryElement(options.side1CanvasElement);

			if (!side1CanvasEl) {
				return Promise.reject(new TypeError("Invalid side 1 canvas element"), null);
			}

			var side2CanvasEl = rdCommon.unwrapJQueryElement(options.side2CanvasElement);

			if (!side2CanvasEl) {
				return Promise.reject(new TypeError("Invalid side 2 canvas element"), null);
			}

			return rdCommon.createSideView({
				canvasElement: side1CanvasEl,
				side: "SIDE1",
				zoomer: zoomer
			}).then(function(side1) {
				return rdCommon.createSideView({
					canvasElement: side2CanvasEl,
					side: "SIDE2",
					zoomer: zoomer
				}).then(function(side2) {
					return { side1: side1, side2: side2, zoomer: zoomer };
				});
			});
		});
	};


	// The definitions below are not used in the code and are only relevant to JSDoc.

	/**
	 * Event handler for jQuery events triggered by a {@link SideView} or {@link TwoSideView}. These view classes
	 * expose methods to override event handling. Inside a handler passed to such methods, the <code>this</code>
	 * parameter refers to the view (<code>SideView</code> or <code>TwoSideView</code>).
	 *
	 * @callback jQueryEventHandler
	 * @param event <a href="https://api.jquery.com/category/events/event-object/">jQuery event object</a>
	 */

	/**
	 * Object describing text lines for both sides of a {@link TwoSideView}.
	 *
	 * @typedef TwoSideLines
	 *
	 * @property side1 {string[]} text lines for side 1 (possibly an empty array)
	 * @property [side2=[]] {string[]} text lines for side 2 (possibly an empty array). Optional when passed to
	 * {@link TwoSideView#setLines}. Always returned by {@link TwoSideView#getLines}.
	 */

	/**
	 * Options object passed to {@link SideView#setTag} and {@link TwoSideView#setTag}.
	 *
	 * @typedef TagOptions
	 *
	 * @property tagSku {string}  tag SKU string in the extended format (e.g. "01-AU-DB-LG");
	 * 		hyphens may be omitted (e.g. "01AUDBLG")
	 * @property [doubleSided=false] {boolean} allow engraving on both sides of the tag; note that the
	 * 		framework does not check if the tag is actually capable of double-side engraving
	 * @property [template=default] {string} template code
	 * @property [blankTagImageFileName] {string} File name to use to display the blank tag image in preview mode.
	 * 		If not set, the main tag image is used as a fallback. If the file name has no extension, ".png" is
	 * 		assumed. The framework will automatically insert the right scale suffix in the file name before the
	 * 		extension if necessary to draw higher-resolution tag images.
	 */

	/**
	 * Object associated with each single-side and two-side view, used to control and synchronize the zoom level.
	 *
	 * @interface Zoomer
	 */
	var _Zoomer = {
		/**
		 * Gets the current zoom level.
		 *
		 * @function Zoomer#getZoom
		 * @return {number} the current zoom level
		 */
		getZoom: function() { },

		/**
		 * Sets the current zoom level.
		 *
		 * @function Zoomer#setZoom
		 * @param scale {number} the new zoom level
		 */
		setZoom: function(scale) { },

		/**
		 * Increases the zoom level.
		 *
		 * @function Zoomer#zoomIn
		 */
		zoomIn: function() { },

		/**
		 * Decreases the zoom level.
		 *
		 * @function Zoomer#zoomOut
		 */
		zoomOut: function() { },

		/**
		 * Resets the zoom level.
		 *
		 * @function Zoomer#resetZoom
		 */
		resetZoom: function() { },

		/**
		 * Listener for scale change events. Has no parameters.
		 *
		 * @callback Zoomer~scaleChangeListener
		 */

		/**
		 * Registers a listener called when the zoom level changes.
		 *
		 * @function Zoomer#addScaleChangeListener
		 * @param listener {Zoomer~scaleChangeListener} listener to register
		 */
		addScaleChangeListener: function(listener) { }
	};

	// Definition of the SideView public API shared between the tag editor framework and the tag preview framework.
	/**
	 * A single-side view without toolbars.
	 *
	 * @interface SideView
	 */
	rdCommon._SideView = {
		/**
		 * Returns the tag options from the last successful call to {@link SideView#setTag}. If <code>setTag</code> was not
		 * previously called, or no call succeeded, returns <code>null</code>.
		 *
		 * @function SideView#getTag
		 * @return {?TagOptions} tag options used previously by <code>setTag</code>
		 */
		getTag: function() { },

		/**
		 * Sets the tag for the single-side view.
		 *
		 * If the side has text on it, and the default layout was not customized, the text is relaid out according to
		 * the default layout of the new tag. A custom layout is untouched, so the text will likely not fit the new tag.
		 * If desired, a custom layout can be manually reset afterwards using {@link SideView#resetLayout}.
		 *
		 * @function SideView#setTag
		 * @param options {TagOptions} tag options
		 * @return {Promise.<void>} promise that is resolved when the operation completes
		 */
		setTag: function(options) { },

		/**
		 * Returns the font set by the last successful call to {@link SideView#setFont}. When a <code>SideView</code> is
		 * created, the font is set to <code>ENGRAVE</code> by default.
		 *
		 * @function SideView#getFont
		 * @return {module:TagEditor.Font} the current font
		 */
		getFont: function() { },

		/**
		 * Sets the font for the single-side view.
		 *
		 * @function SideView#setFont
		 * @param font {module:TagEditor.Font} font identifier
		 * @return {Promise.<void>} promise that is resolved when the operation completes
		 */
		setFont: function(font) { },

		/**
		 * Returns the text lines set by the last successful call to {@link SideView#setLines}. When a
		 * <code>SideView</code> is created, it has no text lines set, and this method returns an empty array.
		 *
		 * @function SideView#getLines
		 * @return {string[]} current text lines
		 */
		getLines: function() { },

		/**
		 * Sets the text lines for the single-side view. Any custom layout changes are reset, and the text is relaid
		 * out according to the default layout rules.
		 *
		 * @function SideView#setLines
		 * @param textLines {string[]} text lines to set
		 * @return {Promise.<void>} promise that is resolved when the operation completes
		 */
		setLines: function(textLines) { },

		/**
		 * Checks if preview is enabled for this side view. When preview is enabled, a translucent version of the tag
		 * image or (for side 1, if set) the blank tag image is displayed under the rendered tag outline and text.
		 *
		 * @function SideView#isPreviewEnabled
		 * @return whether preview is enabled
		 */
		isPreviewEnabled: function() { },

		/**
		 * Sets the preview flag for this side view. Repaints the view if necessary.
		 *
		 * @function SideView#setPreviewEnabled
		 * @param previewEnabled true to enable preview, false to disable
		 */
		setPreviewEnabled: function(previewEnabled) { },

		/**
		 * Repaints the single-side view.
		 *
		 * @function SideView#repaint
		 */
		repaint: function() { },

		/**
		 * Gets the canvas DOM element which this view uses to perform its drawing.
		 *
		 * @function SideView#getCanvasElement
		 * @return {Element} the canvas element
		 */
		getCanvasElement: function() { },

		/**
		 * Convenience method. Sets input focus to the view's canvas element.
		 * Equivalent to <code>getCanvasElement().focus()</code>.
		 *
		 * @function SideView#focus
		 */
		focus: function() { },

		/**
		 * Checks if the side has any selected lines.
		 *
		 * @function SideView#hasSelection
		 * @return {boolean} Whether there are any selected lines on this side
		 */
		hasSelection: function() { },

		/**
		 * Unselects all lines on this side. Does not fire a selection change event.
		 *
		 * @function SideView#resetSelection
		 */
		resetSelection: function() { },

		/**
		 * Forces the side view to use metric units (cm) or imperial units (inches) for rulers, replacing the current
		 * user selection.
		 *
		 * @function SideView#showImperialUnits
		 * @param imperial {boolean} true to use inches, false to use centimeters
		 */
		showImperialUnits: function(imperial) { },

		/**
		 * Listener for selection change events.
		 *
		 * @callback SideView~selectionChangeListener
		 * @param selectedLines {number[]} Indexes of selected lines (zero-based), empty if no selection
		 */

		/**
		 * Registers a selection change listener with this single-side view.
		 *
		 * @function SideView#addSelectionChangeListener
		 * @param listener {SideView~selectionChangeListener} listener to register
		 */
		addSelectionChangeListener: function(listener) { },

		/**
		 * Listener for out of bounds change events.
		 *
		 * @callback SideView~outOfBoundsChangeListener
		 * @param outOfBoundsLines {number[]} Indexes of lines that cross the tag bounds (zero-based), empty if none
		 */

		/**
		 * Registers a of bounds change listener with this single-side view.
		 *
		 * @function SideView#addOutOfBoundsChangeListener
		 * @param listener {SideView~outOfBoundsChangeListener} listener to register
		 */
		addOutOfBoundsChangeListener: function(listener) { },

		/**
		 * Replaces the default mouse wheel handler with <code>handler</code>. The handler listens to the browser
		 * <code>wheel</code> event on the canvas element. Inside the handler, <code>this</code> refers to the
		 * side view.
		 *
		 * @function SideView#setOnWheel
		 * @param handler {jQueryEventHandler} the new wheel event handler
		 */
		// This function is actually defined in createSideView. It is only listed here for JSDoc uniformity.
		setOnWheel: function(handler) { }
	};


	
	rdCommon.minZoomLevel = 1.0;
	
	// Relative CDN paths to tag data and tag images
	rdCommon.tagDataPath = "TagData2/";
	rdCommon.imagePath = "img/tags2/";
	
	// Wrapper that exposes only the public SideView API in the tag preview framework, not the full API exposed by GWT
	function SideView(gwtDelegate) {
		// Disable editing right away, and do not allow re-enabling it
		gwtDelegate.setEditingEnabled(false);
		
		// rdCommon._SideView contains the declarations of the shared API
		var api = rdCommon._SideView;
		
		function wrapFunc(obj, func) {
			return function() {
				return func.apply(obj, arguments);
			}
		}
		
		for (var key in api) {
			if (api.hasOwnProperty(key) && typeof api[key] === "function") {
				// For every function name present in the public API, forward it to delegate
				this[key] = wrapFunc(gwtDelegate, gwtDelegate[key]);
			}
		}
		
		// Definitions specific to tag preview framework
		
		/**
		 * Enables or disables line selection by user. If selection is disabled, current selection is cleared.
		 * 
		 * @function SideView#setSelectionEnabled
		 * @param selectionEnabled {boolean} true to enable selection, false to disable
		 */
		this.setSelectionEnabled = function(selectionEnabled) {
			gwtDelegate.setSelectionEnabled(selectionEnabled);
		}
		
		/**
		 * Specifies which rulers are visible and which are hidden.
		 * 
		 * @function SideView#setVisibleRulers
		 * @param visibleRulers {RulerSides} flags specifying which rulers are visible
		 */
		this.setVisibleRulers = function(visibleRulers) {
			gwtDelegate.setVisibleRulers(visibleRulers);
		}
		
		/**
		 * Specifies which corners will display the measurement unit switcher.
		 * 
		 * @function SideView#setVisibleUnits
		 * @param visibleUnits {UnitCorners} flags specifying which corners show the unit switcher
		 */
		this.setVisibleUnits = function(visibleUnits) {
			gwtDelegate.setVisibleUnits(visibleUnits);
		}
		
		/**
		 * Resets corners that display the measurement unit switcher to defaults.
		 * 
		 * @function SideView#resetVisibleUnits
		 */
		this.resetVisibleUnits = function() {
			gwtDelegate.resetVisibleUnits();
		}
		
		/**
		 * Returns the complete state of this side view.
		 * 
		 * @function SideView#getData
		 * @return {SideViewData} the current view state
		 */
		this.getData = function() {
			var data = gwtDelegate.getData();
			
			// Filter layout out of result
			return {
				tag : data.tag,
				font : data.font,
				lines : data.lines
			}
		}
		
		/**
		 * Sets the complete state of this side view. Any layout customizations are reset to default.
		 * 
		 * @function SideView#setData
		 * @param data {SideViewData} the new view state
		 * @return {Promise.<void>} promise that is resolved when the operation completes
		 */
		this.setData = function(data) {
			return gwtDelegate.setData({
				tag : data.tag,
				font : data.font,
				lines : data.lines
			});
		}
	}
	
	// Monkey patch rdCommon.createSideView to use the wrapper instead of returning the underlying GWT object
	var oldCreateSideView = rdCommon.createSideView;
	rdCommon.createSideView = function() {
		return oldCreateSideView.apply(this, arguments).then(function(view) {
			return new SideView(view);
		});
	};
	
	// Extra definitions used for TwoSideView in tag preview framework
	rdCommon.extend(rdCommon.TwoSideView.prototype, {
		/**
		 * Enables or disables line selection by user. If selection is disabled, current selection is cleared.
		 * 
		 * @function TwoSideView#setSelectionEnabled
		 * @param selectionEnabled {boolean} true to enable selection, false to disable
		 */
		setSelectionEnabled : function(selectionEnabled) {
			this.getSide1().setSelectionEnabled(selectionEnabled);
			this.getSide2().setSelectionEnabled(selectionEnabled);
		},
		
		/**
		 * Specifies which rulers are visible and which are hidden.
		 * 
		 * @function TwoSideView#setVisibleRulers
		 * @param visibleRulers {object} flags specifying which rulers are visible
		 * @param visibleRulers.side1 {RulerSides} ruler flags for side 1
		 * @param visibleRulers.side2 {RulerSides} ruler flags for side 2
		 */
		setVisibleRulers : function(visibleRulers) {
			this.getSide1().setVisibleRulers(visibleRulers.side1 || {});
			this.getSide2().setVisibleRulers(visibleRulers.side2 || {});
		},
		
		/**
		 * Specifies which corners will display the measurement unit switcher.
		 * 
		 * @function TwoSideView#setVisibleUnits
		 * @param visibleUnits {object} flags specifying which corners show the unit switcher
		 * @param visibleUnits.side1 {UnitCorners} unit flags for side 1
		 * @param visibleUnits.side2 {UnitCorners} unit flags for side 2
		 */
		setVisibleUnits : function(visibleUnits) {
			this.getSide1().setVisibleUnits(visibleUnits.side1 || {});
			this.getSide2().setVisibleUnits(visibleUnits.side2 || {});
		},
		
		/**
		 * Resets corners that display the measurement unit switcher to defaults.
		 * 
		 * @function TwoSideView#resetVisibleUnits
		 */
		resetVisibleUnits : function() {
			this.getSide1().resetVisibleUnits();
			this.getSide2().resetVisibleUnits();
		},
		
		/**
		 * Returns the complete state of this two-side view.
		 * 
		 * @function TwoSideView#getData
		 * @return {TwoSideViewData} the current view state
		 */
		getData: function() {
			var side1Data = this.getSide1().getData();
			var side2Data = this.getSide2().getData();
			
			return {
				tag: side1Data.tag,
				font: side1Data.font,
				
				side1: {
					lines: side1Data.lines
				},
				
				side2: {
					lines: side2Data.lines
				}
			};
		},
		
		/**
		 * Asynchronously sets the complete state of this two-side view.
		 * 
		 * @function TwoSideView#setData
		 * @param data {SideViewData} the new view state
		 * @return {Promise.<void>} promise that is resolved when the operation completes
		 */
		setData: function(data) {
			if (!data.tag || !data.side1 || (data.tag.doubleSided && !data.side2)) {
				return Promise.reject(new Error("invalid TwoSideView data"));
			}
			
			var side1Data = {
				tag: data.tag,
				font: data.font,
				lines: data.side1.lines || []
			};
			
			var side2Data = {
				tag: data.tag,
				font: data.font,
				lines: data.side2 ? data.side2.lines || [] : []
			};
			
			var that = this;
			return Promise.all([
					that.getSide1().setData(side1Data),
					that.getSide2().setData(side2Data)
			]);
		}
	});
	
	/**
	 * Functions exported by the TagPreview module.
	 * 
	 * @exports TagPreview
	 */
	var TagPreview = {
		/**
		 * The complete state of a {@link SideView}.
		 * 
		 * @typedef SideViewData
		 * 
		 * @property tag {TagOptions} tag options (as in <code>setTag</code>}
		 * @property font {module:TagPreview.Font} font ID (as in <code>setFont</code>}
		 * @property lines {string[]} text lines
		 */
			
		/**
		 * The complete state of a {@link TwoSideView}.
		 * 
		 * @typedef TwoSideViewData
		 * 
		 * @property tag {TagOptions} tag options (as in <code>setTag</code>}
		 * @property font {module:TagPreview.Font} font ID (as in <code>setFont</code>}
		 * @property side1 {object} side 1 state
		 * @property side1.lines {string[]} side 1 text lines
		 * @property side2 {object} side 2 state
		 * @property side2.lines {string[]} side 2 text lines
		 */
		
		/**
		 * Flags controlling which rulers are visible and which are hidden.
		 * 
		 * @typedef RulerSides
		 * 
		 * @property [top=false] {boolean} whether the top ruler is visible
		 * @property [bottom=false] {boolean} whether the bottom ruler is visible
		 * @property [left=false] {boolean} whether the left ruler is visible
		 * @property [right=false] {boolean} whether the right ruler is visible
		 */
			
		/**
		 * Flags controlling which corners display the measurement unit switcher.
		 * 
		 * @typedef UnitCorners
		 * 
		 * @property [topLeft=false] {boolean} whether units are visible in the top left corner
		 * @property [topRight=false] {boolean} whether units are visible in the top right corner
		 * @property [bottomLeft=false] {boolean} whether units are visible in the bottom left corner
		 * @property [bottomRight=false] {boolean} whether units are visible in the bottom right corner
		 */
			
		/**
		 * A possible font that can be set in a tag view.
		 * 
		 * @readonly
		 * @enum {string}
		 */
		Font: {
			/** Default tag editor font, only reliable for the ASCII range using Latin letters. */
			ENGRAVE: "ENGRAVE",
			/** More complete font, featuring a wide selection of Unicode glyphs. */
			UNICODE: "UNICODE"
		},
		
		/**
		 * Creates a Zoomer object, which provides zoom in/out/reset zoom functionality for tag views.
		 * 
		 * @return {Promise.<Zoomer>} promise resolved with the created Zoomer after the operation completes
		 */
		createZoomer: function() {
			return rdCommon.initGwtBridge().then(function() {
				return rdCommon.gwtBridge.createZoomer(rdCommon.minZoomLevel);
			});
		},
		
		/**
		 * Creates a new tag editor side view asynchronously, binding to the given canvas element.
		 * 
		 * @function
		 * @param options {Object} the options for the side view
		 * @param options.canvasElement {Element|jQuery} the &lt;canvas&gt; DOM element or jQuery object
		 * @param [options.side=SIDE1] {string} the side; "SIDE1" for side 1 or "SIDE2" for side 2
		 * @param [options.zoomer] {Zoomer} the zoomer object to be used by this view; if not set, one is created
		 * @return {Promise.<SideView>} promise resolved with the created SideView after the operation completes
		 */
		createSideView: rdCommon.createSideView,
		
		/**
		 * Creates a two-side view without toolbars, given two canvas elements for side 1 and side 2.
		 * 
		 * @param options {Object} the two-side view creation options
		 * @param options.side1CanvasElement {Element|jQuery} the side 1 &lt;canvas&gt; DOM element or jQuery object
		 * @param options.side2CanvasElement {Element|jQuery} the side 2 &lt;canvas&gt; DOM element or jQuery object
		 * @param [options.zoomer] {Zoomer} the zoomer object to be used by this view; if not set, one is created
		 * @return {Promise.<TwoSideView>} promise resolved with the created TwoSideView after the operation completes
		 */
		createTwoSideView: function(options) {
			return rdCommon.createTwoSideViewPrerequisites(options).then(function(result) {
				return new rdCommon.TwoSideView(result.side1, result.side2, result.zoomer);
			});
		}
	};
	
	return TagPreview;
}));