diff options
author | David Barksdale <amatus@amatus.name> | 2016-12-03 14:23:02 -0600 |
---|---|---|
committer | David Barksdale <amatus@amatus.name> | 2016-12-03 14:23:02 -0600 |
commit | 99e916beaf6afe5b2300bd95e73267550767bf7a (patch) | |
tree | 2b22c71fbdbf507735635c834de2276450b6372a /src | |
parent | 53284a2f5d22d15cd7851fd0f06a53ea1df0c280 (diff) |
Add Semantic-UI and get blockly working
Diffstat (limited to 'src')
52 files changed, 43766 insertions, 2 deletions
diff --git a/src/blockly/core/block.js b/src/blockly/core/block.js new file mode 100644 index 0000000..2021d3f --- /dev/null +++ b/src/blockly/core/block.js @@ -0,0 +1,1364 @@ +/** + * @license + * Visual Blocks Editor + * + * Copyright 2011 Google Inc. + * https://developers.google.com/blockly/ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview The class representing one block. + * @author fraser@google.com (Neil Fraser) + */ +'use strict'; + +goog.provide('Blockly.Block'); + +goog.require('Blockly.Blocks'); +goog.require('Blockly.Comment'); +goog.require('Blockly.Connection'); +goog.require('Blockly.Input'); +goog.require('Blockly.Mutator'); +goog.require('Blockly.Warning'); +goog.require('Blockly.Workspace'); +goog.require('Blockly.Xml'); +goog.require('goog.array'); +goog.require('goog.asserts'); +goog.require('goog.math.Coordinate'); +goog.require('goog.string'); + + +/** + * Class for one block. + * Not normally called directly, workspace.newBlock() is preferred. + * @param {!Blockly.Workspace} workspace The block's workspace. + * @param {?string} prototypeName Name of the language object containing + * type-specific functions for this block. + * @param {=string} opt_id Optional ID. Use this ID if provided, otherwise + * create a new id. + * @constructor + */ +Blockly.Block = function(workspace, prototypeName, opt_id) { + /** @type {string} */ + this.id = (opt_id && !workspace.getBlockById(opt_id)) ? + opt_id : Blockly.genUid(); + workspace.blockDB_[this.id] = this; + /** @type {Blockly.Connection} */ + this.outputConnection = null; + /** @type {Blockly.Connection} */ + this.nextConnection = null; + /** @type {Blockly.Connection} */ + this.previousConnection = null; + /** @type {!Array.<!Blockly.Input>} */ + this.inputList = []; + /** @type {boolean|undefined} */ + this.inputsInline = undefined; + /** @type {boolean} */ + this.disabled = false; + /** @type {string|!Function} */ + this.tooltip = ''; + /** @type {boolean} */ + this.contextMenu = true; + + /** + * @type {Blockly.Block} + * @private + */ + this.parentBlock_ = null; + + /** + * @type {!Array.<!Blockly.Block>} + * @private + */ + this.childBlocks_ = []; + + /** + * @type {boolean} + * @private + */ + this.deletable_ = true; + + /** + * @type {boolean} + * @private + */ + this.movable_ = true; + + /** + * @type {boolean} + * @private + */ + this.editable_ = true; + + /** + * @type {boolean} + * @private + */ + this.isShadow_ = false; + + /** + * @type {boolean} + * @private + */ + this.collapsed_ = false; + + /** @type {string|Blockly.Comment} */ + this.comment = null; + + /** + * @type {!goog.math.Coordinate} + * @private + */ + this.xy_ = new goog.math.Coordinate(0, 0); + + /** @type {!Blockly.Workspace} */ + this.workspace = workspace; + /** @type {boolean} */ + this.isInFlyout = workspace.isFlyout; + /** @type {boolean} */ + this.isInMutator = workspace.isMutator; + + /** @type {boolean} */ + this.RTL = workspace.RTL; + + // Copy the type-specific functions and data from the prototype. + if (prototypeName) { + /** @type {string} */ + this.type = prototypeName; + var prototype = Blockly.Blocks[prototypeName]; + goog.asserts.assertObject(prototype, + 'Error: "%s" is an unknown language block.', prototypeName); + goog.mixin(this, prototype); + } + + workspace.addTopBlock(this); + + // Call an initialization function, if it exists. + if (goog.isFunction(this.init)) { + this.init(); + } + // Record initial inline state. + /** @type {boolean|undefined} */ + this.inputsInlineDefault = this.inputsInline; + if (Blockly.Events.isEnabled()) { + Blockly.Events.fire(new Blockly.Events.Create(this)); + } + // Bind an onchange function, if it exists. + if (goog.isFunction(this.onchange)) { + this.onchangeWrapper_ = this.onchange.bind(this); + this.workspace.addChangeListener(this.onchangeWrapper_); + } +}; + +/** + * Obtain a newly created block. + * @param {!Blockly.Workspace} workspace The block's workspace. + * @param {?string} prototypeName Name of the language object containing + * type-specific functions for this block. + * @return {!Blockly.Block} The created block. + * @deprecated December 2015 + */ +Blockly.Block.obtain = function(workspace, prototypeName) { + console.warn('Deprecated call to Blockly.Block.obtain, ' + + 'use workspace.newBlock instead.'); + return workspace.newBlock(prototypeName); +}; + +/** + * Optional text data that round-trips beween blocks and XML. + * Has no effect. May be used by 3rd parties for meta information. + * @type {?string} + */ +Blockly.Block.prototype.data = null; + +/** + * Colour of the block in '#RRGGBB' format. + * @type {string} + * @private + */ +Blockly.Block.prototype.colour_ = '#000000'; + +/** + * Dispose of this block. + * @param {boolean} healStack If true, then try to heal any gap by connecting + * the next statement with the previous statement. Otherwise, dispose of + * all children of this block. + */ +Blockly.Block.prototype.dispose = function(healStack) { + if (!this.workspace) { + // Already deleted. + return; + } + // Terminate onchange event calls. + if (this.onchangeWrapper_) { + this.workspace.removeChangeListener(this.onchangeWrapper_); + } + this.unplug(healStack); + if (Blockly.Events.isEnabled()) { + Blockly.Events.fire(new Blockly.Events.Delete(this)); + } + Blockly.Events.disable(); + + try { + // This block is now at the top of the workspace. + // Remove this block from the workspace's list of top-most blocks. + if (this.workspace) { + this.workspace.removeTopBlock(this); + // Remove from block database. + delete this.workspace.blockDB_[this.id]; + this.workspace = null; + } + + // Just deleting this block from the DOM would result in a memory leak as + // well as corruption of the connection database. Therefore we must + // methodically step through the blocks and carefully disassemble them. + + // First, dispose of all my children. + for (var i = this.childBlocks_.length - 1; i >= 0; i--) { + this.childBlocks_[i].dispose(false); + } + // Then dispose of myself. + // Dispose of all inputs and their fields. + for (var i = 0, input; input = this.inputList[i]; i++) { + input.dispose(); + } + this.inputList.length = 0; + // Dispose of any remaining connections (next/previous/output). + var connections = this.getConnections_(true); + for (var i = 0; i < connections.length; i++) { + var connection = connections[i]; + if (connection.isConnected()) { + connection.disconnect(); + } + connections[i].dispose(); + } + } finally { + Blockly.Events.enable(); + } +}; + +/** + * Unplug this block from its superior block. If this block is a statement, + * optionally reconnect the block underneath with the block on top. + * @param {boolean} opt_healStack Disconnect child statement and reconnect + * stack. Defaults to false. + */ +Blockly.Block.prototype.unplug = function(opt_healStack) { + if (this.outputConnection) { + if (this.outputConnection.isConnected()) { + // Disconnect from any superior block. + this.outputConnection.disconnect(); + } + } else if (this.previousConnection) { + var previousTarget = null; + if (this.previousConnection.isConnected()) { + // Remember the connection that any next statements need to connect to. + previousTarget = this.previousConnection.targetConnection; + // Detach this block from the parent's tree. + this.previousConnection.disconnect(); + } + var nextBlock = this.getNextBlock(); + if (opt_healStack && nextBlock) { + // Disconnect the next statement. + var nextTarget = this.nextConnection.targetConnection; + nextTarget.disconnect(); + if (previousTarget && previousTarget.checkType_(nextTarget)) { + // Attach the next statement to the previous statement. + previousTarget.connect(nextTarget); + } + } + } +}; + +/** + * Returns all connections originating from this block. + * @return {!Array.<!Blockly.Connection>} Array of connections. + * @private + */ +Blockly.Block.prototype.getConnections_ = function() { + var myConnections = []; + if (this.outputConnection) { + myConnections.push(this.outputConnection); + } + if (this.previousConnection) { + myConnections.push(this.previousConnection); + } + if (this.nextConnection) { + myConnections.push(this.nextConnection); + } + for (var i = 0, input; input = this.inputList[i]; i++) { + if (input.connection) { + myConnections.push(input.connection); + } + } + return myConnections; +}; + +/** + * Walks down a stack of blocks and finds the last next connection on the stack. + * @return {Blockly.Connection} The last next connection on the stack, or null. + * @private + */ +Blockly.Block.prototype.lastConnectionInStack_ = function() { + var nextConnection = this.nextConnection; + while (nextConnection) { + var nextBlock = nextConnection.targetBlock(); + if (!nextBlock) { + // Found a next connection with nothing on the other side. + return nextConnection; + } + nextConnection = nextBlock.nextConnection; + } + // Ran out of next connections. + return null; +}; + +/** + * Bump unconnected blocks out of alignment. Two blocks which aren't actually + * connected should not coincidentally line up on screen. + * @private + */ +Blockly.Block.prototype.bumpNeighbours_ = function() { + if (!this.workspace) { + return; // Deleted block. + } + if (Blockly.dragMode_ != Blockly.DRAG_NONE) { + return; // Don't bump blocks during a drag. + } + var rootBlock = this.getRootBlock(); + if (rootBlock.isInFlyout) { + return; // Don't move blocks around in a flyout. + } + // Loop though every connection on this block. + var myConnections = this.getConnections_(false); + for (var i = 0, connection; connection = myConnections[i]; i++) { + // Spider down from this block bumping all sub-blocks. + if (connection.isConnected() && connection.isSuperior()) { + connection.targetBlock().bumpNeighbours_(); + } + + var neighbours = connection.neighbours_(Blockly.SNAP_RADIUS); + for (var j = 0, otherConnection; otherConnection = neighbours[j]; j++) { + // If both connections are connected, that's probably fine. But if + // either one of them is unconnected, then there could be confusion. + if (!connection.isConnected() || !otherConnection.isConnected()) { + // Only bump blocks if they are from different tree structures. + if (otherConnection.getSourceBlock().getRootBlock() != rootBlock) { + // Always bump the inferior block. + if (connection.isSuperior()) { + otherConnection.bumpAwayFrom_(connection); + } else { + connection.bumpAwayFrom_(otherConnection); + } + } + } + } + } +}; + +/** + * Return the parent block or null if this block is at the top level. + * @return {Blockly.Block} The block that holds the current block. + */ +Blockly.Block.prototype.getParent = function() { + // Look at the DOM to see if we are nested in another block. + return this.parentBlock_; +}; + +/** + * Return the input that connects to the specified block. + * @param {!Blockly.Block} block A block connected to an input on this block. + * @return {Blockly.Input} The input that connects to the specified block. + */ +Blockly.Block.prototype.getInputWithBlock = function(block) { + for (var i = 0, input; input = this.inputList[i]; i++) { + if (input.connection && input.connection.targetBlock() == block) { + return input; + } + } + return null; +}; + +/** + * Return the parent block that surrounds the current block, or null if this + * block has no surrounding block. A parent block might just be the previous + * statement, whereas the surrounding block is an if statement, while loop, etc. + * @return {Blockly.Block} The block that surrounds the current block. + */ +Blockly.Block.prototype.getSurroundParent = function() { + var block = this; + do { + var prevBlock = block; + block = block.getParent(); + if (!block) { + // Ran off the top. + return null; + } + } while (block.getNextBlock() == prevBlock); + // This block is an enclosing parent, not just a statement in a stack. + return block; +}; + +/** + * Return the next statement block directly connected to this block. + * @return {Blockly.Block} The next statement block or null. + */ +Blockly.Block.prototype.getNextBlock = function() { + return this.nextConnection && this.nextConnection.targetBlock(); +}; + +/** + * Return the top-most block in this block's tree. + * This will return itself if this block is at the top level. + * @return {!Blockly.Block} The root block. + */ +Blockly.Block.prototype.getRootBlock = function() { + var rootBlock; + var block = this; + do { + rootBlock = block; + block = rootBlock.parentBlock_; + } while (block); + return rootBlock; +}; + +/** + * Find all the blocks that are directly nested inside this one. + * Includes value and block inputs, as well as any following statement. + * Excludes any connection on an output tab or any preceding statement. + * @return {!Array.<!Blockly.Block>} Array of blocks. + */ +Blockly.Block.prototype.getChildren = function() { + return this.childBlocks_; +}; + +/** + * Set parent of this block to be a new block or null. + * @param {Blockly.Block} newParent New parent block. + */ +Blockly.Block.prototype.setParent = function(newParent) { + if (newParent == this.parentBlock_) { + return; + } + if (this.parentBlock_) { + // Remove this block from the old parent's child list. + var children = this.parentBlock_.childBlocks_; + for (var child, x = 0; child = children[x]; x++) { + if (child == this) { + children.splice(x, 1); + break; + } + } + + // Disconnect from superior blocks. + if (this.previousConnection && this.previousConnection.isConnected()) { + throw 'Still connected to previous block.'; + } + if (this.outputConnection && this.outputConnection.isConnected()) { + throw 'Still connected to parent block.'; + } + this.parentBlock_ = null; + // This block hasn't actually moved on-screen, so there's no need to update + // its connection locations. + } else { + // Remove this block from the workspace's list of top-most blocks. + this.workspace.removeTopBlock(this); + } + + this.parentBlock_ = newParent; + if (newParent) { + // Add this block to the new parent's child list. + newParent.childBlocks_.push(this); + } else { + this.workspace.addTopBlock(this); + } +}; + +/** + * Find all the blocks that are directly or indirectly nested inside this one. + * Includes this block in the list. + * Includes value and block inputs, as well as any following statements. + * Excludes any connection on an output tab or any preceding statements. + * @return {!Array.<!Blockly.Block>} Flattened array of blocks. + */ +Blockly.Block.prototype.getDescendants = function() { + var blocks = [this]; + for (var child, x = 0; child = this.childBlocks_[x]; x++) { + blocks.push.apply(blocks, child.getDescendants()); + } + return blocks; +}; + +/** + * Get whether this block is deletable or not. + * @return {boolean} True if deletable. + */ +Blockly.Block.prototype.isDeletable = function() { + return this.deletable_ && !this.isShadow_ && + !(this.workspace && this.workspace.options.readOnly); +}; + +/** + * Set whether this block is deletable or not. + * @param {boolean} deletable True if deletable. + */ +Blockly.Block.prototype.setDeletable = function(deletable) { + this.deletable_ = deletable; +}; + +/** + * Get whether this block is movable or not. + * @return {boolean} True if movable. + */ +Blockly.Block.prototype.isMovable = function() { + return this.movable_ && !this.isShadow_ && + !(this.workspace && this.workspace.options.readOnly); +}; + +/** + * Set whether this block is movable or not. + * @param {boolean} movable True if movable. + */ +Blockly.Block.prototype.setMovable = function(movable) { + this.movable_ = movable; +}; + +/** + * Get whether this block is a shadow block or not. + * @return {boolean} True if a shadow. + */ +Blockly.Block.prototype.isShadow = function() { + return this.isShadow_; +}; + +/** + * Set whether this block is a shadow block or not. + * @param {boolean} shadow True if a shadow. + */ +Blockly.Block.prototype.setShadow = function(shadow) { + this.isShadow_ = shadow; +}; + +/** + * Get whether this block is editable or not. + * @return {boolean} True if editable. + */ +Blockly.Block.prototype.isEditable = function() { + return this.editable_ && !(this.workspace && this.workspace.options.readOnly); +}; + +/** + * Set whether this block is editable or not. + * @param {boolean} editable True if editable. + */ +Blockly.Block.prototype.setEditable = function(editable) { + this.editable_ = editable; + for (var i = 0, input; input = this.inputList[i]; i++) { + for (var j = 0, field; field = input.fieldRow[j]; j++) { + field.updateEditable(); + } + } +}; + +/** + * Set whether the connections are hidden (not tracked in a database) or not. + * Recursively walk down all child blocks (except collapsed blocks). + * @param {boolean} hidden True if connections are hidden. + */ +Blockly.Block.prototype.setConnectionsHidden = function(hidden) { + if (!hidden && this.isCollapsed()) { + if (this.outputConnection) { + this.outputConnection.setHidden(hidden); + } + if (this.previousConnection) { + this.previousConnection.setHidden(hidden); + } + if (this.nextConnection) { + this.nextConnection.setHidden(hidden); + var child = this.nextConnection.targetBlock(); + if (child) { + child.setConnectionsHidden(hidden); + } + } + } else { + var myConnections = this.getConnections_(true); + for (var i = 0, connection; connection = myConnections[i]; i++) { + connection.setHidden(hidden); + if (connection.isSuperior()) { + var child = connection.targetBlock(); + if (child) { + child.setConnectionsHidden(hidden); + } + } + } + } +}; + +/** + * Set the URL of this block's help page. + * @param {string|Function} url URL string for block help, or function that + * returns a URL. Null for no help. + */ +Blockly.Block.prototype.setHelpUrl = function(url) { + this.helpUrl = url; +}; + +/** + * Change the tooltip text for a block. + * @param {string|!Function} newTip Text for tooltip or a parent element to + * link to for its tooltip. May be a function that returns a string. + */ +Blockly.Block.prototype.setTooltip = function(newTip) { + this.tooltip = newTip; +}; + +/** + * Get the colour of a block. + * @return {string} #RRGGBB string. + */ +Blockly.Block.prototype.getColour = function() { + return this.colour_; +}; + +/** + * Change the colour of a block. + * @param {number|string} colour HSV hue value, or #RRGGBB string. + */ +Blockly.Block.prototype.setColour = function(colour) { + var hue = parseFloat(colour); + if (!isNaN(hue)) { + this.colour_ = Blockly.hueToRgb(hue); + } else if (goog.isString(colour) && colour.match(/^#[0-9a-fA-F]{6}$/)) { + this.colour_ = colour; + } else { + throw 'Invalid colour: ' + colour; + } +}; + +/** + * Returns the named field from a block. + * @param {string} name The name of the field. + * @return {Blockly.Field} Named field, or null if field does not exist. + */ +Blockly.Block.prototype.getField = function(name) { + for (var i = 0, input; input = this.inputList[i]; i++) { + for (var j = 0, field; field = input.fieldRow[j]; j++) { + if (field.name === name) { + return field; + } + } + } + return null; +}; + +/** + * Return all variables referenced by this block. + * @return {!Array.<string>} List of variable names. + */ +Blockly.Block.prototype.getVars = function() { + var vars = []; + for (var i = 0, input; input = this.inputList[i]; i++) { + for (var j = 0, field; field = input.fieldRow[j]; j++) { + if (field instanceof Blockly.FieldVariable) { + vars.push(field.getValue()); + } + } + } + return vars; +}; + +/** + * Notification that a variable is renaming. + * If the name matches one of this block's variables, rename it. + * @param {string} oldName Previous name of variable. + * @param {string} newName Renamed variable. + */ +Blockly.Block.prototype.renameVar = function(oldName, newName) { + for (var i = 0, input; input = this.inputList[i]; i++) { + for (var j = 0, field; field = input.fieldRow[j]; j++) { + if (field instanceof Blockly.FieldVariable && + Blockly.Names.equals(oldName, field.getValue())) { + field.setValue(newName); + } + } + } +}; + +/** + * Returns the language-neutral value from the field of a block. + * @param {string} name The name of the field. + * @return {?string} Value from the field or null if field does not exist. + */ +Blockly.Block.prototype.getFieldValue = function(name) { + var field = this.getField(name); + if (field) { + return field.getValue(); + } + return null; +}; + +/** + * Returns the language-neutral value from the field of a block. + * @param {string} name The name of the field. + * @return {?string} Value from the field or null if field does not exist. + * @deprecated December 2013 + */ +Blockly.Block.prototype.getTitleValue = function(name) { + console.warn('Deprecated call to getTitleValue, use getFieldValue instead.'); + return this.getFieldValue(name); +}; + +/** + * Change the field value for a block (e.g. 'CHOOSE' or 'REMOVE'). + * @param {string} newValue Value to be the new field. + * @param {string} name The name of the field. + */ +Blockly.Block.prototype.setFieldValue = function(newValue, name) { + var field = this.getField(name); + goog.asserts.assertObject(field, 'Field "%s" not found.', name); + field.setValue(newValue); +}; + +/** + * Change the field value for a block (e.g. 'CHOOSE' or 'REMOVE'). + * @param {string} newValue Value to be the new field. + * @param {string} name The name of the field. + * @deprecated December 2013 + */ +Blockly.Block.prototype.setTitleValue = function(newValue, name) { + console.warn('Deprecated call to setTitleValue, use setFieldValue instead.'); + this.setFieldValue(newValue, name); +}; + +/** + * Set whether this block can chain onto the bottom of another block. + * @param {boolean} newBoolean True if there can be a previous statement. + * @param {string|Array.<string>|null|undefined} opt_check Statement type or + * list of statement types. Null/undefined if any type could be connected. + */ +Blockly.Block.prototype.setPreviousStatement = function(newBoolean, opt_check) { + if (newBoolean) { + if (opt_check === undefined) { + opt_check = null; + } + if (!this.previousConnection) { + goog.asserts.assert(!this.outputConnection, + 'Remove output connection prior to adding previous connection.'); + this.previousConnection = + this.makeConnection_(Blockly.PREVIOUS_STATEMENT); + } + this.previousConnection.setCheck(opt_check); + } else { + if (this.previousConnection) { + goog.asserts.assert(!this.previousConnection.isConnected(), + 'Must disconnect previous statement before removing connection.'); + this.previousConnection.dispose(); + this.previousConnection = null; + } + } +}; + +/** + * Set whether another block can chain onto the bottom of this block. + * @param {boolean} newBoolean True if there can be a next statement. + * @param {string|Array.<string>|null|undefined} opt_check Statement type or + * list of statement types. Null/undefined if any type could be connected. + */ +Blockly.Block.prototype.setNextStatement = function(newBoolean, opt_check) { + if (newBoolean) { + if (opt_check === undefined) { + opt_check = null; + } + if (!this.nextConnection) { + this.nextConnection = this.makeConnection_(Blockly.NEXT_STATEMENT); + } + this.nextConnection.setCheck(opt_check); + } else { + if (this.nextConnection) { + goog.asserts.assert(!this.nextConnection.isConnected(), + 'Must disconnect next statement before removing connection.'); + this.nextConnection.dispose(); + this.nextConnection = null; + } + } +}; + +/** + * Set whether this block returns a value. + * @param {boolean} newBoolean True if there is an output. + * @param {string|Array.<string>|null|undefined} opt_check Returned type or list + * of returned types. Null or undefined if any type could be returned + * (e.g. variable get). + */ +Blockly.Block.prototype.setOutput = function(newBoolean, opt_check) { + if (newBoolean) { + if (opt_check === undefined) { + opt_check = null; + } + if (!this.outputConnection) { + goog.asserts.assert(!this.previousConnection, + 'Remove previous connection prior to adding output connection.'); + this.outputConnection = this.makeConnection_(Blockly.OUTPUT_VALUE); + } + this.outputConnection.setCheck(opt_check); + } else { + if (this.outputConnection) { + goog.asserts.assert(!this.outputConnection.isConnected(), + 'Must disconnect output value before removing connection.'); + this.outputConnection.dispose(); + this.outputConnection = null; + } + } +}; + +/** + * Set whether value inputs are arranged horizontally or vertically. + * @param {boolean} newBoolean True if inputs are horizontal. + */ +Blockly.Block.prototype.setInputsInline = function(newBoolean) { + if (this.inputsInline != newBoolean) { + Blockly.Events.fire(new Blockly.Events.Change( + this, 'inline', null, this.inputsInline, newBoolean)); + this.inputsInline = newBoolean; + } +}; + +/** + * Get whether value inputs are arranged horizontally or vertically. + * @return {boolean} True if inputs are horizontal. + */ +Blockly.Block.prototype.getInputsInline = function() { + if (this.inputsInline != undefined) { + // Set explicitly. + return this.inputsInline; + } + // Not defined explicitly. Figure out what would look best. + for (var i = 1; i < this.inputList.length; i++) { + if (this.inputList[i - 1].type == Blockly.DUMMY_INPUT && + this.inputList[i].type == Blockly.DUMMY_INPUT) { + // Two dummy inputs in a row. Don't inline them. + return false; + } + } + for (var i = 1; i < this.inputList.length; i++) { + if (this.inputList[i - 1].type == Blockly.INPUT_VALUE && + this.inputList[i].type == Blockly.DUMMY_INPUT) { + // Dummy input after a value input. Inline them. + return true; + } + } + return false; +}; + +/** + * Set whether the block is disabled or not. + * @param {boolean} disabled True if disabled. + */ +Blockly.Block.prototype.setDisabled = function(disabled) { + if (this.disabled != disabled) { + Blockly.Events.fire(new Blockly.Events.Change( + this, 'disabled', null, this.disabled, disabled)); + this.disabled = disabled; + } +}; + +/** + * Get whether the block is disabled or not due to parents. + * The block's own disabled property is not considered. + * @return {boolean} True if disabled. + */ +Blockly.Block.prototype.getInheritedDisabled = function() { + var block = this; + while (true) { + block = block.getSurroundParent(); + if (!block) { + // Ran off the top. + return false; + } else if (block.disabled) { + return true; + } + } +}; + +/** + * Get whether the block is collapsed or not. + * @return {boolean} True if collapsed. + */ +Blockly.Block.prototype.isCollapsed = function() { + return this.collapsed_; +}; + +/** + * Set whether the block is collapsed or not. + * @param {boolean} collapsed True if collapsed. + */ +Blockly.Block.prototype.setCollapsed = function(collapsed) { + if (this.collapsed_ != collapsed) { + Blockly.Events.fire(new Blockly.Events.Change( + this, 'collapsed', null, this.collapsed_, collapsed)); + this.collapsed_ = collapsed; + } +}; + +/** + * Create a human-readable text representation of this block and any children. + * @param {number=} opt_maxLength Truncate the string to this length. + * @param {string=} opt_emptyToken The placeholder string used to denote an + * empty field. If not specified, '?' is used. + * @return {string} Text of block. + */ +Blockly.Block.prototype.toString = function(opt_maxLength, opt_emptyToken) { + var text = []; + var emptyFieldPlaceholder = opt_emptyToken || '?'; + if (this.collapsed_) { + text.push(this.getInput('_TEMP_COLLAPSED_INPUT').fieldRow[0].text_); + } else { + for (var i = 0, input; input = this.inputList[i]; i++) { + for (var j = 0, field; field = input.fieldRow[j]; j++) { + text.push(field.getText()); + } + if (input.connection) { + var child = input.connection.targetBlock(); + if (child) { + text.push(child.toString(undefined, opt_emptyToken)); + } else { + text.push(emptyFieldPlaceholder); + } + } + } + } + text = goog.string.trim(text.join(' ')) || '???'; + if (opt_maxLength) { + // TODO: Improve truncation so that text from this block is given priority. + // E.g. "1+2+3+4+5+6+7+8+9=0" should be "...6+7+8+9=0", not "1+2+3+4+5...". + // E.g. "1+2+3+4+5=6+7+8+9+0" should be "...4+5=6+7...". + text = goog.string.truncate(text, opt_maxLength); + } + return text; +}; + +/** + * Shortcut for appending a value input row. + * @param {string} name Language-neutral identifier which may used to find this + * input again. Should be unique to this block. + * @return {!Blockly.Input} The input object created. + */ +Blockly.Block.prototype.appendValueInput = function(name) { + return this.appendInput_(Blockly.INPUT_VALUE, name); +}; + +/** + * Shortcut for appending a statement input row. + * @param {string} name Language-neutral identifier which may used to find this + * input again. Should be unique to this block. + * @return {!Blockly.Input} The input object created. + */ +Blockly.Block.prototype.appendStatementInput = function(name) { + return this.appendInput_(Blockly.NEXT_STATEMENT, name); +}; + +/** + * Shortcut for appending a dummy input row. + * @param {string=} opt_name Language-neutral identifier which may used to find + * this input again. Should be unique to this block. + * @return {!Blockly.Input} The input object created. + */ +Blockly.Block.prototype.appendDummyInput = function(opt_name) { + return this.appendInput_(Blockly.DUMMY_INPUT, opt_name || ''); +}; + +/** + * Initialize this block using a cross-platform, internationalization-friendly + * JSON description. + * @param {!Object} json Structured data describing the block. + */ +Blockly.Block.prototype.jsonInit = function(json) { + // Validate inputs. + goog.asserts.assert(json['output'] == undefined || + json['previousStatement'] == undefined, + 'Must not have both an output and a previousStatement.'); + + // Set basic properties of block. + if (json['colour'] !== undefined) { + this.setColour(json['colour']); + } + + // Interpolate the message blocks. + var i = 0; + while (json['message' + i] !== undefined) { + this.interpolate_(json['message' + i], json['args' + i] || [], + json['lastDummyAlign' + i]); + i++; + } + + if (json['inputsInline'] !== undefined) { + this.setInputsInline(json['inputsInline']); + } + // Set output and previous/next connections. + if (json['output'] !== undefined) { + this.setOutput(true, json['output']); + } + if (json['previousStatement'] !== undefined) { + this.setPreviousStatement(true, json['previousStatement']); + } + if (json['nextStatement'] !== undefined) { + this.setNextStatement(true, json['nextStatement']); + } + if (json['tooltip'] !== undefined) { + this.setTooltip(json['tooltip']); + } + if (json['helpUrl'] !== undefined) { + this.setHelpUrl(json['helpUrl']); + } +}; + +/** + * Interpolate a message description onto the block. + * @param {string} message Text contains interpolation tokens (%1, %2, ...) + * that match with fields or inputs defined in the args array. + * @param {!Array} args Array of arguments to be interpolated. + * @param {=string} lastDummyAlign If a dummy input is added at the end, + * how should it be aligned? + * @private + */ +Blockly.Block.prototype.interpolate_ = function(message, args, lastDummyAlign) { + var tokens = Blockly.utils.tokenizeInterpolation(message); + // Interpolate the arguments. Build a list of elements. + var indexDup = []; + var indexCount = 0; + var elements = []; + for (var i = 0; i < tokens.length; i++) { + var token = tokens[i]; + if (typeof token == 'number') { + goog.asserts.assert(token > 0 && token <= args.length, + 'Message index "%s" out of range.', token); + goog.asserts.assert(!indexDup[token], + 'Message index "%s" duplicated.', token); + indexDup[token] = true; + indexCount++; + elements.push(args[token - 1]); + } else { + token = token.trim(); + if (token) { + elements.push(token); + } + } + } + goog.asserts.assert(indexCount == args.length, + 'Message does not reference all %s arg(s).', args.length); + // Add last dummy input if needed. + if (elements.length && (typeof elements[elements.length - 1] == 'string' || + elements[elements.length - 1]['type'].indexOf('field_') == 0)) { + var dummyInput = {type: 'input_dummy'}; + if (lastDummyAlign) { + dummyInput['align'] = lastDummyAlign; + } + elements.push(dummyInput); + } + // Lookup of alignment constants. + var alignmentLookup = { + 'LEFT': Blockly.ALIGN_LEFT, + 'RIGHT': Blockly.ALIGN_RIGHT, + 'CENTRE': Blockly.ALIGN_CENTRE + }; + // Populate block with inputs and fields. + var fieldStack = []; + for (var i = 0; i < elements.length; i++) { + var element = elements[i]; + if (typeof element == 'string') { + fieldStack.push([element, undefined]); + } else { + var field = null; + var input = null; + do { + var altRepeat = false; + if (typeof element == 'string') { + field = new Blockly.FieldLabel(element); + } else { + switch (element['type']) { + case 'input_value': + input = this.appendValueInput(element['name']); + break; + case 'input_statement': + input = this.appendStatementInput(element['name']); + break; + case 'input_dummy': + input = this.appendDummyInput(element['name']); + break; + case 'field_label': + field = new Blockly.FieldLabel(element['text'], element['class']); + break; + case 'field_input': + field = new Blockly.FieldTextInput(element['text']); + if (typeof element['spellcheck'] == 'boolean') { + field.setSpellcheck(element['spellcheck']); + } + break; + case 'field_angle': + field = new Blockly.FieldAngle(element['angle']); + break; + case 'field_checkbox': + field = new Blockly.FieldCheckbox( + element['checked'] ? 'TRUE' : 'FALSE'); + break; + case 'field_colour': + field = new Blockly.FieldColour(element['colour']); + break; + case 'field_variable': + field = new Blockly.FieldVariable(element['variable']); + break; + case 'field_dropdown': + field = new Blockly.FieldDropdown(element['options']); + break; + case 'field_image': + field = new Blockly.FieldImage(element['src'], + element['width'], element['height'], element['alt']); + break; + case 'field_number': + field = new Blockly.FieldNumber(element['value'], + element['min'], element['max'], element['precision']); + break; + case 'field_date': + if (Blockly.FieldDate) { + field = new Blockly.FieldDate(element['date']); + break; + } + // Fall through if FieldDate is not compiled in. + default: + // Unknown field. + if (element['alt']) { + element = element['alt']; + altRepeat = true; + } + } + } + } while (altRepeat); + if (field) { + fieldStack.push([field, element['name']]); + } else if (input) { + if (element['check']) { + input.setCheck(element['check']); + } + if (element['align']) { + input.setAlign(alignmentLookup[element['align']]); + } + for (var j = 0; j < fieldStack.length; j++) { + input.appendField(fieldStack[j][0], fieldStack[j][1]); + } + fieldStack.length = 0; + } + } + } +}; + +/** + * Add a value input, statement input or local variable to this block. + * @param {number} type Either Blockly.INPUT_VALUE or Blockly.NEXT_STATEMENT or + * Blockly.DUMMY_INPUT. + * @param {string} name Language-neutral identifier which may used to find this + * input again. Should be unique to this block. + * @return {!Blockly.Input} The input object created. + * @private + */ +Blockly.Block.prototype.appendInput_ = function(type, name) { + var connection = null; + if (type == Blockly.INPUT_VALUE || type == Blockly.NEXT_STATEMENT) { + connection = this.makeConnection_(type); + } + var input = new Blockly.Input(type, name, this, connection); + // Append input to list. + this.inputList.push(input); + return input; +}; + +/** + * Move a named input to a different location on this block. + * @param {string} name The name of the input to move. + * @param {?string} refName Name of input that should be after the moved input, + * or null to be the input at the end. + */ +Blockly.Block.prototype.moveInputBefore = function(name, refName) { + if (name == refName) { + return; + } + // Find both inputs. + var inputIndex = -1; + var refIndex = refName ? -1 : this.inputList.length; + for (var i = 0, input; input = this.inputList[i]; i++) { + if (input.name == name) { + inputIndex = i; + if (refIndex != -1) { + break; + } + } else if (refName && input.name == refName) { + refIndex = i; + if (inputIndex != -1) { + break; + } + } + } + goog.asserts.assert(inputIndex != -1, 'Named input "%s" not found.', name); + goog.asserts.assert(refIndex != -1, 'Reference input "%s" not found.', + refName); + this.moveNumberedInputBefore(inputIndex, refIndex); +}; + +/** + * Move a numbered input to a different location on this block. + * @param {number} inputIndex Index of the input to move. + * @param {number} refIndex Index of input that should be after the moved input. + */ +Blockly.Block.prototype.moveNumberedInputBefore = function( + inputIndex, refIndex) { + // Validate arguments. + goog.asserts.assert(inputIndex != refIndex, 'Can\'t move input to itself.'); + goog.asserts.assert(inputIndex < this.inputList.length, + 'Input index ' + inputIndex + ' out of bounds.'); + goog.asserts.assert(refIndex <= this.inputList.length, + 'Reference input ' + refIndex + ' out of bounds.'); + // Remove input. + var input = this.inputList[inputIndex]; + this.inputList.splice(inputIndex, 1); + if (inputIndex < refIndex) { + refIndex--; + } + // Reinsert input. + this.inputList.splice(refIndex, 0, input); +}; + +/** + * Remove an input from this block. + * @param {string} name The name of the input. + * @param {boolean=} opt_quiet True to prevent error if input is not present. + * @throws {goog.asserts.AssertionError} if the input is not present and + * opt_quiet is not true. + */ +Blockly.Block.prototype.removeInput = function(name, opt_quiet) { + for (var i = 0, input; input = this.inputList[i]; i++) { + if (input.name == name) { + if (input.connection && input.connection.isConnected()) { + input.connection.setShadowDom(null); + var block = input.connection.targetBlock(); + if (block.isShadow()) { + // Destroy any attached shadow block. + block.dispose(); + } else { + // Disconnect any attached normal block. + block.unplug(); + } + } + input.dispose(); + this.inputList.splice(i, 1); + return; + } + } + if (!opt_quiet) { + goog.asserts.fail('Input "%s" not found.', name); + } +}; + +/** + * Fetches the named input object. + * @param {string} name The name of the input. + * @return {Blockly.Input} The input object, or null if input does not exist. + */ +Blockly.Block.prototype.getInput = function(name) { + for (var i = 0, input; input = this.inputList[i]; i++) { + if (input.name == name) { + return input; + } + } + // This input does not exist. + return null; +}; + +/** + * Fetches the block attached to the named input. + * @param {string} name The name of the input. + * @return {Blockly.Block} The attached value block, or null if the input is + * either disconnected or if the input does not exist. + */ +Blockly.Block.prototype.getInputTargetBlock = function(name) { + var input = this.getInput(name); + return input && input.connection && input.connection.targetBlock(); +}; + +/** + * Returns the comment on this block (or '' if none). + * @return {string} Block's comment. + */ +Blockly.Block.prototype.getCommentText = function() { + return this.comment || ''; +}; + +/** + * Set this block's comment text. + * @param {?string} text The text, or null to delete. + */ +Blockly.Block.prototype.setCommentText = function(text) { + if (this.comment != text) { + Blockly.Events.fire(new Blockly.Events.Change( + this, 'comment', null, this.comment, text || '')); + this.comment = text; + } +}; + +/** + * Set this block's warning text. + * @param {?string} text The text, or null to delete. + */ +Blockly.Block.prototype.setWarningText = function(text) { + // NOP. +}; + +/** + * Give this block a mutator dialog. + * @param {Blockly.Mutator} mutator A mutator dialog instance or null to remove. + */ +Blockly.Block.prototype.setMutator = function(mutator) { + // NOP. +}; + +/** + * Return the coordinates of the top-left corner of this block relative to the + * drawing surface's origin (0,0). + * @return {!goog.math.Coordinate} Object with .x and .y properties. + */ +Blockly.Block.prototype.getRelativeToSurfaceXY = function() { + return this.xy_; +}; + +/** + * Move a block by a relative offset. + * @param {number} dx Horizontal offset. + * @param {number} dy Vertical offset. + */ +Blockly.Block.prototype.moveBy = function(dx, dy) { + goog.asserts.assert(!this.parentBlock_, 'Block has parent.'); + var event = new Blockly.Events.Move(this); + this.xy_.translate(dx, dy); + event.recordNew(); + Blockly.Events.fire(event); +}; + +/** + * Create a connection of the specified type. + * @param {number} type The type of the connection to create. + * @return {!Blockly.Connection} A new connection of the specified type. + * @private + */ +Blockly.Block.prototype.makeConnection_ = function(type) { + return new Blockly.Connection(this, type); +}; diff --git a/src/blockly/core/block_render_svg.js b/src/blockly/core/block_render_svg.js new file mode 100644 index 0000000..14c42b0 --- /dev/null +++ b/src/blockly/core/block_render_svg.js @@ -0,0 +1,969 @@ +/** + * @license + * Visual Blocks Editor + * + * Copyright 2016 Google Inc. + * https://developers.google.com/blockly/ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Methods for graphically rendering a block as SVG. + * @author fenichel@google.com (Rachel Fenichel) + */ + +'use strict'; + +goog.provide('Blockly.BlockSvg.render'); + +goog.require('Blockly.BlockSvg'); + + +// UI constants for rendering blocks. +/** + * Horizontal space between elements. + * @const + */ +Blockly.BlockSvg.SEP_SPACE_X = 10; +/** + * Vertical space between elements. + * @const + */ +Blockly.BlockSvg.SEP_SPACE_Y = 10; +/** + * Vertical padding around inline elements. + * @const + */ +Blockly.BlockSvg.INLINE_PADDING_Y = 5; +/** + * Minimum height of a block. + * @const + */ +Blockly.BlockSvg.MIN_BLOCK_Y = 25; +/** + * Height of horizontal puzzle tab. + * @const + */ +Blockly.BlockSvg.TAB_HEIGHT = 20; +/** + * Width of horizontal puzzle tab. + * @const + */ +Blockly.BlockSvg.TAB_WIDTH = 8; +/** + * Width of vertical tab (inc left margin). + * @const + */ +Blockly.BlockSvg.NOTCH_WIDTH = 30; +/** + * Rounded corner radius. + * @const + */ +Blockly.BlockSvg.CORNER_RADIUS = 8; +/** + * Do blocks with no previous or output connections have a 'hat' on top? + * @const + */ +Blockly.BlockSvg.START_HAT = false; +/** + * Height of the top hat. + * @const + */ +Blockly.BlockSvg.START_HAT_HEIGHT = 15; +/** + * Path of the top hat's curve. + * @const + */ +Blockly.BlockSvg.START_HAT_PATH = 'c 30,-' + + Blockly.BlockSvg.START_HAT_HEIGHT + ' 70,-' + + Blockly.BlockSvg.START_HAT_HEIGHT + ' 100,0'; +/** + * Path of the top hat's curve's highlight in LTR. + * @const + */ +Blockly.BlockSvg.START_HAT_HIGHLIGHT_LTR = + 'c 17.8,-9.2 45.3,-14.9 75,-8.7 M 100.5,0.5'; +/** + * Path of the top hat's curve's highlight in RTL. + * @const + */ +Blockly.BlockSvg.START_HAT_HIGHLIGHT_RTL = + 'm 25,-8.7 c 29.7,-6.2 57.2,-0.5 75,8.7'; +/** + * Distance from shape edge to intersect with a curved corner at 45 degrees. + * Applies to highlighting on around the inside of a curve. + * @const + */ +Blockly.BlockSvg.DISTANCE_45_INSIDE = (1 - Math.SQRT1_2) * + (Blockly.BlockSvg.CORNER_RADIUS - 0.5) + 0.5; +/** + * Distance from shape edge to intersect with a curved corner at 45 degrees. + * Applies to highlighting on around the outside of a curve. + * @const + */ +Blockly.BlockSvg.DISTANCE_45_OUTSIDE = (1 - Math.SQRT1_2) * + (Blockly.BlockSvg.CORNER_RADIUS + 0.5) - 0.5; +/** + * SVG path for drawing next/previous notch from left to right. + * @const + */ +Blockly.BlockSvg.NOTCH_PATH_LEFT = 'l 6,4 3,0 6,-4'; +/** + * SVG path for drawing next/previous notch from left to right with + * highlighting. + * @const + */ +Blockly.BlockSvg.NOTCH_PATH_LEFT_HIGHLIGHT = 'l 6,4 3,0 6,-4'; +/** + * SVG path for drawing next/previous notch from right to left. + * @const + */ +Blockly.BlockSvg.NOTCH_PATH_RIGHT = 'l -6,4 -3,0 -6,-4'; +/** + * SVG path for drawing jagged teeth at the end of collapsed blocks. + * @const + */ +Blockly.BlockSvg.JAGGED_TEETH = 'l 8,0 0,4 8,4 -16,8 8,4'; +/** + * Height of SVG path for jagged teeth at the end of collapsed blocks. + * @const + */ +Blockly.BlockSvg.JAGGED_TEETH_HEIGHT = 20; +/** + * Width of SVG path for jagged teeth at the end of collapsed blocks. + * @const + */ +Blockly.BlockSvg.JAGGED_TEETH_WIDTH = 15; +/** + * SVG path for drawing a horizontal puzzle tab from top to bottom. + * @const + */ +Blockly.BlockSvg.TAB_PATH_DOWN = 'v 5 c 0,10 -' + Blockly.BlockSvg.TAB_WIDTH + + ',-8 -' + Blockly.BlockSvg.TAB_WIDTH + ',7.5 s ' + + Blockly.BlockSvg.TAB_WIDTH + ',-2.5 ' + Blockly.BlockSvg.TAB_WIDTH + ',7.5'; +/** + * SVG path for drawing a horizontal puzzle tab from top to bottom with + * highlighting from the upper-right. + * @const + */ +Blockly.BlockSvg.TAB_PATH_DOWN_HIGHLIGHT_RTL = 'v 6.5 m -' + + (Blockly.BlockSvg.TAB_WIDTH * 0.97) + ',3 q -' + + (Blockly.BlockSvg.TAB_WIDTH * 0.05) + ',10 ' + + (Blockly.BlockSvg.TAB_WIDTH * 0.3) + ',9.5 m ' + + (Blockly.BlockSvg.TAB_WIDTH * 0.67) + ',-1.9 v 1.4'; + +/** + * SVG start point for drawing the top-left corner. + * @const + */ +Blockly.BlockSvg.TOP_LEFT_CORNER_START = + 'm 0,' + Blockly.BlockSvg.CORNER_RADIUS; +/** + * SVG start point for drawing the top-left corner's highlight in RTL. + * @const + */ +Blockly.BlockSvg.TOP_LEFT_CORNER_START_HIGHLIGHT_RTL = + 'm ' + Blockly.BlockSvg.DISTANCE_45_INSIDE + ',' + + Blockly.BlockSvg.DISTANCE_45_INSIDE; +/** + * SVG start point for drawing the top-left corner's highlight in LTR. + * @const + */ +Blockly.BlockSvg.TOP_LEFT_CORNER_START_HIGHLIGHT_LTR = + 'm 0.5,' + (Blockly.BlockSvg.CORNER_RADIUS - 0.5); +/** + * SVG path for drawing the rounded top-left corner. + * @const + */ +Blockly.BlockSvg.TOP_LEFT_CORNER = + 'A ' + Blockly.BlockSvg.CORNER_RADIUS + ',' + + Blockly.BlockSvg.CORNER_RADIUS + ' 0 0,1 ' + + Blockly.BlockSvg.CORNER_RADIUS + ',0'; +/** + * SVG path for drawing the highlight on the rounded top-left corner. + * @const + */ +Blockly.BlockSvg.TOP_LEFT_CORNER_HIGHLIGHT = + 'A ' + (Blockly.BlockSvg.CORNER_RADIUS - 0.5) + ',' + + (Blockly.BlockSvg.CORNER_RADIUS - 0.5) + ' 0 0,1 ' + + Blockly.BlockSvg.CORNER_RADIUS + ',0.5'; +/** + * SVG path for drawing the top-left corner of a statement input. + * Includes the top notch, a horizontal space, and the rounded inside corner. + * @const + */ +Blockly.BlockSvg.INNER_TOP_LEFT_CORNER = + Blockly.BlockSvg.NOTCH_PATH_RIGHT + ' h -' + + (Blockly.BlockSvg.NOTCH_WIDTH - 15 - Blockly.BlockSvg.CORNER_RADIUS) + + ' a ' + Blockly.BlockSvg.CORNER_RADIUS + ',' + + Blockly.BlockSvg.CORNER_RADIUS + ' 0 0,0 -' + + Blockly.BlockSvg.CORNER_RADIUS + ',' + + Blockly.BlockSvg.CORNER_RADIUS; +/** + * SVG path for drawing the bottom-left corner of a statement input. + * Includes the rounded inside corner. + * @const + */ +Blockly.BlockSvg.INNER_BOTTOM_LEFT_CORNER = + 'a ' + Blockly.BlockSvg.CORNER_RADIUS + ',' + + Blockly.BlockSvg.CORNER_RADIUS + ' 0 0,0 ' + + Blockly.BlockSvg.CORNER_RADIUS + ',' + + Blockly.BlockSvg.CORNER_RADIUS; +/** + * SVG path for drawing highlight on the top-left corner of a statement + * input in RTL. + * @const + */ +Blockly.BlockSvg.INNER_TOP_LEFT_CORNER_HIGHLIGHT_RTL = + 'a ' + Blockly.BlockSvg.CORNER_RADIUS + ',' + + Blockly.BlockSvg.CORNER_RADIUS + ' 0 0,0 ' + + (-Blockly.BlockSvg.DISTANCE_45_OUTSIDE - 0.5) + ',' + + (Blockly.BlockSvg.CORNER_RADIUS - + Blockly.BlockSvg.DISTANCE_45_OUTSIDE); +/** + * SVG path for drawing highlight on the bottom-left corner of a statement + * input in RTL. + * @const + */ +Blockly.BlockSvg.INNER_BOTTOM_LEFT_CORNER_HIGHLIGHT_RTL = + 'a ' + (Blockly.BlockSvg.CORNER_RADIUS + 0.5) + ',' + + (Blockly.BlockSvg.CORNER_RADIUS + 0.5) + ' 0 0,0 ' + + (Blockly.BlockSvg.CORNER_RADIUS + 0.5) + ',' + + (Blockly.BlockSvg.CORNER_RADIUS + 0.5); +/** + * SVG path for drawing highlight on the bottom-left corner of a statement + * input in LTR. + * @const + */ +Blockly.BlockSvg.INNER_BOTTOM_LEFT_CORNER_HIGHLIGHT_LTR = + 'a ' + (Blockly.BlockSvg.CORNER_RADIUS + 0.5) + ',' + + (Blockly.BlockSvg.CORNER_RADIUS + 0.5) + ' 0 0,0 ' + + (Blockly.BlockSvg.CORNER_RADIUS - + Blockly.BlockSvg.DISTANCE_45_OUTSIDE) + ',' + + (Blockly.BlockSvg.DISTANCE_45_OUTSIDE + 0.5); + +/** + * Render the block. + * Lays out and reflows a block based on its contents and settings. + * @param {boolean=} opt_bubble If false, just render this block. + * If true, also render block's parent, grandparent, etc. Defaults to true. + */ +Blockly.BlockSvg.prototype.render = function(opt_bubble) { + Blockly.Field.startCache(); + this.rendered = true; + + var cursorX = Blockly.BlockSvg.SEP_SPACE_X; + if (this.RTL) { + cursorX = -cursorX; + } + // Move the icons into position. + var icons = this.getIcons(); + for (var i = 0; i < icons.length; i++) { + cursorX = icons[i].renderIcon(cursorX); + } + cursorX += this.RTL ? + Blockly.BlockSvg.SEP_SPACE_X : -Blockly.BlockSvg.SEP_SPACE_X; + // If there are no icons, cursorX will be 0, otherwise it will be the + // width that the first label needs to move over by. + + var inputRows = this.renderCompute_(cursorX); + this.renderDraw_(cursorX, inputRows); + this.renderMoveConnections_(); + + if (opt_bubble !== false) { + // Render all blocks above this one (propagate a reflow). + var parentBlock = this.getParent(); + if (parentBlock) { + parentBlock.render(true); + } else { + // Top-most block. Fire an event to allow scrollbars to resize. + this.workspace.resizeContents(); + } + } + Blockly.Field.stopCache(); +}; + +/** + * Render a list of fields starting at the specified location. + * @param {!Array.<!Blockly.Field>} fieldList List of fields. + * @param {number} cursorX X-coordinate to start the fields. + * @param {number} cursorY Y-coordinate to start the fields. + * @return {number} X-coordinate of the end of the field row (plus a gap). + * @private + */ +Blockly.BlockSvg.prototype.renderFields_ = + function(fieldList, cursorX, cursorY) { + /* eslint-disable indent */ + cursorY += Blockly.BlockSvg.INLINE_PADDING_Y; + if (this.RTL) { + cursorX = -cursorX; + } + for (var t = 0, field; field = fieldList[t]; t++) { + var root = field.getSvgRoot(); + if (!root) { + continue; + } + if (this.RTL) { + cursorX -= field.renderSep + field.renderWidth; + root.setAttribute('transform', + 'translate(' + cursorX + ',' + cursorY + ')'); + if (field.renderWidth) { + cursorX -= Blockly.BlockSvg.SEP_SPACE_X; + } + } else { + root.setAttribute('transform', + 'translate(' + (cursorX + field.renderSep) + ',' + cursorY + ')'); + if (field.renderWidth) { + cursorX += field.renderSep + field.renderWidth + + Blockly.BlockSvg.SEP_SPACE_X; + } + } + } + return this.RTL ? -cursorX : cursorX; +}; /* eslint-enable indent */ + +/** + * Computes the height and widths for each row and field. + * @param {number} iconWidth Offset of first row due to icons. + * @return {!Array.<!Array.<!Object>>} 2D array of objects, each containing + * position information. + * @private + */ +Blockly.BlockSvg.prototype.renderCompute_ = function(iconWidth) { + var inputList = this.inputList; + var inputRows = []; + inputRows.rightEdge = iconWidth + Blockly.BlockSvg.SEP_SPACE_X * 2; + if (this.previousConnection || this.nextConnection) { + inputRows.rightEdge = Math.max(inputRows.rightEdge, + Blockly.BlockSvg.NOTCH_WIDTH + Blockly.BlockSvg.SEP_SPACE_X); + } + var fieldValueWidth = 0; // Width of longest external value field. + var fieldStatementWidth = 0; // Width of longest statement field. + var hasValue = false; + var hasStatement = false; + var hasDummy = false; + var lastType = undefined; + var isInline = this.getInputsInline() && !this.isCollapsed(); + for (var i = 0, input; input = inputList[i]; i++) { + if (!input.isVisible()) { + continue; + } + var row; + if (!isInline || !lastType || + lastType == Blockly.NEXT_STATEMENT || + input.type == Blockly.NEXT_STATEMENT) { + // Create new row. + lastType = input.type; + row = []; + if (isInline && input.type != Blockly.NEXT_STATEMENT) { + row.type = Blockly.BlockSvg.INLINE; + } else { + row.type = input.type; + } + row.height = 0; + inputRows.push(row); + } else { + row = inputRows[inputRows.length - 1]; + } + row.push(input); + + // Compute minimum input size. + input.renderHeight = Blockly.BlockSvg.MIN_BLOCK_Y; + // The width is currently only needed for inline value inputs. + if (isInline && input.type == Blockly.INPUT_VALUE) { + input.renderWidth = Blockly.BlockSvg.TAB_WIDTH + + Blockly.BlockSvg.SEP_SPACE_X * 1.25; + } else { + input.renderWidth = 0; + } + // Expand input size if there is a connection. + if (input.connection && input.connection.isConnected()) { + var linkedBlock = input.connection.targetBlock(); + var bBox = linkedBlock.getHeightWidth(); + input.renderHeight = Math.max(input.renderHeight, bBox.height); + input.renderWidth = Math.max(input.renderWidth, bBox.width); + } + // Blocks have a one pixel shadow that should sometimes overhang. + if (!isInline && i == inputList.length - 1) { + // Last value input should overhang. + input.renderHeight--; + } else if (!isInline && input.type == Blockly.INPUT_VALUE && + inputList[i + 1] && inputList[i + 1].type == Blockly.NEXT_STATEMENT) { + // Value input above statement input should overhang. + input.renderHeight--; + } + + row.height = Math.max(row.height, input.renderHeight); + input.fieldWidth = 0; + if (inputRows.length == 1) { + // The first row gets shifted to accommodate any icons. + input.fieldWidth += this.RTL ? -iconWidth : iconWidth; + } + var previousFieldEditable = false; + for (var j = 0, field; field = input.fieldRow[j]; j++) { + if (j != 0) { + input.fieldWidth += Blockly.BlockSvg.SEP_SPACE_X; + } + // Get the dimensions of the field. + var fieldSize = field.getSize(); + field.renderWidth = fieldSize.width; + field.renderSep = (previousFieldEditable && field.EDITABLE) ? + Blockly.BlockSvg.SEP_SPACE_X : 0; + input.fieldWidth += field.renderWidth + field.renderSep; + row.height = Math.max(row.height, fieldSize.height); + previousFieldEditable = field.EDITABLE; + } + + if (row.type != Blockly.BlockSvg.INLINE) { + if (row.type == Blockly.NEXT_STATEMENT) { + hasStatement = true; + fieldStatementWidth = Math.max(fieldStatementWidth, input.fieldWidth); + } else { + if (row.type == Blockly.INPUT_VALUE) { + hasValue = true; + } else if (row.type == Blockly.DUMMY_INPUT) { + hasDummy = true; + } + fieldValueWidth = Math.max(fieldValueWidth, input.fieldWidth); + } + } + } + + // Make inline rows a bit thicker in order to enclose the values. + for (var y = 0, row; row = inputRows[y]; y++) { + row.thicker = false; + if (row.type == Blockly.BlockSvg.INLINE) { + for (var z = 0, input; input = row[z]; z++) { + if (input.type == Blockly.INPUT_VALUE) { + row.height += 2 * Blockly.BlockSvg.INLINE_PADDING_Y; + row.thicker = true; + break; + } + } + } + } + + // Compute the statement edge. + // This is the width of a block where statements are nested. + inputRows.statementEdge = 2 * Blockly.BlockSvg.SEP_SPACE_X + + fieldStatementWidth; + // Compute the preferred right edge. Inline blocks may extend beyond. + // This is the width of the block where external inputs connect. + if (hasStatement) { + inputRows.rightEdge = Math.max(inputRows.rightEdge, + inputRows.statementEdge + Blockly.BlockSvg.NOTCH_WIDTH); + } + if (hasValue) { + inputRows.rightEdge = Math.max(inputRows.rightEdge, fieldValueWidth + + Blockly.BlockSvg.SEP_SPACE_X * 2 + Blockly.BlockSvg.TAB_WIDTH); + } else if (hasDummy) { + inputRows.rightEdge = Math.max(inputRows.rightEdge, fieldValueWidth + + Blockly.BlockSvg.SEP_SPACE_X * 2); + } + + inputRows.hasValue = hasValue; + inputRows.hasStatement = hasStatement; + inputRows.hasDummy = hasDummy; + return inputRows; +}; + + +/** + * Draw the path of the block. + * Move the fields to the correct locations. + * @param {number} iconWidth Offset of first row due to icons. + * @param {!Array.<!Array.<!Object>>} inputRows 2D array of objects, each + * containing position information. + * @private + */ +Blockly.BlockSvg.prototype.renderDraw_ = function(iconWidth, inputRows) { + this.startHat_ = false; + // Reset the height to zero and let the rendering process add in + // portions of the block height as it goes. (e.g. hats, inputs, etc.) + this.height = 0; + // Should the top and bottom left corners be rounded or square? + if (this.outputConnection) { + this.squareTopLeftCorner_ = true; + this.squareBottomLeftCorner_ = true; + } else { + this.squareTopLeftCorner_ = false; + this.squareBottomLeftCorner_ = false; + // If this block is in the middle of a stack, square the corners. + if (this.previousConnection) { + var prevBlock = this.previousConnection.targetBlock(); + if (prevBlock && prevBlock.getNextBlock() == this) { + this.squareTopLeftCorner_ = true; + } + } else if (Blockly.BlockSvg.START_HAT) { + // No output or previous connection. + this.squareTopLeftCorner_ = true; + this.startHat_ = true; + this.height += Blockly.BlockSvg.START_HAT_HEIGHT; + inputRows.rightEdge = Math.max(inputRows.rightEdge, 100); + } + var nextBlock = this.getNextBlock(); + if (nextBlock) { + this.squareBottomLeftCorner_ = true; + } + } + + // Assemble the block's path. + var steps = []; + var inlineSteps = []; + // The highlighting applies to edges facing the upper-left corner. + // Since highlighting is a two-pixel wide border, it would normally overhang + // the edge of the block by a pixel. So undersize all measurements by a pixel. + var highlightSteps = []; + var highlightInlineSteps = []; + + this.renderDrawTop_(steps, highlightSteps, inputRows.rightEdge); + var cursorY = this.renderDrawRight_(steps, highlightSteps, inlineSteps, + highlightInlineSteps, inputRows, iconWidth); + this.renderDrawBottom_(steps, highlightSteps, cursorY); + this.renderDrawLeft_(steps, highlightSteps); + + var pathString = steps.join(' ') + '\n' + inlineSteps.join(' '); + this.svgPath_.setAttribute('d', pathString); + this.svgPathDark_.setAttribute('d', pathString); + pathString = highlightSteps.join(' ') + '\n' + highlightInlineSteps.join(' '); + this.svgPathLight_.setAttribute('d', pathString); + if (this.RTL) { + // Mirror the block's path. + this.svgPath_.setAttribute('transform', 'scale(-1 1)'); + this.svgPathLight_.setAttribute('transform', 'scale(-1 1)'); + this.svgPathDark_.setAttribute('transform', 'translate(1,1) scale(-1 1)'); + } +}; + +/** + * Update all of the connections on this block with the new locations calculated + * in renderCompute. Also move all of the connected blocks based on the new + * connection locations. + * @private + */ +Blockly.BlockSvg.prototype.renderMoveConnections_ = function() { + var blockTL = this.getRelativeToSurfaceXY(); + // Don't tighten previous or output connecitons because they are inferior + // connections. + if (this.previousConnection) { + this.previousConnection.moveToOffset(blockTL); + } + if (this.outputConnection) { + this.outputConnection.moveToOffset(blockTL); + } + + for (var i = 0; i < this.inputList.length; i++) { + var conn = this.inputList[i].connection; + if (conn) { + conn.moveToOffset(blockTL); + if (conn.isConnected()) { + conn.tighten_(); + } + } + } + + if (this.nextConnection) { + this.nextConnection.moveToOffset(blockTL); + if (this.nextConnection.isConnected()) { + this.nextConnection.tighten_(); + } + } + +}; + +/** + * Render the top edge of the block. + * @param {!Array.<string>} steps Path of block outline. + * @param {!Array.<string>} highlightSteps Path of block highlights. + * @param {number} rightEdge Minimum width of block. + * @private + */ +Blockly.BlockSvg.prototype.renderDrawTop_ = + function(steps, highlightSteps, rightEdge) { + /* eslint-disable indent */ + // Position the cursor at the top-left starting point. + if (this.squareTopLeftCorner_) { + steps.push('m 0,0'); + highlightSteps.push('m 0.5,0.5'); + if (this.startHat_) { + steps.push(Blockly.BlockSvg.START_HAT_PATH); + highlightSteps.push(this.RTL ? + Blockly.BlockSvg.START_HAT_HIGHLIGHT_RTL : + Blockly.BlockSvg.START_HAT_HIGHLIGHT_LTR); + } + } else { + steps.push(Blockly.BlockSvg.TOP_LEFT_CORNER_START); + highlightSteps.push(this.RTL ? + Blockly.BlockSvg.TOP_LEFT_CORNER_START_HIGHLIGHT_RTL : + Blockly.BlockSvg.TOP_LEFT_CORNER_START_HIGHLIGHT_LTR); + // Top-left rounded corner. + steps.push(Blockly.BlockSvg.TOP_LEFT_CORNER); + highlightSteps.push(Blockly.BlockSvg.TOP_LEFT_CORNER_HIGHLIGHT); + } + + // Top edge. + if (this.previousConnection) { + steps.push('H', Blockly.BlockSvg.NOTCH_WIDTH - 15); + highlightSteps.push('H', Blockly.BlockSvg.NOTCH_WIDTH - 15); + steps.push(Blockly.BlockSvg.NOTCH_PATH_LEFT); + highlightSteps.push(Blockly.BlockSvg.NOTCH_PATH_LEFT_HIGHLIGHT); + + var connectionX = (this.RTL ? + -Blockly.BlockSvg.NOTCH_WIDTH : Blockly.BlockSvg.NOTCH_WIDTH); + this.previousConnection.setOffsetInBlock(connectionX, 0); + } + steps.push('H', rightEdge); + highlightSteps.push('H', rightEdge - 0.5); + this.width = rightEdge; +}; /* eslint-enable indent */ + +/** + * Render the right edge of the block. + * @param {!Array.<string>} steps Path of block outline. + * @param {!Array.<string>} highlightSteps Path of block highlights. + * @param {!Array.<string>} inlineSteps Inline block outlines. + * @param {!Array.<string>} highlightInlineSteps Inline block highlights. + * @param {!Array.<!Array.<!Object>>} inputRows 2D array of objects, each + * containing position information. + * @param {number} iconWidth Offset of first row due to icons. + * @return {number} Height of block. + * @private + */ +Blockly.BlockSvg.prototype.renderDrawRight_ = function(steps, highlightSteps, + inlineSteps, highlightInlineSteps, inputRows, iconWidth) { + var cursorX; + var cursorY = 0; + var connectionX, connectionY; + for (var y = 0, row; row = inputRows[y]; y++) { + cursorX = Blockly.BlockSvg.SEP_SPACE_X; + if (y == 0) { + cursorX += this.RTL ? -iconWidth : iconWidth; + } + highlightSteps.push('M', (inputRows.rightEdge - 0.5) + ',' + + (cursorY + 0.5)); + if (this.isCollapsed()) { + // Jagged right edge. + var input = row[0]; + var fieldX = cursorX; + var fieldY = cursorY; + this.renderFields_(input.fieldRow, fieldX, fieldY); + steps.push(Blockly.BlockSvg.JAGGED_TEETH); + highlightSteps.push('h 8'); + var remainder = row.height - Blockly.BlockSvg.JAGGED_TEETH_HEIGHT; + steps.push('v', remainder); + if (this.RTL) { + highlightSteps.push('v 3.9 l 7.2,3.4 m -14.5,8.9 l 7.3,3.5'); + highlightSteps.push('v', remainder - 0.7); + } + this.width += Blockly.BlockSvg.JAGGED_TEETH_WIDTH; + } else if (row.type == Blockly.BlockSvg.INLINE) { + // Inline inputs. + for (var x = 0, input; input = row[x]; x++) { + var fieldX = cursorX; + var fieldY = cursorY; + if (row.thicker) { + // Lower the field slightly. + fieldY += Blockly.BlockSvg.INLINE_PADDING_Y; + } + // TODO: Align inline field rows (left/right/centre). + cursorX = this.renderFields_(input.fieldRow, fieldX, fieldY); + if (input.type != Blockly.DUMMY_INPUT) { + cursorX += input.renderWidth + Blockly.BlockSvg.SEP_SPACE_X; + } + if (input.type == Blockly.INPUT_VALUE) { + inlineSteps.push('M', (cursorX - Blockly.BlockSvg.SEP_SPACE_X) + + ',' + (cursorY + Blockly.BlockSvg.INLINE_PADDING_Y)); + inlineSteps.push('h', Blockly.BlockSvg.TAB_WIDTH - 2 - + input.renderWidth); + inlineSteps.push(Blockly.BlockSvg.TAB_PATH_DOWN); + inlineSteps.push('v', input.renderHeight + 1 - + Blockly.BlockSvg.TAB_HEIGHT); + inlineSteps.push('h', input.renderWidth + 2 - + Blockly.BlockSvg.TAB_WIDTH); + inlineSteps.push('z'); + if (this.RTL) { + // Highlight right edge, around back of tab, and bottom. + highlightInlineSteps.push('M', + (cursorX - Blockly.BlockSvg.SEP_SPACE_X - 2.5 + + Blockly.BlockSvg.TAB_WIDTH - input.renderWidth) + ',' + + (cursorY + Blockly.BlockSvg.INLINE_PADDING_Y + 0.5)); + highlightInlineSteps.push( + Blockly.BlockSvg.TAB_PATH_DOWN_HIGHLIGHT_RTL); + highlightInlineSteps.push('v', + input.renderHeight - Blockly.BlockSvg.TAB_HEIGHT + 2.5); + highlightInlineSteps.push('h', + input.renderWidth - Blockly.BlockSvg.TAB_WIDTH + 2); + } else { + // Highlight right edge, bottom. + highlightInlineSteps.push('M', + (cursorX - Blockly.BlockSvg.SEP_SPACE_X + 0.5) + ',' + + (cursorY + Blockly.BlockSvg.INLINE_PADDING_Y + 0.5)); + highlightInlineSteps.push('v', input.renderHeight + 1); + highlightInlineSteps.push('h', Blockly.BlockSvg.TAB_WIDTH - 2 - + input.renderWidth); + // Short highlight glint at bottom of tab. + highlightInlineSteps.push('M', + (cursorX - input.renderWidth - Blockly.BlockSvg.SEP_SPACE_X + + 0.9) + ',' + (cursorY + Blockly.BlockSvg.INLINE_PADDING_Y + + Blockly.BlockSvg.TAB_HEIGHT - 0.7)); + highlightInlineSteps.push('l', + (Blockly.BlockSvg.TAB_WIDTH * 0.46) + ',-2.1'); + } + // Create inline input connection. + if (this.RTL) { + connectionX = -cursorX - + Blockly.BlockSvg.TAB_WIDTH + Blockly.BlockSvg.SEP_SPACE_X + + input.renderWidth + 1; + } else { + connectionX = cursorX + + Blockly.BlockSvg.TAB_WIDTH - Blockly.BlockSvg.SEP_SPACE_X - + input.renderWidth - 1; + } + connectionY = cursorY + Blockly.BlockSvg.INLINE_PADDING_Y + 1; + input.connection.setOffsetInBlock(connectionX, connectionY); + } + } + + cursorX = Math.max(cursorX, inputRows.rightEdge); + this.width = Math.max(this.width, cursorX); + steps.push('H', cursorX); + highlightSteps.push('H', cursorX - 0.5); + steps.push('v', row.height); + if (this.RTL) { + highlightSteps.push('v', row.height - 1); + } + } else if (row.type == Blockly.INPUT_VALUE) { + // External input. + var input = row[0]; + var fieldX = cursorX; + var fieldY = cursorY; + if (input.align != Blockly.ALIGN_LEFT) { + var fieldRightX = inputRows.rightEdge - input.fieldWidth - + Blockly.BlockSvg.TAB_WIDTH - 2 * Blockly.BlockSvg.SEP_SPACE_X; + if (input.align == Blockly.ALIGN_RIGHT) { + fieldX += fieldRightX; + } else if (input.align == Blockly.ALIGN_CENTRE) { + fieldX += fieldRightX / 2; + } + } + this.renderFields_(input.fieldRow, fieldX, fieldY); + steps.push(Blockly.BlockSvg.TAB_PATH_DOWN); + var v = row.height - Blockly.BlockSvg.TAB_HEIGHT; + steps.push('v', v); + if (this.RTL) { + // Highlight around back of tab. + highlightSteps.push(Blockly.BlockSvg.TAB_PATH_DOWN_HIGHLIGHT_RTL); + highlightSteps.push('v', v + 0.5); + } else { + // Short highlight glint at bottom of tab. + highlightSteps.push('M', (inputRows.rightEdge - 5) + ',' + + (cursorY + Blockly.BlockSvg.TAB_HEIGHT - 0.7)); + highlightSteps.push('l', (Blockly.BlockSvg.TAB_WIDTH * 0.46) + + ',-2.1'); + } + // Create external input connection. + connectionX = this.RTL ? -inputRows.rightEdge - 1 : + inputRows.rightEdge + 1; + input.connection.setOffsetInBlock(connectionX, cursorY); + if (input.connection.isConnected()) { + this.width = Math.max(this.width, inputRows.rightEdge + + input.connection.targetBlock().getHeightWidth().width - + Blockly.BlockSvg.TAB_WIDTH + 1); + } + } else if (row.type == Blockly.DUMMY_INPUT) { + // External naked field. + var input = row[0]; + var fieldX = cursorX; + var fieldY = cursorY; + if (input.align != Blockly.ALIGN_LEFT) { + var fieldRightX = inputRows.rightEdge - input.fieldWidth - + 2 * Blockly.BlockSvg.SEP_SPACE_X; + if (inputRows.hasValue) { + fieldRightX -= Blockly.BlockSvg.TAB_WIDTH; + } + if (input.align == Blockly.ALIGN_RIGHT) { + fieldX += fieldRightX; + } else if (input.align == Blockly.ALIGN_CENTRE) { + fieldX += fieldRightX / 2; + } + } + this.renderFields_(input.fieldRow, fieldX, fieldY); + steps.push('v', row.height); + if (this.RTL) { + highlightSteps.push('v', row.height - 1); + } + } else if (row.type == Blockly.NEXT_STATEMENT) { + // Nested statement. + var input = row[0]; + if (y == 0) { + // If the first input is a statement stack, add a small row on top. + steps.push('v', Blockly.BlockSvg.SEP_SPACE_Y); + if (this.RTL) { + highlightSteps.push('v', Blockly.BlockSvg.SEP_SPACE_Y - 1); + } + cursorY += Blockly.BlockSvg.SEP_SPACE_Y; + } + var fieldX = cursorX; + var fieldY = cursorY; + if (input.align != Blockly.ALIGN_LEFT) { + var fieldRightX = inputRows.statementEdge - input.fieldWidth - + 2 * Blockly.BlockSvg.SEP_SPACE_X; + if (input.align == Blockly.ALIGN_RIGHT) { + fieldX += fieldRightX; + } else if (input.align == Blockly.ALIGN_CENTRE) { + fieldX += fieldRightX / 2; + } + } + this.renderFields_(input.fieldRow, fieldX, fieldY); + cursorX = inputRows.statementEdge + Blockly.BlockSvg.NOTCH_WIDTH; + steps.push('H', cursorX); + steps.push(Blockly.BlockSvg.INNER_TOP_LEFT_CORNER); + steps.push('v', row.height - 2 * Blockly.BlockSvg.CORNER_RADIUS); + steps.push(Blockly.BlockSvg.INNER_BOTTOM_LEFT_CORNER); + steps.push('H', inputRows.rightEdge); + if (this.RTL) { + highlightSteps.push('M', + (cursorX - Blockly.BlockSvg.NOTCH_WIDTH + + Blockly.BlockSvg.DISTANCE_45_OUTSIDE) + + ',' + (cursorY + Blockly.BlockSvg.DISTANCE_45_OUTSIDE)); + highlightSteps.push( + Blockly.BlockSvg.INNER_TOP_LEFT_CORNER_HIGHLIGHT_RTL); + highlightSteps.push('v', + row.height - 2 * Blockly.BlockSvg.CORNER_RADIUS); + highlightSteps.push( + Blockly.BlockSvg.INNER_BOTTOM_LEFT_CORNER_HIGHLIGHT_RTL); + highlightSteps.push('H', inputRows.rightEdge - 0.5); + } else { + highlightSteps.push('M', + (cursorX - Blockly.BlockSvg.NOTCH_WIDTH + + Blockly.BlockSvg.DISTANCE_45_OUTSIDE) + ',' + + (cursorY + row.height - Blockly.BlockSvg.DISTANCE_45_OUTSIDE)); + highlightSteps.push( + Blockly.BlockSvg.INNER_BOTTOM_LEFT_CORNER_HIGHLIGHT_LTR); + highlightSteps.push('H', inputRows.rightEdge - 0.5); + } + // Create statement connection. + connectionX = this.RTL ? -cursorX : cursorX + 1; + input.connection.setOffsetInBlock(connectionX, cursorY + 1); + + if (input.connection.isConnected()) { + this.width = Math.max(this.width, inputRows.statementEdge + + input.connection.targetBlock().getHeightWidth().width); + } + if (y == inputRows.length - 1 || + inputRows[y + 1].type == Blockly.NEXT_STATEMENT) { + // If the final input is a statement stack, add a small row underneath. + // Consecutive statement stacks are also separated by a small divider. + steps.push('v', Blockly.BlockSvg.SEP_SPACE_Y); + if (this.RTL) { + highlightSteps.push('v', Blockly.BlockSvg.SEP_SPACE_Y - 1); + } + cursorY += Blockly.BlockSvg.SEP_SPACE_Y; + } + } + cursorY += row.height; + } + if (!inputRows.length) { + cursorY = Blockly.BlockSvg.MIN_BLOCK_Y; + steps.push('V', cursorY); + if (this.RTL) { + highlightSteps.push('V', cursorY - 1); + } + } + return cursorY; +}; + +/** + * Render the bottom edge of the block. + * @param {!Array.<string>} steps Path of block outline. + * @param {!Array.<string>} highlightSteps Path of block highlights. + * @param {number} cursorY Height of block. + * @private + */ +Blockly.BlockSvg.prototype.renderDrawBottom_ = + function(steps, highlightSteps, cursorY) { + /* eslint-disable indent */ + this.height += cursorY + 1; // Add one for the shadow. + if (this.nextConnection) { + steps.push('H', (Blockly.BlockSvg.NOTCH_WIDTH + (this.RTL ? 0.5 : - 0.5)) + + ' ' + Blockly.BlockSvg.NOTCH_PATH_RIGHT); + // Create next block connection. + var connectionX; + if (this.RTL) { + connectionX = -Blockly.BlockSvg.NOTCH_WIDTH; + } else { + connectionX = Blockly.BlockSvg.NOTCH_WIDTH; + } + this.nextConnection.setOffsetInBlock(connectionX, cursorY + 1); + this.height += 4; // Height of tab. + } + + // Should the bottom-left corner be rounded or square? + if (this.squareBottomLeftCorner_) { + steps.push('H 0'); + if (!this.RTL) { + highlightSteps.push('M', '0.5,' + (cursorY - 0.5)); + } + } else { + steps.push('H', Blockly.BlockSvg.CORNER_RADIUS); + steps.push('a', Blockly.BlockSvg.CORNER_RADIUS + ',' + + Blockly.BlockSvg.CORNER_RADIUS + ' 0 0,1 -' + + Blockly.BlockSvg.CORNER_RADIUS + ',-' + + Blockly.BlockSvg.CORNER_RADIUS); + if (!this.RTL) { + highlightSteps.push('M', Blockly.BlockSvg.DISTANCE_45_INSIDE + ',' + + (cursorY - Blockly.BlockSvg.DISTANCE_45_INSIDE)); + highlightSteps.push('A', (Blockly.BlockSvg.CORNER_RADIUS - 0.5) + ',' + + (Blockly.BlockSvg.CORNER_RADIUS - 0.5) + ' 0 0,1 ' + + '0.5,' + (cursorY - Blockly.BlockSvg.CORNER_RADIUS)); + } + } +}; /* eslint-enable indent */ + +/** + * Render the left edge of the block. + * @param {!Array.<string>} steps Path of block outline. + * @param {!Array.<string>} highlightSteps Path of block highlights. + * @private + */ +Blockly.BlockSvg.prototype.renderDrawLeft_ = function(steps, highlightSteps) { + if (this.outputConnection) { + // Create output connection. + this.outputConnection.setOffsetInBlock(0, 0); + steps.push('V', Blockly.BlockSvg.TAB_HEIGHT); + steps.push('c 0,-10 -' + Blockly.BlockSvg.TAB_WIDTH + ',8 -' + + Blockly.BlockSvg.TAB_WIDTH + ',-7.5 s ' + Blockly.BlockSvg.TAB_WIDTH + + ',2.5 ' + Blockly.BlockSvg.TAB_WIDTH + ',-7.5'); + if (this.RTL) { + highlightSteps.push('M', (Blockly.BlockSvg.TAB_WIDTH * -0.25) + ',8.4'); + highlightSteps.push('l', (Blockly.BlockSvg.TAB_WIDTH * -0.45) + ',-2.1'); + } else { + highlightSteps.push('V', Blockly.BlockSvg.TAB_HEIGHT - 1.5); + highlightSteps.push('m', (Blockly.BlockSvg.TAB_WIDTH * -0.92) + + ',-0.5 q ' + (Blockly.BlockSvg.TAB_WIDTH * -0.19) + + ',-5.5 0,-11'); + highlightSteps.push('m', (Blockly.BlockSvg.TAB_WIDTH * 0.92) + + ',1 V 0.5 H 1'); + } + this.width += Blockly.BlockSvg.TAB_WIDTH; + } else if (!this.RTL) { + if (this.squareTopLeftCorner_) { + // Statement block in a stack. + highlightSteps.push('V', 0.5); + } else { + highlightSteps.push('V', Blockly.BlockSvg.CORNER_RADIUS); + } + } + steps.push('z'); +}; diff --git a/src/blockly/core/block_svg.js b/src/blockly/core/block_svg.js new file mode 100644 index 0000000..4d4a729 --- /dev/null +++ b/src/blockly/core/block_svg.js @@ -0,0 +1,1629 @@ +/** + * @license + * Visual Blocks Editor + * + * Copyright 2012 Google Inc. + * https://developers.google.com/blockly/ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Methods for graphically rendering a block as SVG. + * @author fraser@google.com (Neil Fraser) + */ +'use strict'; + +goog.provide('Blockly.BlockSvg'); + +goog.require('Blockly.Block'); +goog.require('Blockly.ContextMenu'); +goog.require('Blockly.RenderedConnection'); +goog.require('goog.Timer'); +goog.require('goog.asserts'); +goog.require('goog.dom'); +goog.require('goog.math.Coordinate'); +goog.require('goog.userAgent'); + + +/** + * Class for a block's SVG representation. + * Not normally called directly, workspace.newBlock() is preferred. + * @param {!Blockly.Workspace} workspace The block's workspace. + * @param {?string} prototypeName Name of the language object containing + * type-specific functions for this block. + * @param {=string} opt_id Optional ID. Use this ID if provided, otherwise + * create a new id. + * @extends {Blockly.Block} + * @constructor + */ +Blockly.BlockSvg = function(workspace, prototypeName, opt_id) { + // Create core elements for the block. + /** + * @type {SVGElement} + * @private + */ + this.svgGroup_ = Blockly.createSvgElement('g', {}, null); + + /** + * @type {SVGElement} + * @private + */ + this.svgPathDark_ = Blockly.createSvgElement('path', + {'class': 'blocklyPathDark', 'transform': 'translate(1,1)'}, + this.svgGroup_); + + /** + * @type {SVGElement} + * @private + */ + this.svgPath_ = Blockly.createSvgElement('path', {'class': 'blocklyPath'}, + this.svgGroup_); + + /** + * @type {SVGElement} + * @private + */ + this.svgPathLight_ = Blockly.createSvgElement('path', + {'class': 'blocklyPathLight'}, this.svgGroup_); + this.svgPath_.tooltip = this; + + /** @type {boolean} */ + this.rendered = false; + + Blockly.Tooltip.bindMouseEvents(this.svgPath_); + Blockly.BlockSvg.superClass_.constructor.call(this, + workspace, prototypeName, opt_id); +}; +goog.inherits(Blockly.BlockSvg, Blockly.Block); + +/** + * Height of this block, not including any statement blocks above or below. + */ +Blockly.BlockSvg.prototype.height = 0; +/** + * Width of this block, including any connected value blocks. + */ +Blockly.BlockSvg.prototype.width = 0; + +/** + * Original location of block being dragged. + * @type {goog.math.Coordinate} + * @private + */ +Blockly.BlockSvg.prototype.dragStartXY_ = null; + +/** + * Constant for identifying rows that are to be rendered inline. + * Don't collide with Blockly.INPUT_VALUE and friends. + * @const + */ +Blockly.BlockSvg.INLINE = -1; + +/** + * Create and initialize the SVG representation of the block. + * May be called more than once. + */ +Blockly.BlockSvg.prototype.initSvg = function() { + goog.asserts.assert(this.workspace.rendered, 'Workspace is headless.'); + for (var i = 0, input; input = this.inputList[i]; i++) { + input.init(); + } + var icons = this.getIcons(); + for (var i = 0; i < icons.length; i++) { + icons[i].createIcon(); + } + this.updateColour(); + this.updateMovable(); + if (!this.workspace.options.readOnly && !this.eventsInit_) { + Blockly.bindEvent_(this.getSvgRoot(), 'mousedown', this, + this.onMouseDown_); + var thisBlock = this; + Blockly.bindEvent_(this.getSvgRoot(), 'touchstart', null, + function(e) {Blockly.longStart_(e, thisBlock);}); + } + this.eventsInit_ = true; + + if (!this.getSvgRoot().parentNode) { + this.workspace.getCanvas().appendChild(this.getSvgRoot()); + } +}; + +/** + * Select this block. Highlight it visually. + */ +Blockly.BlockSvg.prototype.select = function() { + if (this.isShadow() && this.getParent()) { + // Shadow blocks should not be selected. + this.getParent().select(); + return; + } + if (Blockly.selected == this) { + return; + } + var oldId = null; + if (Blockly.selected) { + oldId = Blockly.selected.id; + // Unselect any previously selected block. + Blockly.Events.disable(); + try { + Blockly.selected.unselect(); + } finally { + Blockly.Events.enable(); + } + } + var event = new Blockly.Events.Ui(null, 'selected', oldId, this.id); + event.workspaceId = this.workspace.id; + Blockly.Events.fire(event); + Blockly.selected = this; + this.addSelect(); +}; + +/** + * Unselect this block. Remove its highlighting. + */ +Blockly.BlockSvg.prototype.unselect = function() { + if (Blockly.selected != this) { + return; + } + var event = new Blockly.Events.Ui(null, 'selected', this.id, null); + event.workspaceId = this.workspace.id; + Blockly.Events.fire(event); + Blockly.selected = null; + this.removeSelect(); +}; + +/** + * Block's mutator icon (if any). + * @type {Blockly.Mutator} + */ +Blockly.BlockSvg.prototype.mutator = null; + +/** + * Block's comment icon (if any). + * @type {Blockly.Comment} + */ +Blockly.BlockSvg.prototype.comment = null; + +/** + * Block's warning icon (if any). + * @type {Blockly.Warning} + */ +Blockly.BlockSvg.prototype.warning = null; + +/** + * Returns a list of mutator, comment, and warning icons. + * @return {!Array} List of icons. + */ +Blockly.BlockSvg.prototype.getIcons = function() { + var icons = []; + if (this.mutator) { + icons.push(this.mutator); + } + if (this.comment) { + icons.push(this.comment); + } + if (this.warning) { + icons.push(this.warning); + } + return icons; +}; + +/** + * Wrapper function called when a mouseUp occurs during a drag operation. + * @type {Array.<!Array>} + * @private + */ +Blockly.BlockSvg.onMouseUpWrapper_ = null; + +/** + * Wrapper function called when a mouseMove occurs during a drag operation. + * @type {Array.<!Array>} + * @private + */ +Blockly.BlockSvg.onMouseMoveWrapper_ = null; + +/** + * Stop binding to the global mouseup and mousemove events. + * @package + */ +Blockly.BlockSvg.terminateDrag = function() { + Blockly.BlockSvg.disconnectUiStop_(); + if (Blockly.BlockSvg.onMouseUpWrapper_) { + Blockly.unbindEvent_(Blockly.BlockSvg.onMouseUpWrapper_); + Blockly.BlockSvg.onMouseUpWrapper_ = null; + } + if (Blockly.BlockSvg.onMouseMoveWrapper_) { + Blockly.unbindEvent_(Blockly.BlockSvg.onMouseMoveWrapper_); + Blockly.BlockSvg.onMouseMoveWrapper_ = null; + } + var selected = Blockly.selected; + if (Blockly.dragMode_ == Blockly.DRAG_FREE) { + // Terminate a drag operation. + if (selected) { + // Update the connection locations. + var xy = selected.getRelativeToSurfaceXY(); + var dxy = goog.math.Coordinate.difference(xy, selected.dragStartXY_); + var event = new Blockly.Events.Move(selected); + event.oldCoordinate = selected.dragStartXY_; + event.recordNew(); + Blockly.Events.fire(event); + + selected.moveConnections_(dxy.x, dxy.y); + delete selected.draggedBubbles_; + selected.setDragging_(false); + selected.render(); + // Ensure that any stap and bump are part of this move's event group. + var group = Blockly.Events.getGroup(); + setTimeout(function() { + Blockly.Events.setGroup(group); + selected.snapToGrid(); + Blockly.Events.setGroup(false); + }, Blockly.BUMP_DELAY / 2); + setTimeout(function() { + Blockly.Events.setGroup(group); + selected.bumpNeighbours_(); + Blockly.Events.setGroup(false); + }, Blockly.BUMP_DELAY); + // Fire an event to allow scrollbars to resize. + selected.workspace.resizeContents(); + } + } + Blockly.dragMode_ = Blockly.DRAG_NONE; + Blockly.Css.setCursor(Blockly.Css.Cursor.OPEN); +}; + +/** + * Set parent of this block to be a new block or null. + * @param {Blockly.BlockSvg} newParent New parent block. + */ +Blockly.BlockSvg.prototype.setParent = function(newParent) { + if (newParent == this.parentBlock_) { + return; + } + var svgRoot = this.getSvgRoot(); + if (this.parentBlock_ && svgRoot) { + // Move this block up the DOM. Keep track of x/y translations. + var xy = this.getRelativeToSurfaceXY(); + this.workspace.getCanvas().appendChild(svgRoot); + svgRoot.setAttribute('transform', 'translate(' + xy.x + ',' + xy.y + ')'); + } + + Blockly.Field.startCache(); + Blockly.BlockSvg.superClass_.setParent.call(this, newParent); + Blockly.Field.stopCache(); + + if (newParent) { + var oldXY = this.getRelativeToSurfaceXY(); + newParent.getSvgRoot().appendChild(svgRoot); + var newXY = this.getRelativeToSurfaceXY(); + // Move the connections to match the child's new position. + this.moveConnections_(newXY.x - oldXY.x, newXY.y - oldXY.y); + } +}; + +/** + * Return the coordinates of the top-left corner of this block relative to the + * drawing surface's origin (0,0). + * @return {!goog.math.Coordinate} Object with .x and .y properties. + */ +Blockly.BlockSvg.prototype.getRelativeToSurfaceXY = function() { + var x = 0; + var y = 0; + var element = this.getSvgRoot(); + if (element) { + do { + // Loop through this block and every parent. + var xy = Blockly.getRelativeXY_(element); + x += xy.x; + y += xy.y; + element = element.parentNode; + } while (element && element != this.workspace.getCanvas()); + } + return new goog.math.Coordinate(x, y); +}; + +/** + * Move a block by a relative offset. + * @param {number} dx Horizontal offset. + * @param {number} dy Vertical offset. + */ +Blockly.BlockSvg.prototype.moveBy = function(dx, dy) { + goog.asserts.assert(!this.parentBlock_, 'Block has parent.'); + var event = new Blockly.Events.Move(this); + var xy = this.getRelativeToSurfaceXY(); + this.getSvgRoot().setAttribute('transform', + 'translate(' + (xy.x + dx) + ',' + (xy.y + dy) + ')'); + this.moveConnections_(dx, dy); + event.recordNew(); + this.workspace.resizeContents(); + Blockly.Events.fire(event); +}; + +/** + * Snap this block to the nearest grid point. + */ +Blockly.BlockSvg.prototype.snapToGrid = function() { + if (!this.workspace) { + return; // Deleted block. + } + if (Blockly.dragMode_ != Blockly.DRAG_NONE) { + return; // Don't bump blocks during a drag. + } + if (this.getParent()) { + return; // Only snap top-level blocks. + } + if (this.isInFlyout) { + return; // Don't move blocks around in a flyout. + } + if (!this.workspace.options.gridOptions || + !this.workspace.options.gridOptions['snap']) { + return; // Config says no snapping. + } + var spacing = this.workspace.options.gridOptions['spacing']; + var half = spacing / 2; + var xy = this.getRelativeToSurfaceXY(); + var dx = Math.round((xy.x - half) / spacing) * spacing + half - xy.x; + var dy = Math.round((xy.y - half) / spacing) * spacing + half - xy.y; + dx = Math.round(dx); + dy = Math.round(dy); + if (dx != 0 || dy != 0) { + this.moveBy(dx, dy); + } +}; + +/** + * Returns a bounding box describing the dimensions of this block + * and any blocks stacked below it. + * @return {!{height: number, width: number}} Object with height and width + * properties. + */ +Blockly.BlockSvg.prototype.getHeightWidth = function() { + var height = this.height; + var width = this.width; + // Recursively add size of subsequent blocks. + var nextBlock = this.getNextBlock(); + if (nextBlock) { + var nextHeightWidth = nextBlock.getHeightWidth(); + height += nextHeightWidth.height - 4; // Height of tab. + width = Math.max(width, nextHeightWidth.width); + } else if (!this.nextConnection && !this.outputConnection) { + // Add a bit of margin under blocks with no bottom tab. + height += 2; + } + return {height: height, width: width}; +}; + +/** + * Returns the coordinates of a bounding box describing the dimensions of this + * block and any blocks stacked below it. + * @return {!{topLeft: goog.math.Coordinate, bottomRight: goog.math.Coordinate}} + * Object with top left and bottom right coordinates of the bounding box. + */ +Blockly.BlockSvg.prototype.getBoundingRectangle = function() { + var blockXY = this.getRelativeToSurfaceXY(this); + var tab = this.outputConnection ? Blockly.BlockSvg.TAB_WIDTH : 0; + var blockBounds = this.getHeightWidth(); + var topLeft; + var bottomRight; + if (this.RTL) { + // Width has the tab built into it already so subtract it here. + topLeft = new goog.math.Coordinate(blockXY.x - (blockBounds.width - tab), + blockXY.y); + // Add the width of the tab/puzzle piece knob to the x coordinate + // since X is the corner of the rectangle, not the whole puzzle piece. + bottomRight = new goog.math.Coordinate(blockXY.x + tab, + blockXY.y + blockBounds.height); + } else { + // Subtract the width of the tab/puzzle piece knob to the x coordinate + // since X is the corner of the rectangle, not the whole puzzle piece. + topLeft = new goog.math.Coordinate(blockXY.x - tab, blockXY.y); + // Width has the tab built into it already so subtract it here. + bottomRight = new goog.math.Coordinate(blockXY.x + blockBounds.width - tab, + blockXY.y + blockBounds.height); + } + return {topLeft: topLeft, bottomRight: bottomRight}; +}; + +/** + * Set whether the block is collapsed or not. + * @param {boolean} collapsed True if collapsed. + */ +Blockly.BlockSvg.prototype.setCollapsed = function(collapsed) { + if (this.collapsed_ == collapsed) { + return; + } + var renderList = []; + // Show/hide the inputs. + for (var i = 0, input; input = this.inputList[i]; i++) { + renderList.push.apply(renderList, input.setVisible(!collapsed)); + } + + var COLLAPSED_INPUT_NAME = '_TEMP_COLLAPSED_INPUT'; + if (collapsed) { + var icons = this.getIcons(); + for (var i = 0; i < icons.length; i++) { + icons[i].setVisible(false); + } + var text = this.toString(Blockly.COLLAPSE_CHARS); + this.appendDummyInput(COLLAPSED_INPUT_NAME).appendField(text).init(); + } else { + this.removeInput(COLLAPSED_INPUT_NAME); + // Clear any warnings inherited from enclosed blocks. + this.setWarningText(null); + } + Blockly.BlockSvg.superClass_.setCollapsed.call(this, collapsed); + + if (!renderList.length) { + // No child blocks, just render this block. + renderList[0] = this; + } + if (this.rendered) { + for (var i = 0, block; block = renderList[i]; i++) { + block.render(); + } + // Don't bump neighbours. + // Although bumping neighbours would make sense, users often collapse + // all their functions and store them next to each other. Expanding and + // bumping causes all their definitions to go out of alignment. + } +}; + +/** + * Open the next (or previous) FieldTextInput. + * @param {Blockly.Field|Blockly.Block} start Current location. + * @param {boolean} forward If true go forward, otherwise backward. + */ +Blockly.BlockSvg.prototype.tab = function(start, forward) { + // This function need not be efficient since it runs once on a keypress. + // Create an ordered list of all text fields and connected inputs. + var list = []; + for (var i = 0, input; input = this.inputList[i]; i++) { + for (var j = 0, field; field = input.fieldRow[j]; j++) { + if (field instanceof Blockly.FieldTextInput) { + // TODO: Also support dropdown fields. + list.push(field); + } + } + if (input.connection) { + var block = input.connection.targetBlock(); + if (block) { + list.push(block); + } + } + } + var i = list.indexOf(start); + if (i == -1) { + // No start location, start at the beginning or end. + i = forward ? -1 : list.length; + } + var target = list[forward ? i + 1 : i - 1]; + if (!target) { + // Ran off of list. + var parent = this.getParent(); + if (parent) { + parent.tab(this, forward); + } + } else if (target instanceof Blockly.Field) { + target.showEditor_(); + } else { + target.tab(null, forward); + } +}; + +/** + * Handle a mouse-down on an SVG block. + * @param {!Event} e Mouse down event. + * @private + */ +Blockly.BlockSvg.prototype.onMouseDown_ = function(e) { + if (this.workspace.options.readOnly) { + return; + } + if (this.isInFlyout) { + return; + } + if (this.isInMutator) { + // Mutator's coordinate system could be out of date because the bubble was + // dragged, the block was moved, the parent workspace zoomed, etc. + this.workspace.resize(); + } + + this.workspace.updateScreenCalculationsIfScrolled(); + this.workspace.markFocused(); + Blockly.terminateDrag_(); + this.select(); + Blockly.hideChaff(); + if (Blockly.isRightButton(e)) { + // Right-click. + this.showContextMenu_(e); + } else if (!this.isMovable()) { + // Allow immovable blocks to be selected and context menued, but not + // dragged. Let this event bubble up to document, so the workspace may be + // dragged instead. + return; + } else { + if (!Blockly.Events.getGroup()) { + Blockly.Events.setGroup(true); + } + // Left-click (or middle click) + Blockly.Css.setCursor(Blockly.Css.Cursor.CLOSED); + + this.dragStartXY_ = this.getRelativeToSurfaceXY(); + this.workspace.startDrag(e, this.dragStartXY_); + + Blockly.dragMode_ = Blockly.DRAG_STICKY; + Blockly.BlockSvg.onMouseUpWrapper_ = Blockly.bindEvent_(document, + 'mouseup', this, this.onMouseUp_); + Blockly.BlockSvg.onMouseMoveWrapper_ = Blockly.bindEvent_(document, + 'mousemove', this, this.onMouseMove_); + // Build a list of bubbles that need to be moved and where they started. + this.draggedBubbles_ = []; + var descendants = this.getDescendants(); + for (var i = 0, descendant; descendant = descendants[i]; i++) { + var icons = descendant.getIcons(); + for (var j = 0; j < icons.length; j++) { + var data = icons[j].getIconLocation(); + data.bubble = icons[j]; + this.draggedBubbles_.push(data); + } + } + } + // This event has been handled. No need to bubble up to the document. + e.stopPropagation(); + e.preventDefault(); +}; + +/** + * Handle a mouse-up anywhere in the SVG pane. Is only registered when a + * block is clicked. We can't use mouseUp on the block since a fast-moving + * cursor can briefly escape the block before it catches up. + * @param {!Event} e Mouse up event. + * @private + */ +Blockly.BlockSvg.prototype.onMouseUp_ = function(e) { + if (Blockly.dragMode_ != Blockly.DRAG_FREE && + !Blockly.WidgetDiv.isVisible()) { + Blockly.Events.fire( + new Blockly.Events.Ui(this, 'click', undefined, undefined)); + } + Blockly.terminateDrag_(); + if (Blockly.selected && Blockly.highlightedConnection_) { + // Connect two blocks together. + Blockly.localConnection_.connect(Blockly.highlightedConnection_); + if (this.rendered) { + // Trigger a connection animation. + // Determine which connection is inferior (lower in the source stack). + var inferiorConnection = Blockly.localConnection_.isSuperior() ? + Blockly.highlightedConnection_ : Blockly.localConnection_; + inferiorConnection.getSourceBlock().connectionUiEffect(); + } + if (this.workspace.trashcan) { + // Don't throw an object in the trash can if it just got connected. + this.workspace.trashcan.close(); + } + } else if (!this.getParent() && Blockly.selected.isDeletable() && + this.workspace.isDeleteArea(e)) { + var trashcan = this.workspace.trashcan; + if (trashcan) { + goog.Timer.callOnce(trashcan.close, 100, trashcan); + } + Blockly.selected.dispose(false, true); + } + if (Blockly.highlightedConnection_) { + Blockly.highlightedConnection_.unhighlight(); + Blockly.highlightedConnection_ = null; + } + Blockly.Css.setCursor(Blockly.Css.Cursor.OPEN); + if (!Blockly.WidgetDiv.isVisible()) { + Blockly.Events.setGroup(false); + } +}; + +/** + * Load the block's help page in a new window. + * @private + */ +Blockly.BlockSvg.prototype.showHelp_ = function() { + var url = goog.isFunction(this.helpUrl) ? this.helpUrl() : this.helpUrl; + if (url) { + window.open(url); + } +}; + +/** + * Show the context menu for this block. + * @param {!Event} e Mouse event. + * @private + */ +Blockly.BlockSvg.prototype.showContextMenu_ = function(e) { + if (this.workspace.options.readOnly || !this.contextMenu) { + return; + } + // Save the current block in a variable for use in closures. + var block = this; + var menuOptions = []; + + if (this.isDeletable() && this.isMovable() && !block.isInFlyout) { + // Option to duplicate this block. + var duplicateOption = { + text: Blockly.Msg.DUPLICATE_BLOCK, + enabled: true, + callback: function() { + Blockly.duplicate_(block); + } + }; + if (this.getDescendants().length > this.workspace.remainingCapacity()) { + duplicateOption.enabled = false; + } + menuOptions.push(duplicateOption); + + if (this.isEditable() && !this.collapsed_ && + this.workspace.options.comments) { + // Option to add/remove a comment. + var commentOption = {enabled: !goog.userAgent.IE}; + if (this.comment) { + commentOption.text = Blockly.Msg.REMOVE_COMMENT; + commentOption.callback = function() { + block.setCommentText(null); + }; + } else { + commentOption.text = Blockly.Msg.ADD_COMMENT; + commentOption.callback = function() { + block.setCommentText(''); + }; + } + menuOptions.push(commentOption); + } + + // Option to make block inline. + if (!this.collapsed_) { + for (var i = 1; i < this.inputList.length; i++) { + if (this.inputList[i - 1].type != Blockly.NEXT_STATEMENT && + this.inputList[i].type != Blockly.NEXT_STATEMENT) { + // Only display this option if there are two value or dummy inputs + // next to each other. + var inlineOption = {enabled: true}; + var isInline = this.getInputsInline(); + inlineOption.text = isInline ? + Blockly.Msg.EXTERNAL_INPUTS : Blockly.Msg.INLINE_INPUTS; + inlineOption.callback = function() { + block.setInputsInline(!isInline); + }; + menuOptions.push(inlineOption); + break; + } + } + } + + if (this.workspace.options.collapse) { + // Option to collapse/expand block. + if (this.collapsed_) { + var expandOption = {enabled: true}; + expandOption.text = Blockly.Msg.EXPAND_BLOCK; + expandOption.callback = function() { + block.setCollapsed(false); + }; + menuOptions.push(expandOption); + } else { + var collapseOption = {enabled: true}; + collapseOption.text = Blockly.Msg.COLLAPSE_BLOCK; + collapseOption.callback = function() { + block.setCollapsed(true); + }; + menuOptions.push(collapseOption); + } + } + + if (this.workspace.options.disable) { + // Option to disable/enable block. + var disableOption = { + text: this.disabled ? + Blockly.Msg.ENABLE_BLOCK : Blockly.Msg.DISABLE_BLOCK, + enabled: !this.getInheritedDisabled(), + callback: function() { + block.setDisabled(!block.disabled); + } + }; + menuOptions.push(disableOption); + } + + // Option to delete this block. + // Count the number of blocks that are nested in this block. + var descendantCount = this.getDescendants().length; + var nextBlock = this.getNextBlock(); + if (nextBlock) { + // Blocks in the current stack would survive this block's deletion. + descendantCount -= nextBlock.getDescendants().length; + } + var deleteOption = { + text: descendantCount == 1 ? Blockly.Msg.DELETE_BLOCK : + Blockly.Msg.DELETE_X_BLOCKS.replace('%1', String(descendantCount)), + enabled: true, + callback: function() { + Blockly.Events.setGroup(true); + block.dispose(true, true); + Blockly.Events.setGroup(false); + } + }; + menuOptions.push(deleteOption); + } + + // Option to get help. + var url = goog.isFunction(this.helpUrl) ? this.helpUrl() : this.helpUrl; + var helpOption = {enabled: !!url}; + helpOption.text = Blockly.Msg.HELP; + helpOption.callback = function() { + block.showHelp_(); + }; + menuOptions.push(helpOption); + + // Allow the block to add or modify menuOptions. + if (this.customContextMenu && !block.isInFlyout) { + this.customContextMenu(menuOptions); + } + + Blockly.ContextMenu.show(e, menuOptions, this.RTL); + Blockly.ContextMenu.currentBlock = this; +}; + +/** + * Move the connections for this block and all blocks attached under it. + * Also update any attached bubbles. + * @param {number} dx Horizontal offset from current location. + * @param {number} dy Vertical offset from current location. + * @private + */ +Blockly.BlockSvg.prototype.moveConnections_ = function(dx, dy) { + if (!this.rendered) { + // Rendering is required to lay out the blocks. + // This is probably an invisible block attached to a collapsed block. + return; + } + var myConnections = this.getConnections_(false); + for (var i = 0; i < myConnections.length; i++) { + myConnections[i].moveBy(dx, dy); + } + var icons = this.getIcons(); + for (var i = 0; i < icons.length; i++) { + icons[i].computeIconLocation(); + } + + // Recurse through all blocks attached under this one. + for (var i = 0; i < this.childBlocks_.length; i++) { + this.childBlocks_[i].moveConnections_(dx, dy); + } +}; + +/** + * Recursively adds or removes the dragging class to this node and its children. + * @param {boolean} adding True if adding, false if removing. + * @private + */ +Blockly.BlockSvg.prototype.setDragging_ = function(adding) { + if (adding) { + var group = this.getSvgRoot(); + group.translate_ = ''; + group.skew_ = ''; + this.addDragging(); + Blockly.draggingConnections_ = + Blockly.draggingConnections_.concat(this.getConnections_(true)); + } else { + this.removeDragging(); + Blockly.draggingConnections_ = []; + } + // Recurse through all blocks attached under this one. + for (var i = 0; i < this.childBlocks_.length; i++) { + this.childBlocks_[i].setDragging_(adding); + } +}; + +/** + * Drag this block to follow the mouse. + * @param {!Event} e Mouse move event. + * @private + */ +Blockly.BlockSvg.prototype.onMouseMove_ = function(e) { + if (e.type == 'mousemove' && e.clientX <= 1 && e.clientY == 0 && + e.button == 0) { + /* HACK: + Safari Mobile 6.0 and Chrome for Android 18.0 fire rogue mousemove + events on certain touch actions. Ignore events with these signatures. + This may result in a one-pixel blind spot in other browsers, + but this shouldn't be noticeable. */ + e.stopPropagation(); + return; + } + + var oldXY = this.getRelativeToSurfaceXY(); + var newXY = this.workspace.moveDrag(e); + + if (Blockly.dragMode_ == Blockly.DRAG_STICKY) { + // Still dragging within the sticky DRAG_RADIUS. + var dr = goog.math.Coordinate.distance(oldXY, newXY) * this.workspace.scale; + if (dr > Blockly.DRAG_RADIUS) { + // Switch to unrestricted dragging. + Blockly.dragMode_ = Blockly.DRAG_FREE; + Blockly.longStop_(); + if (this.parentBlock_) { + // Push this block to the very top of the stack. + this.unplug(); + var group = this.getSvgRoot(); + group.translate_ = 'translate(' + newXY.x + ',' + newXY.y + ')'; + this.disconnectUiEffect(); + } + this.setDragging_(true); + } + } + if (Blockly.dragMode_ == Blockly.DRAG_FREE) { + // Unrestricted dragging. + var dxy = goog.math.Coordinate.difference(oldXY, this.dragStartXY_); + var group = this.getSvgRoot(); + group.translate_ = 'translate(' + newXY.x + ',' + newXY.y + ')'; + group.setAttribute('transform', group.translate_ + group.skew_); + // Drag all the nested bubbles. + for (var i = 0; i < this.draggedBubbles_.length; i++) { + var commentData = this.draggedBubbles_[i]; + commentData.bubble.setIconLocation( + goog.math.Coordinate.sum(commentData, dxy)); + } + + // Check to see if any of this block's connections are within range of + // another block's connection. + var myConnections = this.getConnections_(false); + // Also check the last connection on this stack + var lastOnStack = this.lastConnectionInStack_(); + if (lastOnStack && lastOnStack != this.nextConnection) { + myConnections.push(lastOnStack); + } + var closestConnection = null; + var localConnection = null; + var radiusConnection = Blockly.SNAP_RADIUS; + for (var i = 0; i < myConnections.length; i++) { + var myConnection = myConnections[i]; + var neighbour = myConnection.closest(radiusConnection, dxy); + if (neighbour.connection) { + closestConnection = neighbour.connection; + localConnection = myConnection; + radiusConnection = neighbour.radius; + } + } + + // Remove connection highlighting if needed. + if (Blockly.highlightedConnection_ && + Blockly.highlightedConnection_ != closestConnection) { + Blockly.highlightedConnection_.unhighlight(); + Blockly.highlightedConnection_ = null; + Blockly.localConnection_ = null; + } + // Add connection highlighting if needed. + if (closestConnection && + closestConnection != Blockly.highlightedConnection_) { + closestConnection.highlight(); + Blockly.highlightedConnection_ = closestConnection; + Blockly.localConnection_ = localConnection; + } + // Provide visual indication of whether the block will be deleted if + // dropped here. + if (this.isDeletable()) { + this.workspace.isDeleteArea(e); + } + } + // This event has been handled. No need to bubble up to the document. + e.stopPropagation(); + e.preventDefault(); +}; + +/** + * Add or remove the UI indicating if this block is movable or not. + */ +Blockly.BlockSvg.prototype.updateMovable = function() { + if (this.isMovable()) { + Blockly.addClass_(/** @type {!Element} */ (this.svgGroup_), + 'blocklyDraggable'); + } else { + Blockly.removeClass_(/** @type {!Element} */ (this.svgGroup_), + 'blocklyDraggable'); + } +}; + +/** + * Set whether this block is movable or not. + * @param {boolean} movable True if movable. + */ +Blockly.BlockSvg.prototype.setMovable = function(movable) { + Blockly.BlockSvg.superClass_.setMovable.call(this, movable); + this.updateMovable(); +}; + +/** + * Set whether this block is editable or not. + * @param {boolean} editable True if editable. + */ +Blockly.BlockSvg.prototype.setEditable = function(editable) { + Blockly.BlockSvg.superClass_.setEditable.call(this, editable); + var icons = this.getIcons(); + for (var i = 0; i < icons.length; i++) { + icons[i].updateEditable(); + } +}; + +/** + * Set whether this block is a shadow block or not. + * @param {boolean} shadow True if a shadow. + */ +Blockly.BlockSvg.prototype.setShadow = function(shadow) { + Blockly.BlockSvg.superClass_.setShadow.call(this, shadow); + this.updateColour(); +}; + +/** + * Return the root node of the SVG or null if none exists. + * @return {Element} The root SVG node (probably a group). + */ +Blockly.BlockSvg.prototype.getSvgRoot = function() { + return this.svgGroup_; +}; + +/** + * Dispose of this block. + * @param {boolean} healStack If true, then try to heal any gap by connecting + * the next statement with the previous statement. Otherwise, dispose of + * all children of this block. + * @param {boolean} animate If true, show a disposal animation and sound. + */ +Blockly.BlockSvg.prototype.dispose = function(healStack, animate) { + if (!this.workspace) { + // The block has already been deleted. + return; + } + Blockly.Tooltip.hide(); + Blockly.Field.startCache(); + // Save the block's workspace temporarily so we can resize the + // contents once the block is disposed. + var blockWorkspace = this.workspace; + // If this block is being dragged, unlink the mouse events. + if (Blockly.selected == this) { + this.unselect(); + Blockly.terminateDrag_(); + } + // If this block has a context menu open, close it. + if (Blockly.ContextMenu.currentBlock == this) { + Blockly.ContextMenu.hide(); + } + + if (animate && this.rendered) { + this.unplug(healStack); + this.disposeUiEffect(); + } + // Stop rerendering. + this.rendered = false; + + Blockly.Events.disable(); + try { + var icons = this.getIcons(); + for (var i = 0; i < icons.length; i++) { + icons[i].dispose(); + } + } finally { + Blockly.Events.enable(); + } + Blockly.BlockSvg.superClass_.dispose.call(this, healStack); + + goog.dom.removeNode(this.svgGroup_); + blockWorkspace.resizeContents(); + // Sever JavaScript to DOM connections. + this.svgGroup_ = null; + this.svgPath_ = null; + this.svgPathLight_ = null; + this.svgPathDark_ = null; + Blockly.Field.stopCache(); +}; + +/** + * Play some UI effects (sound, animation) when disposing of a block. + */ +Blockly.BlockSvg.prototype.disposeUiEffect = function() { + this.workspace.playAudio('delete'); + + var xy = Blockly.getSvgXY_(/** @type {!Element} */ (this.svgGroup_), + this.workspace); + // Deeply clone the current block. + var clone = this.svgGroup_.cloneNode(true); + clone.translateX_ = xy.x; + clone.translateY_ = xy.y; + clone.setAttribute('transform', + 'translate(' + clone.translateX_ + ',' + clone.translateY_ + ')'); + this.workspace.getParentSvg().appendChild(clone); + clone.bBox_ = clone.getBBox(); + // Start the animation. + Blockly.BlockSvg.disposeUiStep_(clone, this.RTL, new Date, + this.workspace.scale); +}; + +/** + * Animate a cloned block and eventually dispose of it. + * This is a class method, not an instace method since the original block has + * been destroyed and is no longer accessible. + * @param {!Element} clone SVG element to animate and dispose of. + * @param {boolean} rtl True if RTL, false if LTR. + * @param {!Date} start Date of animation's start. + * @param {number} workspaceScale Scale of workspace. + * @private + */ +Blockly.BlockSvg.disposeUiStep_ = function(clone, rtl, start, workspaceScale) { + var ms = new Date - start; + var percent = ms / 150; + if (percent > 1) { + goog.dom.removeNode(clone); + } else { + var x = clone.translateX_ + + (rtl ? -1 : 1) * clone.bBox_.width * workspaceScale / 2 * percent; + var y = clone.translateY_ + clone.bBox_.height * workspaceScale * percent; + var scale = (1 - percent) * workspaceScale; + clone.setAttribute('transform', 'translate(' + x + ',' + y + ')' + + ' scale(' + scale + ')'); + var closure = function() { + Blockly.BlockSvg.disposeUiStep_(clone, rtl, start, workspaceScale); + }; + setTimeout(closure, 10); + } +}; + +/** + * Play some UI effects (sound, ripple) after a connection has been established. + */ +Blockly.BlockSvg.prototype.connectionUiEffect = function() { + this.workspace.playAudio('click'); + if (this.workspace.scale < 1) { + return; // Too small to care about visual effects. + } + // Determine the absolute coordinates of the inferior block. + var xy = Blockly.getSvgXY_(/** @type {!Element} */ (this.svgGroup_), + this.workspace); + // Offset the coordinates based on the two connection types, fix scale. + if (this.outputConnection) { + xy.x += (this.RTL ? 3 : -3) * this.workspace.scale; + xy.y += 13 * this.workspace.scale; + } else if (this.previousConnection) { + xy.x += (this.RTL ? -23 : 23) * this.workspace.scale; + xy.y += 3 * this.workspace.scale; + } + var ripple = Blockly.createSvgElement('circle', + {'cx': xy.x, 'cy': xy.y, 'r': 0, 'fill': 'none', + 'stroke': '#888', 'stroke-width': 10}, + this.workspace.getParentSvg()); + // Start the animation. + Blockly.BlockSvg.connectionUiStep_(ripple, new Date, this.workspace.scale); +}; + +/** + * Expand a ripple around a connection. + * @param {!Element} ripple Element to animate. + * @param {!Date} start Date of animation's start. + * @param {number} workspaceScale Scale of workspace. + * @private + */ +Blockly.BlockSvg.connectionUiStep_ = function(ripple, start, workspaceScale) { + var ms = new Date - start; + var percent = ms / 150; + if (percent > 1) { + goog.dom.removeNode(ripple); + } else { + ripple.setAttribute('r', percent * 25 * workspaceScale); + ripple.style.opacity = 1 - percent; + var closure = function() { + Blockly.BlockSvg.connectionUiStep_(ripple, start, workspaceScale); + }; + Blockly.BlockSvg.disconnectUiStop_.pid_ = setTimeout(closure, 10); + } +}; + +/** + * Play some UI effects (sound, animation) when disconnecting a block. + */ +Blockly.BlockSvg.prototype.disconnectUiEffect = function() { + this.workspace.playAudio('disconnect'); + if (this.workspace.scale < 1) { + return; // Too small to care about visual effects. + } + // Horizontal distance for bottom of block to wiggle. + var DISPLACEMENT = 10; + // Scale magnitude of skew to height of block. + var height = this.getHeightWidth().height; + var magnitude = Math.atan(DISPLACEMENT / height) / Math.PI * 180; + if (!this.RTL) { + magnitude *= -1; + } + // Start the animation. + Blockly.BlockSvg.disconnectUiStep_(this.svgGroup_, magnitude, new Date); +}; + +/** + * Animate a brief wiggle of a disconnected block. + * @param {!Element} group SVG element to animate. + * @param {number} magnitude Maximum degrees skew (reversed for RTL). + * @param {!Date} start Date of animation's start. + * @private + */ +Blockly.BlockSvg.disconnectUiStep_ = function(group, magnitude, start) { + var DURATION = 200; // Milliseconds. + var WIGGLES = 3; // Half oscillations. + + var ms = new Date - start; + var percent = ms / DURATION; + + if (percent > 1) { + group.skew_ = ''; + } else { + var skew = Math.round(Math.sin(percent * Math.PI * WIGGLES) * + (1 - percent) * magnitude); + group.skew_ = 'skewX(' + skew + ')'; + var closure = function() { + Blockly.BlockSvg.disconnectUiStep_(group, magnitude, start); + }; + Blockly.BlockSvg.disconnectUiStop_.group = group; + Blockly.BlockSvg.disconnectUiStop_.pid = setTimeout(closure, 10); + } + group.setAttribute('transform', group.translate_ + group.skew_); +}; + +/** + * Stop the disconnect UI animation immediately. + * @private + */ +Blockly.BlockSvg.disconnectUiStop_ = function() { + if (Blockly.BlockSvg.disconnectUiStop_.group) { + clearTimeout(Blockly.BlockSvg.disconnectUiStop_.pid); + var group = Blockly.BlockSvg.disconnectUiStop_.group; + group.skew_ = ''; + group.setAttribute('transform', group.translate_); + Blockly.BlockSvg.disconnectUiStop_.group = null; + } +}; + +/** + * PID of disconnect UI animation. There can only be one at a time. + * @type {number} + */ +Blockly.BlockSvg.disconnectUiStop_.pid = 0; + +/** + * SVG group of wobbling block. There can only be one at a time. + * @type {Element} + */ +Blockly.BlockSvg.disconnectUiStop_.group = null; + +/** + * Change the colour of a block. + */ +Blockly.BlockSvg.prototype.updateColour = function() { + if (this.disabled) { + // Disabled blocks don't have colour. + return; + } + var hexColour = this.getColour(); + var rgb = goog.color.hexToRgb(hexColour); + if (this.isShadow()) { + rgb = goog.color.lighten(rgb, 0.6); + hexColour = goog.color.rgbArrayToHex(rgb); + this.svgPathLight_.style.display = 'none'; + this.svgPathDark_.setAttribute('fill', hexColour); + } else { + this.svgPathLight_.style.display = ''; + var hexLight = goog.color.rgbArrayToHex(goog.color.lighten(rgb, 0.3)); + var hexDark = goog.color.rgbArrayToHex(goog.color.darken(rgb, 0.2)); + this.svgPathLight_.setAttribute('stroke', hexLight); + this.svgPathDark_.setAttribute('fill', hexDark); + } + this.svgPath_.setAttribute('fill', hexColour); + + var icons = this.getIcons(); + for (var i = 0; i < icons.length; i++) { + icons[i].updateColour(); + } + + // Bump every dropdown to change its colour. + for (var x = 0, input; input = this.inputList[x]; x++) { + for (var y = 0, field; field = input.fieldRow[y]; y++) { + field.setText(null); + } + } +}; + +/** + * Enable or disable a block. + */ +Blockly.BlockSvg.prototype.updateDisabled = function() { + var hasClass = Blockly.hasClass_(/** @type {!Element} */ (this.svgGroup_), + 'blocklyDisabled'); + if (this.disabled || this.getInheritedDisabled()) { + if (!hasClass) { + Blockly.addClass_(/** @type {!Element} */ (this.svgGroup_), + 'blocklyDisabled'); + this.svgPath_.setAttribute('fill', + 'url(#' + this.workspace.options.disabledPatternId + ')'); + } + } else { + if (hasClass) { + Blockly.removeClass_(/** @type {!Element} */ (this.svgGroup_), + 'blocklyDisabled'); + this.updateColour(); + } + } + var children = this.getChildren(); + for (var i = 0, child; child = children[i]; i++) { + child.updateDisabled(); + } +}; + +/** + * Returns the comment on this block (or '' if none). + * @return {string} Block's comment. + */ +Blockly.BlockSvg.prototype.getCommentText = function() { + if (this.comment) { + var comment = this.comment.getText(); + // Trim off trailing whitespace. + return comment.replace(/\s+$/, '').replace(/ +\n/g, '\n'); + } + return ''; +}; + +/** + * Set this block's comment text. + * @param {?string} text The text, or null to delete. + */ +Blockly.BlockSvg.prototype.setCommentText = function(text) { + var changedState = false; + if (goog.isString(text)) { + if (!this.comment) { + this.comment = new Blockly.Comment(this); + changedState = true; + } + this.comment.setText(/** @type {string} */ (text)); + } else { + if (this.comment) { + this.comment.dispose(); + changedState = true; + } + } + if (changedState && this.rendered) { + this.render(); + // Adding or removing a comment icon will cause the block to change shape. + this.bumpNeighbours_(); + } +}; + +/** + * Set this block's warning text. + * @param {?string} text The text, or null to delete. + * @param {string=} opt_id An optional ID for the warning text to be able to + * maintain multiple warnings. + */ +Blockly.BlockSvg.prototype.setWarningText = function(text, opt_id) { + if (!this.setWarningText.pid_) { + // Create a database of warning PIDs. + // Only runs once per block (and only those with warnings). + this.setWarningText.pid_ = Object.create(null); + } + var id = opt_id || ''; + if (!id) { + // Kill all previous pending processes, this edit supercedes them all. + for (var n in this.setWarningText.pid_) { + clearTimeout(this.setWarningText.pid_[n]); + delete this.setWarningText.pid_[n]; + } + } else if (this.setWarningText.pid_[id]) { + // Only queue up the latest change. Kill any earlier pending process. + clearTimeout(this.setWarningText.pid_[id]); + delete this.setWarningText.pid_[id]; + } + if (Blockly.dragMode_ == Blockly.DRAG_FREE) { + // Don't change the warning text during a drag. + // Wait until the drag finishes. + var thisBlock = this; + this.setWarningText.pid_[id] = setTimeout(function() { + if (thisBlock.workspace) { // Check block wasn't deleted. + delete thisBlock.setWarningText.pid_[id]; + thisBlock.setWarningText(text, id); + } + }, 100); + return; + } + if (this.isInFlyout) { + text = null; + } + + // Bubble up to add a warning on top-most collapsed block. + var parent = this.getSurroundParent(); + var collapsedParent = null; + while (parent) { + if (parent.isCollapsed()) { + collapsedParent = parent; + } + parent = parent.getSurroundParent(); + } + if (collapsedParent) { + collapsedParent.setWarningText(text, 'collapsed ' + this.id + ' ' + id); + } + + var changedState = false; + if (goog.isString(text)) { + if (!this.warning) { + this.warning = new Blockly.Warning(this); + changedState = true; + } + this.warning.setText(/** @type {string} */ (text), id); + } else { + // Dispose all warnings if no id is given. + if (this.warning && !id) { + this.warning.dispose(); + changedState = true; + } else if (this.warning) { + var oldText = this.warning.getText(); + this.warning.setText('', id); + var newText = this.warning.getText(); + if (!newText) { + this.warning.dispose(); + } + changedState = oldText == newText; + } + } + if (changedState && this.rendered) { + this.render(); + // Adding or removing a warning icon will cause the block to change shape. + this.bumpNeighbours_(); + } +}; + +/** + * Give this block a mutator dialog. + * @param {Blockly.Mutator} mutator A mutator dialog instance or null to remove. + */ +Blockly.BlockSvg.prototype.setMutator = function(mutator) { + if (this.mutator && this.mutator !== mutator) { + this.mutator.dispose(); + } + if (mutator) { + mutator.block_ = this; + this.mutator = mutator; + mutator.createIcon(); + } +}; + +/** + * Set whether the block is disabled or not. + * @param {boolean} disabled True if disabled. + */ +Blockly.BlockSvg.prototype.setDisabled = function(disabled) { + if (this.disabled != disabled) { + Blockly.BlockSvg.superClass_.setDisabled.call(this, disabled); + if (this.rendered) { + this.updateDisabled(); + } + } +}; + +/** + * Select this block. Highlight it visually. + */ +Blockly.BlockSvg.prototype.addSelect = function() { + Blockly.addClass_(/** @type {!Element} */ (this.svgGroup_), + 'blocklySelected'); + // Move the selected block to the top of the stack. + var block = this; + do { + var root = block.getSvgRoot(); + root.parentNode.appendChild(root); + block = block.getParent(); + } while (block); +}; + +/** + * Unselect this block. Remove its highlighting. + */ +Blockly.BlockSvg.prototype.removeSelect = function() { + Blockly.removeClass_(/** @type {!Element} */ (this.svgGroup_), + 'blocklySelected'); +}; + +/** + * Adds the dragging class to this block. + * Also disables the highlights/shadows to improve performance. + */ +Blockly.BlockSvg.prototype.addDragging = function() { + Blockly.addClass_(/** @type {!Element} */ (this.svgGroup_), + 'blocklyDragging'); +}; + +/** + * Removes the dragging class from this block. + */ +Blockly.BlockSvg.prototype.removeDragging = function() { + Blockly.removeClass_(/** @type {!Element} */ (this.svgGroup_), + 'blocklyDragging'); +}; + +// Overrides of functions on Blockly.Block that take into account whether the +// block has been rendered. + +/** + * Change the colour of a block. + * @param {number|string} colour HSV hue value, or #RRGGBB string. + */ +Blockly.BlockSvg.prototype.setColour = function(colour) { + Blockly.BlockSvg.superClass_.setColour.call(this, colour); + + if (this.rendered) { + this.updateColour(); + } +}; + +/** + * Set whether this block can chain onto the bottom of another block. + * @param {boolean} newBoolean True if there can be a previous statement. + * @param {string|Array.<string>|null|undefined} opt_check Statement type or + * list of statement types. Null/undefined if any type could be connected. + */ +Blockly.BlockSvg.prototype.setPreviousStatement = + function(newBoolean, opt_check) { + /* eslint-disable indent */ + Blockly.BlockSvg.superClass_.setPreviousStatement.call(this, newBoolean, + opt_check); + + if (this.rendered) { + this.render(); + this.bumpNeighbours_(); + } +}; /* eslint-enable indent */ + +/** + * Set whether another block can chain onto the bottom of this block. + * @param {boolean} newBoolean True if there can be a next statement. + * @param {string|Array.<string>|null|undefined} opt_check Statement type or + * list of statement types. Null/undefined if any type could be connected. + */ +Blockly.BlockSvg.prototype.setNextStatement = function(newBoolean, opt_check) { + Blockly.BlockSvg.superClass_.setNextStatement.call(this, newBoolean, + opt_check); + + if (this.rendered) { + this.render(); + this.bumpNeighbours_(); + } +}; + +/** + * Set whether this block returns a value. + * @param {boolean} newBoolean True if there is an output. + * @param {string|Array.<string>|null|undefined} opt_check Returned type or list + * of returned types. Null or undefined if any type could be returned + * (e.g. variable get). + */ +Blockly.BlockSvg.prototype.setOutput = function(newBoolean, opt_check) { + Blockly.BlockSvg.superClass_.setOutput.call(this, newBoolean, opt_check); + + if (this.rendered) { + this.render(); + this.bumpNeighbours_(); + } +}; + +/** + * Set whether value inputs are arranged horizontally or vertically. + * @param {boolean} newBoolean True if inputs are horizontal. + */ +Blockly.BlockSvg.prototype.setInputsInline = function(newBoolean) { + Blockly.BlockSvg.superClass_.setInputsInline.call(this, newBoolean); + + if (this.rendered) { + this.render(); + this.bumpNeighbours_(); + } +}; + +/** + * Remove an input from this block. + * @param {string} name The name of the input. + * @param {boolean=} opt_quiet True to prevent error if input is not present. + * @throws {goog.asserts.AssertionError} if the input is not present and + * opt_quiet is not true. + */ +Blockly.BlockSvg.prototype.removeInput = function(name, opt_quiet) { + Blockly.BlockSvg.superClass_.removeInput.call(this, name, opt_quiet); + + if (this.rendered) { + this.render(); + // Removing an input will cause the block to change shape. + this.bumpNeighbours_(); + } +}; + +/** + * Move a numbered input to a different location on this block. + * @param {number} inputIndex Index of the input to move. + * @param {number} refIndex Index of input that should be after the moved input. + */ +Blockly.BlockSvg.prototype.moveNumberedInputBefore = function( + inputIndex, refIndex) { + Blockly.BlockSvg.superClass_.moveNumberedInputBefore.call(this, inputIndex, + refIndex); + + if (this.rendered) { + this.render(); + // Moving an input will cause the block to change shape. + this.bumpNeighbours_(); + } +}; + +/** + * Add a value input, statement input or local variable to this block. + * @param {number} type Either Blockly.INPUT_VALUE or Blockly.NEXT_STATEMENT or + * Blockly.DUMMY_INPUT. + * @param {string} name Language-neutral identifier which may used to find this + * input again. Should be unique to this block. + * @return {!Blockly.Input} The input object created. + * @private + */ +Blockly.BlockSvg.prototype.appendInput_ = function(type, name) { + var input = Blockly.BlockSvg.superClass_.appendInput_.call(this, type, name); + + if (this.rendered) { + this.render(); + // Adding an input will cause the block to change shape. + this.bumpNeighbours_(); + } + return input; +}; + +/** + * Returns connections originating from this block. + * @param {boolean} all If true, return all connections even hidden ones. + * Otherwise, for a non-rendered block return an empty list, and for a + * collapsed block don't return inputs connections. + * @return {!Array.<!Blockly.Connection>} Array of connections. + * @private + */ +Blockly.BlockSvg.prototype.getConnections_ = function(all) { + var myConnections = []; + if (all || this.rendered) { + if (this.outputConnection) { + myConnections.push(this.outputConnection); + } + if (this.previousConnection) { + myConnections.push(this.previousConnection); + } + if (this.nextConnection) { + myConnections.push(this.nextConnection); + } + if (all || !this.collapsed_) { + for (var i = 0, input; input = this.inputList[i]; i++) { + if (input.connection) { + myConnections.push(input.connection); + } + } + } + } + return myConnections; +}; + +/** + * Create a connection of the specified type. + * @param {number} type The type of the connection to create. + * @return {!Blockly.RenderedConnection} A new connection of the specified type. + * @private + */ +Blockly.BlockSvg.prototype.makeConnection_ = function(type) { + return new Blockly.RenderedConnection(this, type); +}; diff --git a/src/blockly/core/blockly.js b/src/blockly/core/blockly.js new file mode 100644 index 0000000..fb6562e --- /dev/null +++ b/src/blockly/core/blockly.js @@ -0,0 +1,453 @@ +/** + * @license + * Visual Blocks Editor + * + * Copyright 2011 Google Inc. + * https://developers.google.com/blockly/ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Core JavaScript library for Blockly. + * @author fraser@google.com (Neil Fraser) + */ +'use strict'; + +// Top level object for Blockly. +goog.provide('Blockly'); + +goog.require('Blockly.BlockSvg.render'); +goog.require('Blockly.Events'); +goog.require('Blockly.FieldAngle'); +goog.require('Blockly.FieldCheckbox'); +goog.require('Blockly.FieldColour'); +// Date picker commented out since it increases footprint by 60%. +// Add it only if you need it. +//goog.require('Blockly.FieldDate'); +goog.require('Blockly.FieldDropdown'); +goog.require('Blockly.FieldImage'); +goog.require('Blockly.FieldTextInput'); +goog.require('Blockly.FieldNumber'); +goog.require('Blockly.FieldVariable'); +goog.require('Blockly.Generator'); +goog.require('Blockly.Msg'); +goog.require('Blockly.Procedures'); +goog.require('Blockly.Toolbox'); +goog.require('Blockly.WidgetDiv'); +goog.require('Blockly.WorkspaceSvg'); +goog.require('Blockly.constants'); +goog.require('Blockly.inject'); +goog.require('Blockly.utils'); +goog.require('goog.color'); +goog.require('goog.userAgent'); + + +// Turn off debugging when compiled. +var CLOSURE_DEFINES = {'goog.DEBUG': false}; + +/** + * The main workspace most recently used. + * Set by Blockly.WorkspaceSvg.prototype.markFocused + * @type {Blockly.Workspace} + */ +Blockly.mainWorkspace = null; + +/** + * Currently selected block. + * @type {Blockly.Block} + */ +Blockly.selected = null; + +/** + * Currently highlighted connection (during a drag). + * @type {Blockly.Connection} + * @private + */ +Blockly.highlightedConnection_ = null; + +/** + * Connection on dragged block that matches the highlighted connection. + * @type {Blockly.Connection} + * @private + */ +Blockly.localConnection_ = null; + +/** + * All of the connections on blocks that are currently being dragged. + * @type {!Array.<!Blockly.Connection>} + * @private + */ +Blockly.draggingConnections_ = []; + +/** + * Contents of the local clipboard. + * @type {Element} + * @private + */ +Blockly.clipboardXml_ = null; + +/** + * Source of the local clipboard. + * @type {Blockly.WorkspaceSvg} + * @private + */ +Blockly.clipboardSource_ = null; + +/** + * Is the mouse dragging a block? + * DRAG_NONE - No drag operation. + * DRAG_STICKY - Still inside the sticky DRAG_RADIUS. + * DRAG_FREE - Freely draggable. + * @private + */ +Blockly.dragMode_ = Blockly.DRAG_NONE; + +/** + * Wrapper function called when a touch mouseUp occurs during a drag operation. + * @type {Array.<!Array>} + * @private + */ +Blockly.onTouchUpWrapper_ = null; + +/** + * Convert a hue (HSV model) into an RGB hex triplet. + * @param {number} hue Hue on a colour wheel (0-360). + * @return {string} RGB code, e.g. '#5ba65b'. + */ +Blockly.hueToRgb = function(hue) { + return goog.color.hsvToHex(hue, Blockly.HSV_SATURATION, + Blockly.HSV_VALUE * 255); +}; + +/** + * Returns the dimensions of the specified SVG image. + * @param {!Element} svg SVG image. + * @return {!Object} Contains width and height properties. + */ +Blockly.svgSize = function(svg) { + return {width: svg.cachedWidth_, + height: svg.cachedHeight_}; +}; + +/** + * Size the workspace when the contents change. This also updates + * scrollbars accordingly. + * @param {!Blockly.WorkspaceSvg} workspace The workspace to resize. + */ +Blockly.resizeSvgContents = function(workspace) { + workspace.resizeContents(); +}; + +/** + * Size the SVG image to completely fill its container. Call this when the view + * actually changes sizes (e.g. on a window resize/device orientation change). + * See Blockly.resizeSvgContents to resize the workspace when the contents + * change (e.g. when a block is added or removed). + * Record the height/width of the SVG image. + * @param {!Blockly.WorkspaceSvg} workspace Any workspace in the SVG. + */ +Blockly.svgResize = function(workspace) { + var mainWorkspace = workspace; + while (mainWorkspace.options.parentWorkspace) { + mainWorkspace = mainWorkspace.options.parentWorkspace; + } + var svg = mainWorkspace.getParentSvg(); + var div = svg.parentNode; + if (!div) { + // Workspace deleted, or something. + return; + } + var width = div.offsetWidth; + var height = div.offsetHeight; + if (svg.cachedWidth_ != width) { + svg.setAttribute('width', width + 'px'); + svg.cachedWidth_ = width; + } + if (svg.cachedHeight_ != height) { + svg.setAttribute('height', height + 'px'); + svg.cachedHeight_ = height; + } + mainWorkspace.resize(); +}; + +/** + * Handle a mouse-up anywhere on the page. + * @param {!Event} e Mouse up event. + * @private + */ +Blockly.onMouseUp_ = function(e) { + var workspace = Blockly.getMainWorkspace(); + Blockly.Css.setCursor(Blockly.Css.Cursor.OPEN); + workspace.dragMode_ = Blockly.DRAG_NONE; + // Unbind the touch event if it exists. + if (Blockly.onTouchUpWrapper_) { + Blockly.unbindEvent_(Blockly.onTouchUpWrapper_); + Blockly.onTouchUpWrapper_ = null; + } + if (Blockly.onMouseMoveWrapper_) { + Blockly.unbindEvent_(Blockly.onMouseMoveWrapper_); + Blockly.onMouseMoveWrapper_ = null; + } +}; + +/** + * Handle a mouse-move on SVG drawing surface. + * @param {!Event} e Mouse move event. + * @private + */ +Blockly.onMouseMove_ = function(e) { + if (e.touches && e.touches.length >= 2) { + return; // Multi-touch gestures won't have e.clientX. + } + var workspace = Blockly.getMainWorkspace(); + if (workspace.dragMode_ != Blockly.DRAG_NONE) { + var dx = e.clientX - workspace.startDragMouseX; + var dy = e.clientY - workspace.startDragMouseY; + var metrics = workspace.startDragMetrics; + var x = workspace.startScrollX + dx; + var y = workspace.startScrollY + dy; + x = Math.min(x, -metrics.contentLeft); + y = Math.min(y, -metrics.contentTop); + x = Math.max(x, metrics.viewWidth - metrics.contentLeft - + metrics.contentWidth); + y = Math.max(y, metrics.viewHeight - metrics.contentTop - + metrics.contentHeight); + + // Move the scrollbars and the page will scroll automatically. + workspace.scrollbar.set(-x - metrics.contentLeft, + -y - metrics.contentTop); + // Cancel the long-press if the drag has moved too far. + if (Math.sqrt(dx * dx + dy * dy) > Blockly.DRAG_RADIUS) { + Blockly.longStop_(); + workspace.dragMode_ = Blockly.DRAG_FREE; + } + e.stopPropagation(); + e.preventDefault(); + } +}; + +/** + * Handle a key-down on SVG drawing surface. + * @param {!Event} e Key down event. + * @private + */ +Blockly.onKeyDown_ = function(e) { + if (Blockly.mainWorkspace.options.readOnly || Blockly.isTargetInput_(e)) { + // No key actions on readonly workspaces. + // When focused on an HTML text input widget, don't trap any keys. + return; + } + var deleteBlock = false; + if (e.keyCode == 27) { + // Pressing esc closes the context menu. + Blockly.hideChaff(); + } else if (e.keyCode == 8 || e.keyCode == 46) { + // Delete or backspace. + // Stop the browser from going back to the previous page. + // Do this first to prevent an error in the delete code from resulting in + // data loss. + e.preventDefault(); + if (Blockly.selected && Blockly.selected.isDeletable()) { + deleteBlock = true; + } + } else if (e.altKey || e.ctrlKey || e.metaKey) { + if (Blockly.selected && + Blockly.selected.isDeletable() && Blockly.selected.isMovable()) { + if (e.keyCode == 67) { + // 'c' for copy. + Blockly.hideChaff(); + Blockly.copy_(Blockly.selected); + } else if (e.keyCode == 88) { + // 'x' for cut. + Blockly.copy_(Blockly.selected); + deleteBlock = true; + } + } + if (e.keyCode == 86) { + // 'v' for paste. + if (Blockly.clipboardXml_) { + Blockly.Events.setGroup(true); + Blockly.clipboardSource_.paste(Blockly.clipboardXml_); + Blockly.Events.setGroup(false); + } + } else if (e.keyCode == 90) { + // 'z' for undo 'Z' is for redo. + Blockly.hideChaff(); + Blockly.mainWorkspace.undo(e.shiftKey); + } + } + if (deleteBlock) { + // Common code for delete and cut. + Blockly.Events.setGroup(true); + Blockly.hideChaff(); + var heal = Blockly.dragMode_ != Blockly.DRAG_FREE; + Blockly.selected.dispose(heal, true); + if (Blockly.highlightedConnection_) { + Blockly.highlightedConnection_.unhighlight(); + Blockly.highlightedConnection_ = null; + } + Blockly.Events.setGroup(false); + } +}; + +/** + * Stop binding to the global mouseup and mousemove events. + * @private + */ +Blockly.terminateDrag_ = function() { + Blockly.BlockSvg.terminateDrag(); + Blockly.Flyout.terminateDrag_(); +}; + +/** + * PID of queued long-press task. + * @private + */ +Blockly.longPid_ = 0; + +/** + * Context menus on touch devices are activated using a long-press. + * Unfortunately the contextmenu touch event is currently (2015) only suported + * by Chrome. This function is fired on any touchstart event, queues a task, + * which after about a second opens the context menu. The tasks is killed + * if the touch event terminates early. + * @param {!Event} e Touch start event. + * @param {!Blockly.Block|!Blockly.WorkspaceSvg} uiObject The block or workspace + * under the touchstart event. + * @private + */ +Blockly.longStart_ = function(e, uiObject) { + Blockly.longStop_(); + Blockly.longPid_ = setTimeout(function() { + e.button = 2; // Simulate a right button click. + uiObject.onMouseDown_(e); + }, Blockly.LONGPRESS); +}; + +/** + * Nope, that's not a long-press. Either touchend or touchcancel was fired, + * or a drag hath begun. Kill the queued long-press task. + * @private + */ +Blockly.longStop_ = function() { + if (Blockly.longPid_) { + clearTimeout(Blockly.longPid_); + Blockly.longPid_ = 0; + } +}; + +/** + * Copy a block onto the local clipboard. + * @param {!Blockly.Block} block Block to be copied. + * @private + */ +Blockly.copy_ = function(block) { + var xmlBlock = Blockly.Xml.blockToDom(block); + if (Blockly.dragMode_ != Blockly.DRAG_FREE) { + Blockly.Xml.deleteNext(xmlBlock); + } + // Encode start position in XML. + var xy = block.getRelativeToSurfaceXY(); + xmlBlock.setAttribute('x', block.RTL ? -xy.x : xy.x); + xmlBlock.setAttribute('y', xy.y); + Blockly.clipboardXml_ = xmlBlock; + Blockly.clipboardSource_ = block.workspace; +}; + +/** + * Duplicate this block and its children. + * @param {!Blockly.Block} block Block to be copied. + * @private + */ +Blockly.duplicate_ = function(block) { + // Save the clipboard. + var clipboardXml = Blockly.clipboardXml_; + var clipboardSource = Blockly.clipboardSource_; + + // Create a duplicate via a copy/paste operation. + Blockly.copy_(block); + block.workspace.paste(Blockly.clipboardXml_); + + // Restore the clipboard. + Blockly.clipboardXml_ = clipboardXml; + Blockly.clipboardSource_ = clipboardSource; +}; + +/** + * Cancel the native context menu, unless the focus is on an HTML input widget. + * @param {!Event} e Mouse down event. + * @private + */ +Blockly.onContextMenu_ = function(e) { + if (!Blockly.isTargetInput_(e)) { + // When focused on an HTML text input widget, don't cancel the context menu. + e.preventDefault(); + } +}; + +/** + * Close tooltips, context menus, dropdown selections, etc. + * @param {boolean=} opt_allowToolbox If true, don't close the toolbox. + */ +Blockly.hideChaff = function(opt_allowToolbox) { + Blockly.Tooltip.hide(); + Blockly.WidgetDiv.hide(); + if (!opt_allowToolbox) { + var workspace = Blockly.getMainWorkspace(); + if (workspace.toolbox_ && + workspace.toolbox_.flyout_ && + workspace.toolbox_.flyout_.autoClose) { + workspace.toolbox_.clearSelection(); + } + } +}; + +/** + * When something in Blockly's workspace changes, call a function. + * @param {!Function} func Function to call. + * @return {!Array.<!Array>} Opaque data that can be passed to + * removeChangeListener. + * @deprecated April 2015 + */ +Blockly.addChangeListener = function(func) { + // Backwards compatability from before there could be multiple workspaces. + console.warn('Deprecated call to Blockly.addChangeListener, ' + + 'use workspace.addChangeListener instead.'); + return Blockly.getMainWorkspace().addChangeListener(func); +}; + +/** + * Returns the main workspace. Returns the last used main workspace (based on + * focus). Try not to use this function, particularly if there are multiple + * Blockly instances on a page. + * @return {!Blockly.Workspace} The main workspace. + */ +Blockly.getMainWorkspace = function() { + return Blockly.mainWorkspace; +}; + +// IE9 does not have a console. Create a stub to stop errors. +if (!goog.global['console']) { + goog.global['console'] = { + 'log': function() {}, + 'warn': function() {} + }; +} + +// Export symbols that would otherwise be renamed by Closure compiler. +if (!goog.global['Blockly']) { + goog.global['Blockly'] = {}; +} +goog.global['Blockly']['getMainWorkspace'] = Blockly.getMainWorkspace; +goog.global['Blockly']['addChangeListener'] = Blockly.addChangeListener; diff --git a/src/blockly/core/blocks.js b/src/blockly/core/blocks.js new file mode 100644 index 0000000..d6932ce --- /dev/null +++ b/src/blockly/core/blocks.js @@ -0,0 +1,33 @@ +/** + * @license + * Visual Blocks Editor + * + * Copyright 2013 Google Inc. + * https://developers.google.com/blockly/ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Empty name space for the Blocks singleton. + * @author spertus@google.com (Ellen Spertus) + */ +'use strict'; + +goog.provide('Blockly.Blocks'); + +/** + * Allow for switching between one and zero based indexing for lists and text, + * one based by default. + */ +Blockly.Blocks.ONE_BASED_INDEXING = true; diff --git a/src/blockly/core/bubble.js b/src/blockly/core/bubble.js new file mode 100644 index 0000000..d4c1e27 --- /dev/null +++ b/src/blockly/core/bubble.js @@ -0,0 +1,579 @@ +/** + * @license + * Visual Blocks Editor + * + * Copyright 2012 Google Inc. + * https://developers.google.com/blockly/ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Object representing a UI bubble. + * @author fraser@google.com (Neil Fraser) + */ +'use strict'; + +goog.provide('Blockly.Bubble'); + +goog.require('Blockly.Workspace'); +goog.require('goog.dom'); +goog.require('goog.math'); +goog.require('goog.math.Coordinate'); +goog.require('goog.userAgent'); + + +/** + * Class for UI bubble. + * @param {!Blockly.WorkspaceSvg} workspace The workspace on which to draw the + * bubble. + * @param {!Element} content SVG content for the bubble. + * @param {Element} shape SVG element to avoid eclipsing. + * @param {!goog.math.Coodinate} anchorXY Absolute position of bubble's anchor + * point. + * @param {?number} bubbleWidth Width of bubble, or null if not resizable. + * @param {?number} bubbleHeight Height of bubble, or null if not resizable. + * @constructor + */ +Blockly.Bubble = function(workspace, content, shape, anchorXY, + bubbleWidth, bubbleHeight) { + this.workspace_ = workspace; + this.content_ = content; + this.shape_ = shape; + + var angle = Blockly.Bubble.ARROW_ANGLE; + if (this.workspace_.RTL) { + angle = -angle; + } + this.arrow_radians_ = goog.math.toRadians(angle); + + var canvas = workspace.getBubbleCanvas(); + canvas.appendChild(this.createDom_(content, !!(bubbleWidth && bubbleHeight))); + + this.setAnchorLocation(anchorXY); + if (!bubbleWidth || !bubbleHeight) { + var bBox = /** @type {SVGLocatable} */ (this.content_).getBBox(); + bubbleWidth = bBox.width + 2 * Blockly.Bubble.BORDER_WIDTH; + bubbleHeight = bBox.height + 2 * Blockly.Bubble.BORDER_WIDTH; + } + this.setBubbleSize(bubbleWidth, bubbleHeight); + + // Render the bubble. + this.positionBubble_(); + this.renderArrow_(); + this.rendered_ = true; + + if (!workspace.options.readOnly) { + Blockly.bindEvent_(this.bubbleBack_, 'mousedown', this, + this.bubbleMouseDown_); + if (this.resizeGroup_) { + Blockly.bindEvent_(this.resizeGroup_, 'mousedown', this, + this.resizeMouseDown_); + } + } +}; + +/** + * Width of the border around the bubble. + */ +Blockly.Bubble.BORDER_WIDTH = 6; + +/** + * Determines the thickness of the base of the arrow in relation to the size + * of the bubble. Higher numbers result in thinner arrows. + */ +Blockly.Bubble.ARROW_THICKNESS = 10; + +/** + * The number of degrees that the arrow bends counter-clockwise. + */ +Blockly.Bubble.ARROW_ANGLE = 20; + +/** + * The sharpness of the arrow's bend. Higher numbers result in smoother arrows. + */ +Blockly.Bubble.ARROW_BEND = 4; + +/** + * Distance between arrow point and anchor point. + */ +Blockly.Bubble.ANCHOR_RADIUS = 8; + +/** + * Wrapper function called when a mouseUp occurs during a drag operation. + * @type {Array.<!Array>} + * @private + */ +Blockly.Bubble.onMouseUpWrapper_ = null; + +/** + * Wrapper function called when a mouseMove occurs during a drag operation. + * @type {Array.<!Array>} + * @private + */ +Blockly.Bubble.onMouseMoveWrapper_ = null; + +/** + * Function to call on resize of bubble. + * @type {Function} + */ +Blockly.Bubble.prototype.resizeCallback_ = null; + +/** + * Stop binding to the global mouseup and mousemove events. + * @private + */ +Blockly.Bubble.unbindDragEvents_ = function() { + if (Blockly.Bubble.onMouseUpWrapper_) { + Blockly.unbindEvent_(Blockly.Bubble.onMouseUpWrapper_); + Blockly.Bubble.onMouseUpWrapper_ = null; + } + if (Blockly.Bubble.onMouseMoveWrapper_) { + Blockly.unbindEvent_(Blockly.Bubble.onMouseMoveWrapper_); + Blockly.Bubble.onMouseMoveWrapper_ = null; + } +}; + +/** + * Flag to stop incremental rendering during construction. + * @private + */ +Blockly.Bubble.prototype.rendered_ = false; + +/** + * Absolute coordinate of anchor point. + * @type {goog.math.Coordinate} + * @private + */ +Blockly.Bubble.prototype.anchorXY_ = null; + +/** + * Relative X coordinate of bubble with respect to the anchor's centre. + * In RTL mode the initial value is negated. + * @private + */ +Blockly.Bubble.prototype.relativeLeft_ = 0; + +/** + * Relative Y coordinate of bubble with respect to the anchor's centre. + * @private + */ +Blockly.Bubble.prototype.relativeTop_ = 0; + +/** + * Width of bubble. + * @private + */ +Blockly.Bubble.prototype.width_ = 0; + +/** + * Height of bubble. + * @private + */ +Blockly.Bubble.prototype.height_ = 0; + +/** + * Automatically position and reposition the bubble. + * @private + */ +Blockly.Bubble.prototype.autoLayout_ = true; + +/** + * Create the bubble's DOM. + * @param {!Element} content SVG content for the bubble. + * @param {boolean} hasResize Add diagonal resize gripper if true. + * @return {!Element} The bubble's SVG group. + * @private + */ +Blockly.Bubble.prototype.createDom_ = function(content, hasResize) { + /* Create the bubble. Here's the markup that will be generated: + <g> + <g filter="url(#blocklyEmbossFilter837493)"> + <path d="... Z" /> + <rect class="blocklyDraggable" rx="8" ry="8" width="180" height="180"/> + </g> + <g transform="translate(165, 165)" class="blocklyResizeSE"> + <polygon points="0,15 15,15 15,0"/> + <line class="blocklyResizeLine" x1="5" y1="14" x2="14" y2="5"/> + <line class="blocklyResizeLine" x1="10" y1="14" x2="14" y2="10"/> + </g> + [...content goes here...] + </g> + */ + this.bubbleGroup_ = Blockly.createSvgElement('g', {}, null); + var filter = + {'filter': 'url(#' + this.workspace_.options.embossFilterId + ')'}; + if (goog.userAgent.getUserAgentString().indexOf('JavaFX') != -1) { + // Multiple reports that JavaFX can't handle filters. UserAgent: + // Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.44 + // (KHTML, like Gecko) JavaFX/8.0 Safari/537.44 + // https://github.com/google/blockly/issues/99 + filter = {}; + } + var bubbleEmboss = Blockly.createSvgElement('g', + filter, this.bubbleGroup_); + this.bubbleArrow_ = Blockly.createSvgElement('path', {}, bubbleEmboss); + this.bubbleBack_ = Blockly.createSvgElement('rect', + {'class': 'blocklyDraggable', 'x': 0, 'y': 0, + 'rx': Blockly.Bubble.BORDER_WIDTH, 'ry': Blockly.Bubble.BORDER_WIDTH}, + bubbleEmboss); + if (hasResize) { + this.resizeGroup_ = Blockly.createSvgElement('g', + {'class': this.workspace_.RTL ? + 'blocklyResizeSW' : 'blocklyResizeSE'}, + this.bubbleGroup_); + var resizeSize = 2 * Blockly.Bubble.BORDER_WIDTH; + Blockly.createSvgElement('polygon', + {'points': '0,x x,x x,0'.replace(/x/g, resizeSize.toString())}, + this.resizeGroup_); + Blockly.createSvgElement('line', + {'class': 'blocklyResizeLine', + 'x1': resizeSize / 3, 'y1': resizeSize - 1, + 'x2': resizeSize - 1, 'y2': resizeSize / 3}, this.resizeGroup_); + Blockly.createSvgElement('line', + {'class': 'blocklyResizeLine', + 'x1': resizeSize * 2 / 3, 'y1': resizeSize - 1, + 'x2': resizeSize - 1, 'y2': resizeSize * 2 / 3}, this.resizeGroup_); + } else { + this.resizeGroup_ = null; + } + this.bubbleGroup_.appendChild(content); + return this.bubbleGroup_; +}; + +/** + * Handle a mouse-down on bubble's border. + * @param {!Event} e Mouse down event. + * @private + */ +Blockly.Bubble.prototype.bubbleMouseDown_ = function(e) { + this.promote_(); + Blockly.Bubble.unbindDragEvents_(); + if (Blockly.isRightButton(e)) { + // No right-click. + e.stopPropagation(); + return; + } else if (Blockly.isTargetInput_(e)) { + // When focused on an HTML text input widget, don't trap any events. + return; + } + // Left-click (or middle click) + Blockly.Css.setCursor(Blockly.Css.Cursor.CLOSED); + + this.workspace_.startDrag(e, new goog.math.Coordinate( + this.workspace_.RTL ? -this.relativeLeft_ : this.relativeLeft_, + this.relativeTop_)); + + Blockly.Bubble.onMouseUpWrapper_ = Blockly.bindEvent_(document, + 'mouseup', this, Blockly.Bubble.unbindDragEvents_); + Blockly.Bubble.onMouseMoveWrapper_ = Blockly.bindEvent_(document, + 'mousemove', this, this.bubbleMouseMove_); + Blockly.hideChaff(); + // This event has been handled. No need to bubble up to the document. + e.stopPropagation(); +}; + +/** + * Drag this bubble to follow the mouse. + * @param {!Event} e Mouse move event. + * @private + */ +Blockly.Bubble.prototype.bubbleMouseMove_ = function(e) { + this.autoLayout_ = false; + var newXY = this.workspace_.moveDrag(e); + this.relativeLeft_ = this.workspace_.RTL ? -newXY.x : newXY.x; + this.relativeTop_ = newXY.y; + this.positionBubble_(); + this.renderArrow_(); +}; + +/** + * Handle a mouse-down on bubble's resize corner. + * @param {!Event} e Mouse down event. + * @private + */ +Blockly.Bubble.prototype.resizeMouseDown_ = function(e) { + this.promote_(); + Blockly.Bubble.unbindDragEvents_(); + if (Blockly.isRightButton(e)) { + // No right-click. + e.stopPropagation(); + return; + } + // Left-click (or middle click) + Blockly.Css.setCursor(Blockly.Css.Cursor.CLOSED); + + this.workspace_.startDrag(e, new goog.math.Coordinate( + this.workspace_.RTL ? -this.width_ : this.width_, this.height_)); + + Blockly.Bubble.onMouseUpWrapper_ = Blockly.bindEvent_(document, + 'mouseup', this, Blockly.Bubble.unbindDragEvents_); + Blockly.Bubble.onMouseMoveWrapper_ = Blockly.bindEvent_(document, + 'mousemove', this, this.resizeMouseMove_); + Blockly.hideChaff(); + // This event has been handled. No need to bubble up to the document. + e.stopPropagation(); +}; + +/** + * Resize this bubble to follow the mouse. + * @param {!Event} e Mouse move event. + * @private + */ +Blockly.Bubble.prototype.resizeMouseMove_ = function(e) { + this.autoLayout_ = false; + var newXY = this.workspace_.moveDrag(e); + this.setBubbleSize(this.workspace_.RTL ? -newXY.x : newXY.x, newXY.y); + if (this.workspace_.RTL) { + // RTL requires the bubble to move its left edge. + this.positionBubble_(); + } +}; + +/** + * Register a function as a callback event for when the bubble is resized. + * @param {!Function} callback The function to call on resize. + */ +Blockly.Bubble.prototype.registerResizeEvent = function(callback) { + this.resizeCallback_ = callback; +}; + +/** + * Move this bubble to the top of the stack. + * @private + */ +Blockly.Bubble.prototype.promote_ = function() { + var svgGroup = this.bubbleGroup_.parentNode; + svgGroup.appendChild(this.bubbleGroup_); +}; + +/** + * Notification that the anchor has moved. + * Update the arrow and bubble accordingly. + * @param {!goog.math.Coordinate} xy Absolute location. + */ +Blockly.Bubble.prototype.setAnchorLocation = function(xy) { + this.anchorXY_ = xy; + if (this.rendered_) { + this.positionBubble_(); + } +}; + +/** + * Position the bubble so that it does not fall off-screen. + * @private + */ +Blockly.Bubble.prototype.layoutBubble_ = function() { + // Compute the preferred bubble location. + var relativeLeft = -this.width_ / 4; + var relativeTop = -this.height_ - Blockly.BlockSvg.MIN_BLOCK_Y; + // Prevent the bubble from being off-screen. + var metrics = this.workspace_.getMetrics(); + metrics.viewWidth /= this.workspace_.scale; + metrics.viewLeft /= this.workspace_.scale; + var anchorX = this.anchorXY_.x; + if (this.workspace_.RTL) { + if (anchorX - metrics.viewLeft - relativeLeft - this.width_ < + Blockly.Scrollbar.scrollbarThickness) { + // Slide the bubble right until it is onscreen. + relativeLeft = anchorX - metrics.viewLeft - this.width_ - + Blockly.Scrollbar.scrollbarThickness; + } else if (anchorX - metrics.viewLeft - relativeLeft > + metrics.viewWidth) { + // Slide the bubble left until it is onscreen. + relativeLeft = anchorX - metrics.viewLeft - metrics.viewWidth; + } + } else { + if (anchorX + relativeLeft < metrics.viewLeft) { + // Slide the bubble right until it is onscreen. + relativeLeft = metrics.viewLeft - anchorX; + } else if (metrics.viewLeft + metrics.viewWidth < + anchorX + relativeLeft + this.width_ + + Blockly.BlockSvg.SEP_SPACE_X + + Blockly.Scrollbar.scrollbarThickness) { + // Slide the bubble left until it is onscreen. + relativeLeft = metrics.viewLeft + metrics.viewWidth - anchorX - + this.width_ - Blockly.Scrollbar.scrollbarThickness; + } + } + if (this.anchorXY_.y + relativeTop < metrics.viewTop) { + // Slide the bubble below the block. + var bBox = /** @type {SVGLocatable} */ (this.shape_).getBBox(); + relativeTop = bBox.height; + } + this.relativeLeft_ = relativeLeft; + this.relativeTop_ = relativeTop; +}; + +/** + * Move the bubble to a location relative to the anchor's centre. + * @private + */ +Blockly.Bubble.prototype.positionBubble_ = function() { + var left = this.anchorXY_.x; + if (this.workspace_.RTL) { + left -= this.relativeLeft_ + this.width_; + } else { + left += this.relativeLeft_; + } + var top = this.relativeTop_ + this.anchorXY_.y; + this.bubbleGroup_.setAttribute('transform', + 'translate(' + left + ',' + top + ')'); +}; + +/** + * Get the dimensions of this bubble. + * @return {!Object} Object with width and height properties. + */ +Blockly.Bubble.prototype.getBubbleSize = function() { + return {width: this.width_, height: this.height_}; +}; + +/** + * Size this bubble. + * @param {number} width Width of the bubble. + * @param {number} height Height of the bubble. + */ +Blockly.Bubble.prototype.setBubbleSize = function(width, height) { + var doubleBorderWidth = 2 * Blockly.Bubble.BORDER_WIDTH; + // Minimum size of a bubble. + width = Math.max(width, doubleBorderWidth + 45); + height = Math.max(height, doubleBorderWidth + 20); + this.width_ = width; + this.height_ = height; + this.bubbleBack_.setAttribute('width', width); + this.bubbleBack_.setAttribute('height', height); + if (this.resizeGroup_) { + if (this.workspace_.RTL) { + // Mirror the resize group. + var resizeSize = 2 * Blockly.Bubble.BORDER_WIDTH; + this.resizeGroup_.setAttribute('transform', 'translate(' + + resizeSize + ',' + (height - doubleBorderWidth) + ') scale(-1 1)'); + } else { + this.resizeGroup_.setAttribute('transform', 'translate(' + + (width - doubleBorderWidth) + ',' + + (height - doubleBorderWidth) + ')'); + } + } + if (this.rendered_) { + if (this.autoLayout_) { + this.layoutBubble_(); + } + this.positionBubble_(); + this.renderArrow_(); + } + // Allow the contents to resize. + if (this.resizeCallback_) { + this.resizeCallback_(); + } +}; + +/** + * Draw the arrow between the bubble and the origin. + * @private + */ +Blockly.Bubble.prototype.renderArrow_ = function() { + var steps = []; + // Find the relative coordinates of the center of the bubble. + var relBubbleX = this.width_ / 2; + var relBubbleY = this.height_ / 2; + // Find the relative coordinates of the center of the anchor. + var relAnchorX = -this.relativeLeft_; + var relAnchorY = -this.relativeTop_; + if (relBubbleX == relAnchorX && relBubbleY == relAnchorY) { + // Null case. Bubble is directly on top of the anchor. + // Short circuit this rather than wade through divide by zeros. + steps.push('M ' + relBubbleX + ',' + relBubbleY); + } else { + // Compute the angle of the arrow's line. + var rise = relAnchorY - relBubbleY; + var run = relAnchorX - relBubbleX; + if (this.workspace_.RTL) { + run *= -1; + } + var hypotenuse = Math.sqrt(rise * rise + run * run); + var angle = Math.acos(run / hypotenuse); + if (rise < 0) { + angle = 2 * Math.PI - angle; + } + // Compute a line perpendicular to the arrow. + var rightAngle = angle + Math.PI / 2; + if (rightAngle > Math.PI * 2) { + rightAngle -= Math.PI * 2; + } + var rightRise = Math.sin(rightAngle); + var rightRun = Math.cos(rightAngle); + + // Calculate the thickness of the base of the arrow. + var bubbleSize = this.getBubbleSize(); + var thickness = (bubbleSize.width + bubbleSize.height) / + Blockly.Bubble.ARROW_THICKNESS; + thickness = Math.min(thickness, bubbleSize.width, bubbleSize.height) / 2; + + // Back the tip of the arrow off of the anchor. + var backoffRatio = 1 - Blockly.Bubble.ANCHOR_RADIUS / hypotenuse; + relAnchorX = relBubbleX + backoffRatio * run; + relAnchorY = relBubbleY + backoffRatio * rise; + + // Coordinates for the base of the arrow. + var baseX1 = relBubbleX + thickness * rightRun; + var baseY1 = relBubbleY + thickness * rightRise; + var baseX2 = relBubbleX - thickness * rightRun; + var baseY2 = relBubbleY - thickness * rightRise; + + // Distortion to curve the arrow. + var swirlAngle = angle + this.arrow_radians_; + if (swirlAngle > Math.PI * 2) { + swirlAngle -= Math.PI * 2; + } + var swirlRise = Math.sin(swirlAngle) * + hypotenuse / Blockly.Bubble.ARROW_BEND; + var swirlRun = Math.cos(swirlAngle) * + hypotenuse / Blockly.Bubble.ARROW_BEND; + + steps.push('M' + baseX1 + ',' + baseY1); + steps.push('C' + (baseX1 + swirlRun) + ',' + (baseY1 + swirlRise) + + ' ' + relAnchorX + ',' + relAnchorY + + ' ' + relAnchorX + ',' + relAnchorY); + steps.push('C' + relAnchorX + ',' + relAnchorY + + ' ' + (baseX2 + swirlRun) + ',' + (baseY2 + swirlRise) + + ' ' + baseX2 + ',' + baseY2); + } + steps.push('z'); + this.bubbleArrow_.setAttribute('d', steps.join(' ')); +}; + +/** + * Change the colour of a bubble. + * @param {string} hexColour Hex code of colour. + */ +Blockly.Bubble.prototype.setColour = function(hexColour) { + this.bubbleBack_.setAttribute('fill', hexColour); + this.bubbleArrow_.setAttribute('fill', hexColour); +}; + +/** + * Dispose of this bubble. + */ +Blockly.Bubble.prototype.dispose = function() { + Blockly.Bubble.unbindDragEvents_(); + // Dispose of and unlink the bubble. + goog.dom.removeNode(this.bubbleGroup_); + this.bubbleGroup_ = null; + this.bubbleArrow_ = null; + this.bubbleBack_ = null; + this.resizeGroup_ = null; + this.workspace_ = null; + this.content_ = null; + this.shape_ = null; +}; diff --git a/src/blockly/core/comment.js b/src/blockly/core/comment.js new file mode 100644 index 0000000..2121530 --- /dev/null +++ b/src/blockly/core/comment.js @@ -0,0 +1,278 @@ +/** + * @license + * Visual Blocks Editor + * + * Copyright 2011 Google Inc. + * https://developers.google.com/blockly/ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Object representing a code comment. + * @author fraser@google.com (Neil Fraser) + */ +'use strict'; + +goog.provide('Blockly.Comment'); + +goog.require('Blockly.Bubble'); +goog.require('Blockly.Icon'); +goog.require('goog.userAgent'); + + +/** + * Class for a comment. + * @param {!Blockly.Block} block The block associated with this comment. + * @extends {Blockly.Icon} + * @constructor + */ +Blockly.Comment = function(block) { + Blockly.Comment.superClass_.constructor.call(this, block); + this.createIcon(); +}; +goog.inherits(Blockly.Comment, Blockly.Icon); + +/** + * Comment text (if bubble is not visible). + * @private + */ +Blockly.Comment.prototype.text_ = ''; + +/** + * Width of bubble. + * @private + */ +Blockly.Comment.prototype.width_ = 160; + +/** + * Height of bubble. + * @private + */ +Blockly.Comment.prototype.height_ = 80; + +/** + * Draw the comment icon. + * @param {!Element} group The icon group. + * @private + */ +Blockly.Comment.prototype.drawIcon_ = function(group) { + // Circle. + Blockly.createSvgElement('circle', + {'class': 'blocklyIconShape', 'r': '8', 'cx': '8', 'cy': '8'}, + group); + // Can't use a real '?' text character since different browsers and operating + // systems render it differently. + // Body of question mark. + Blockly.createSvgElement('path', + {'class': 'blocklyIconSymbol', + 'd': 'm6.8,10h2c0.003,-0.617 0.271,-0.962 0.633,-1.266 2.875,-2.405 0.607,-5.534 -3.765,-3.874v1.7c3.12,-1.657 3.698,0.118 2.336,1.25 -1.201,0.998 -1.201,1.528 -1.204,2.19z'}, + group); + // Dot of question point. + Blockly.createSvgElement('rect', + {'class': 'blocklyIconSymbol', + 'x': '6.8', 'y': '10.78', 'height': '2', 'width': '2'}, + group); +}; + +/** + * Create the editor for the comment's bubble. + * @return {!Element} The top-level node of the editor. + * @private + */ +Blockly.Comment.prototype.createEditor_ = function() { + /* Create the editor. Here's the markup that will be generated: + <foreignObject x="8" y="8" width="164" height="164"> + <body xmlns="http://www.w3.org/1999/xhtml" class="blocklyMinimalBody"> + <textarea xmlns="http://www.w3.org/1999/xhtml" + class="blocklyCommentTextarea" + style="height: 164px; width: 164px;"></textarea> + </body> + </foreignObject> + */ + this.foreignObject_ = Blockly.createSvgElement('foreignObject', + {'x': Blockly.Bubble.BORDER_WIDTH, 'y': Blockly.Bubble.BORDER_WIDTH}, + null); + var body = document.createElementNS(Blockly.HTML_NS, 'body'); + body.setAttribute('xmlns', Blockly.HTML_NS); + body.className = 'blocklyMinimalBody'; + var textarea = document.createElementNS(Blockly.HTML_NS, 'textarea'); + textarea.className = 'blocklyCommentTextarea'; + textarea.setAttribute('dir', this.block_.RTL ? 'RTL' : 'LTR'); + body.appendChild(textarea); + this.textarea_ = textarea; + this.foreignObject_.appendChild(body); + Blockly.bindEvent_(textarea, 'mouseup', this, this.textareaFocus_); + // Don't zoom with mousewheel. + Blockly.bindEvent_(textarea, 'wheel', this, function(e) { + e.stopPropagation(); + }); + Blockly.bindEvent_(textarea, 'change', this, function(e) { + if (this.text_ != textarea.value) { + Blockly.Events.fire(new Blockly.Events.Change( + this.block_, 'comment', null, this.text_, textarea.value)); + this.text_ = textarea.value; + } + }); + setTimeout(function() { + textarea.focus(); + }, 0); + return this.foreignObject_; +}; + +/** + * Add or remove editability of the comment. + * @override + */ +Blockly.Comment.prototype.updateEditable = function() { + if (this.isVisible()) { + // Toggling visibility will force a rerendering. + this.setVisible(false); + this.setVisible(true); + } + // Allow the icon to update. + Blockly.Icon.prototype.updateEditable.call(this); +}; + +/** + * Callback function triggered when the bubble has resized. + * Resize the text area accordingly. + * @private + */ +Blockly.Comment.prototype.resizeBubble_ = function() { + if (this.isVisible()) { + var size = this.bubble_.getBubbleSize(); + var doubleBorderWidth = 2 * Blockly.Bubble.BORDER_WIDTH; + this.foreignObject_.setAttribute('width', size.width - doubleBorderWidth); + this.foreignObject_.setAttribute('height', size.height - doubleBorderWidth); + this.textarea_.style.width = (size.width - doubleBorderWidth - 4) + 'px'; + this.textarea_.style.height = (size.height - doubleBorderWidth - 4) + 'px'; + } +}; + +/** + * Show or hide the comment bubble. + * @param {boolean} visible True if the bubble should be visible. + */ +Blockly.Comment.prototype.setVisible = function(visible) { + if (visible == this.isVisible()) { + // No change. + return; + } + Blockly.Events.fire( + new Blockly.Events.Ui(this.block_, 'commentOpen', !visible, visible)); + if ((!this.block_.isEditable() && !this.textarea_) || goog.userAgent.IE) { + // Steal the code from warnings to make an uneditable text bubble. + // MSIE does not support foreignobject; textareas are impossible. + // http://msdn.microsoft.com/en-us/library/hh834675%28v=vs.85%29.aspx + // Always treat comments in IE as uneditable. + Blockly.Warning.prototype.setVisible.call(this, visible); + return; + } + // Save the bubble stats before the visibility switch. + var text = this.getText(); + var size = this.getBubbleSize(); + if (visible) { + // Create the bubble. + this.bubble_ = new Blockly.Bubble( + /** @type {!Blockly.WorkspaceSvg} */ (this.block_.workspace), + this.createEditor_(), this.block_.svgPath_, + this.iconXY_, this.width_, this.height_); + this.bubble_.registerResizeEvent(this.resizeBubble_.bind(this)); + this.updateColour(); + } else { + // Dispose of the bubble. + this.bubble_.dispose(); + this.bubble_ = null; + this.textarea_ = null; + this.foreignObject_ = null; + } + // Restore the bubble stats after the visibility switch. + this.setText(text); + this.setBubbleSize(size.width, size.height); +}; + +/** + * Bring the comment to the top of the stack when clicked on. + * @param {!Event} e Mouse up event. + * @private + */ +Blockly.Comment.prototype.textareaFocus_ = function(e) { + // Ideally this would be hooked to the focus event for the comment. + // However doing so in Firefox swallows the cursor for unknown reasons. + // So this is hooked to mouseup instead. No big deal. + this.bubble_.promote_(); + // Since the act of moving this node within the DOM causes a loss of focus, + // we need to reapply the focus. + this.textarea_.focus(); +}; + +/** + * Get the dimensions of this comment's bubble. + * @return {!Object} Object with width and height properties. + */ +Blockly.Comment.prototype.getBubbleSize = function() { + if (this.isVisible()) { + return this.bubble_.getBubbleSize(); + } else { + return {width: this.width_, height: this.height_}; + } +}; + +/** + * Size this comment's bubble. + * @param {number} width Width of the bubble. + * @param {number} height Height of the bubble. + */ +Blockly.Comment.prototype.setBubbleSize = function(width, height) { + if (this.textarea_) { + this.bubble_.setBubbleSize(width, height); + } else { + this.width_ = width; + this.height_ = height; + } +}; + +/** + * Returns this comment's text. + * @return {string} Comment text. + */ +Blockly.Comment.prototype.getText = function() { + return this.textarea_ ? this.textarea_.value : this.text_; +}; + +/** + * Set this comment's text. + * @param {string} text Comment text. + */ +Blockly.Comment.prototype.setText = function(text) { + if (this.text_ != text) { + Blockly.Events.fire(new Blockly.Events.Change( + this.block_, 'comment', null, this.text_, text)); + this.text_ = text; + } + if (this.textarea_) { + this.textarea_.value = text; + } +}; + +/** + * Dispose of this comment. + */ +Blockly.Comment.prototype.dispose = function() { + if (Blockly.Events.isEnabled()) { + this.setText(''); // Fire event to delete comment. + } + this.block_.comment = null; + Blockly.Icon.prototype.dispose.call(this); +}; diff --git a/src/blockly/core/connection.js b/src/blockly/core/connection.js new file mode 100644 index 0000000..3b8c82a --- /dev/null +++ b/src/blockly/core/connection.js @@ -0,0 +1,615 @@ +/** + * @license + * Visual Blocks Editor + * + * Copyright 2011 Google Inc. + * https://developers.google.com/blockly/ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Components for creating connections between blocks. + * @author fraser@google.com (Neil Fraser) + */ +'use strict'; + +goog.provide('Blockly.Connection'); + +goog.require('goog.asserts'); +goog.require('goog.dom'); + + +/** + * Class for a connection between blocks. + * @param {!Blockly.Block} source The block establishing this connection. + * @param {number} type The type of the connection. + * @constructor + */ +Blockly.Connection = function(source, type) { + /** + * @type {!Blockly.Block} + * @private + */ + this.sourceBlock_ = source; + /** @type {number} */ + this.type = type; + // Shortcut for the databases for this connection's workspace. + if (source.workspace.connectionDBList) { + this.db_ = source.workspace.connectionDBList[type]; + this.dbOpposite_ = + source.workspace.connectionDBList[Blockly.OPPOSITE_TYPE[type]]; + this.hidden_ = !this.db_; + } +}; + +/** + * Constants for checking whether two connections are compatible. + */ +Blockly.Connection.CAN_CONNECT = 0; +Blockly.Connection.REASON_SELF_CONNECTION = 1; +Blockly.Connection.REASON_WRONG_TYPE = 2; +Blockly.Connection.REASON_TARGET_NULL = 3; +Blockly.Connection.REASON_CHECKS_FAILED = 4; +Blockly.Connection.REASON_DIFFERENT_WORKSPACES = 5; +Blockly.Connection.REASON_SHADOW_PARENT = 6; + +/** + * Connection this connection connects to. Null if not connected. + * @type {Blockly.Connection} + */ +Blockly.Connection.prototype.targetConnection = null; + +/** + * List of compatible value types. Null if all types are compatible. + * @type {Array} + * @private + */ +Blockly.Connection.prototype.check_ = null; + +/** + * DOM representation of a shadow block, or null if none. + * @type {Element} + * @private + */ +Blockly.Connection.prototype.shadowDom_ = null; + +/** + * Horizontal location of this connection. + * @type {number} + * @private + */ +Blockly.Connection.prototype.x_ = 0; + +/** + * Vertical location of this connection. + * @type {number} + * @private + */ +Blockly.Connection.prototype.y_ = 0; + +/** + * Has this connection been added to the connection database? + * @type {boolean} + * @private + */ +Blockly.Connection.prototype.inDB_ = false; + +/** + * Connection database for connections of this type on the current workspace. + * @type {Blockly.ConnectionDB} + * @private + */ +Blockly.Connection.prototype.db_ = null; + +/** + * Connection database for connections compatible with this type on the + * current workspace. + * @type {Blockly.ConnectionDB} + * @private + */ +Blockly.Connection.prototype.dbOpposite_ = null; + +/** + * Whether this connections is hidden (not tracked in a database) or not. + * @type {boolean} + * @private + */ +Blockly.Connection.prototype.hidden_ = null; + +/** + * Connect two connections together. This is the connection on the superior + * block. + * @param {!Blockly.Connection} childConnection Connection on inferior block. + * @private + */ +Blockly.Connection.prototype.connect_ = function(childConnection) { + var parentConnection = this; + var parentBlock = parentConnection.getSourceBlock(); + var childBlock = childConnection.getSourceBlock(); + // Disconnect any existing parent on the child connection. + if (childConnection.isConnected()) { + childConnection.disconnect(); + } + if (parentConnection.isConnected()) { + // Other connection is already connected to something. + // Disconnect it and reattach it or bump it as needed. + var orphanBlock = parentConnection.targetBlock(); + var shadowDom = parentConnection.getShadowDom(); + // Temporarily set the shadow DOM to null so it does not respawn. + parentConnection.setShadowDom(null); + // Displaced shadow blocks dissolve rather than reattaching or bumping. + if (orphanBlock.isShadow()) { + // Save the shadow block so that field values are preserved. + shadowDom = Blockly.Xml.blockToDom(orphanBlock); + orphanBlock.dispose(); + orphanBlock = null; + } else if (parentConnection.type == Blockly.INPUT_VALUE) { + // Value connections. + // If female block is already connected, disconnect and bump the male. + if (!orphanBlock.outputConnection) { + throw 'Orphan block does not have an output connection.'; + } + // Attempt to reattach the orphan at the end of the newly inserted + // block. Since this block may be a row, walk down to the end + // or to the first (and only) shadow block. + var connection = Blockly.Connection.lastConnectionInRow_( + childBlock, orphanBlock); + if (connection) { + orphanBlock.outputConnection.connect(connection); + orphanBlock = null; + } + } else if (parentConnection.type == Blockly.NEXT_STATEMENT) { + // Statement connections. + // Statement blocks may be inserted into the middle of a stack. + // Split the stack. + if (!orphanBlock.previousConnection) { + throw 'Orphan block does not have a previous connection.'; + } + // Attempt to reattach the orphan at the bottom of the newly inserted + // block. Since this block may be a stack, walk down to the end. + var newBlock = childBlock; + while (newBlock.nextConnection) { + var nextBlock = newBlock.getNextBlock(); + if (nextBlock && !nextBlock.isShadow()) { + newBlock = nextBlock; + } else { + if (orphanBlock.previousConnection.checkType_( + newBlock.nextConnection)) { + newBlock.nextConnection.connect(orphanBlock.previousConnection); + orphanBlock = null; + } + break; + } + } + } + if (orphanBlock) { + // Unable to reattach orphan. + parentConnection.disconnect(); + if (Blockly.Events.recordUndo) { + // Bump it off to the side after a moment. + var group = Blockly.Events.getGroup(); + setTimeout(function() { + // Verify orphan hasn't been deleted or reconnected (user on meth). + if (orphanBlock.workspace && !orphanBlock.getParent()) { + Blockly.Events.setGroup(group); + if (orphanBlock.outputConnection) { + orphanBlock.outputConnection.bumpAwayFrom_(parentConnection); + } else if (orphanBlock.previousConnection) { + orphanBlock.previousConnection.bumpAwayFrom_(parentConnection); + } + Blockly.Events.setGroup(false); + } + }, Blockly.BUMP_DELAY); + } + } + // Restore the shadow DOM. + parentConnection.setShadowDom(shadowDom); + } + + var event; + if (Blockly.Events.isEnabled()) { + event = new Blockly.Events.Move(childBlock); + } + // Establish the connections. + Blockly.Connection.connectReciprocally_(parentConnection, childConnection); + // Demote the inferior block so that one is a child of the superior one. + childBlock.setParent(parentBlock); + if (event) { + event.recordNew(); + Blockly.Events.fire(event); + } +}; + +/** + * Sever all links to this connection (not including from the source object). + */ +Blockly.Connection.prototype.dispose = function() { + if (this.isConnected()) { + throw 'Disconnect connection before disposing of it.'; + } + if (this.inDB_) { + this.db_.removeConnection_(this); + } + if (Blockly.highlightedConnection_ == this) { + Blockly.highlightedConnection_ = null; + } + if (Blockly.localConnection_ == this) { + Blockly.localConnection_ = null; + } + this.db_ = null; + this.dbOpposite_ = null; +}; + +/** + * Get the source block for this connection. + * @return {Blockly.Block} The source block, or null if there is none. + */ +Blockly.Connection.prototype.getSourceBlock = function() { + return this.sourceBlock_; +}; + +/** + * Does the connection belong to a superior block (higher in the source stack)? + * @return {boolean} True if connection faces down or right. + */ +Blockly.Connection.prototype.isSuperior = function() { + return this.type == Blockly.INPUT_VALUE || + this.type == Blockly.NEXT_STATEMENT; +}; + +/** + * Is the connection connected? + * @return {boolean} True if connection is connected to another connection. + */ +Blockly.Connection.prototype.isConnected = function() { + return !!this.targetConnection; +}; + +/** + * Checks whether the current connection can connect with the target + * connection. + * @param {Blockly.Connection} target Connection to check compatibility with. + * @return {number} Blockly.Connection.CAN_CONNECT if the connection is legal, + * an error code otherwise. + * @private + */ +Blockly.Connection.prototype.canConnectWithReason_ = function(target) { + if (!target) { + return Blockly.Connection.REASON_TARGET_NULL; + } + if (this.isSuperior()) { + var blockA = this.sourceBlock_; + var blockB = target.getSourceBlock(); + } else { + var blockB = this.sourceBlock_; + var blockA = target.getSourceBlock(); + } + if (blockA && blockA == blockB) { + return Blockly.Connection.REASON_SELF_CONNECTION; + } else if (target.type != Blockly.OPPOSITE_TYPE[this.type]) { + return Blockly.Connection.REASON_WRONG_TYPE; + } else if (blockA && blockB && blockA.workspace !== blockB.workspace) { + return Blockly.Connection.REASON_DIFFERENT_WORKSPACES; + } else if (!this.checkType_(target)) { + return Blockly.Connection.REASON_CHECKS_FAILED; + } else if (blockA.isShadow() && !blockB.isShadow()) { + return Blockly.Connection.REASON_SHADOW_PARENT; + } + return Blockly.Connection.CAN_CONNECT; +}; + +/** + * Checks whether the current connection and target connection are compatible + * and throws an exception if they are not. + * @param {Blockly.Connection} target The connection to check compatibility + * with. + * @private + */ +Blockly.Connection.prototype.checkConnection_ = function(target) { + switch (this.canConnectWithReason_(target)) { + case Blockly.Connection.CAN_CONNECT: + break; + case Blockly.Connection.REASON_SELF_CONNECTION: + throw 'Attempted to connect a block to itself.'; + case Blockly.Connection.REASON_DIFFERENT_WORKSPACES: + // Usually this means one block has been deleted. + throw 'Blocks not on same workspace.'; + case Blockly.Connection.REASON_WRONG_TYPE: + throw 'Attempt to connect incompatible types.'; + case Blockly.Connection.REASON_TARGET_NULL: + throw 'Target connection is null.'; + case Blockly.Connection.REASON_CHECKS_FAILED: + throw 'Connection checks failed.'; + case Blockly.Connection.REASON_SHADOW_PARENT: + throw 'Connecting non-shadow to shadow block.'; + default: + throw 'Unknown connection failure: this should never happen!'; + } +}; + +/** + * Check if the two connections can be dragged to connect to each other. + * @param {!Blockly.Connection} candidate A nearby connection to check. + * @return {boolean} True if the connection is allowed, false otherwise. + */ +Blockly.Connection.prototype.isConnectionAllowed = function(candidate) { + // Type checking. + var canConnect = this.canConnectWithReason_(candidate); + if (canConnect != Blockly.Connection.CAN_CONNECT) { + return false; + } + + // Don't offer to connect an already connected left (male) value plug to + // an available right (female) value plug. Don't offer to connect the + // bottom of a statement block to one that's already connected. + if (candidate.type == Blockly.OUTPUT_VALUE || + candidate.type == Blockly.PREVIOUS_STATEMENT) { + if (candidate.isConnected() || this.isConnected()) { + return false; + } + } + + // Offering to connect the left (male) of a value block to an already + // connected value pair is ok, we'll splice it in. + // However, don't offer to splice into an immovable block. + if (candidate.type == Blockly.INPUT_VALUE && candidate.isConnected() && + !candidate.targetBlock().isMovable() && + !candidate.targetBlock().isShadow()) { + return false; + } + + // Don't let a block with no next connection bump other blocks out of the + // stack. But covering up a shadow block or stack of shadow blocks is fine. + // Similarly, replacing a terminal statement with another terminal statement + // is allowed. + if (this.type == Blockly.PREVIOUS_STATEMENT && + candidate.isConnected() && + !this.sourceBlock_.nextConnection && + !candidate.targetBlock().isShadow() && + candidate.targetBlock().nextConnection) { + return false; + } + + // Don't let blocks try to connect to themselves or ones they nest. + if (Blockly.draggingConnections_.indexOf(candidate) != -1) { + return false; + } + + return true; +}; + +/** + * Connect this connection to another connection. + * @param {!Blockly.Connection} otherConnection Connection to connect to. + */ +Blockly.Connection.prototype.connect = function(otherConnection) { + if (this.targetConnection == otherConnection) { + // Already connected together. NOP. + return; + } + this.checkConnection_(otherConnection); + // Determine which block is superior (higher in the source stack). + if (this.isSuperior()) { + // Superior block. + this.connect_(otherConnection); + } else { + // Inferior block. + otherConnection.connect_(this); + } +}; + +/** + * Update two connections to target each other. + * @param {Blockly.Connection} first The first connection to update. + * @param {Blockly.Connection} second The second conneciton to update. + * @private + */ +Blockly.Connection.connectReciprocally_ = function(first, second) { + goog.asserts.assert(first && second, 'Cannot connect null connections.'); + first.targetConnection = second; + second.targetConnection = first; +}; + +/** + * Does the given block have one and only one connection point that will accept + * an orphaned block? + * @param {!Blockly.Block} block The superior block. + * @param {!Blockly.Block} orphanBlock The inferior block. + * @return {Blockly.Connection} The suitable connection point on 'block', + * or null. + * @private + */ +Blockly.Connection.singleConnection_ = function(block, orphanBlock) { + var connection = false; + for (var i = 0; i < block.inputList.length; i++) { + var thisConnection = block.inputList[i].connection; + if (thisConnection && thisConnection.type == Blockly.INPUT_VALUE && + orphanBlock.outputConnection.checkType_(thisConnection)) { + if (connection) { + return null; // More than one connection. + } + connection = thisConnection; + } + } + return connection; +}; + +/** + * Walks down a row a blocks, at each stage checking if there are any + * connections that will accept the orphaned block. If at any point there + * are zero or multiple eligible connections, returns null. Otherwise + * returns the only input on the last block in the chain. + * Terminates early for shadow blocks. + * @param {!Blockly.Block} startBlock The block on which to start the search. + * @param {!Blockly.Block} orphanBlock The block that is looking for a home. + * @return {Blockly.Connection} The suitable connection point on the chain + * of blocks, or null. + * @private + */ +Blockly.Connection.lastConnectionInRow_ = function(startBlock, orphanBlock) { + var newBlock = startBlock; + var connection; + while (connection = Blockly.Connection.singleConnection_( + /** @type {!Blockly.Block} */ (newBlock), orphanBlock)) { + // '=' is intentional in line above. + newBlock = connection.targetBlock(); + if (!newBlock || newBlock.isShadow()) { + return connection; + } + } + return null; +}; + +/** + * Disconnect this connection. + */ +Blockly.Connection.prototype.disconnect = function() { + var otherConnection = this.targetConnection; + goog.asserts.assert(otherConnection, 'Source connection not connected.'); + goog.asserts.assert(otherConnection.targetConnection == this, + 'Target connection not connected to source connection.'); + + var parentBlock, childBlock, parentConnection; + if (this.isSuperior()) { + // Superior block. + parentBlock = this.sourceBlock_; + childBlock = otherConnection.getSourceBlock(); + parentConnection = this; + } else { + // Inferior block. + parentBlock = otherConnection.getSourceBlock(); + childBlock = this.sourceBlock_; + parentConnection = otherConnection; + } + this.disconnectInternal_(parentBlock, childBlock); + parentConnection.respawnShadow_(); +}; + +/** + * Disconnect two blocks that are connected by this connection. + * @param {!Blockly.Block} parentBlock The superior block. + * @param {!Blockly.Block} childBlock The inferior block. + * @private + */ +Blockly.Connection.prototype.disconnectInternal_ = function(parentBlock, + childBlock) { + var event; + if (Blockly.Events.isEnabled()) { + event = new Blockly.Events.Move(childBlock); + } + var otherConnection = this.targetConnection; + otherConnection.targetConnection = null; + this.targetConnection = null; + childBlock.setParent(null); + if (event) { + event.recordNew(); + Blockly.Events.fire(event); + } +}; + +/** + * Respawn the shadow block if there was one connected to the this connection. + * @private + */ +Blockly.Connection.prototype.respawnShadow_ = function() { + var parentBlock = this.getSourceBlock(); + var shadow = this.getShadowDom(); + if (parentBlock.workspace && shadow && Blockly.Events.recordUndo) { + var blockShadow = + Blockly.Xml.domToBlock(shadow, parentBlock.workspace); + if (blockShadow.outputConnection) { + this.connect(blockShadow.outputConnection); + } else if (blockShadow.previousConnection) { + this.connect(blockShadow.previousConnection); + } else { + throw 'Child block does not have output or previous statement.'; + } + } +}; + +/** + * Returns the block that this connection connects to. + * @return {Blockly.Block} The connected block or null if none is connected. + */ +Blockly.Connection.prototype.targetBlock = function() { + if (this.isConnected()) { + return this.targetConnection.getSourceBlock(); + } + return null; +}; + +/** + * Is this connection compatible with another connection with respect to the + * value type system. E.g. square_root("Hello") is not compatible. + * @param {!Blockly.Connection} otherConnection Connection to compare against. + * @return {boolean} True if the connections share a type. + * @private + */ +Blockly.Connection.prototype.checkType_ = function(otherConnection) { + if (!this.check_ || !otherConnection.check_) { + // One or both sides are promiscuous enough that anything will fit. + return true; + } + // Find any intersection in the check lists. + for (var i = 0; i < this.check_.length; i++) { + if (otherConnection.check_.indexOf(this.check_[i]) != -1) { + return true; + } + } + // No intersection. + return false; +}; + +/** + * Change a connection's compatibility. + * @param {*} check Compatible value type or list of value types. + * Null if all types are compatible. + * @return {!Blockly.Connection} The connection being modified + * (to allow chaining). + */ +Blockly.Connection.prototype.setCheck = function(check) { + if (check) { + // Ensure that check is in an array. + if (!goog.isArray(check)) { + check = [check]; + } + this.check_ = check; + // The new value type may not be compatible with the existing connection. + if (this.isConnected() && !this.checkType_(this.targetConnection)) { + var child = this.isSuperior() ? this.targetBlock() : this.sourceBlock_; + child.unplug(); + // Bump away. + this.sourceBlock_.bumpNeighbours_(); + } + } else { + this.check_ = null; + } + return this; +}; + +/** + * Change a connection's shadow block. + * @param {Element} shadow DOM representation of a block or null. + */ +Blockly.Connection.prototype.setShadowDom = function(shadow) { + this.shadowDom_ = shadow; +}; + +/** + * Return a connection's shadow block. + * @return {Element} shadow DOM representation of a block or null. + */ +Blockly.Connection.prototype.getShadowDom = function() { + return this.shadowDom_; +}; diff --git a/src/blockly/core/connection_db.js b/src/blockly/core/connection_db.js new file mode 100644 index 0000000..8b3c300 --- /dev/null +++ b/src/blockly/core/connection_db.js @@ -0,0 +1,301 @@ +/** + * @license + * Visual Blocks Editor + * + * Copyright 2011 Google Inc. + * https://developers.google.com/blockly/ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Components for managing connections between blocks. + * @author fraser@google.com (Neil Fraser) + */ +'use strict'; + +goog.provide('Blockly.ConnectionDB'); + +goog.require('Blockly.Connection'); + + +/** + * Database of connections. + * Connections are stored in order of their vertical component. This way + * connections in an area may be looked up quickly using a binary search. + * @constructor + */ +Blockly.ConnectionDB = function() { +}; + +Blockly.ConnectionDB.prototype = new Array(); +/** + * Don't inherit the constructor from Array. + * @type {!Function} + */ +Blockly.ConnectionDB.constructor = Blockly.ConnectionDB; + +/** + * Add a connection to the database. Must not already exist in DB. + * @param {!Blockly.Connection} connection The connection to be added. + */ +Blockly.ConnectionDB.prototype.addConnection = function(connection) { + if (connection.inDB_) { + throw 'Connection already in database.'; + } + if (connection.getSourceBlock().isInFlyout) { + // Don't bother maintaining a database of connections in a flyout. + return; + } + var position = this.findPositionForConnection_(connection); + this.splice(position, 0, connection); + connection.inDB_ = true; +}; + +/** + * Find the given connection. + * Starts by doing a binary search to find the approximate location, then + * linearly searches nearby for the exact connection. + * @param {!Blockly.Connection} conn The connection to find. + * @return {number} The index of the connection, or -1 if the connection was + * not found. + */ +Blockly.ConnectionDB.prototype.findConnection = function(conn) { + if (!this.length) { + return -1; + } + + var bestGuess = this.findPositionForConnection_(conn); + if (bestGuess >= this.length) { + // Not in list + return -1; + } + + var yPos = conn.y_; + // Walk forward and back on the y axis looking for the connection. + var pointerMin = bestGuess; + var pointerMax = bestGuess; + while (pointerMin >= 0 && this[pointerMin].y_ == yPos) { + if (this[pointerMin] == conn) { + return pointerMin; + } + pointerMin--; + } + + while (pointerMax < this.length && this[pointerMax].y_ == yPos) { + if (this[pointerMax] == conn) { + return pointerMax; + } + pointerMax++; + } + return -1; +}; + +/** + * Finds a candidate position for inserting this connection into the list. + * This will be in the correct y order but makes no guarantees about ordering in + * the x axis. + * @param {!Blockly.Connection} connection The connection to insert. + * @return {number} The candidate index. + * @private + */ +Blockly.ConnectionDB.prototype.findPositionForConnection_ = + function(connection) { + /* eslint-disable indent */ + if (!this.length) { + return 0; + } + var pointerMin = 0; + var pointerMax = this.length; + while (pointerMin < pointerMax) { + var pointerMid = Math.floor((pointerMin + pointerMax) / 2); + if (this[pointerMid].y_ < connection.y_) { + pointerMin = pointerMid + 1; + } else if (this[pointerMid].y_ > connection.y_) { + pointerMax = pointerMid; + } else { + pointerMin = pointerMid; + break; + } + } + return pointerMin; +}; /* eslint-enable indent */ + +/** + * Remove a connection from the database. Must already exist in DB. + * @param {!Blockly.Connection} connection The connection to be removed. + * @private + */ +Blockly.ConnectionDB.prototype.removeConnection_ = function(connection) { + if (!connection.inDB_) { + throw 'Connection not in database.'; + } + var removalIndex = this.findConnection(connection); + if (removalIndex == -1) { + throw 'Unable to find connection in connectionDB.'; + } + connection.inDB_ = false; + this.splice(removalIndex, 1); +}; + +/** + * Find all nearby connections to the given connection. + * Type checking does not apply, since this function is used for bumping. + * @param {!Blockly.Connection} connection The connection whose neighbours + * should be returned. + * @param {number} maxRadius The maximum radius to another connection. + * @return {!Array.<Blockly.Connection>} List of connections. + */ +Blockly.ConnectionDB.prototype.getNeighbours = function(connection, maxRadius) { + var db = this; + var currentX = connection.x_; + var currentY = connection.y_; + + // Binary search to find the closest y location. + var pointerMin = 0; + var pointerMax = db.length - 2; + var pointerMid = pointerMax; + while (pointerMin < pointerMid) { + if (db[pointerMid].y_ < currentY) { + pointerMin = pointerMid; + } else { + pointerMax = pointerMid; + } + pointerMid = Math.floor((pointerMin + pointerMax) / 2); + } + + var neighbours = []; + /** + * Computes if the current connection is within the allowed radius of another + * connection. + * This function is a closure and has access to outside variables. + * @param {number} yIndex The other connection's index in the database. + * @return {boolean} True if the current connection's vertical distance from + * the other connection is less than the allowed radius. + */ + function checkConnection_(yIndex) { + var dx = currentX - db[yIndex].x_; + var dy = currentY - db[yIndex].y_; + var r = Math.sqrt(dx * dx + dy * dy); + if (r <= maxRadius) { + neighbours.push(db[yIndex]); + } + return dy < maxRadius; + } + + // Walk forward and back on the y axis looking for the closest x,y point. + pointerMin = pointerMid; + pointerMax = pointerMid; + if (db.length) { + while (pointerMin >= 0 && checkConnection_(pointerMin)) { + pointerMin--; + } + do { + pointerMax++; + } while (pointerMax < db.length && checkConnection_(pointerMax)); + } + + return neighbours; +}; + + +/** + * Is the candidate connection close to the reference connection. + * Extremely fast; only looks at Y distance. + * @param {number} index Index in database of candidate connection. + * @param {number} baseY Reference connection's Y value. + * @param {number} maxRadius The maximum radius to another connection. + * @return {boolean} True if connection is in range. + * @private + */ +Blockly.ConnectionDB.prototype.isInYRange_ = function(index, baseY, maxRadius) { + return (Math.abs(this[index].y_ - baseY) <= maxRadius); +}; + +/** + * Find the closest compatible connection to this connection. + * @param {!Blockly.Connection} conn The connection searching for a compatible + * mate. + * @param {number} maxRadius The maximum radius to another connection. + * @param {!goog.math.Coordinate} dxy Offset between this connection's location + * in the database and the current location (as a result of dragging). + * @return {!{connection: ?Blockly.Connection, radius: number}} Contains two + * properties:' connection' which is either another connection or null, + * and 'radius' which is the distance. + */ +Blockly.ConnectionDB.prototype.searchForClosest = function(conn, maxRadius, + dxy) { + // Don't bother. + if (!this.length) { + return {connection: null, radius: maxRadius}; + } + + // Stash the values of x and y from before the drag. + var baseY = conn.y_; + var baseX = conn.x_; + + conn.x_ = baseX + dxy.x; + conn.y_ = baseY + dxy.y; + + // findPositionForConnection finds an index for insertion, which is always + // after any block with the same y index. We want to search both forward + // and back, so search on both sides of the index. + var closestIndex = this.findPositionForConnection_(conn); + + var bestConnection = null; + var bestRadius = maxRadius; + var temp; + + // Walk forward and back on the y axis looking for the closest x,y point. + var pointerMin = closestIndex - 1; + while (pointerMin >= 0 && this.isInYRange_(pointerMin, conn.y_, maxRadius)) { + temp = this[pointerMin]; + if (conn.isConnectionAllowed(temp, bestRadius)) { + bestConnection = temp; + bestRadius = temp.distanceFrom(conn); + } + pointerMin--; + } + + var pointerMax = closestIndex; + while (pointerMax < this.length && this.isInYRange_(pointerMax, conn.y_, + maxRadius)) { + temp = this[pointerMax]; + if (conn.isConnectionAllowed(temp, bestRadius)) { + bestConnection = temp; + bestRadius = temp.distanceFrom(conn); + } + pointerMax++; + } + + // Reset the values of x and y. + conn.x_ = baseX; + conn.y_ = baseY; + + // If there were no valid connections, bestConnection will be null. + return {connection: bestConnection, radius: bestRadius}; +}; + +/** + * Initialize a set of connection DBs for a specified workspace. + * @param {!Blockly.Workspace} workspace The workspace this DB is for. + */ +Blockly.ConnectionDB.init = function(workspace) { + // Create four databases, one for each connection type. + var dbList = []; + dbList[Blockly.INPUT_VALUE] = new Blockly.ConnectionDB(); + dbList[Blockly.OUTPUT_VALUE] = new Blockly.ConnectionDB(); + dbList[Blockly.NEXT_STATEMENT] = new Blockly.ConnectionDB(); + dbList[Blockly.PREVIOUS_STATEMENT] = new Blockly.ConnectionDB(); + workspace.connectionDBList = dbList; +}; diff --git a/src/blockly/core/constants.js b/src/blockly/core/constants.js new file mode 100644 index 0000000..0f327e2 --- /dev/null +++ b/src/blockly/core/constants.js @@ -0,0 +1,202 @@ +/** + * @license + * Visual Blocks Editor + * + * Copyright 2016 Google Inc. + * https://developers.google.com/blockly/ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Blockly constants. + * @author fenichel@google.com (Rachel Fenichel) + */ +'use strict'; + +goog.provide('Blockly.constants'); + + +/** + * Number of pixels the mouse must move before a drag starts. + */ +Blockly.DRAG_RADIUS = 5; + +/** + * Maximum misalignment between connections for them to snap together. + */ +Blockly.SNAP_RADIUS = 20; + +/** + * Delay in ms between trigger and bumping unconnected block out of alignment. + */ +Blockly.BUMP_DELAY = 250; + +/** + * Number of characters to truncate a collapsed block to. + */ +Blockly.COLLAPSE_CHARS = 30; + +/** + * Length in ms for a touch to become a long press. + */ +Blockly.LONGPRESS = 750; + +/** + * Prevent a sound from playing if another sound preceded it within this many + * miliseconds. + */ +Blockly.SOUND_LIMIT = 100; + +/** + * The richness of block colours, regardless of the hue. + * Must be in the range of 0 (inclusive) to 1 (exclusive). + */ +Blockly.HSV_SATURATION = 0.45; + +/** + * The intensity of block colours, regardless of the hue. + * Must be in the range of 0 (inclusive) to 1 (exclusive). + */ +Blockly.HSV_VALUE = 0.65; + +/** + * Sprited icons and images. + */ +Blockly.SPRITE = { + width: 96, + height: 124, + url: 'sprites.png' +}; + +// Constants below this point are not intended to be changed. + +/** + * Required name space for SVG elements. + * @const + */ +Blockly.SVG_NS = 'http://www.w3.org/2000/svg'; + +/** + * Required name space for HTML elements. + * @const + */ +Blockly.HTML_NS = 'http://www.w3.org/1999/xhtml'; + +/** + * ENUM for a right-facing value input. E.g. 'set item to' or 'return'. + * @const + */ +Blockly.INPUT_VALUE = 1; + +/** + * ENUM for a left-facing value output. E.g. 'random fraction'. + * @const + */ +Blockly.OUTPUT_VALUE = 2; + +/** + * ENUM for a down-facing block stack. E.g. 'if-do' or 'else'. + * @const + */ +Blockly.NEXT_STATEMENT = 3; + +/** + * ENUM for an up-facing block stack. E.g. 'break out of loop'. + * @const + */ +Blockly.PREVIOUS_STATEMENT = 4; + +/** + * ENUM for an dummy input. Used to add field(s) with no input. + * @const + */ +Blockly.DUMMY_INPUT = 5; + +/** + * ENUM for left alignment. + * @const + */ +Blockly.ALIGN_LEFT = -1; + +/** + * ENUM for centre alignment. + * @const + */ +Blockly.ALIGN_CENTRE = 0; + +/** + * ENUM for right alignment. + * @const + */ +Blockly.ALIGN_RIGHT = 1; + +/** + * ENUM for no drag operation. + * @const + */ +Blockly.DRAG_NONE = 0; + +/** + * ENUM for inside the sticky DRAG_RADIUS. + * @const + */ +Blockly.DRAG_STICKY = 1; + +/** + * ENUM for inside the non-sticky DRAG_RADIUS, for differentiating between + * clicks and drags. + * @const + */ +Blockly.DRAG_BEGIN = 1; + +/** + * ENUM for freely draggable (outside the DRAG_RADIUS, if one applies). + * @const + */ +Blockly.DRAG_FREE = 2; + +/** + * Lookup table for determining the opposite type of a connection. + * @const + */ +Blockly.OPPOSITE_TYPE = []; +Blockly.OPPOSITE_TYPE[Blockly.INPUT_VALUE] = Blockly.OUTPUT_VALUE; +Blockly.OPPOSITE_TYPE[Blockly.OUTPUT_VALUE] = Blockly.INPUT_VALUE; +Blockly.OPPOSITE_TYPE[Blockly.NEXT_STATEMENT] = Blockly.PREVIOUS_STATEMENT; +Blockly.OPPOSITE_TYPE[Blockly.PREVIOUS_STATEMENT] = Blockly.NEXT_STATEMENT; + + +/** + * ENUM for toolbox and flyout at top of screen. + * @const + */ +Blockly.TOOLBOX_AT_TOP = 0; + +/** + * ENUM for toolbox and flyout at bottom of screen. + * @const + */ +Blockly.TOOLBOX_AT_BOTTOM = 1; + +/** + * ENUM for toolbox and flyout at left of screen. + * @const + */ +Blockly.TOOLBOX_AT_LEFT = 2; + +/** + * ENUM for toolbox and flyout at right of screen. + * @const + */ +Blockly.TOOLBOX_AT_RIGHT = 3; diff --git a/src/blockly/core/contextmenu.js b/src/blockly/core/contextmenu.js new file mode 100644 index 0000000..462ad0b --- /dev/null +++ b/src/blockly/core/contextmenu.js @@ -0,0 +1,148 @@ +/** + * @license + * Visual Blocks Editor + * + * Copyright 2011 Google Inc. + * https://developers.google.com/blockly/ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Functionality for the right-click context menus. + * @author fraser@google.com (Neil Fraser) + */ +'use strict'; + +goog.provide('Blockly.ContextMenu'); + +goog.require('goog.dom'); +goog.require('goog.events'); +goog.require('goog.style'); +goog.require('goog.ui.Menu'); +goog.require('goog.ui.MenuItem'); + + +/** + * Which block is the context menu attached to? + * @type {Blockly.Block} + */ +Blockly.ContextMenu.currentBlock = null; + +/** + * Construct the menu based on the list of options and show the menu. + * @param {!Event} e Mouse event. + * @param {!Array.<!Object>} options Array of menu options. + * @param {boolean} rtl True if RTL, false if LTR. + */ +Blockly.ContextMenu.show = function(e, options, rtl) { + Blockly.WidgetDiv.show(Blockly.ContextMenu, rtl, null); + if (!options.length) { + Blockly.ContextMenu.hide(); + return; + } + /* Here's what one option object looks like: + {text: 'Make It So', + enabled: true, + callback: Blockly.MakeItSo} + */ + var menu = new goog.ui.Menu(); + menu.setRightToLeft(rtl); + for (var i = 0, option; option = options[i]; i++) { + var menuItem = new goog.ui.MenuItem(option.text); + menuItem.setRightToLeft(rtl); + menu.addChild(menuItem, true); + menuItem.setEnabled(option.enabled); + if (option.enabled) { + goog.events.listen(menuItem, goog.ui.Component.EventType.ACTION, + option.callback); + } + } + goog.events.listen(menu, goog.ui.Component.EventType.ACTION, + Blockly.ContextMenu.hide); + // Record windowSize and scrollOffset before adding menu. + var windowSize = goog.dom.getViewportSize(); + var scrollOffset = goog.style.getViewportPageOffset(document); + var div = Blockly.WidgetDiv.DIV; + menu.render(div); + var menuDom = menu.getElement(); + Blockly.addClass_(menuDom, 'blocklyContextMenu'); + // Prevent system context menu when right-clicking a Blockly context menu. + Blockly.bindEvent_(menuDom, 'contextmenu', null, Blockly.noEvent); + // Record menuSize after adding menu. + var menuSize = goog.style.getSize(menuDom); + + // Position the menu. + var x = e.clientX + scrollOffset.x; + var y = e.clientY + scrollOffset.y; + // Flip menu vertically if off the bottom. + if (e.clientY + menuSize.height >= windowSize.height) { + y -= menuSize.height; + } + // Flip menu horizontally if off the edge. + if (rtl) { + if (menuSize.width >= e.clientX) { + x += menuSize.width; + } + } else { + if (e.clientX + menuSize.width >= windowSize.width) { + x -= menuSize.width; + } + } + Blockly.WidgetDiv.position(x, y, windowSize, scrollOffset, rtl); + + menu.setAllowAutoFocus(true); + // 1ms delay is required for focusing on context menus because some other + // mouse event is still waiting in the queue and clears focus. + setTimeout(function() {menuDom.focus();}, 1); + Blockly.ContextMenu.currentBlock = null; // May be set by Blockly.Block. +}; + +/** + * Hide the context menu. + */ +Blockly.ContextMenu.hide = function() { + Blockly.WidgetDiv.hideIfOwner(Blockly.ContextMenu); + Blockly.ContextMenu.currentBlock = null; +}; + +/** + * Create a callback function that creates and configures a block, + * then places the new block next to the original. + * @param {!Blockly.Block} block Original block. + * @param {!Element} xml XML representation of new block. + * @return {!Function} Function that creates a block. + */ +Blockly.ContextMenu.callbackFactory = function(block, xml) { + return function() { + Blockly.Events.disable(); + try { + var newBlock = Blockly.Xml.domToBlock(xml, block.workspace); + // Move the new block next to the old block. + var xy = block.getRelativeToSurfaceXY(); + if (block.RTL) { + xy.x -= Blockly.SNAP_RADIUS; + } else { + xy.x += Blockly.SNAP_RADIUS; + } + xy.y += Blockly.SNAP_RADIUS * 2; + newBlock.moveBy(xy.x, xy.y); + } finally { + Blockly.Events.enable(); + } + if (Blockly.Events.isEnabled() && !newBlock.isShadow()) { + Blockly.Events.fire(new Blockly.Events.Create(newBlock)); + } + newBlock.select(); + }; +}; diff --git a/src/blockly/core/css.js b/src/blockly/core/css.js new file mode 100644 index 0000000..c45ccc9 --- /dev/null +++ b/src/blockly/core/css.js @@ -0,0 +1,782 @@ +/** + * @license + * Visual Blocks Editor + * + * Copyright 2013 Google Inc. + * https://developers.google.com/blockly/ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Inject Blockly's CSS synchronously. + * @author fraser@google.com (Neil Fraser) + */ +'use strict'; + +goog.provide('Blockly.Css'); + + +/** + * List of cursors. + * @enum {string} + */ +Blockly.Css.Cursor = { + OPEN: 'handopen', + CLOSED: 'handclosed', + DELETE: 'handdelete' +}; + +/** + * Current cursor (cached value). + * @type {string} + * @private + */ +Blockly.Css.currentCursor_ = ''; + +/** + * Large stylesheet added by Blockly.Css.inject. + * @type {Element} + * @private + */ +Blockly.Css.styleSheet_ = null; + +/** + * Path to media directory, with any trailing slash removed. + * @type {string} + * @private + */ +Blockly.Css.mediaPath_ = ''; + +/** + * Inject the CSS into the DOM. This is preferable over using a regular CSS + * file since: + * a) It loads synchronously and doesn't force a redraw later. + * b) It speeds up loading by not blocking on a separate HTTP transfer. + * c) The CSS content may be made dynamic depending on init options. + * @param {boolean} hasCss If false, don't inject CSS + * (providing CSS becomes the document's responsibility). + * @param {string} pathToMedia Path from page to the Blockly media directory. + */ +Blockly.Css.inject = function(hasCss, pathToMedia) { + // Only inject the CSS once. + if (Blockly.Css.styleSheet_) { + return; + } + // Placeholder for cursor rule. Must be first rule (index 0). + var text = '.blocklyDraggable {}\n'; + if (hasCss) { + text += Blockly.Css.CONTENT.join('\n'); + if (Blockly.FieldDate) { + text += Blockly.FieldDate.CSS.join('\n'); + } + } + // Strip off any trailing slash (either Unix or Windows). + Blockly.Css.mediaPath_ = pathToMedia.replace(/[\\\/]$/, ''); + text = text.replace(/<<<PATH>>>/g, Blockly.Css.mediaPath_); + // Inject CSS tag. + var cssNode = document.createElement('style'); + document.head.appendChild(cssNode); + var cssTextNode = document.createTextNode(text); + cssNode.appendChild(cssTextNode); + Blockly.Css.styleSheet_ = cssNode.sheet; + Blockly.Css.setCursor(Blockly.Css.Cursor.OPEN); +}; + +/** + * Set the cursor to be displayed when over something draggable. + * @param {Blockly.Css.Cursor} cursor Enum. + */ +Blockly.Css.setCursor = function(cursor) { + if (Blockly.Css.currentCursor_ == cursor) { + return; + } + Blockly.Css.currentCursor_ = cursor; + var url = 'url(' + Blockly.Css.mediaPath_ + '/' + cursor + '.cur), auto'; + // There are potentially hundreds of draggable objects. Changing their style + // properties individually is too slow, so change the CSS rule instead. + var rule = '.blocklyDraggable {\n cursor: ' + url + ';\n}\n'; + Blockly.Css.styleSheet_.deleteRule(0); + Blockly.Css.styleSheet_.insertRule(rule, 0); + // There is probably only one toolbox, so just change its style property. + var toolboxen = document.getElementsByClassName('blocklyToolboxDiv'); + for (var i = 0, toolbox; toolbox = toolboxen[i]; i++) { + if (cursor == Blockly.Css.Cursor.DELETE) { + toolbox.style.cursor = url; + } else { + toolbox.style.cursor = ''; + } + } + // Set cursor on the whole document, so that rapid movements + // don't result in cursor changing to an arrow momentarily. + var html = document.body.parentNode; + if (cursor == Blockly.Css.Cursor.OPEN) { + html.style.cursor = ''; + } else { + html.style.cursor = url; + } +}; + +/** + * Array making up the CSS content for Blockly. + */ +Blockly.Css.CONTENT = [ + '.blocklySvg {', + 'background-color: #fff;', + 'outline: none;', + 'overflow: hidden;', /* IE overflows by default. */ + 'display: block;', + '}', + + '.blocklyWidgetDiv {', + 'display: none;', + 'position: absolute;', + 'z-index: 99999;', /* big value for bootstrap3 compatibility */ + '}', + + '.injectionDiv {', + 'height: 100%;', + 'position: relative;', + '}', + + '.blocklyNonSelectable {', + 'user-select: none;', + '-moz-user-select: none;', + '-webkit-user-select: none;', + '-ms-user-select: none;', + '}', + + '.blocklyTooltipDiv {', + 'background-color: #ffffc7;', + 'border: 1px solid #ddc;', + 'box-shadow: 4px 4px 20px 1px rgba(0,0,0,.15);', + 'color: #000;', + 'display: none;', + 'font-family: sans-serif;', + 'font-size: 9pt;', + 'opacity: 0.9;', + 'padding: 2px;', + 'position: absolute;', + 'z-index: 100000;', /* big value for bootstrap3 compatibility */ + '}', + + '.blocklyResizeSE {', + 'cursor: se-resize;', + 'fill: #aaa;', + '}', + + '.blocklyResizeSW {', + 'cursor: sw-resize;', + 'fill: #aaa;', + '}', + + '.blocklyResizeLine {', + 'stroke: #888;', + 'stroke-width: 1;', + '}', + + '.blocklyHighlightedConnectionPath {', + 'fill: none;', + 'stroke: #fc3;', + 'stroke-width: 4px;', + '}', + + '.blocklyPathLight {', + 'fill: none;', + 'stroke-linecap: round;', + 'stroke-width: 1;', + '}', + + '.blocklySelected>.blocklyPath {', + 'stroke: #fc3;', + 'stroke-width: 3px;', + '}', + + '.blocklySelected>.blocklyPathLight {', + 'display: none;', + '}', + + '.blocklyDragging>.blocklyPath,', + '.blocklyDragging>.blocklyPathLight {', + 'fill-opacity: .8;', + 'stroke-opacity: .8;', + '}', + + '.blocklyDragging>.blocklyPathDark {', + 'display: none;', + '}', + + '.blocklyDisabled>.blocklyPath {', + 'fill-opacity: .5;', + 'stroke-opacity: .5;', + '}', + + '.blocklyDisabled>.blocklyPathLight,', + '.blocklyDisabled>.blocklyPathDark {', + 'display: none;', + '}', + + '.blocklyText {', + 'cursor: default;', + 'fill: #fff;', + 'font-family: sans-serif;', + 'font-size: 11pt;', + '}', + + '.blocklyNonEditableText>text {', + 'pointer-events: none;', + '}', + + '.blocklyNonEditableText>rect,', + '.blocklyEditableText>rect {', + 'fill: #fff;', + 'fill-opacity: .6;', + '}', + + '.blocklyNonEditableText>text,', + '.blocklyEditableText>text {', + 'fill: #000;', + '}', + + '.blocklyEditableText:hover>rect {', + 'stroke: #fff;', + 'stroke-width: 2;', + '}', + + '.blocklyBubbleText {', + 'fill: #000;', + '}', + + '.blocklyFlyoutButton {', + 'fill: #888;', + 'cursor: default', + '}', + + '.blocklyFlyoutButton:hover {', + 'fill: #ccc;', + '}', + + /* + Don't allow users to select text. It gets annoying when trying to + drag a block and selected text moves instead. + */ + '.blocklySvg text {', + 'user-select: none;', + '-moz-user-select: none;', + '-webkit-user-select: none;', + 'cursor: inherit;', + '}', + + '.blocklyHidden {', + 'display: none;', + '}', + + '.blocklyFieldDropdown:not(.blocklyHidden) {', + 'display: block;', + '}', + + '.blocklyIconGroup {', + 'cursor: default;', + '}', + + '.blocklyIconGroup:not(:hover),', + '.blocklyIconGroupReadonly {', + 'opacity: .6;', + '}', + + '.blocklyIconShape {', + 'fill: #00f;', + 'stroke: #fff;', + 'stroke-width: 1px;', + '}', + + '.blocklyIconSymbol {', + 'fill: #fff;', + '}', + + '.blocklyMinimalBody {', + 'margin: 0;', + 'padding: 0;', + '}', + + '.blocklyCommentTextarea {', + 'background-color: #ffc;', + 'border: 0;', + 'margin: 0;', + 'padding: 2px;', + 'resize: none;', + '}', + + '.blocklyHtmlInput {', + 'border: none;', + 'border-radius: 4px;', + 'font-family: sans-serif;', + 'height: 100%;', + 'margin: 0;', + 'outline: none;', + 'padding: 0 1px;', + 'width: 100%', + '}', + + '.blocklyMainBackground {', + 'stroke-width: 1;', + 'stroke: #c6c6c6;', /* Equates to #ddd due to border being off-pixel. */ + '}', + + '.blocklyMutatorBackground {', + 'fill: #fff;', + 'stroke: #ddd;', + 'stroke-width: 1;', + '}', + + '.blocklyFlyoutBackground {', + 'fill: #ddd;', + 'fill-opacity: .8;', + '}', + + '.blocklyScrollbarBackground {', + 'opacity: 0;', + '}', + + '.blocklyScrollbarHandle {', + 'fill: #ccc;', + '}', + + '.blocklyScrollbarBackground:hover+.blocklyScrollbarHandle,', + '.blocklyScrollbarHandle:hover {', + 'fill: #bbb;', + '}', + + '.blocklyZoom>image {', + 'opacity: .4;', + '}', + + '.blocklyZoom>image:hover {', + 'opacity: .6;', + '}', + + '.blocklyZoom>image:active {', + 'opacity: .8;', + '}', + + /* Darken flyout scrollbars due to being on a grey background. */ + /* By contrast, workspace scrollbars are on a white background. */ + '.blocklyFlyout .blocklyScrollbarHandle {', + 'fill: #bbb;', + '}', + + '.blocklyFlyout .blocklyScrollbarBackground:hover+.blocklyScrollbarHandle,', + '.blocklyFlyout .blocklyScrollbarHandle:hover {', + 'fill: #aaa;', + '}', + + '.blocklyInvalidInput {', + 'background: #faa;', + '}', + + '.blocklyAngleCircle {', + 'stroke: #444;', + 'stroke-width: 1;', + 'fill: #ddd;', + 'fill-opacity: .8;', + '}', + + '.blocklyAngleMarks {', + 'stroke: #444;', + 'stroke-width: 1;', + '}', + + '.blocklyAngleGauge {', + 'fill: #f88;', + 'fill-opacity: .8;', + '}', + + '.blocklyAngleLine {', + 'stroke: #f00;', + 'stroke-width: 2;', + 'stroke-linecap: round;', + '}', + + '.blocklyContextMenu {', + 'border-radius: 4px;', + '}', + + '.blocklyDropdownMenu {', + 'padding: 0 !important;', + '}', + + /* Override the default Closure URL. */ + '.blocklyWidgetDiv .goog-option-selected .goog-menuitem-checkbox,', + '.blocklyWidgetDiv .goog-option-selected .goog-menuitem-icon {', + 'background: url(<<<PATH>>>/sprites.png) no-repeat -48px -16px !important;', + '}', + + /* Category tree in Toolbox. */ + '.blocklyToolboxDiv {', + 'background-color: #ddd;', + 'overflow-x: visible;', + 'overflow-y: auto;', + 'position: absolute;', + '}', + + '.blocklyTreeRoot {', + 'padding: 4px 0;', + '}', + + '.blocklyTreeRoot:focus {', + 'outline: none;', + '}', + + '.blocklyTreeRow {', + 'height: 22px;', + 'line-height: 22px;', + 'margin-bottom: 3px;', + 'padding-right: 8px;', + 'white-space: nowrap;', + '}', + + '.blocklyHorizontalTree {', + 'float: left;', + 'margin: 1px 5px 8px 0;', + '}', + + '.blocklyHorizontalTreeRtl {', + 'float: right;', + 'margin: 1px 0 8px 5px;', + '}', + + '.blocklyToolboxDiv[dir="RTL"] .blocklyTreeRow {', + 'margin-left: 8px;', + '}', + + '.blocklyTreeRow:not(.blocklyTreeSelected):hover {', + 'background-color: #e4e4e4;', + '}', + + '.blocklyTreeSeparator {', + 'border-bottom: solid #e5e5e5 1px;', + 'height: 0;', + 'margin: 5px 0;', + '}', + + '.blocklyTreeSeparatorHorizontal {', + 'border-right: solid #e5e5e5 1px;', + 'width: 0;', + 'padding: 5px 0;', + 'margin: 0 5px;', + '}', + + + '.blocklyTreeIcon {', + 'background-image: url(<<<PATH>>>/sprites.png);', + 'height: 16px;', + 'vertical-align: middle;', + 'width: 16px;', + '}', + + '.blocklyTreeIconClosedLtr {', + 'background-position: -32px -1px;', + '}', + + '.blocklyTreeIconClosedRtl {', + 'background-position: 0px -1px;', + '}', + + '.blocklyTreeIconOpen {', + 'background-position: -16px -1px;', + '}', + + '.blocklyTreeSelected>.blocklyTreeIconClosedLtr {', + 'background-position: -32px -17px;', + '}', + + '.blocklyTreeSelected>.blocklyTreeIconClosedRtl {', + 'background-position: 0px -17px;', + '}', + + '.blocklyTreeSelected>.blocklyTreeIconOpen {', + 'background-position: -16px -17px;', + '}', + + '.blocklyTreeIconNone,', + '.blocklyTreeSelected>.blocklyTreeIconNone {', + 'background-position: -48px -1px;', + '}', + + '.blocklyTreeLabel {', + 'cursor: default;', + 'font-family: sans-serif;', + 'font-size: 16px;', + 'padding: 0 3px;', + 'vertical-align: middle;', + '}', + + '.blocklyTreeSelected .blocklyTreeLabel {', + 'color: #fff;', + '}', + + /* Copied from: goog/css/colorpicker-simplegrid.css */ + /* + * Copyright 2007 The Closure Library Authors. All Rights Reserved. + * + * Use of this source code is governed by the Apache License, Version 2.0. + * See the COPYING file for details. + */ + + /* Author: pupius@google.com (Daniel Pupius) */ + + /* + Styles to make the colorpicker look like the old gmail color picker + NOTE: without CSS scoping this will override styles defined in palette.css + */ + '.blocklyWidgetDiv .goog-palette {', + 'outline: none;', + 'cursor: default;', + '}', + + '.blocklyWidgetDiv .goog-palette-table {', + 'border: 1px solid #666;', + 'border-collapse: collapse;', + '}', + + '.blocklyWidgetDiv .goog-palette-cell {', + 'height: 13px;', + 'width: 15px;', + 'margin: 0;', + 'border: 0;', + 'text-align: center;', + 'vertical-align: middle;', + 'border-right: 1px solid #666;', + 'font-size: 1px;', + '}', + + '.blocklyWidgetDiv .goog-palette-colorswatch {', + 'position: relative;', + 'height: 13px;', + 'width: 15px;', + 'border: 1px solid #666;', + '}', + + '.blocklyWidgetDiv .goog-palette-cell-hover .goog-palette-colorswatch {', + 'border: 1px solid #FFF;', + '}', + + '.blocklyWidgetDiv .goog-palette-cell-selected .goog-palette-colorswatch {', + 'border: 1px solid #000;', + 'color: #fff;', + '}', + + /* Copied from: goog/css/menu.css */ + /* + * Copyright 2009 The Closure Library Authors. All Rights Reserved. + * + * Use of this source code is governed by the Apache License, Version 2.0. + * See the COPYING file for details. + */ + + /** + * Standard styling for menus created by goog.ui.MenuRenderer. + * + * @author attila@google.com (Attila Bodis) + */ + + '.blocklyWidgetDiv .goog-menu {', + 'background: #fff;', + 'border-color: #ccc #666 #666 #ccc;', + 'border-style: solid;', + 'border-width: 1px;', + 'cursor: default;', + 'font: normal 13px Arial, sans-serif;', + 'margin: 0;', + 'outline: none;', + 'padding: 4px 0;', + 'position: absolute;', + 'overflow-y: auto;', + 'overflow-x: hidden;', + 'max-height: 100%;', + 'z-index: 20000;', /* Arbitrary, but some apps depend on it... */ + '}', + + /* Copied from: goog/css/menuitem.css */ + /* + * Copyright 2009 The Closure Library Authors. All Rights Reserved. + * + * Use of this source code is governed by the Apache License, Version 2.0. + * See the COPYING file for details. + */ + + /** + * Standard styling for menus created by goog.ui.MenuItemRenderer. + * + * @author attila@google.com (Attila Bodis) + */ + + /** + * State: resting. + * + * NOTE(mleibman,chrishenry): + * The RTL support in Closure is provided via two mechanisms -- "rtl" CSS + * classes and BiDi flipping done by the CSS compiler. Closure supports RTL + * with or without the use of the CSS compiler. In order for them not + * to conflict with each other, the "rtl" CSS classes need to have the #noflip + * annotation. The non-rtl counterparts should ideally have them as well, but, + * since .goog-menuitem existed without .goog-menuitem-rtl for so long before + * being added, there is a risk of people having templates where they are not + * rendering the .goog-menuitem-rtl class when in RTL and instead rely solely + * on the BiDi flipping by the CSS compiler. That's why we're not adding the + * #noflip to .goog-menuitem. + */ + '.blocklyWidgetDiv .goog-menuitem {', + 'color: #000;', + 'font: normal 13px Arial, sans-serif;', + 'list-style: none;', + 'margin: 0;', + /* 28px on the left for icon or checkbox; 7em on the right for shortcut. */ + 'padding: 4px 7em 4px 28px;', + 'white-space: nowrap;', + '}', + + /* BiDi override for the resting state. */ + /* #noflip */ + '.blocklyWidgetDiv .goog-menuitem.goog-menuitem-rtl {', + /* Flip left/right padding for BiDi. */ + 'padding-left: 7em;', + 'padding-right: 28px;', + '}', + + /* If a menu doesn't have checkable items or items with icons, remove padding. */ + '.blocklyWidgetDiv .goog-menu-nocheckbox .goog-menuitem,', + '.blocklyWidgetDiv .goog-menu-noicon .goog-menuitem {', + 'padding-left: 12px;', + '}', + + /* + * If a menu doesn't have items with shortcuts, leave just enough room for + * submenu arrows, if they are rendered. + */ + '.blocklyWidgetDiv .goog-menu-noaccel .goog-menuitem {', + 'padding-right: 20px;', + '}', + + '.blocklyWidgetDiv .goog-menuitem-content {', + 'color: #000;', + 'font: normal 13px Arial, sans-serif;', + '}', + + /* State: disabled. */ + '.blocklyWidgetDiv .goog-menuitem-disabled .goog-menuitem-accel,', + '.blocklyWidgetDiv .goog-menuitem-disabled .goog-menuitem-content {', + 'color: #ccc !important;', + '}', + + '.blocklyWidgetDiv .goog-menuitem-disabled .goog-menuitem-icon {', + 'opacity: 0.3;', + '-moz-opacity: 0.3;', + 'filter: alpha(opacity=30);', + '}', + + /* State: hover. */ + '.blocklyWidgetDiv .goog-menuitem-highlight,', + '.blocklyWidgetDiv .goog-menuitem-hover {', + 'background-color: #d6e9f8;', + /* Use an explicit top and bottom border so that the selection is visible', + * in high contrast mode. */ + 'border-color: #d6e9f8;', + 'border-style: dotted;', + 'border-width: 1px 0;', + 'padding-bottom: 3px;', + 'padding-top: 3px;', + '}', + + /* State: selected/checked. */ + '.blocklyWidgetDiv .goog-menuitem-checkbox,', + '.blocklyWidgetDiv .goog-menuitem-icon {', + 'background-repeat: no-repeat;', + 'height: 16px;', + 'left: 6px;', + 'position: absolute;', + 'right: auto;', + 'vertical-align: middle;', + 'width: 16px;', + '}', + + /* BiDi override for the selected/checked state. */ + /* #noflip */ + '.blocklyWidgetDiv .goog-menuitem-rtl .goog-menuitem-checkbox,', + '.blocklyWidgetDiv .goog-menuitem-rtl .goog-menuitem-icon {', + /* Flip left/right positioning. */ + 'left: auto;', + 'right: 6px;', + '}', + + '.blocklyWidgetDiv .goog-option-selected .goog-menuitem-checkbox,', + '.blocklyWidgetDiv .goog-option-selected .goog-menuitem-icon {', + /* Client apps may override the URL at which they serve the sprite. */ + 'background: url(//ssl.gstatic.com/editor/editortoolbar.png) no-repeat -512px 0;', + '}', + + /* Keyboard shortcut ("accelerator") style. */ + '.blocklyWidgetDiv .goog-menuitem-accel {', + 'color: #999;', + /* Keyboard shortcuts are untranslated; always left-to-right. */ + /* #noflip */ + 'direction: ltr;', + 'left: auto;', + 'padding: 0 6px;', + 'position: absolute;', + 'right: 0;', + 'text-align: right;', + '}', + + /* BiDi override for shortcut style. */ + /* #noflip */ + '.blocklyWidgetDiv .goog-menuitem-rtl .goog-menuitem-accel {', + /* Flip left/right positioning and text alignment. */ + 'left: 0;', + 'right: auto;', + 'text-align: left;', + '}', + + /* Mnemonic styles. */ + '.blocklyWidgetDiv .goog-menuitem-mnemonic-hint {', + 'text-decoration: underline;', + '}', + + '.blocklyWidgetDiv .goog-menuitem-mnemonic-separator {', + 'color: #999;', + 'font-size: 12px;', + 'padding-left: 4px;', + '}', + + /* Copied from: goog/css/menuseparator.css */ + /* + * Copyright 2009 The Closure Library Authors. All Rights Reserved. + * + * Use of this source code is governed by the Apache License, Version 2.0. + * See the COPYING file for details. + */ + + /** + * Standard styling for menus created by goog.ui.MenuSeparatorRenderer. + * + * @author attila@google.com (Attila Bodis) + */ + + '.blocklyWidgetDiv .goog-menuseparator {', + 'border-top: 1px solid #ccc;', + 'margin: 4px 0;', + 'padding: 0;', + '}', + + '' +]; diff --git a/src/blockly/core/events.js b/src/blockly/core/events.js new file mode 100644 index 0000000..1d1e2b7 --- /dev/null +++ b/src/blockly/core/events.js @@ -0,0 +1,818 @@ +/** + * @license + * Visual Blocks Editor + * + * Copyright 2016 Google Inc. + * https://developers.google.com/blockly/ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Events fired as a result of actions in Blockly's editor. + * @author fraser@google.com (Neil Fraser) + */ +'use strict'; + +goog.provide('Blockly.Events'); + +goog.require('goog.math.Coordinate'); + + +/** + * Group ID for new events. Grouped events are indivisible. + * @type {string} + * @private + */ +Blockly.Events.group_ = ''; + +/** + * Sets whether events should be added to the undo stack. + * @type {boolean} + */ +Blockly.Events.recordUndo = true; + +/** + * Allow change events to be created and fired. + * @type {number} + * @private + */ +Blockly.Events.disabled_ = 0; + +/** + * Name of event that creates a block. + * @const + */ +Blockly.Events.CREATE = 'create'; + +/** + * Name of event that deletes a block. + * @const + */ +Blockly.Events.DELETE = 'delete'; + +/** + * Name of event that changes a block. + * @const + */ +Blockly.Events.CHANGE = 'change'; + +/** + * Name of event that moves a block. + * @const + */ +Blockly.Events.MOVE = 'move'; + +/** + * Name of event that records a UI change. + * @const + */ +Blockly.Events.UI = 'ui'; + +/** + * List of events queued for firing. + * @private + */ +Blockly.Events.FIRE_QUEUE_ = []; + +/** + * Create a custom event and fire it. + * @param {!Blockly.Events.Abstract} event Custom data for event. + */ +Blockly.Events.fire = function(event) { + if (!Blockly.Events.isEnabled()) { + return; + } + if (!Blockly.Events.FIRE_QUEUE_.length) { + // First event added; schedule a firing of the event queue. + setTimeout(Blockly.Events.fireNow_, 0); + } + Blockly.Events.FIRE_QUEUE_.push(event); +}; + +/** + * Fire all queued events. + * @private + */ +Blockly.Events.fireNow_ = function() { + var queue = Blockly.Events.filter(Blockly.Events.FIRE_QUEUE_, true); + Blockly.Events.FIRE_QUEUE_.length = 0; + for (var i = 0, event; event = queue[i]; i++) { + var workspace = Blockly.Workspace.getById(event.workspaceId); + if (workspace) { + workspace.fireChangeListener(event); + } + } +}; + +/** + * Filter the queued events and merge duplicates. + * @param {!Array.<!Blockly.Events.Abstract>} queueIn Array of events. + * @param {boolean} forward True if forward (redo), false if backward (undo). + * @return {!Array.<!Blockly.Events.Abstract>} Array of filtered events. + */ +Blockly.Events.filter = function(queueIn, forward) { + var queue = goog.array.clone(queueIn); + if (!forward) { + // Undo is merged in reverse order. + queue.reverse(); + } + // Merge duplicates. O(n^2), but n should be very small. + for (var i = 0, event1; event1 = queue[i]; i++) { + for (var j = i + 1, event2; event2 = queue[j]; j++) { + if (event1.type == event2.type && + event1.blockId == event2.blockId && + event1.workspaceId == event2.workspaceId) { + if (event1.type == Blockly.Events.MOVE) { + // Merge move events. + event1.newParentId = event2.newParentId; + event1.newInputName = event2.newInputName; + event1.newCoordinate = event2.newCoordinate; + queue.splice(j, 1); + j--; + } else if (event1.type == Blockly.Events.CHANGE && + event1.element == event2.element && + event1.name == event2.name) { + // Merge change events. + event1.newValue = event2.newValue; + queue.splice(j, 1); + j--; + } else if (event1.type == Blockly.Events.UI && + event2.element == 'click' && + (event1.element == 'commentOpen' || + event1.element == 'mutatorOpen' || + event1.element == 'warningOpen')) { + // Merge change events. + event1.newValue = event2.newValue; + queue.splice(j, 1); + j--; + } + } + } + } + // Remove null events. + for (var i = queue.length - 1; i >= 0; i--) { + if (queue[i].isNull()) { + queue.splice(i, 1); + } + } + if (!forward) { + // Restore undo order. + queue.reverse(); + } + // Move mutation events to the top of the queue. + // Intentionally skip first event. + for (var i = 1, event; event = queue[i]; i++) { + if (event.type == Blockly.Events.CHANGE && + event.element == 'mutation') { + queue.unshift(queue.splice(i, 1)[0]); + } + } + return queue; +}; + +/** + * Modify pending undo events so that when they are fired they don't land + * in the undo stack. Called by Blockly.Workspace.clearUndo. + */ +Blockly.Events.clearPendingUndo = function() { + for (var i = 0, event; event = Blockly.Events.FIRE_QUEUE_[i]; i++) { + event.recordUndo = false; + } +}; + +/** + * Stop sending events. Every call to this function MUST also call enable. + */ +Blockly.Events.disable = function() { + Blockly.Events.disabled_++; +}; + +/** + * Start sending events. Unless events were already disabled when the + * corresponding call to disable was made. + */ +Blockly.Events.enable = function() { + Blockly.Events.disabled_--; +}; + +/** + * Returns whether events may be fired or not. + * @return {boolean} True if enabled. + */ +Blockly.Events.isEnabled = function() { + return Blockly.Events.disabled_ == 0; +}; + +/** + * Current group. + * @return {string} ID string. + */ +Blockly.Events.getGroup = function() { + return Blockly.Events.group_; +}; + +/** + * Start or stop a group. + * @param {boolean|string} state True to start new group, false to end group. + * String to set group explicitly. + */ +Blockly.Events.setGroup = function(state) { + if (typeof state == 'boolean') { + Blockly.Events.group_ = state ? Blockly.genUid() : ''; + } else { + Blockly.Events.group_ = state; + } +}; + +/** + * Compute a list of the IDs of the specified block and all its descendants. + * @param {!Blockly.Block} block The root block. + * @return {!Array.<string>} List of block IDs. + * @private + */ +Blockly.Events.getDescendantIds_ = function(block) { + var ids = []; + var descendants = block.getDescendants(); + for (var i = 0, descendant; descendant = descendants[i]; i++) { + ids[i] = descendant.id; + } + return ids; +}; + +/** + * Decode the JSON into an event. + * @param {!Object} json JSON representation. + * @param {!Blockly.Workspace} workspace Target workspace for event. + * @return {!Blockly.Events.Abstract} The event represented by the JSON. + */ +Blockly.Events.fromJson = function(json, workspace) { + var event; + switch (json.type) { + case Blockly.Events.CREATE: + event = new Blockly.Events.Create(null); + break; + case Blockly.Events.DELETE: + event = new Blockly.Events.Delete(null); + break; + case Blockly.Events.CHANGE: + event = new Blockly.Events.Change(null); + break; + case Blockly.Events.MOVE: + event = new Blockly.Events.Move(null); + break; + case Blockly.Events.UI: + event = new Blockly.Events.Ui(null); + break; + default: + throw 'Unknown event type.'; + } + event.fromJson(json); + event.workspaceId = workspace.id; + return event; +}; + +/** + * Abstract class for an event. + * @param {Blockly.Block} block The block. + * @constructor + */ +Blockly.Events.Abstract = function(block) { + if (block) { + this.blockId = block.id; + this.workspaceId = block.workspace.id; + } + this.group = Blockly.Events.group_; + this.recordUndo = Blockly.Events.recordUndo; +}; + +/** + * Encode the event as JSON. + * @return {!Object} JSON representation. + */ +Blockly.Events.Abstract.prototype.toJson = function() { + var json = { + 'type': this.type, + }; + if (this.blockId) { + json['blockId'] = this.blockId; + } + if (this.group) { + json['group'] = this.group; + } + return json; +}; + +/** + * Decode the JSON event. + * @param {!Object} json JSON representation. + */ +Blockly.Events.Abstract.prototype.fromJson = function(json) { + this.blockId = json['blockId']; + this.group = json['group']; +}; + +/** + * Does this event record any change of state? + * @return {boolean} True if null, false if something changed. + */ +Blockly.Events.Abstract.prototype.isNull = function() { + return false; +}; + +/** + * Run an event. + * @param {boolean} forward True if run forward, false if run backward (undo). + */ +Blockly.Events.Abstract.prototype.run = function(forward) { + // Defined by subclasses. +}; + +/** + * Class for a block creation event. + * @param {Blockly.Block} block The created block. Null for a blank event. + * @extends {Blockly.Events.Abstract} + * @constructor + */ +Blockly.Events.Create = function(block) { + if (!block) { + return; // Blank event to be populated by fromJson. + } + Blockly.Events.Create.superClass_.constructor.call(this, block); + this.xml = Blockly.Xml.blockToDomWithXY(block); + this.ids = Blockly.Events.getDescendantIds_(block); +}; +goog.inherits(Blockly.Events.Create, Blockly.Events.Abstract); + +/** + * Type of this event. + * @type {string} + */ +Blockly.Events.Create.prototype.type = Blockly.Events.CREATE; + +/** + * Encode the event as JSON. + * @return {!Object} JSON representation. + */ +Blockly.Events.Create.prototype.toJson = function() { + var json = Blockly.Events.Create.superClass_.toJson.call(this); + json['xml'] = Blockly.Xml.domToText(this.xml); + json['ids'] = this.ids; + return json; +}; + +/** + * Decode the JSON event. + * @param {!Object} json JSON representation. + */ +Blockly.Events.Create.prototype.fromJson = function(json) { + Blockly.Events.Create.superClass_.fromJson.call(this, json); + this.xml = Blockly.Xml.textToDom('<xml>' + json['xml'] + '</xml>').firstChild; + this.ids = json['ids']; +}; + +/** + * Run a creation event. + * @param {boolean} forward True if run forward, false if run backward (undo). + */ +Blockly.Events.Create.prototype.run = function(forward) { + var workspace = Blockly.Workspace.getById(this.workspaceId); + if (forward) { + var xml = goog.dom.createDom('xml'); + xml.appendChild(this.xml); + Blockly.Xml.domToWorkspace(xml, workspace); + } else { + for (var i = 0, id; id = this.ids[i]; i++) { + var block = workspace.getBlockById(id); + if (block) { + block.dispose(false, false); + } else if (id == this.blockId) { + // Only complain about root-level block. + console.warn("Can't uncreate non-existant block: " + id); + } + } + } +}; + +/** + * Class for a block deletion event. + * @param {Blockly.Block} block The deleted block. Null for a blank event. + * @extends {Blockly.Events.Abstract} + * @constructor + */ +Blockly.Events.Delete = function(block) { + if (!block) { + return; // Blank event to be populated by fromJson. + } + if (block.getParent()) { + throw 'Connected blocks cannot be deleted.'; + } + Blockly.Events.Delete.superClass_.constructor.call(this, block); + this.oldXml = Blockly.Xml.blockToDomWithXY(block); + this.ids = Blockly.Events.getDescendantIds_(block); +}; +goog.inherits(Blockly.Events.Delete, Blockly.Events.Abstract); + +/** + * Type of this event. + * @type {string} + */ +Blockly.Events.Delete.prototype.type = Blockly.Events.DELETE; + +/** + * Encode the event as JSON. + * @return {!Object} JSON representation. + */ +Blockly.Events.Delete.prototype.toJson = function() { + var json = Blockly.Events.Delete.superClass_.toJson.call(this); + json['ids'] = this.ids; + return json; +}; + +/** + * Decode the JSON event. + * @param {!Object} json JSON representation. + */ +Blockly.Events.Delete.prototype.fromJson = function(json) { + Blockly.Events.Delete.superClass_.fromJson.call(this, json); + this.ids = json['ids']; +}; + +/** + * Run a deletion event. + * @param {boolean} forward True if run forward, false if run backward (undo). + */ +Blockly.Events.Delete.prototype.run = function(forward) { + var workspace = Blockly.Workspace.getById(this.workspaceId); + if (forward) { + for (var i = 0, id; id = this.ids[i]; i++) { + var block = workspace.getBlockById(id); + if (block) { + block.dispose(false, false); + } else if (id == this.blockId) { + // Only complain about root-level block. + console.warn("Can't delete non-existant block: " + id); + } + } + } else { + var xml = goog.dom.createDom('xml'); + xml.appendChild(this.oldXml); + Blockly.Xml.domToWorkspace(xml, workspace); + } +}; + +/** + * Class for a block change event. + * @param {Blockly.Block} block The changed block. Null for a blank event. + * @param {string} element One of 'field', 'comment', 'disabled', etc. + * @param {?string} name Name of input or field affected, or null. + * @param {string} oldValue Previous value of element. + * @param {string} newValue New value of element. + * @extends {Blockly.Events.Abstract} + * @constructor + */ +Blockly.Events.Change = function(block, element, name, oldValue, newValue) { + if (!block) { + return; // Blank event to be populated by fromJson. + } + Blockly.Events.Change.superClass_.constructor.call(this, block); + this.element = element; + this.name = name; + this.oldValue = oldValue; + this.newValue = newValue; +}; +goog.inherits(Blockly.Events.Change, Blockly.Events.Abstract); + +/** + * Type of this event. + * @type {string} + */ +Blockly.Events.Change.prototype.type = Blockly.Events.CHANGE; + +/** + * Encode the event as JSON. + * @return {!Object} JSON representation. + */ +Blockly.Events.Change.prototype.toJson = function() { + var json = Blockly.Events.Change.superClass_.toJson.call(this); + json['element'] = this.element; + if (this.name) { + json['name'] = this.name; + } + json['newValue'] = this.newValue; + return json; +}; + +/** + * Decode the JSON event. + * @param {!Object} json JSON representation. + */ +Blockly.Events.Change.prototype.fromJson = function(json) { + Blockly.Events.Change.superClass_.fromJson.call(this, json); + this.element = json['element']; + this.name = json['name']; + this.newValue = json['newValue']; +}; + +/** + * Does this event record any change of state? + * @return {boolean} True if something changed. + */ +Blockly.Events.Change.prototype.isNull = function() { + return this.oldValue == this.newValue; +}; + +/** + * Run a change event. + * @param {boolean} forward True if run forward, false if run backward (undo). + */ +Blockly.Events.Change.prototype.run = function(forward) { + var workspace = Blockly.Workspace.getById(this.workspaceId); + var block = workspace.getBlockById(this.blockId); + if (!block) { + console.warn("Can't change non-existant block: " + this.blockId); + return; + } + if (block.mutator) { + // Close the mutator (if open) since we don't want to update it. + block.mutator.setVisible(false); + } + var value = forward ? this.newValue : this.oldValue; + switch (this.element) { + case 'field': + var field = block.getField(this.name); + if (field) { + // Run the validator for any side-effects it may have. + // The validator's opinion on validity is ignored. + field.callValidator(value); + field.setValue(value); + } else { + console.warn("Can't set non-existant field: " + this.name); + } + break; + case 'comment': + block.setCommentText(value || null); + break; + case 'collapsed': + block.setCollapsed(value); + break; + case 'disabled': + block.setDisabled(value); + break; + case 'inline': + block.setInputsInline(value); + break; + case 'mutation': + var oldMutation = ''; + if (block.mutationToDom) { + var oldMutationDom = block.mutationToDom(); + oldMutation = oldMutationDom && Blockly.Xml.domToText(oldMutationDom); + } + if (block.domToMutation) { + value = value || '<mutation></mutation>'; + var dom = Blockly.Xml.textToDom('<xml>' + value + '</xml>'); + block.domToMutation(dom.firstChild); + } + Blockly.Events.fire(new Blockly.Events.Change( + block, 'mutation', null, oldMutation, value)); + break; + default: + console.warn('Unknown change type: ' + this.element); + } +}; + +/** + * Class for a block move event. Created before the move. + * @param {Blockly.Block} block The moved block. Null for a blank event. + * @extends {Blockly.Events.Abstract} + * @constructor + */ +Blockly.Events.Move = function(block) { + if (!block) { + return; // Blank event to be populated by fromJson. + } + Blockly.Events.Move.superClass_.constructor.call(this, block); + var location = this.currentLocation_(); + this.oldParentId = location.parentId; + this.oldInputName = location.inputName; + this.oldCoordinate = location.coordinate; +}; +goog.inherits(Blockly.Events.Move, Blockly.Events.Abstract); + +/** + * Type of this event. + * @type {string} + */ +Blockly.Events.Move.prototype.type = Blockly.Events.MOVE; + +/** + * Encode the event as JSON. + * @return {!Object} JSON representation. + */ +Blockly.Events.Move.prototype.toJson = function() { + var json = Blockly.Events.Move.superClass_.toJson.call(this); + if (this.newParentId) { + json['newParentId'] = this.newParentId; + } + if (this.newInputName) { + json['newInputName'] = this.newInputName; + } + if (this.newCoordinate) { + json['newCoordinate'] = Math.round(this.newCoordinate.x) + ',' + + Math.round(this.newCoordinate.y); + } + return json; +}; + +/** + * Decode the JSON event. + * @param {!Object} json JSON representation. + */ +Blockly.Events.Move.prototype.fromJson = function(json) { + Blockly.Events.Move.superClass_.fromJson.call(this, json); + this.newParentId = json['newParentId']; + this.newInputName = json['newInputName']; + if (json['newCoordinate']) { + var xy = json['newCoordinate'].split(','); + this.newCoordinate = + new goog.math.Coordinate(parseFloat(xy[0]), parseFloat(xy[1])); + } +}; + +/** + * Record the block's new location. Called after the move. + */ +Blockly.Events.Move.prototype.recordNew = function() { + var location = this.currentLocation_(); + this.newParentId = location.parentId; + this.newInputName = location.inputName; + this.newCoordinate = location.coordinate; +}; + +/** + * Returns the parentId and input if the block is connected, + * or the XY location if disconnected. + * @return {!Object} Collection of location info. + * @private + */ +Blockly.Events.Move.prototype.currentLocation_ = function() { + var workspace = Blockly.Workspace.getById(this.workspaceId); + var block = workspace.getBlockById(this.blockId); + var location = {}; + var parent = block.getParent(); + if (parent) { + location.parentId = parent.id; + var input = parent.getInputWithBlock(block); + if (input) { + location.inputName = input.name; + } + } else { + location.coordinate = block.getRelativeToSurfaceXY(); + } + return location; +}; + +/** + * Does this event record any change of state? + * @return {boolean} True if something changed. + */ +Blockly.Events.Move.prototype.isNull = function() { + return this.oldParentId == this.newParentId && + this.oldInputName == this.newInputName && + goog.math.Coordinate.equals(this.oldCoordinate, this.newCoordinate); +}; + +/** + * Run a move event. + * @param {boolean} forward True if run forward, false if run backward (undo). + */ +Blockly.Events.Move.prototype.run = function(forward) { + var workspace = Blockly.Workspace.getById(this.workspaceId); + var block = workspace.getBlockById(this.blockId); + if (!block) { + console.warn("Can't move non-existant block: " + this.blockId); + return; + } + var parentId = forward ? this.newParentId : this.oldParentId; + var inputName = forward ? this.newInputName : this.oldInputName; + var coordinate = forward ? this.newCoordinate : this.oldCoordinate; + var parentBlock = null; + if (parentId) { + parentBlock = workspace.getBlockById(parentId); + if (!parentBlock) { + console.warn("Can't connect to non-existant block: " + parentId); + return; + } + } + if (block.getParent()) { + block.unplug(); + } + if (coordinate) { + var xy = block.getRelativeToSurfaceXY(); + block.moveBy(coordinate.x - xy.x, coordinate.y - xy.y); + } else { + var blockConnection = block.outputConnection || block.previousConnection; + var parentConnection; + if (inputName) { + var input = parentBlock.getInput(inputName); + if (input) { + parentConnection = input.connection; + } + } else if (blockConnection.type == Blockly.PREVIOUS_STATEMENT) { + parentConnection = parentBlock.nextConnection; + } + if (parentConnection) { + blockConnection.connect(parentConnection); + } else { + console.warn("Can't connect to non-existant input: " + inputName); + } + } +}; + +/** + * Class for a UI event. + * @param {Blockly.Block} block The affected block. + * @param {string} element One of 'selected', 'comment', 'mutator', etc. + * @param {string} oldValue Previous value of element. + * @param {string} newValue New value of element. + * @extends {Blockly.Events.Abstract} + * @constructor + */ +Blockly.Events.Ui = function(block, element, oldValue, newValue) { + Blockly.Events.Ui.superClass_.constructor.call(this, block); + this.element = element; + this.oldValue = oldValue; + this.newValue = newValue; + this.recordUndo = false; +}; +goog.inherits(Blockly.Events.Ui, Blockly.Events.Abstract); + +/** + * Type of this event. + * @type {string} + */ +Blockly.Events.Ui.prototype.type = Blockly.Events.UI; + +/** + * Encode the event as JSON. + * @return {!Object} JSON representation. + */ +Blockly.Events.Ui.prototype.toJson = function() { + var json = Blockly.Events.Ui.superClass_.toJson.call(this); + json['element'] = this.element; + if (this.newValue !== undefined) { + json['newValue'] = this.newValue; + } + return json; +}; + +/** + * Decode the JSON event. + * @param {!Object} json JSON representation. + */ +Blockly.Events.Ui.prototype.fromJson = function(json) { + Blockly.Events.Ui.superClass_.fromJson.call(this, json); + this.element = json['element']; + this.newValue = json['newValue']; +}; + +/** + * Enable/disable a block depending on whether it is properly connected. + * Use this on applications where all blocks should be connected to a top block. + * Recommend setting the 'disable' option to 'false' in the config so that + * users don't try to reenable disabled orphan blocks. + * @param {!Blockly.Events.Abstract} event Custom data for event. + */ +Blockly.Events.disableOrphans = function(event) { + if (event.type == Blockly.Events.MOVE || + event.type == Blockly.Events.CREATE) { + Blockly.Events.disable(); + var workspace = Blockly.Workspace.getById(event.workspaceId); + var block = workspace.getBlockById(event.blockId); + if (block) { + if (block.getParent() && !block.getParent().disabled) { + var children = block.getDescendants(); + for (var i = 0, child; child = children[i]; i++) { + child.setDisabled(false); + } + } else if ((block.outputConnection || block.previousConnection) && + Blockly.dragMode_ == Blockly.DRAG_NONE) { + do { + block.setDisabled(true); + block = block.getNextBlock(); + } while (block); + } + } + Blockly.Events.enable(); + } +}; diff --git a/src/blockly/core/field.js b/src/blockly/core/field.js new file mode 100644 index 0000000..131a626 --- /dev/null +++ b/src/blockly/core/field.js @@ -0,0 +1,495 @@ +/** + * @license + * Visual Blocks Editor + * + * Copyright 2012 Google Inc. + * https://developers.google.com/blockly/ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Field. Used for editable titles, variables, etc. + * This is an abstract class that defines the UI on the block. Actual + * instances would be Blockly.FieldTextInput, Blockly.FieldDropdown, etc. + * @author fraser@google.com (Neil Fraser) + */ +'use strict'; + +goog.provide('Blockly.Field'); + +goog.require('goog.asserts'); +goog.require('goog.dom'); +goog.require('goog.math.Size'); +goog.require('goog.style'); +goog.require('goog.userAgent'); + + +/** + * Abstract class for an editable field. + * @param {string} text The initial content of the field. + * @param {Function=} opt_validator An optional function that is called + * to validate any constraints on what the user entered. Takes the new + * text as an argument and returns either the accepted text, a replacement + * text, or null to abort the change. + * @constructor + */ +Blockly.Field = function(text, opt_validator) { + this.size_ = new goog.math.Size(0, 25); + this.setValue(text); + this.setValidator(opt_validator); +}; + +/** + * Temporary cache of text widths. + * @type {Object} + * @private + */ +Blockly.Field.cacheWidths_ = null; + +/** + * Number of current references to cache. + * @type {number} + * @private + */ +Blockly.Field.cacheReference_ = 0; + + +/** + * Name of field. Unique within each block. + * Static labels are usually unnamed. + * @type {string=} + */ +Blockly.Field.prototype.name = undefined; + +/** + * Maximum characters of text to display before adding an ellipsis. + * @type {number} + */ +Blockly.Field.prototype.maxDisplayLength = 50; + +/** + * Visible text to display. + * @type {string} + * @private + */ +Blockly.Field.prototype.text_ = ''; + +/** + * Block this field is attached to. Starts as null, then in set in init. + * @type {Blockly.Block} + * @private + */ +Blockly.Field.prototype.sourceBlock_ = null; + +/** + * Is the field visible, or hidden due to the block being collapsed? + * @type {boolean} + * @private + */ +Blockly.Field.prototype.visible_ = true; + +/** + * Validation function called when user edits an editable field. + * @type {Function} + * @private + */ +Blockly.Field.prototype.validator_ = null; + +/** + * Non-breaking space. + * @const + */ +Blockly.Field.NBSP = '\u00A0'; + +/** + * Editable fields are saved by the XML renderer, non-editable fields are not. + */ +Blockly.Field.prototype.EDITABLE = true; + +/** + * Attach this field to a block. + * @param {!Blockly.Block} block The block containing this field. + */ +Blockly.Field.prototype.setSourceBlock = function(block) { + goog.asserts.assert(!this.sourceBlock_, 'Field already bound to a block.'); + this.sourceBlock_ = block; +}; + +/** + * Install this field on a block. + */ +Blockly.Field.prototype.init = function() { + if (this.fieldGroup_) { + // Field has already been initialized once. + return; + } + // Build the DOM. + this.fieldGroup_ = Blockly.createSvgElement('g', {}, null); + if (!this.visible_) { + this.fieldGroup_.style.display = 'none'; + } + this.borderRect_ = Blockly.createSvgElement('rect', + {'rx': 4, + 'ry': 4, + 'x': -Blockly.BlockSvg.SEP_SPACE_X / 2, + 'y': 0, + 'height': 16}, this.fieldGroup_, this.sourceBlock_.workspace); + /** @type {!Element} */ + this.textElement_ = Blockly.createSvgElement('text', + {'class': 'blocklyText', 'y': this.size_.height - 12.5}, + this.fieldGroup_); + + this.updateEditable(); + this.sourceBlock_.getSvgRoot().appendChild(this.fieldGroup_); + this.mouseUpWrapper_ = + Blockly.bindEvent_(this.fieldGroup_, 'mouseup', this, this.onMouseUp_); + // Force a render. + this.updateTextNode_(); +}; + +/** + * Dispose of all DOM objects belonging to this editable field. + */ +Blockly.Field.prototype.dispose = function() { + if (this.mouseUpWrapper_) { + Blockly.unbindEvent_(this.mouseUpWrapper_); + this.mouseUpWrapper_ = null; + } + this.sourceBlock_ = null; + goog.dom.removeNode(this.fieldGroup_); + this.fieldGroup_ = null; + this.textElement_ = null; + this.borderRect_ = null; + this.validator_ = null; +}; + +/** + * Add or remove the UI indicating if this field is editable or not. + */ +Blockly.Field.prototype.updateEditable = function() { + var group = this.fieldGroup_; + if (!this.EDITABLE || !group) { + return; + } + if (this.sourceBlock_.isEditable()) { + Blockly.addClass_(group, 'blocklyEditableText'); + Blockly.removeClass_(group, 'blocklyNonEditableText'); + this.fieldGroup_.style.cursor = this.CURSOR; + } else { + Blockly.addClass_(group, 'blocklyNonEditableText'); + Blockly.removeClass_(group, 'blocklyEditableText'); + this.fieldGroup_.style.cursor = ''; + } +}; + +/** + * Gets whether this editable field is visible or not. + * @return {boolean} True if visible. + */ +Blockly.Field.prototype.isVisible = function() { + return this.visible_; +}; + +/** + * Sets whether this editable field is visible or not. + * @param {boolean} visible True if visible. + */ +Blockly.Field.prototype.setVisible = function(visible) { + if (this.visible_ == visible) { + return; + } + this.visible_ = visible; + var root = this.getSvgRoot(); + if (root) { + root.style.display = visible ? 'block' : 'none'; + this.render_(); + } +}; + +/** + * Sets a new validation function for editable fields. + * @param {Function} handler New validation function, or null. + */ +Blockly.Field.prototype.setValidator = function(handler) { + this.validator_ = handler; +}; + +/** + * Gets the validation function for editable fields. + * @return {Function} Validation function, or null. + */ +Blockly.Field.prototype.getValidator = function() { + return this.validator_; +}; + +/** + * Validates a change. Does nothing. Subclasses may override this. + * @param {string} text The user's text. + * @return {string} No change needed. + */ +Blockly.Field.prototype.classValidator = function(text) { + return text; +}; + +/** + * Calls the validation function for this field, as well as all the validation + * function for the field's class and its parents. + * @param {string} text Proposed text. + * @return {?string} Revised text, or null if invalid. + */ +Blockly.Field.prototype.callValidator = function(text) { + var classResult = this.classValidator(text); + if (classResult === null) { + // Class validator rejects value. Game over. + return null; + } else if (classResult !== undefined) { + text = classResult; + } + var userValidator = this.getValidator(); + if (userValidator) { + var userResult = userValidator.call(this, text); + if (userResult === null) { + // User validator rejects value. Game over. + return null; + } else if (userResult !== undefined) { + text = userResult; + } + } + return text; +}; + +/** + * Gets the group element for this editable field. + * Used for measuring the size and for positioning. + * @return {!Element} The group element. + */ +Blockly.Field.prototype.getSvgRoot = function() { + return /** @type {!Element} */ (this.fieldGroup_); +}; + +/** + * Draws the border with the correct width. + * Saves the computed width in a property. + * @private + */ +Blockly.Field.prototype.render_ = function() { + if (this.visible_ && this.textElement_) { + var key = this.textElement_.textContent + '\n' + + this.textElement_.className.baseVal; + if (Blockly.Field.cacheWidths_ && Blockly.Field.cacheWidths_[key]) { + var width = Blockly.Field.cacheWidths_[key]; + } else { + try { + var width = this.textElement_.getComputedTextLength(); + } catch (e) { + // MSIE 11 is known to throw "Unexpected call to method or property + // access." if Blockly is hidden. + var width = this.textElement_.textContent.length * 8; + } + if (Blockly.Field.cacheWidths_) { + Blockly.Field.cacheWidths_[key] = width; + } + } + if (this.borderRect_) { + this.borderRect_.setAttribute('width', + width + Blockly.BlockSvg.SEP_SPACE_X); + } + } else { + var width = 0; + } + this.size_.width = width; +}; + +/** + * Start caching field widths. Every call to this function MUST also call + * stopCache. Caches must not survive between execution threads. + */ +Blockly.Field.startCache = function() { + Blockly.Field.cacheReference_++; + if (!Blockly.Field.cacheWidths_) { + Blockly.Field.cacheWidths_ = {}; + } +}; + +/** + * Stop caching field widths. Unless caching was already on when the + * corresponding call to startCache was made. + */ +Blockly.Field.stopCache = function() { + Blockly.Field.cacheReference_--; + if (!Blockly.Field.cacheReference_) { + Blockly.Field.cacheWidths_ = null; + } +}; + +/** + * Returns the height and width of the field. + * @return {!goog.math.Size} Height and width. + */ +Blockly.Field.prototype.getSize = function() { + if (!this.size_.width) { + this.render_(); + } + return this.size_; +}; + +/** + * Returns the height and width of the field, + * accounting for the workspace scaling. + * @return {!goog.math.Size} Height and width. + * @private + */ +Blockly.Field.prototype.getScaledBBox_ = function() { + var bBox = this.borderRect_.getBBox(); + // Create new object, as getBBox can return an uneditable SVGRect in IE. + return new goog.math.Size(bBox.width * this.sourceBlock_.workspace.scale, + bBox.height * this.sourceBlock_.workspace.scale); +}; + +/** + * Get the text from this field. + * @return {string} Current text. + */ +Blockly.Field.prototype.getText = function() { + return this.text_; +}; + +/** + * Set the text in this field. Trigger a rerender of the source block. + * @param {*} text New text. + */ +Blockly.Field.prototype.setText = function(text) { + if (text === null) { + // No change if null. + return; + } + text = String(text); + if (text === this.text_) { + // No change. + return; + } + this.text_ = text; + this.updateTextNode_(); + + if (this.sourceBlock_ && this.sourceBlock_.rendered) { + this.sourceBlock_.render(); + this.sourceBlock_.bumpNeighbours_(); + } +}; + +/** + * Update the text node of this field to display the current text. + * @private + */ +Blockly.Field.prototype.updateTextNode_ = function() { + if (!this.textElement_) { + // Not rendered yet. + return; + } + var text = this.text_; + if (text.length > this.maxDisplayLength) { + // Truncate displayed string and add an ellipsis ('...'). + text = text.substring(0, this.maxDisplayLength - 2) + '\u2026'; + } + // Empty the text element. + goog.dom.removeChildren(/** @type {!Element} */ (this.textElement_)); + // Replace whitespace with non-breaking spaces so the text doesn't collapse. + text = text.replace(/\s/g, Blockly.Field.NBSP); + if (this.sourceBlock_.RTL && text) { + // The SVG is LTR, force text to be RTL. + text += '\u200F'; + } + if (!text) { + // Prevent the field from disappearing if empty. + text = Blockly.Field.NBSP; + } + var textNode = document.createTextNode(text); + this.textElement_.appendChild(textNode); + + // Cached width is obsolete. Clear it. + this.size_.width = 0; +}; + +/** + * By default there is no difference between the human-readable text and + * the language-neutral values. Subclasses (such as dropdown) may define this. + * @return {string} Current text. + */ +Blockly.Field.prototype.getValue = function() { + return this.getText(); +}; + +/** + * By default there is no difference between the human-readable text and + * the language-neutral values. Subclasses (such as dropdown) may define this. + * @param {string} newText New text. + */ +Blockly.Field.prototype.setValue = function(newText) { + if (newText === null) { + // No change if null. + return; + } + var oldText = this.getValue(); + if (oldText == newText) { + return; + } + if (this.sourceBlock_ && Blockly.Events.isEnabled()) { + Blockly.Events.fire(new Blockly.Events.Change( + this.sourceBlock_, 'field', this.name, oldText, newText)); + } + this.setText(newText); +}; + +/** + * Handle a mouse up event on an editable field. + * @param {!Event} e Mouse up event. + * @private + */ +Blockly.Field.prototype.onMouseUp_ = function(e) { + if ((goog.userAgent.IPHONE || goog.userAgent.IPAD) && + !goog.userAgent.isVersionOrHigher('537.51.2') && + e.layerX !== 0 && e.layerY !== 0) { + // Old iOS spawns a bogus event on the next touch after a 'prompt()' edit. + // Unlike the real events, these have a layerX and layerY set. + return; + } else if (Blockly.isRightButton(e)) { + // Right-click. + return; + } else if (this.sourceBlock_.workspace.isDragging()) { + // Drag operation is concluding. Don't open the editor. + return; + } else if (this.sourceBlock_.isEditable()) { + // Non-abstract sub-classes must define a showEditor_ method. + this.showEditor_(); + } +}; + +/** + * Change the tooltip text for this field. + * @param {string|!Element} newTip Text for tooltip or a parent element to + * link to for its tooltip. + */ +Blockly.Field.prototype.setTooltip = function(newTip) { + // Non-abstract sub-classes may wish to implement this. See FieldLabel. +}; + +/** + * Return the absolute coordinates of the top-left corner of this field. + * The origin (0,0) is the top-left corner of the page body. + * @return {!goog.math.Coordinate} Object with .x and .y properties. + * @private + */ +Blockly.Field.prototype.getAbsoluteXY_ = function() { + return goog.style.getPageOffset(this.borderRect_); +}; diff --git a/src/blockly/core/field_angle.js b/src/blockly/core/field_angle.js new file mode 100644 index 0000000..a294948 --- /dev/null +++ b/src/blockly/core/field_angle.js @@ -0,0 +1,294 @@ +/** + * @license + * Visual Blocks Editor + * + * Copyright 2013 Google Inc. + * https://developers.google.com/blockly/ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Angle input field. + * @author fraser@google.com (Neil Fraser) + */ +'use strict'; + +goog.provide('Blockly.FieldAngle'); + +goog.require('Blockly.FieldTextInput'); +goog.require('goog.math'); +goog.require('goog.userAgent'); + + +/** + * Class for an editable angle field. + * @param {string} text The initial content of the field. + * @param {Function=} opt_validator An optional function that is called + * to validate any constraints on what the user entered. Takes the new + * text as an argument and returns the accepted text or null to abort + * the change. + * @extends {Blockly.FieldTextInput} + * @constructor + */ +Blockly.FieldAngle = function(text, opt_validator) { + // Add degree symbol: "360°" (LTR) or "°360" (RTL) + this.symbol_ = Blockly.createSvgElement('tspan', {}, null); + this.symbol_.appendChild(document.createTextNode('\u00B0')); + + Blockly.FieldAngle.superClass_.constructor.call(this, text, opt_validator); +}; +goog.inherits(Blockly.FieldAngle, Blockly.FieldTextInput); + +/** + * Round angles to the nearest 15 degrees when using mouse. + * Set to 0 to disable rounding. + */ +Blockly.FieldAngle.ROUND = 15; + +/** + * Half the width of protractor image. + */ +Blockly.FieldAngle.HALF = 100 / 2; + +/* The following two settings work together to set the behaviour of the angle + * picker. While many combinations are possible, two modes are typical: + * Math mode. + * 0 deg is right, 90 is up. This is the style used by protractors. + * Blockly.FieldAngle.CLOCKWISE = false; + * Blockly.FieldAngle.OFFSET = 0; + * Compass mode. + * 0 deg is up, 90 is right. This is the style used by maps. + * Blockly.FieldAngle.CLOCKWISE = true; + * Blockly.FieldAngle.OFFSET = 90; + */ + +/** + * Angle increases clockwise (true) or counterclockwise (false). + */ +Blockly.FieldAngle.CLOCKWISE = false; + +/** + * Offset the location of 0 degrees (and all angles) by a constant. + * Usually either 0 (0 = right) or 90 (0 = up). + */ +Blockly.FieldAngle.OFFSET = 0; + +/** + * Maximum allowed angle before wrapping. + * Usually either 360 (for 0 to 359.9) or 180 (for -179.9 to 180). + */ +Blockly.FieldAngle.WRAP = 360; + + +/** + * Radius of protractor circle. Slightly smaller than protractor size since + * otherwise SVG crops off half the border at the edges. + */ +Blockly.FieldAngle.RADIUS = Blockly.FieldAngle.HALF - 1; + +/** + * Clean up this FieldAngle, as well as the inherited FieldTextInput. + * @return {!Function} Closure to call on destruction of the WidgetDiv. + * @private + */ +Blockly.FieldAngle.prototype.dispose_ = function() { + var thisField = this; + return function() { + Blockly.FieldAngle.superClass_.dispose_.call(thisField)(); + thisField.gauge_ = null; + if (thisField.clickWrapper_) { + Blockly.unbindEvent_(thisField.clickWrapper_); + } + if (thisField.moveWrapper1_) { + Blockly.unbindEvent_(thisField.moveWrapper1_); + } + if (thisField.moveWrapper2_) { + Blockly.unbindEvent_(thisField.moveWrapper2_); + } + }; +}; + +/** + * Show the inline free-text editor on top of the text. + * @private + */ +Blockly.FieldAngle.prototype.showEditor_ = function() { + var noFocus = + goog.userAgent.MOBILE || goog.userAgent.ANDROID || goog.userAgent.IPAD; + // Mobile browsers have issues with in-line textareas (focus & keyboards). + Blockly.FieldAngle.superClass_.showEditor_.call(this, noFocus); + var div = Blockly.WidgetDiv.DIV; + if (!div.firstChild) { + // Mobile interface uses window.prompt. + return; + } + // Build the SVG DOM. + var svg = Blockly.createSvgElement('svg', { + 'xmlns': 'http://www.w3.org/2000/svg', + 'xmlns:html': 'http://www.w3.org/1999/xhtml', + 'xmlns:xlink': 'http://www.w3.org/1999/xlink', + 'version': '1.1', + 'height': (Blockly.FieldAngle.HALF * 2) + 'px', + 'width': (Blockly.FieldAngle.HALF * 2) + 'px' + }, div); + var circle = Blockly.createSvgElement('circle', { + 'cx': Blockly.FieldAngle.HALF, 'cy': Blockly.FieldAngle.HALF, + 'r': Blockly.FieldAngle.RADIUS, + 'class': 'blocklyAngleCircle' + }, svg); + this.gauge_ = Blockly.createSvgElement('path', + {'class': 'blocklyAngleGauge'}, svg); + this.line_ = Blockly.createSvgElement('line', + {'x1': Blockly.FieldAngle.HALF, + 'y1': Blockly.FieldAngle.HALF, + 'class': 'blocklyAngleLine'}, svg); + // Draw markers around the edge. + for (var angle = 0; angle < 360; angle += 15) { + Blockly.createSvgElement('line', { + 'x1': Blockly.FieldAngle.HALF + Blockly.FieldAngle.RADIUS, + 'y1': Blockly.FieldAngle.HALF, + 'x2': Blockly.FieldAngle.HALF + Blockly.FieldAngle.RADIUS - + (angle % 45 == 0 ? 10 : 5), + 'y2': Blockly.FieldAngle.HALF, + 'class': 'blocklyAngleMarks', + 'transform': 'rotate(' + angle + ',' + + Blockly.FieldAngle.HALF + ',' + Blockly.FieldAngle.HALF + ')' + }, svg); + } + svg.style.marginLeft = (15 - Blockly.FieldAngle.RADIUS) + 'px'; + this.clickWrapper_ = + Blockly.bindEvent_(svg, 'click', this, Blockly.WidgetDiv.hide); + this.moveWrapper1_ = + Blockly.bindEvent_(circle, 'mousemove', this, this.onMouseMove); + this.moveWrapper2_ = + Blockly.bindEvent_(this.gauge_, 'mousemove', this, this.onMouseMove); + this.updateGraph_(); +}; + +/** + * Set the angle to match the mouse's position. + * @param {!Event} e Mouse move event. + */ +Blockly.FieldAngle.prototype.onMouseMove = function(e) { + var bBox = this.gauge_.ownerSVGElement.getBoundingClientRect(); + var dx = e.clientX - bBox.left - Blockly.FieldAngle.HALF; + var dy = e.clientY - bBox.top - Blockly.FieldAngle.HALF; + var angle = Math.atan(-dy / dx); + if (isNaN(angle)) { + // This shouldn't happen, but let's not let this error propogate further. + return; + } + angle = goog.math.toDegrees(angle); + // 0: East, 90: North, 180: West, 270: South. + if (dx < 0) { + angle += 180; + } else if (dy > 0) { + angle += 360; + } + if (Blockly.FieldAngle.CLOCKWISE) { + angle = Blockly.FieldAngle.OFFSET + 360 - angle; + } else { + angle -= Blockly.FieldAngle.OFFSET; + } + if (Blockly.FieldAngle.ROUND) { + angle = Math.round(angle / Blockly.FieldAngle.ROUND) * + Blockly.FieldAngle.ROUND; + } + angle = this.callValidator(angle); + Blockly.FieldTextInput.htmlInput_.value = angle; + this.setValue(angle); + this.validate_(); + this.resizeEditor_(); +}; + +/** + * Insert a degree symbol. + * @param {?string} text New text. + */ +Blockly.FieldAngle.prototype.setText = function(text) { + Blockly.FieldAngle.superClass_.setText.call(this, text); + if (!this.textElement_) { + // Not rendered yet. + return; + } + this.updateGraph_(); + // Insert degree symbol. + if (this.sourceBlock_.RTL) { + this.textElement_.insertBefore(this.symbol_, this.textElement_.firstChild); + } else { + this.textElement_.appendChild(this.symbol_); + } + // Cached width is obsolete. Clear it. + this.size_.width = 0; +}; + +/** + * Redraw the graph with the current angle. + * @private + */ +Blockly.FieldAngle.prototype.updateGraph_ = function() { + if (!this.gauge_) { + return; + } + var angleDegrees = Number(this.getText()) + Blockly.FieldAngle.OFFSET; + var angleRadians = goog.math.toRadians(angleDegrees); + var path = ['M ', Blockly.FieldAngle.HALF, ',', Blockly.FieldAngle.HALF]; + var x2 = Blockly.FieldAngle.HALF; + var y2 = Blockly.FieldAngle.HALF; + if (!isNaN(angleRadians)) { + var angle1 = goog.math.toRadians(Blockly.FieldAngle.OFFSET); + var x1 = Math.cos(angle1) * Blockly.FieldAngle.RADIUS; + var y1 = Math.sin(angle1) * -Blockly.FieldAngle.RADIUS; + if (Blockly.FieldAngle.CLOCKWISE) { + angleRadians = 2 * angle1 - angleRadians; + } + x2 += Math.cos(angleRadians) * Blockly.FieldAngle.RADIUS; + y2 -= Math.sin(angleRadians) * Blockly.FieldAngle.RADIUS; + // Don't ask how the flag calculations work. They just do. + var largeFlag = Math.abs(Math.floor((angleRadians - angle1) / Math.PI) % 2); + if (Blockly.FieldAngle.CLOCKWISE) { + largeFlag = 1 - largeFlag; + } + var sweepFlag = Number(Blockly.FieldAngle.CLOCKWISE); + path.push(' l ', x1, ',', y1, + ' A ', Blockly.FieldAngle.RADIUS, ',', Blockly.FieldAngle.RADIUS, + ' 0 ', largeFlag, ' ', sweepFlag, ' ', x2, ',', y2, ' z'); + } + this.gauge_.setAttribute('d', path.join('')); + this.line_.setAttribute('x2', x2); + this.line_.setAttribute('y2', y2); +}; + +/** + * Ensure that only an angle may be entered. + * @param {string} text The user's text. + * @return {?string} A string representing a valid angle, or null if invalid. + */ +Blockly.FieldAngle.prototype.classValidator = function(text) { + if (text === null) { + return null; + } + var n = parseFloat(text || 0); + if (isNaN(n)) { + return null; + } + n = n % 360; + if (n < 0) { + n += 360; + } + if (n > Blockly.FieldAngle.WRAP) { + n -= 360; + } + return String(n); +}; diff --git a/src/blockly/core/field_checkbox.js b/src/blockly/core/field_checkbox.js new file mode 100644 index 0000000..638aba9 --- /dev/null +++ b/src/blockly/core/field_checkbox.js @@ -0,0 +1,117 @@ +/** + * @license + * Visual Blocks Editor + * + * Copyright 2012 Google Inc. + * https://developers.google.com/blockly/ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Checkbox field. Checked or not checked. + * @author fraser@google.com (Neil Fraser) + */ +'use strict'; + +goog.provide('Blockly.FieldCheckbox'); + +goog.require('Blockly.Field'); + + +/** + * Class for a checkbox field. + * @param {string} state The initial state of the field ('TRUE' or 'FALSE'). + * @param {Function=} opt_validator A function that is executed when a new + * option is selected. Its sole argument is the new checkbox state. If + * it returns a value, this becomes the new checkbox state, unless the + * value is null, in which case the change is aborted. + * @extends {Blockly.Field} + * @constructor + */ +Blockly.FieldCheckbox = function(state, opt_validator) { + Blockly.FieldCheckbox.superClass_.constructor.call(this, '', opt_validator); + // Set the initial state. + this.setValue(state); +}; +goog.inherits(Blockly.FieldCheckbox, Blockly.Field); + +/** + * Character for the checkmark. + */ +Blockly.FieldCheckbox.CHECK_CHAR = '\u2713'; + +/** + * Mouse cursor style when over the hotspot that initiates editability. + */ +Blockly.FieldCheckbox.prototype.CURSOR = 'default'; + +/** + * Install this checkbox on a block. + */ +Blockly.FieldCheckbox.prototype.init = function() { + if (this.fieldGroup_) { + // Checkbox has already been initialized once. + return; + } + Blockly.FieldCheckbox.superClass_.init.call(this); + // The checkbox doesn't use the inherited text element. + // Instead it uses a custom checkmark element that is either visible or not. + this.checkElement_ = Blockly.createSvgElement('text', + {'class': 'blocklyText blocklyCheckbox', 'x': -3, 'y': 14}, + this.fieldGroup_); + var textNode = document.createTextNode(Blockly.FieldCheckbox.CHECK_CHAR); + this.checkElement_.appendChild(textNode); + this.checkElement_.style.display = this.state_ ? 'block' : 'none'; +}; + +/** + * Return 'TRUE' if the checkbox is checked, 'FALSE' otherwise. + * @return {string} Current state. + */ +Blockly.FieldCheckbox.prototype.getValue = function() { + return String(this.state_).toUpperCase(); +}; + +/** + * Set the checkbox to be checked if strBool is 'TRUE', unchecks otherwise. + * @param {string} strBool New state. + */ +Blockly.FieldCheckbox.prototype.setValue = function(strBool) { + var newState = (strBool == 'TRUE'); + if (this.state_ !== newState) { + if (this.sourceBlock_ && Blockly.Events.isEnabled()) { + Blockly.Events.fire(new Blockly.Events.Change( + this.sourceBlock_, 'field', this.name, this.state_, newState)); + } + this.state_ = newState; + if (this.checkElement_) { + this.checkElement_.style.display = newState ? 'block' : 'none'; + } + } +}; + +/** + * Toggle the state of the checkbox. + * @private + */ +Blockly.FieldCheckbox.prototype.showEditor_ = function() { + var newState = !this.state_; + if (this.sourceBlock_) { + // Call any validation function, and allow it to override. + newState = this.callValidator(newState); + } + if (newState !== null) { + this.setValue(String(newState).toUpperCase()); + } +}; diff --git a/src/blockly/core/field_colour.js b/src/blockly/core/field_colour.js new file mode 100644 index 0000000..30b7dc5 --- /dev/null +++ b/src/blockly/core/field_colour.js @@ -0,0 +1,234 @@ +/** + * @license + * Visual Blocks Editor + * + * Copyright 2012 Google Inc. + * https://developers.google.com/blockly/ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Colour input field. + * @author fraser@google.com (Neil Fraser) + */ +'use strict'; + +goog.provide('Blockly.FieldColour'); + +goog.require('Blockly.Field'); +goog.require('goog.dom'); +goog.require('goog.events'); +goog.require('goog.style'); +goog.require('goog.ui.ColorPicker'); + + +/** + * Class for a colour input field. + * @param {string} colour The initial colour in '#rrggbb' format. + * @param {Function=} opt_validator A function that is executed when a new + * colour is selected. Its sole argument is the new colour value. Its + * return value becomes the selected colour, unless it is undefined, in + * which case the new colour stands, or it is null, in which case the change + * is aborted. + * @extends {Blockly.Field} + * @constructor + */ +Blockly.FieldColour = function(colour, opt_validator) { + Blockly.FieldColour.superClass_.constructor.call(this, colour, opt_validator); + this.setText(Blockly.Field.NBSP + Blockly.Field.NBSP + Blockly.Field.NBSP); +}; +goog.inherits(Blockly.FieldColour, Blockly.Field); + +/** + * By default use the global constants for colours. + * @type {Array.<string>} + * @private + */ +Blockly.FieldColour.prototype.colours_ = null; + +/** + * By default use the global constants for columns. + * @type {number} + * @private + */ +Blockly.FieldColour.prototype.columns_ = 0; + +/** + * Install this field on a block. + */ +Blockly.FieldColour.prototype.init = function() { + Blockly.FieldColour.superClass_.init.call(this); + this.borderRect_.style['fillOpacity'] = 1; + this.setValue(this.getValue()); +}; + +/** + * Mouse cursor style when over the hotspot that initiates the editor. + */ +Blockly.FieldColour.prototype.CURSOR = 'default'; + +/** + * Close the colour picker if this input is being deleted. + */ +Blockly.FieldColour.prototype.dispose = function() { + Blockly.WidgetDiv.hideIfOwner(this); + Blockly.FieldColour.superClass_.dispose.call(this); +}; + +/** + * Return the current colour. + * @return {string} Current colour in '#rrggbb' format. + */ +Blockly.FieldColour.prototype.getValue = function() { + return this.colour_; +}; + +/** + * Set the colour. + * @param {string} colour The new colour in '#rrggbb' format. + */ +Blockly.FieldColour.prototype.setValue = function(colour) { + if (this.sourceBlock_ && Blockly.Events.isEnabled() && + this.colour_ != colour) { + Blockly.Events.fire(new Blockly.Events.Change( + this.sourceBlock_, 'field', this.name, this.colour_, colour)); + } + this.colour_ = colour; + if (this.borderRect_) { + this.borderRect_.style.fill = colour; + } +}; + +/** + * Get the text from this field. Used when the block is collapsed. + * @return {string} Current text. + */ +Blockly.FieldColour.prototype.getText = function() { + var colour = this.colour_; + // Try to use #rgb format if possible, rather than #rrggbb. + var m = colour.match(/^#(.)\1(.)\2(.)\3$/); + if (m) { + colour = '#' + m[1] + m[2] + m[3]; + } + return colour; +}; + +/** + * An array of colour strings for the palette. + * See bottom of this page for the default: + * http://docs.closure-library.googlecode.com/git/closure_goog_ui_colorpicker.js.source.html + * @type {!Array.<string>} + */ +Blockly.FieldColour.COLOURS = goog.ui.ColorPicker.SIMPLE_GRID_COLORS; + +/** + * Number of columns in the palette. + */ +Blockly.FieldColour.COLUMNS = 7; + +/** + * Set a custom colour grid for this field. + * @param {Array.<string>} colours Array of colours for this block, + * or null to use default (Blockly.FieldColour.COLOURS). + * @return {!Blockly.FieldColour} Returns itself (for method chaining). + */ +Blockly.FieldColour.prototype.setColours = function(colours) { + this.colours_ = colours; + return this; +}; + +/** + * Set a custom grid size for this field. + * @param {number} columns Number of columns for this block, + * or 0 to use default (Blockly.FieldColour.COLUMNS). + * @return {!Blockly.FieldColour} Returns itself (for method chaining). + */ +Blockly.FieldColour.prototype.setColumns = function(columns) { + this.columns_ = columns; + return this; +}; + +/** + * Create a palette under the colour field. + * @private + */ +Blockly.FieldColour.prototype.showEditor_ = function() { + Blockly.WidgetDiv.show(this, this.sourceBlock_.RTL, + Blockly.FieldColour.widgetDispose_); + // Create the palette using Closure. + var picker = new goog.ui.ColorPicker(); + picker.setSize(this.columns_ || Blockly.FieldColour.COLUMNS); + picker.setColors(this.colours_ || Blockly.FieldColour.COLOURS); + + // Position the palette to line up with the field. + // Record windowSize and scrollOffset before adding the palette. + var windowSize = goog.dom.getViewportSize(); + var scrollOffset = goog.style.getViewportPageOffset(document); + var xy = this.getAbsoluteXY_(); + var borderBBox = this.getScaledBBox_(); + var div = Blockly.WidgetDiv.DIV; + picker.render(div); + picker.setSelectedColor(this.getValue()); + // Record paletteSize after adding the palette. + var paletteSize = goog.style.getSize(picker.getElement()); + + // Flip the palette vertically if off the bottom. + if (xy.y + paletteSize.height + borderBBox.height >= + windowSize.height + scrollOffset.y) { + xy.y -= paletteSize.height - 1; + } else { + xy.y += borderBBox.height - 1; + } + if (this.sourceBlock_.RTL) { + xy.x += borderBBox.width; + xy.x -= paletteSize.width; + // Don't go offscreen left. + if (xy.x < scrollOffset.x) { + xy.x = scrollOffset.x; + } + } else { + // Don't go offscreen right. + if (xy.x > windowSize.width + scrollOffset.x - paletteSize.width) { + xy.x = windowSize.width + scrollOffset.x - paletteSize.width; + } + } + Blockly.WidgetDiv.position(xy.x, xy.y, windowSize, scrollOffset, + this.sourceBlock_.RTL); + + // Configure event handler. + var thisField = this; + Blockly.FieldColour.changeEventKey_ = goog.events.listen(picker, + goog.ui.ColorPicker.EventType.CHANGE, + function(event) { + var colour = event.target.getSelectedColor() || '#000000'; + Blockly.WidgetDiv.hide(); + if (thisField.sourceBlock_) { + // Call any validation function, and allow it to override. + colour = thisField.callValidator(colour); + } + if (colour !== null) { + thisField.setValue(colour); + } + }); +}; + +/** + * Hide the colour palette. + * @private + */ +Blockly.FieldColour.widgetDispose_ = function() { + if (Blockly.FieldColour.changeEventKey_) { + goog.events.unlistenByKey(Blockly.FieldColour.changeEventKey_); + } +}; diff --git a/src/blockly/core/field_date.js b/src/blockly/core/field_date.js new file mode 100644 index 0000000..24f3239 --- /dev/null +++ b/src/blockly/core/field_date.js @@ -0,0 +1,346 @@ +/** + * @license + * Visual Blocks Editor + * + * Copyright 2015 Google Inc. + * https://developers.google.com/blockly/ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Date input field. + * @author pkendall64@gmail.com (Paul Kendall) + */ +'use strict'; + +goog.provide('Blockly.FieldDate'); + +goog.require('Blockly.Field'); +goog.require('goog.date'); +goog.require('goog.dom'); +goog.require('goog.events'); +goog.require('goog.i18n.DateTimeSymbols'); +goog.require('goog.i18n.DateTimeSymbols_he'); +goog.require('goog.style'); +goog.require('goog.ui.DatePicker'); + + +/** + * Class for a date input field. + * @param {string} date The initial date. + * @param {Function=} opt_validator A function that is executed when a new + * date is selected. Its sole argument is the new date value. Its + * return value becomes the selected date, unless it is undefined, in + * which case the new date stands, or it is null, in which case the change + * is aborted. + * @extends {Blockly.Field} + * @constructor + */ +Blockly.FieldDate = function(date, opt_validator) { + if (!date) { + date = new goog.date.Date().toIsoString(true); + } + Blockly.FieldDate.superClass_.constructor.call(this, date, opt_validator); + this.setValue(date); +}; +goog.inherits(Blockly.FieldDate, Blockly.Field); + +/** + * Mouse cursor style when over the hotspot that initiates the editor. + */ +Blockly.FieldDate.prototype.CURSOR = 'text'; + +/** + * Close the colour picker if this input is being deleted. + */ +Blockly.FieldDate.prototype.dispose = function() { + Blockly.WidgetDiv.hideIfOwner(this); + Blockly.FieldDate.superClass_.dispose.call(this); +}; + +/** + * Return the current date. + * @return {string} Current date. + */ +Blockly.FieldDate.prototype.getValue = function() { + return this.date_; +}; + +/** + * Set the date. + * @param {string} date The new date. + */ +Blockly.FieldDate.prototype.setValue = function(date) { + if (this.sourceBlock_) { + var validated = this.callValidator(date); + // If the new date is invalid, validation returns null. + // In this case we still want to display the illegal result. + if (validated !== null) { + date = validated; + } + } + this.date_ = date; + Blockly.Field.prototype.setText.call(this, date); +}; + +/** + * Create a date picker under the date field. + * @private + */ +Blockly.FieldDate.prototype.showEditor_ = function() { + Blockly.WidgetDiv.show(this, this.sourceBlock_.RTL, + Blockly.FieldDate.widgetDispose_); + // Create the date picker using Closure. + Blockly.FieldDate.loadLanguage_(); + var picker = new goog.ui.DatePicker(); + picker.setAllowNone(false); + picker.setShowWeekNum(false); + + // Position the picker to line up with the field. + // Record windowSize and scrollOffset before adding the picker. + var windowSize = goog.dom.getViewportSize(); + var scrollOffset = goog.style.getViewportPageOffset(document); + var xy = this.getAbsoluteXY_(); + var borderBBox = this.getScaledBBox_(); + var div = Blockly.WidgetDiv.DIV; + picker.render(div); + picker.setDate(goog.date.fromIsoString(this.getValue())); + // Record pickerSize after adding the date picker. + var pickerSize = goog.style.getSize(picker.getElement()); + + // Flip the picker vertically if off the bottom. + if (xy.y + pickerSize.height + borderBBox.height >= + windowSize.height + scrollOffset.y) { + xy.y -= pickerSize.height - 1; + } else { + xy.y += borderBBox.height - 1; + } + if (this.sourceBlock_.RTL) { + xy.x += borderBBox.width; + xy.x -= pickerSize.width; + // Don't go offscreen left. + if (xy.x < scrollOffset.x) { + xy.x = scrollOffset.x; + } + } else { + // Don't go offscreen right. + if (xy.x > windowSize.width + scrollOffset.x - pickerSize.width) { + xy.x = windowSize.width + scrollOffset.x - pickerSize.width; + } + } + Blockly.WidgetDiv.position(xy.x, xy.y, windowSize, scrollOffset, + this.sourceBlock_.RTL); + + // Configure event handler. + var thisField = this; + Blockly.FieldDate.changeEventKey_ = goog.events.listen(picker, + goog.ui.DatePicker.Events.CHANGE, + function(event) { + var date = event.date ? event.date.toIsoString(true) : ''; + Blockly.WidgetDiv.hide(); + if (thisField.sourceBlock_) { + // Call any validation function, and allow it to override. + date = thisField.callValidator(date); + } + thisField.setValue(date); + }); +}; + +/** + * Hide the date picker. + * @private + */ +Blockly.FieldDate.widgetDispose_ = function() { + if (Blockly.FieldDate.changeEventKey_) { + goog.events.unlistenByKey(Blockly.FieldDate.changeEventKey_); + } +}; + +/** + * Load the best language pack by scanning the Blockly.Msg object for a + * language that matches the available languages in Closure. + * @private + */ +Blockly.FieldDate.loadLanguage_ = function() { + var reg = /^DateTimeSymbols_(.+)$/; + for (var prop in goog.i18n) { + var m = prop.match(reg); + if (m) { + var lang = m[1].toLowerCase().replace('_', '.'); // E.g. 'pt.br' + if (goog.getObjectByName(lang, Blockly.Msg)) { + goog.i18n.DateTimeSymbols = goog.i18n[prop]; + } + } + } +}; + +/** + * CSS for date picker. See css.js for use. + */ +Blockly.FieldDate.CSS = [ + /* Copied from: goog/css/datepicker.css */ + /** + * Copyright 2009 The Closure Library Authors. All Rights Reserved. + * + * Use of this source code is governed by the Apache License, Version 2.0. + * See the COPYING file for details. + */ + + /** + * Standard styling for a goog.ui.DatePicker. + * + * @author arv@google.com (Erik Arvidsson) + */ + + '.blocklyWidgetDiv .goog-date-picker,', + '.blocklyWidgetDiv .goog-date-picker th,', + '.blocklyWidgetDiv .goog-date-picker td {', + ' font: 13px Arial, sans-serif;', + '}', + + '.blocklyWidgetDiv .goog-date-picker {', + ' -moz-user-focus: normal;', + ' -moz-user-select: none;', + ' position: relative;', + ' border: 1px solid #000;', + ' float: left;', + ' padding: 2px;', + ' color: #000;', + ' background: #c3d9ff;', + ' cursor: default;', + '}', + + '.blocklyWidgetDiv .goog-date-picker th {', + ' text-align: center;', + '}', + + '.blocklyWidgetDiv .goog-date-picker td {', + ' text-align: center;', + ' vertical-align: middle;', + ' padding: 1px 3px;', + '}', + + '.blocklyWidgetDiv .goog-date-picker-menu {', + ' position: absolute;', + ' background: threedface;', + ' border: 1px solid gray;', + ' -moz-user-focus: normal;', + ' z-index: 1;', + ' outline: none;', + '}', + + '.blocklyWidgetDiv .goog-date-picker-menu ul {', + ' list-style: none;', + ' margin: 0px;', + ' padding: 0px;', + '}', + + '.blocklyWidgetDiv .goog-date-picker-menu ul li {', + ' cursor: default;', + '}', + + '.blocklyWidgetDiv .goog-date-picker-menu-selected {', + ' background: #ccf;', + '}', + + '.blocklyWidgetDiv .goog-date-picker th {', + ' font-size: .9em;', + '}', + + '.blocklyWidgetDiv .goog-date-picker td div {', + ' float: left;', + '}', + + '.blocklyWidgetDiv .goog-date-picker button {', + ' padding: 0px;', + ' margin: 1px 0;', + ' border: 0;', + ' color: #20c;', + ' font-weight: bold;', + ' background: transparent;', + '}', + + '.blocklyWidgetDiv .goog-date-picker-date {', + ' background: #fff;', + '}', + + '.blocklyWidgetDiv .goog-date-picker-week,', + '.blocklyWidgetDiv .goog-date-picker-wday {', + ' padding: 1px 3px;', + ' border: 0;', + ' border-color: #a2bbdd;', + ' border-style: solid;', + '}', + + '.blocklyWidgetDiv .goog-date-picker-week {', + ' border-right-width: 1px;', + '}', + + '.blocklyWidgetDiv .goog-date-picker-wday {', + ' border-bottom-width: 1px;', + '}', + + '.blocklyWidgetDiv .goog-date-picker-head td {', + ' text-align: center;', + '}', + + /** Use td.className instead of !important */ + '.blocklyWidgetDiv td.goog-date-picker-today-cont {', + ' text-align: center;', + '}', + + /** Use td.className instead of !important */ + '.blocklyWidgetDiv td.goog-date-picker-none-cont {', + ' text-align: center;', + '}', + + '.blocklyWidgetDiv .goog-date-picker-month {', + ' min-width: 11ex;', + ' white-space: nowrap;', + '}', + + '.blocklyWidgetDiv .goog-date-picker-year {', + ' min-width: 6ex;', + ' white-space: nowrap;', + '}', + + '.blocklyWidgetDiv .goog-date-picker-monthyear {', + ' white-space: nowrap;', + '}', + + '.blocklyWidgetDiv .goog-date-picker table {', + ' border-collapse: collapse;', + '}', + + '.blocklyWidgetDiv .goog-date-picker-other-month {', + ' color: #888;', + '}', + + '.blocklyWidgetDiv .goog-date-picker-wkend-start,', + '.blocklyWidgetDiv .goog-date-picker-wkend-end {', + ' background: #eee;', + '}', + + /** Use td.className instead of !important */ + '.blocklyWidgetDiv td.goog-date-picker-selected {', + ' background: #c3d9ff;', + '}', + + '.blocklyWidgetDiv .goog-date-picker-today {', + ' background: #9ab;', + ' font-weight: bold !important;', + ' border-color: #246 #9bd #9bd #246;', + ' color: #fff;', + '}' +]; diff --git a/src/blockly/core/field_dropdown.js b/src/blockly/core/field_dropdown.js new file mode 100644 index 0000000..ec3dd4f --- /dev/null +++ b/src/blockly/core/field_dropdown.js @@ -0,0 +1,320 @@ +/** + * @license + * Visual Blocks Editor + * + * Copyright 2012 Google Inc. + * https://developers.google.com/blockly/ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Dropdown input field. Used for editable titles and variables. + * In the interests of a consistent UI, the toolbox shares some functions and + * properties with the context menu. + * @author fraser@google.com (Neil Fraser) + */ +'use strict'; + +goog.provide('Blockly.FieldDropdown'); + +goog.require('Blockly.Field'); +goog.require('goog.dom'); +goog.require('goog.events'); +goog.require('goog.style'); +goog.require('goog.ui.Menu'); +goog.require('goog.ui.MenuItem'); +goog.require('goog.userAgent'); + + +/** + * Class for an editable dropdown field. + * @param {(!Array.<!Array.<string>>|!Function)} menuGenerator An array of + * options for a dropdown list, or a function which generates these options. + * @param {Function=} opt_validator A function that is executed when a new + * option is selected, with the newly selected value as its sole argument. + * If it returns a value, that value (which must be one of the options) will + * become selected in place of the newly selected option, unless the return + * value is null, in which case the change is aborted. + * @extends {Blockly.Field} + * @constructor + */ +Blockly.FieldDropdown = function(menuGenerator, opt_validator) { + this.menuGenerator_ = menuGenerator; + this.trimOptions_(); + var firstTuple = this.getOptions_()[0]; + + // Call parent's constructor. + Blockly.FieldDropdown.superClass_.constructor.call(this, firstTuple[1], + opt_validator); +}; +goog.inherits(Blockly.FieldDropdown, Blockly.Field); + +/** + * Horizontal distance that a checkmark ovehangs the dropdown. + */ +Blockly.FieldDropdown.CHECKMARK_OVERHANG = 25; + +/** + * Android can't (in 2014) display "▾", so use "▼" instead. + */ +Blockly.FieldDropdown.ARROW_CHAR = goog.userAgent.ANDROID ? '\u25BC' : '\u25BE'; + +/** + * Mouse cursor style when over the hotspot that initiates the editor. + */ +Blockly.FieldDropdown.prototype.CURSOR = 'default'; + +/** + * Install this dropdown on a block. + */ +Blockly.FieldDropdown.prototype.init = function() { + if (this.fieldGroup_) { + // Dropdown has already been initialized once. + return; + } + // Add dropdown arrow: "option ▾" (LTR) or "▾ אופציה" (RTL) + this.arrow_ = Blockly.createSvgElement('tspan', {}, null); + this.arrow_.appendChild(document.createTextNode( + this.sourceBlock_.RTL ? Blockly.FieldDropdown.ARROW_CHAR + ' ' : + ' ' + Blockly.FieldDropdown.ARROW_CHAR)); + + Blockly.FieldDropdown.superClass_.init.call(this); + // Force a reset of the text to add the arrow. + var text = this.text_; + this.text_ = null; + this.setText(text); +}; + +/** + * Create a dropdown menu under the text. + * @private + */ +Blockly.FieldDropdown.prototype.showEditor_ = function() { + Blockly.WidgetDiv.show(this, this.sourceBlock_.RTL, null); + var thisField = this; + + function callback(e) { + var menuItem = e.target; + if (menuItem) { + var value = menuItem.getValue(); + if (thisField.sourceBlock_) { + // Call any validation function, and allow it to override. + value = thisField.callValidator(value); + } + if (value !== null) { + thisField.setValue(value); + } + } + Blockly.WidgetDiv.hideIfOwner(thisField); + } + + var menu = new goog.ui.Menu(); + menu.setRightToLeft(this.sourceBlock_.RTL); + var options = this.getOptions_(); + for (var i = 0; i < options.length; i++) { + var text = options[i][0]; // Human-readable text. + var value = options[i][1]; // Language-neutral value. + var menuItem = new goog.ui.MenuItem(text); + menuItem.setRightToLeft(this.sourceBlock_.RTL); + menuItem.setValue(value); + menuItem.setCheckable(true); + menu.addChild(menuItem, true); + menuItem.setChecked(value == this.value_); + } + // Listen for mouse/keyboard events. + goog.events.listen(menu, goog.ui.Component.EventType.ACTION, callback); + // Listen for touch events (why doesn't Closure handle this already?). + function callbackTouchStart(e) { + var control = this.getOwnerControl(/** @type {Node} */ (e.target)); + // Highlight the menu item. + control.handleMouseDown(e); + } + function callbackTouchEnd(e) { + var control = this.getOwnerControl(/** @type {Node} */ (e.target)); + // Activate the menu item. + control.performActionInternal(e); + } + menu.getHandler().listen(menu.getElement(), goog.events.EventType.TOUCHSTART, + callbackTouchStart); + menu.getHandler().listen(menu.getElement(), goog.events.EventType.TOUCHEND, + callbackTouchEnd); + + // Record windowSize and scrollOffset before adding menu. + var windowSize = goog.dom.getViewportSize(); + var scrollOffset = goog.style.getViewportPageOffset(document); + var xy = this.getAbsoluteXY_(); + var borderBBox = this.getScaledBBox_(); + var div = Blockly.WidgetDiv.DIV; + menu.render(div); + var menuDom = menu.getElement(); + Blockly.addClass_(menuDom, 'blocklyDropdownMenu'); + // Record menuSize after adding menu. + var menuSize = goog.style.getSize(menuDom); + // Recalculate height for the total content, not only box height. + menuSize.height = menuDom.scrollHeight; + + // Position the menu. + // Flip menu vertically if off the bottom. + if (xy.y + menuSize.height + borderBBox.height >= + windowSize.height + scrollOffset.y) { + xy.y -= menuSize.height + 2; + } else { + xy.y += borderBBox.height; + } + if (this.sourceBlock_.RTL) { + xy.x += borderBBox.width; + xy.x += Blockly.FieldDropdown.CHECKMARK_OVERHANG; + // Don't go offscreen left. + if (xy.x < scrollOffset.x + menuSize.width) { + xy.x = scrollOffset.x + menuSize.width; + } + } else { + xy.x -= Blockly.FieldDropdown.CHECKMARK_OVERHANG; + // Don't go offscreen right. + if (xy.x > windowSize.width + scrollOffset.x - menuSize.width) { + xy.x = windowSize.width + scrollOffset.x - menuSize.width; + } + } + Blockly.WidgetDiv.position(xy.x, xy.y, windowSize, scrollOffset, + this.sourceBlock_.RTL); + menu.setAllowAutoFocus(true); + menuDom.focus(); +}; + +/** + * Factor out common words in statically defined options. + * Create prefix and/or suffix labels. + * @private + */ +Blockly.FieldDropdown.prototype.trimOptions_ = function() { + this.prefixField = null; + this.suffixField = null; + var options = this.menuGenerator_; + if (!goog.isArray(options) || options.length < 2) { + return; + } + var strings = options.map(function(t) {return t[0];}); + var shortest = Blockly.shortestStringLength(strings); + var prefixLength = Blockly.commonWordPrefix(strings, shortest); + var suffixLength = Blockly.commonWordSuffix(strings, shortest); + if (!prefixLength && !suffixLength) { + return; + } + if (shortest <= prefixLength + suffixLength) { + // One or more strings will entirely vanish if we proceed. Abort. + return; + } + if (prefixLength) { + this.prefixField = strings[0].substring(0, prefixLength - 1); + } + if (suffixLength) { + this.suffixField = strings[0].substr(1 - suffixLength); + } + // Remove the prefix and suffix from the options. + var newOptions = []; + for (var i = 0; i < options.length; i++) { + var text = options[i][0]; + var value = options[i][1]; + text = text.substring(prefixLength, text.length - suffixLength); + newOptions[i] = [text, value]; + } + this.menuGenerator_ = newOptions; +}; + +/** + * Return a list of the options for this dropdown. + * @return {!Array.<!Array.<string>>} Array of option tuples: + * (human-readable text, language-neutral name). + * @private + */ +Blockly.FieldDropdown.prototype.getOptions_ = function() { + if (goog.isFunction(this.menuGenerator_)) { + return this.menuGenerator_.call(this); + } + return /** @type {!Array.<!Array.<string>>} */ (this.menuGenerator_); +}; + +/** + * Get the language-neutral value from this dropdown menu. + * @return {string} Current text. + */ +Blockly.FieldDropdown.prototype.getValue = function() { + return this.value_; +}; + +/** + * Set the language-neutral value for this dropdown menu. + * @param {string} newValue New value to set. + */ +Blockly.FieldDropdown.prototype.setValue = function(newValue) { + if (newValue === null || newValue === this.value_) { + return; // No change if null. + } + if (this.sourceBlock_ && Blockly.Events.isEnabled()) { + Blockly.Events.fire(new Blockly.Events.Change( + this.sourceBlock_, 'field', this.name, this.value_, newValue)); + } + this.value_ = newValue; + // Look up and display the human-readable text. + var options = this.getOptions_(); + for (var i = 0; i < options.length; i++) { + // Options are tuples of human-readable text and language-neutral values. + if (options[i][1] == newValue) { + this.setText(options[i][0]); + return; + } + } + // Value not found. Add it, maybe it will become valid once set + // (like variable names). + this.setText(newValue); +}; + +/** + * Set the text in this field. Trigger a rerender of the source block. + * @param {?string} text New text. + */ +Blockly.FieldDropdown.prototype.setText = function(text) { + if (this.sourceBlock_ && this.arrow_) { + // Update arrow's colour. + this.arrow_.style.fill = this.sourceBlock_.getColour(); + } + if (text === null || text === this.text_) { + // No change if null. + return; + } + this.text_ = text; + this.updateTextNode_(); + + if (this.textElement_) { + // Insert dropdown arrow. + if (this.sourceBlock_.RTL) { + this.textElement_.insertBefore(this.arrow_, this.textElement_.firstChild); + } else { + this.textElement_.appendChild(this.arrow_); + } + } + + if (this.sourceBlock_ && this.sourceBlock_.rendered) { + this.sourceBlock_.render(); + this.sourceBlock_.bumpNeighbours_(); + } +}; + +/** + * Close the dropdown menu if this input is being deleted. + */ +Blockly.FieldDropdown.prototype.dispose = function() { + Blockly.WidgetDiv.hideIfOwner(this); + Blockly.FieldDropdown.superClass_.dispose.call(this); +}; diff --git a/src/blockly/core/field_image.js b/src/blockly/core/field_image.js new file mode 100644 index 0000000..71d8052 --- /dev/null +++ b/src/blockly/core/field_image.js @@ -0,0 +1,171 @@ +/** + * @license + * Visual Blocks Editor + * + * Copyright 2012 Google Inc. + * https://developers.google.com/blockly/ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Image field. Used for titles, labels, etc. + * @author fraser@google.com (Neil Fraser) + */ +'use strict'; + +goog.provide('Blockly.FieldImage'); + +goog.require('Blockly.Field'); +goog.require('goog.dom'); +goog.require('goog.math.Size'); +goog.require('goog.userAgent'); + + +/** + * Class for an image. + * @param {string} src The URL of the image. + * @param {number} width Width of the image. + * @param {number} height Height of the image. + * @param {string=} opt_alt Optional alt text for when block is collapsed. + * @extends {Blockly.Field} + * @constructor + */ +Blockly.FieldImage = function(src, width, height, opt_alt) { + this.sourceBlock_ = null; + // Ensure height and width are numbers. Strings are bad at math. + this.height_ = Number(height); + this.width_ = Number(width); + this.size_ = new goog.math.Size(this.width_, + this.height_ + 2 * Blockly.BlockSvg.INLINE_PADDING_Y); + this.text_ = opt_alt || ''; + this.setValue(src); +}; +goog.inherits(Blockly.FieldImage, Blockly.Field); + +/** + * Rectangular mask used by Firefox. + * @type {Element} + * @private + */ +Blockly.FieldImage.prototype.rectElement_ = null; + +/** + * Editable fields are saved by the XML renderer, non-editable fields are not. + */ +Blockly.FieldImage.prototype.EDITABLE = false; + +/** + * Install this image on a block. + */ +Blockly.FieldImage.prototype.init = function() { + if (this.fieldGroup_) { + // Image has already been initialized once. + return; + } + // Build the DOM. + /** @type {SVGElement} */ + this.fieldGroup_ = Blockly.createSvgElement('g', {}, null); + if (!this.visible_) { + this.fieldGroup_.style.display = 'none'; + } + /** @type {SVGElement} */ + this.imageElement_ = Blockly.createSvgElement('image', + {'height': this.height_ + 'px', + 'width': this.width_ + 'px'}, this.fieldGroup_); + this.setValue(this.src_); + if (goog.userAgent.GECKO) { + /** + * Due to a Firefox bug which eats mouse events on image elements, + * a transparent rectangle needs to be placed on top of the image. + * @type {SVGElement} + */ + this.rectElement_ = Blockly.createSvgElement('rect', + {'height': this.height_ + 'px', + 'width': this.width_ + 'px', + 'fill-opacity': 0}, this.fieldGroup_); + } + this.sourceBlock_.getSvgRoot().appendChild(this.fieldGroup_); + + // Configure the field to be transparent with respect to tooltips. + var topElement = this.rectElement_ || this.imageElement_; + topElement.tooltip = this.sourceBlock_; + Blockly.Tooltip.bindMouseEvents(topElement); +}; + +/** + * Dispose of all DOM objects belonging to this text. + */ +Blockly.FieldImage.prototype.dispose = function() { + goog.dom.removeNode(this.fieldGroup_); + this.fieldGroup_ = null; + this.imageElement_ = null; + this.rectElement_ = null; +}; + +/** + * Change the tooltip text for this field. + * @param {string|!Element} newTip Text for tooltip or a parent element to + * link to for its tooltip. + */ +Blockly.FieldImage.prototype.setTooltip = function(newTip) { + var topElement = this.rectElement_ || this.imageElement_; + topElement.tooltip = newTip; +}; + +/** + * Get the source URL of this image. + * @return {string} Current text. + * @override + */ +Blockly.FieldImage.prototype.getValue = function() { + return this.src_; +}; + +/** + * Set the source URL of this image. + * @param {?string} src New source. + * @override + */ +Blockly.FieldImage.prototype.setValue = function(src) { + if (src === null) { + // No change if null. + return; + } + this.src_ = src; + if (this.imageElement_) { + this.imageElement_.setAttributeNS('http://www.w3.org/1999/xlink', + 'xlink:href', goog.isString(src) ? src : ''); + } +}; + +/** + * Set the alt text of this image. + * @param {?string} alt New alt text. + * @override + */ +Blockly.FieldImage.prototype.setText = function(alt) { + if (alt === null) { + // No change if null. + return; + } + this.text_ = alt; +}; + +/** + * Images are fixed width, no need to render. + * @private + */ +Blockly.FieldImage.prototype.render_ = function() { + // NOP +}; diff --git a/src/blockly/core/field_label.js b/src/blockly/core/field_label.js new file mode 100644 index 0000000..cb5fa7d --- /dev/null +++ b/src/blockly/core/field_label.js @@ -0,0 +1,104 @@ +/** + * @license + * Visual Blocks Editor + * + * Copyright 2012 Google Inc. + * https://developers.google.com/blockly/ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Non-editable text field. Used for titles, labels, etc. + * @author fraser@google.com (Neil Fraser) + */ +'use strict'; + +goog.provide('Blockly.FieldLabel'); + +goog.require('Blockly.Field'); +goog.require('Blockly.Tooltip'); +goog.require('goog.dom'); +goog.require('goog.math.Size'); + + +/** + * Class for a non-editable field. + * @param {string} text The initial content of the field. + * @param {string=} opt_class Optional CSS class for the field's text. + * @extends {Blockly.Field} + * @constructor + */ +Blockly.FieldLabel = function(text, opt_class) { + this.size_ = new goog.math.Size(0, 17.5); + this.class_ = opt_class; + this.setValue(text); +}; +goog.inherits(Blockly.FieldLabel, Blockly.Field); + +/** + * Editable fields are saved by the XML renderer, non-editable fields are not. + */ +Blockly.FieldLabel.prototype.EDITABLE = false; + +/** + * Install this text on a block. + */ +Blockly.FieldLabel.prototype.init = function() { + if (this.textElement_) { + // Text has already been initialized once. + return; + } + // Build the DOM. + this.textElement_ = Blockly.createSvgElement('text', + {'class': 'blocklyText', 'y': this.size_.height - 5}, null); + if (this.class_) { + Blockly.addClass_(this.textElement_, this.class_); + } + if (!this.visible_) { + this.textElement_.style.display = 'none'; + } + this.sourceBlock_.getSvgRoot().appendChild(this.textElement_); + + // Configure the field to be transparent with respect to tooltips. + this.textElement_.tooltip = this.sourceBlock_; + Blockly.Tooltip.bindMouseEvents(this.textElement_); + // Force a render. + this.updateTextNode_(); +}; + +/** + * Dispose of all DOM objects belonging to this text. + */ +Blockly.FieldLabel.prototype.dispose = function() { + goog.dom.removeNode(this.textElement_); + this.textElement_ = null; +}; + +/** + * Gets the group element for this field. + * Used for measuring the size and for positioning. + * @return {!Element} The group element. + */ +Blockly.FieldLabel.prototype.getSvgRoot = function() { + return /** @type {!Element} */ (this.textElement_); +}; + +/** + * Change the tooltip text for this field. + * @param {string|!Element} newTip Text for tooltip or a parent element to + * link to for its tooltip. + */ +Blockly.FieldLabel.prototype.setTooltip = function(newTip) { + this.textElement_.tooltip = newTip; +}; diff --git a/src/blockly/core/field_number.js b/src/blockly/core/field_number.js new file mode 100644 index 0000000..f72e7f0 --- /dev/null +++ b/src/blockly/core/field_number.js @@ -0,0 +1,101 @@ +/** + * @license + * Visual Blocks Editor + * + * Copyright 2016 Google Inc. + * https://developers.google.com/blockly/ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Number input field + * @author fenichel@google.com (Rachel Fenichel) + */ +'use strict'; + +goog.provide('Blockly.FieldNumber'); + +goog.require('Blockly.FieldTextInput'); +goog.require('goog.math'); + +/** + * Class for an editable number field. + * @param {number|string} value The initial content of the field. + * @param {number|string|undefined} opt_min Minimum value. + * @param {number|string|undefined} opt_max Maximum value. + * @param {number|string|undefined} opt_precision Precision for value. + * @param {Function=} opt_validator An optional function that is called + * to validate any constraints on what the user entered. Takes the new + * text as an argument and returns either the accepted text, a replacement + * text, or null to abort the change. + * @extends {Blockly.FieldTextInput} + * @constructor + */ +Blockly.FieldNumber = + function(value, opt_min, opt_max, opt_precision, opt_validator) { + value = String(value); + Blockly.FieldNumber.superClass_.constructor.call(this, value, opt_validator); + this.setConstraints(opt_min, opt_max, opt_precision); +}; +goog.inherits(Blockly.FieldNumber, Blockly.FieldTextInput); + +/** + * Set the maximum, minimum and precision constraints on this field. + * Any of these properties may be undefiend or NaN to be disabled. + * Setting precision (usually a power of 10) enforces a minimum step between + * values. That is, the user's value will rounded to the closest multiple of + * precision. The least significant digit place is inferred from the precision. + * Integers values can be enforces by choosing an integer precision. + * @param {number|string|undefined} min Minimum value. + * @param {number|string|undefined} max Maximum value. + * @param {number|string|undefined} precision Precision for value. + */ +Blockly.FieldNumber.prototype.setConstraints = function(min, max, precision) { + precision = parseFloat(precision); + this.precision_ = isNaN(precision) ? 0 : precision; + min = parseFloat(min); + this.min_ = isNaN(min) ? -Infinity : min; + max = parseFloat(max); + this.max_ = isNaN(max) ? Infinity : max; + this.setValue(this.callValidator(this.getValue())); +}; + +/** + * Ensure that only a number in the correct range may be entered. + * @param {string} text The user's text. + * @return {?string} A string representing a valid number, or null if invalid. + */ +Blockly.FieldNumber.prototype.classValidator = function(text) { + if (text === null) { + return null; + } + text = String(text); + // TODO: Handle cases like 'ten', '1.203,14', etc. + // 'O' is sometimes mistaken for '0' by inexperienced users. + text = text.replace(/O/ig, '0'); + // Strip out thousands separators. + text = text.replace(/,/g, ''); + var n = parseFloat(text || 0); + if (isNaN(n)) { + // Invalid number. + return null; + } + // Round to nearest multiple of precision. + if (this.precision_ && isFinite(n)) { + n = Math.round(n / this.precision_) * this.precision_; + } + // Get the value in range. + n = goog.math.clamp(n, this.min_, this.max_); + return String(n); +}; diff --git a/src/blockly/core/field_textinput.js b/src/blockly/core/field_textinput.js new file mode 100644 index 0000000..5af8bf9 --- /dev/null +++ b/src/blockly/core/field_textinput.js @@ -0,0 +1,327 @@ +/** + * @license + * Visual Blocks Editor + * + * Copyright 2012 Google Inc. + * https://developers.google.com/blockly/ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Text input field. + * @author fraser@google.com (Neil Fraser) + */ +'use strict'; + +goog.provide('Blockly.FieldTextInput'); + +goog.require('Blockly.Field'); +goog.require('Blockly.Msg'); +goog.require('goog.asserts'); +goog.require('goog.dom'); +goog.require('goog.dom.TagName'); +goog.require('goog.userAgent'); + + +/** + * Class for an editable text field. + * @param {string} text The initial content of the field. + * @param {Function=} opt_validator An optional function that is called + * to validate any constraints on what the user entered. Takes the new + * text as an argument and returns either the accepted text, a replacement + * text, or null to abort the change. + * @extends {Blockly.Field} + * @constructor + */ +Blockly.FieldTextInput = function(text, opt_validator) { + Blockly.FieldTextInput.superClass_.constructor.call(this, text, + opt_validator); +}; +goog.inherits(Blockly.FieldTextInput, Blockly.Field); + +/** + * Point size of text. Should match blocklyText's font-size in CSS. + */ +Blockly.FieldTextInput.FONTSIZE = 11; + +/** + * Mouse cursor style when over the hotspot that initiates the editor. + */ +Blockly.FieldTextInput.prototype.CURSOR = 'text'; + +/** + * Allow browser to spellcheck this field. + * @private + */ +Blockly.FieldTextInput.prototype.spellcheck_ = true; + +/** + * Close the input widget if this input is being deleted. + */ +Blockly.FieldTextInput.prototype.dispose = function() { + Blockly.WidgetDiv.hideIfOwner(this); + Blockly.FieldTextInput.superClass_.dispose.call(this); +}; + +/** + * Set the text in this field. + * @param {?string} text New text. + * @override + */ +Blockly.FieldTextInput.prototype.setValue = function(text) { + if (text === null) { + return; // No change if null. + } + if (this.sourceBlock_) { + var validated = this.callValidator(text); + // If the new text is invalid, validation returns null. + // In this case we still want to display the illegal result. + if (validated !== null) { + text = validated; + } + } + Blockly.Field.prototype.setValue.call(this, text); +}; + +/** + * Set whether this field is spellchecked by the browser. + * @param {boolean} check True if checked. + */ +Blockly.FieldTextInput.prototype.setSpellcheck = function(check) { + this.spellcheck_ = check; +}; + +/** + * Show the inline free-text editor on top of the text. + * @param {boolean=} opt_quietInput True if editor should be created without + * focus. Defaults to false. + * @private + */ +Blockly.FieldTextInput.prototype.showEditor_ = function(opt_quietInput) { + this.workspace_ = this.sourceBlock_.workspace; + var quietInput = opt_quietInput || false; + if (!quietInput && (goog.userAgent.MOBILE || goog.userAgent.ANDROID || + goog.userAgent.IPAD)) { + // Mobile browsers have issues with in-line textareas (focus & keyboards). + var newValue = window.prompt(Blockly.Msg.CHANGE_VALUE_TITLE, this.text_); + if (this.sourceBlock_) { + newValue = this.callValidator(newValue); + } + this.setValue(newValue); + return; + } + + Blockly.WidgetDiv.show(this, this.sourceBlock_.RTL, this.widgetDispose_()); + var div = Blockly.WidgetDiv.DIV; + // Create the input. + var htmlInput = + goog.dom.createDom(goog.dom.TagName.INPUT, 'blocklyHtmlInput'); + htmlInput.setAttribute('spellcheck', this.spellcheck_); + var fontSize = + (Blockly.FieldTextInput.FONTSIZE * this.workspace_.scale) + 'pt'; + div.style.fontSize = fontSize; + htmlInput.style.fontSize = fontSize; + /** @type {!HTMLInputElement} */ + Blockly.FieldTextInput.htmlInput_ = htmlInput; + div.appendChild(htmlInput); + + htmlInput.value = htmlInput.defaultValue = this.text_; + htmlInput.oldValue_ = null; + this.validate_(); + this.resizeEditor_(); + if (!quietInput) { + htmlInput.focus(); + htmlInput.select(); + } + + // Bind to keydown -- trap Enter without IME and Esc to hide. + htmlInput.onKeyDownWrapper_ = + Blockly.bindEvent_(htmlInput, 'keydown', this, this.onHtmlInputKeyDown_); + // Bind to keyup -- trap Enter; resize after every keystroke. + htmlInput.onKeyUpWrapper_ = + Blockly.bindEvent_(htmlInput, 'keyup', this, this.onHtmlInputChange_); + // Bind to keyPress -- repeatedly resize when holding down a key. + htmlInput.onKeyPressWrapper_ = + Blockly.bindEvent_(htmlInput, 'keypress', this, this.onHtmlInputChange_); + htmlInput.onWorkspaceChangeWrapper_ = this.resizeEditor_.bind(this); + this.workspace_.addChangeListener(htmlInput.onWorkspaceChangeWrapper_); +}; + +/** + * Handle key down to the editor. + * @param {!Event} e Keyboard event. + * @private + */ +Blockly.FieldTextInput.prototype.onHtmlInputKeyDown_ = function(e) { + var htmlInput = Blockly.FieldTextInput.htmlInput_; + var tabKey = 9, enterKey = 13, escKey = 27; + if (e.keyCode == enterKey) { + Blockly.WidgetDiv.hide(); + } else if (e.keyCode == escKey) { + htmlInput.value = htmlInput.defaultValue; + Blockly.WidgetDiv.hide(); + } else if (e.keyCode == tabKey) { + Blockly.WidgetDiv.hide(); + this.sourceBlock_.tab(this, !e.shiftKey); + e.preventDefault(); + } +}; + +/** + * Handle a change to the editor. + * @param {!Event} e Keyboard event. + * @private + */ +Blockly.FieldTextInput.prototype.onHtmlInputChange_ = function(e) { + var htmlInput = Blockly.FieldTextInput.htmlInput_; + // Update source block. + var text = htmlInput.value; + if (text !== htmlInput.oldValue_) { + htmlInput.oldValue_ = text; + this.setValue(text); + this.validate_(); + } else if (goog.userAgent.WEBKIT) { + // Cursor key. Render the source block to show the caret moving. + // Chrome only (version 26, OS X). + this.sourceBlock_.render(); + } + this.resizeEditor_(); + Blockly.svgResize(this.sourceBlock_.workspace); +}; + +/** + * Check to see if the contents of the editor validates. + * Style the editor accordingly. + * @private + */ +Blockly.FieldTextInput.prototype.validate_ = function() { + var valid = true; + goog.asserts.assertObject(Blockly.FieldTextInput.htmlInput_); + var htmlInput = Blockly.FieldTextInput.htmlInput_; + if (this.sourceBlock_) { + valid = this.callValidator(htmlInput.value); + } + if (valid === null) { + Blockly.addClass_(htmlInput, 'blocklyInvalidInput'); + } else { + Blockly.removeClass_(htmlInput, 'blocklyInvalidInput'); + } +}; + +/** + * Resize the editor and the underlying block to fit the text. + * @private + */ +Blockly.FieldTextInput.prototype.resizeEditor_ = function() { + var div = Blockly.WidgetDiv.DIV; + var bBox = this.fieldGroup_.getBBox(); + div.style.width = bBox.width * this.workspace_.scale + 'px'; + div.style.height = bBox.height * this.workspace_.scale + 'px'; + var xy = this.getAbsoluteXY_(); + // In RTL mode block fields and LTR input fields the left edge moves, + // whereas the right edge is fixed. Reposition the editor. + if (this.sourceBlock_.RTL) { + var borderBBox = this.getScaledBBox_(); + xy.x += borderBBox.width; + xy.x -= div.offsetWidth; + } + // Shift by a few pixels to line up exactly. + xy.y += 1; + if (goog.userAgent.GECKO && Blockly.WidgetDiv.DIV.style.top) { + // Firefox mis-reports the location of the border by a pixel + // once the WidgetDiv is moved into position. + xy.x -= 1; + xy.y -= 1; + } + if (goog.userAgent.WEBKIT) { + xy.y -= 3; + } + div.style.left = xy.x + 'px'; + div.style.top = xy.y + 'px'; +}; + +/** + * Close the editor, save the results, and dispose of the editable + * text field's elements. + * @return {!Function} Closure to call on destruction of the WidgetDiv. + * @private + */ +Blockly.FieldTextInput.prototype.widgetDispose_ = function() { + var thisField = this; + return function() { + var htmlInput = Blockly.FieldTextInput.htmlInput_; + // Save the edit (if it validates). + var text = htmlInput.value; + if (thisField.sourceBlock_) { + var text1 = thisField.callValidator(text); + if (text1 === null) { + // Invalid edit. + text = htmlInput.defaultValue; + } else { + // Validation function has changed the text. + text = text1; + if (thisField.onFinishEditing_) { + thisField.onFinishEditing_(text); + } + } + } + thisField.setValue(text); + thisField.sourceBlock_.rendered && thisField.sourceBlock_.render(); + Blockly.unbindEvent_(htmlInput.onKeyDownWrapper_); + Blockly.unbindEvent_(htmlInput.onKeyUpWrapper_); + Blockly.unbindEvent_(htmlInput.onKeyPressWrapper_); + thisField.workspace_.removeChangeListener( + htmlInput.onWorkspaceChangeWrapper_); + Blockly.FieldTextInput.htmlInput_ = null; + // Delete style properties. + var style = Blockly.WidgetDiv.DIV.style; + style.width = 'auto'; + style.height = 'auto'; + style.fontSize = ''; + }; +}; + +/** + * Ensure that only a number may be entered. + * @param {string} text The user's text. + * @return {?string} A string representing a valid number, or null if invalid. + */ +Blockly.FieldTextInput.numberValidator = function(text) { + console.warn('Blockly.FieldTextInput.numberValidator is deprecated. ' + + 'Use Blockly.FieldNumber instead.'); + if (text === null) { + return null; + } + text = String(text); + // TODO: Handle cases like 'ten', '1.203,14', etc. + // 'O' is sometimes mistaken for '0' by inexperienced users. + text = text.replace(/O/ig, '0'); + // Strip out thousands separators. + text = text.replace(/,/g, ''); + var n = parseFloat(text || 0); + return isNaN(n) ? null : String(n); +}; + +/** + * Ensure that only a nonnegative integer may be entered. + * @param {string} text The user's text. + * @return {?string} A string representing a valid int, or null if invalid. + */ +Blockly.FieldTextInput.nonnegativeIntegerValidator = function(text) { + var n = Blockly.FieldTextInput.numberValidator(text); + if (n) { + n = String(Math.max(0, Math.floor(n))); + } + return n; +}; diff --git a/src/blockly/core/field_variable.js b/src/blockly/core/field_variable.js new file mode 100644 index 0000000..f93caa1 --- /dev/null +++ b/src/blockly/core/field_variable.js @@ -0,0 +1,155 @@ +/** + * @license + * Visual Blocks Editor + * + * Copyright 2012 Google Inc. + * https://developers.google.com/blockly/ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Variable input field. + * @author fraser@google.com (Neil Fraser) + */ +'use strict'; + +goog.provide('Blockly.FieldVariable'); + +goog.require('Blockly.FieldDropdown'); +goog.require('Blockly.Msg'); +goog.require('Blockly.Variables'); +goog.require('goog.string'); + + +/** + * Class for a variable's dropdown field. + * @param {?string} varname The default name for the variable. If null, + * a unique variable name will be generated. + * @param {Function=} opt_validator A function that is executed when a new + * option is selected. Its sole argument is the new option value. + * @extends {Blockly.FieldDropdown} + * @constructor + */ +Blockly.FieldVariable = function(varname, opt_validator) { + Blockly.FieldVariable.superClass_.constructor.call(this, + Blockly.FieldVariable.dropdownCreate, opt_validator); + this.setValue(varname || ''); +}; +goog.inherits(Blockly.FieldVariable, Blockly.FieldDropdown); + +/** + * Install this dropdown on a block. + */ +Blockly.FieldVariable.prototype.init = function() { + if (this.fieldGroup_) { + // Dropdown has already been initialized once. + return; + } + Blockly.FieldVariable.superClass_.init.call(this); + if (!this.getValue()) { + // Variables without names get uniquely named for this workspace. + var workspace = + this.sourceBlock_.isInFlyout ? + this.sourceBlock_.workspace.targetWorkspace : + this.sourceBlock_.workspace; + this.setValue(Blockly.Variables.generateUniqueName(workspace)); + } + // If the selected variable doesn't exist yet, create it. + // For instance, some blocks in the toolbox have variable dropdowns filled + // in by default. + if (!this.sourceBlock_.isInFlyout) { + this.sourceBlock_.workspace.createVariable(this.getValue()); + } +}; + +/** + * Get the variable's name (use a variableDB to convert into a real name). + * Unline a regular dropdown, variables are literal and have no neutral value. + * @return {string} Current text. + */ +Blockly.FieldVariable.prototype.getValue = function() { + return this.getText(); +}; + +/** + * Set the variable name. + * @param {string} newValue New text. + */ +Blockly.FieldVariable.prototype.setValue = function(newValue) { + if (this.sourceBlock_ && Blockly.Events.isEnabled()) { + Blockly.Events.fire(new Blockly.Events.Change( + this.sourceBlock_, 'field', this.name, this.value_, newValue)); + } + this.value_ = newValue; + this.setText(newValue); +}; + +/** + * Return a sorted list of variable names for variable dropdown menus. + * Include a special option at the end for creating a new variable name. + * @return {!Array.<string>} Array of variable names. + * @this {!Blockly.FieldVariable} + */ +Blockly.FieldVariable.dropdownCreate = function() { + if (this.sourceBlock_ && this.sourceBlock_.workspace) { + // Get a copy of the list, so that adding rename and new variable options + // doesn't modify the workspace's list. + var variableList = this.sourceBlock_.workspace.variableList.slice(0); + } else { + var variableList = []; + } + // Ensure that the currently selected variable is an option. + var name = this.getText(); + if (name && variableList.indexOf(name) == -1) { + variableList.push(name); + } + variableList.sort(goog.string.caseInsensitiveCompare); + variableList.push(Blockly.Msg.RENAME_VARIABLE); + variableList.push(Blockly.Msg.DELETE_VARIABLE.replace('%1', name)); + // Variables are not language-specific, use the name as both the user-facing + // text and the internal representation. + var options = []; + for (var i = 0; i < variableList.length; i++) { + options[i] = [variableList[i], variableList[i]]; + } + return options; +}; + +/** + * Event handler for a change in variable name. + * Special case the 'Rename variable...' and 'Delete variable...' options. + * In the rename case, prompt the user for a new name. + * @param {string} text The selected dropdown menu option. + * @return {null|undefined|string} An acceptable new variable name, or null if + * change is to be either aborted (cancel button) or has been already + * handled (rename), or undefined if an existing variable was chosen. + */ +Blockly.FieldVariable.prototype.classValidator = function(text) { + var workspace = this.sourceBlock_.workspace; + if (text == Blockly.Msg.RENAME_VARIABLE) { + var oldVar = this.getText(); + Blockly.hideChaff(); + text = Blockly.Variables.promptName( + Blockly.Msg.RENAME_VARIABLE_TITLE.replace('%1', oldVar), oldVar); + if (text) { + workspace.renameVariable(oldVar, text); + } + return null; + } else if (text == Blockly.Msg.DELETE_VARIABLE.replace('%1', + this.getText())) { + workspace.deleteVariable(this.getText()); + return null; + } + return undefined; +}; diff --git a/src/blockly/core/flyout.js b/src/blockly/core/flyout.js new file mode 100644 index 0000000..03ea150 --- /dev/null +++ b/src/blockly/core/flyout.js @@ -0,0 +1,1364 @@ +/** + * @license + * Visual Blocks Editor + * + * Copyright 2011 Google Inc. + * https://developers.google.com/blockly/ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Flyout tray containing blocks which may be created. + * @author fraser@google.com (Neil Fraser) + */ +'use strict'; + +goog.provide('Blockly.Flyout'); + +goog.require('Blockly.Block'); +goog.require('Blockly.Comment'); +goog.require('Blockly.Events'); +goog.require('Blockly.FlyoutButton'); +goog.require('Blockly.WorkspaceSvg'); +goog.require('goog.dom'); +goog.require('goog.events'); +goog.require('goog.math.Rect'); +goog.require('goog.userAgent'); + + +/** + * Class for a flyout. + * @param {!Object} workspaceOptions Dictionary of options for the workspace. + * @constructor + */ +Blockly.Flyout = function(workspaceOptions) { + workspaceOptions.getMetrics = this.getMetrics_.bind(this); + workspaceOptions.setMetrics = this.setMetrics_.bind(this); + /** + * @type {!Blockly.Workspace} + * @private + */ + this.workspace_ = new Blockly.WorkspaceSvg(workspaceOptions); + this.workspace_.isFlyout = true; + + /** + * Is RTL vs LTR. + * @type {boolean} + */ + this.RTL = !!workspaceOptions.RTL; + + /** + * Flyout should be laid out horizontally vs vertically. + * @type {boolean} + * @private + */ + this.horizontalLayout_ = workspaceOptions.horizontalLayout; + + /** + * Position of the toolbox and flyout relative to the workspace. + * @type {number} + * @private + */ + this.toolboxPosition_ = workspaceOptions.toolboxPosition; + + /** + * Opaque data that can be passed to Blockly.unbindEvent_. + * @type {!Array.<!Array>} + * @private + */ + this.eventWrappers_ = []; + + /** + * List of background buttons that lurk behind each block to catch clicks + * landing in the blocks' lakes and bays. + * @type {!Array.<!Element>} + * @private + */ + this.backgroundButtons_ = []; + + /** + * List of visible buttons. + * @type {!Array.<!Blockly.FlyoutButton>} + * @private + */ + this.buttons_ = []; + + /** + * List of event listeners. + * @type {!Array.<!Array>} + * @private + */ + this.listeners_ = []; + + /** + * List of blocks that should always be disabled. + * @type {!Array.<!Blockly.Block>} + * @private + */ + this.permanentlyDisabled_ = []; + + /** + * y coordinate of mousedown - used to calculate scroll distances. + * @private {number} + */ + this.startDragMouseY_ = 0; + + /** + * x coordinate of mousedown - used to calculate scroll distances. + * @private {number} + */ + this.startDragMouseX_ = 0; +}; + +/** + * When a flyout drag is in progress, this is a reference to the flyout being + * dragged. This is used by Flyout.terminateDrag_ to reset dragMode_. + * @private {Blockly.Flyout} + */ +Blockly.Flyout.startFlyout_ = null; + +/** + * Event that started a drag. Used to determine the drag distance/direction and + * also passed to BlockSvg.onMouseDown_() after creating a new block. + * @private {Event} + */ +Blockly.Flyout.startDownEvent_ = null; + +/** + * Flyout block where the drag/click was initiated. Used to fire click events or + * create a new block. + * @private {Event} + */ +Blockly.Flyout.startBlock_ = null; + +/** + * Wrapper function called when a mouseup occurs during a background or block + * drag operation. + * @private {Array.<!Array>} + */ +Blockly.Flyout.onMouseUpWrapper_ = null; + +/** + * Wrapper function called when a mousemove occurs during a background drag. + * @private {Array.<!Array>} + */ +Blockly.Flyout.onMouseMoveWrapper_ = null; + +/** + * Wrapper function called when a mousemove occurs during a block drag. + * @private {Array.<!Array>} + */ +Blockly.Flyout.onMouseMoveBlockWrapper_ = null; + +/** + * Does the flyout automatically close when a block is created? + * @type {boolean} + */ +Blockly.Flyout.prototype.autoClose = true; + +/** + * Corner radius of the flyout background. + * @type {number} + * @const + */ +Blockly.Flyout.prototype.CORNER_RADIUS = 8; + +/** + * Number of pixels the mouse must move before a drag/scroll starts. Because the + * drag-intention is determined when this is reached, it is larger than + * Blockly.DRAG_RADIUS so that the drag-direction is clearer. + */ +Blockly.Flyout.prototype.DRAG_RADIUS = 10; + +/** + * Margin around the edges of the blocks in the flyout. + * @type {number} + * @const + */ +Blockly.Flyout.prototype.MARGIN = Blockly.Flyout.prototype.CORNER_RADIUS; + +/** + * Gap between items in horizontal flyouts. Can be overridden with the "sep" + * element. + * @const {number} + */ +Blockly.Flyout.prototype.GAP_X = Blockly.Flyout.prototype.MARGIN * 3; + +/** + * Gap between items in vertical flyouts. Can be overridden with the "sep" + * element. + * @const {number} + */ +Blockly.Flyout.prototype.GAP_Y = Blockly.Flyout.prototype.MARGIN * 3; + +/** + * Top/bottom padding between scrollbar and edge of flyout background. + * @type {number} + * @const + */ +Blockly.Flyout.prototype.SCROLLBAR_PADDING = 2; + +/** + * Width of flyout. + * @type {number} + * @private + */ +Blockly.Flyout.prototype.width_ = 0; + +/** + * Height of flyout. + * @type {number} + * @private + */ +Blockly.Flyout.prototype.height_ = 0; + +/** + * Is the flyout dragging (scrolling)? + * DRAG_NONE - no drag is ongoing or state is undetermined. + * DRAG_STICKY - still within the sticky drag radius. + * DRAG_FREE - in scroll mode (never create a new block). + * @private + */ +Blockly.Flyout.prototype.dragMode_ = Blockly.DRAG_NONE; + +/** + * Range of a drag angle from a flyout considered "dragging toward workspace". + * Drags that are within the bounds of this many degrees from the orthogonal + * line to the flyout edge are considered to be "drags toward the workspace". + * Example: + * Flyout Edge Workspace + * [block] / <-within this angle, drags "toward workspace" | + * [block] ---- orthogonal to flyout boundary ---- | + * [block] \ | + * The angle is given in degrees from the orthogonal. + * + * This is used to know when to create a new block and when to scroll the + * flyout. Setting it to 360 means that all drags create a new block. + * @type {number} + * @private +*/ +Blockly.Flyout.prototype.dragAngleRange_ = 70; + +/** + * Creates the flyout's DOM. Only needs to be called once. + * @return {!Element} The flyout's SVG group. + */ +Blockly.Flyout.prototype.createDom = function() { + /* + <g> + <path class="blocklyFlyoutBackground"/> + <g class="blocklyFlyout"></g> + </g> + */ + this.svgGroup_ = Blockly.createSvgElement('g', + {'class': 'blocklyFlyout'}, null); + this.svgBackground_ = Blockly.createSvgElement('path', + {'class': 'blocklyFlyoutBackground'}, this.svgGroup_); + this.svgGroup_.appendChild(this.workspace_.createDom()); + return this.svgGroup_; +}; + +/** + * Initializes the flyout. + * @param {!Blockly.Workspace} targetWorkspace The workspace in which to create + * new blocks. + */ +Blockly.Flyout.prototype.init = function(targetWorkspace) { + this.targetWorkspace_ = targetWorkspace; + this.workspace_.targetWorkspace = targetWorkspace; + // Add scrollbar. + this.scrollbar_ = new Blockly.Scrollbar(this.workspace_, + this.horizontalLayout_, false); + + this.hide(); + + Array.prototype.push.apply(this.eventWrappers_, + Blockly.bindEvent_(this.svgGroup_, 'wheel', this, this.wheel_)); + if (!this.autoClose) { + this.filterWrapper_ = this.filterForCapacity_.bind(this); + this.targetWorkspace_.addChangeListener(this.filterWrapper_); + } + // Dragging the flyout up and down. + Array.prototype.push.apply(this.eventWrappers_, + Blockly.bindEvent_(this.svgGroup_, 'mousedown', this, this.onMouseDown_)); +}; + +/** + * Dispose of this flyout. + * Unlink from all DOM elements to prevent memory leaks. + */ +Blockly.Flyout.prototype.dispose = function() { + this.hide(); + Blockly.unbindEvent_(this.eventWrappers_); + if (this.filterWrapper_) { + this.targetWorkspace_.removeChangeListener(this.filterWrapper_); + this.filterWrapper_ = null; + } + if (this.scrollbar_) { + this.scrollbar_.dispose(); + this.scrollbar_ = null; + } + if (this.workspace_) { + this.workspace_.targetWorkspace = null; + this.workspace_.dispose(); + this.workspace_ = null; + } + if (this.svgGroup_) { + goog.dom.removeNode(this.svgGroup_); + this.svgGroup_ = null; + } + this.svgBackground_ = null; + this.targetWorkspace_ = null; +}; + +/** + * Get the width of the flyout. + * @return {number} The width of the flyout. + */ +Blockly.Flyout.prototype.getWidth = function() { + return this.width_; +}; + +/** + * Get the height of the flyout. + * @return {number} The width of the flyout. + */ +Blockly.Flyout.prototype.getHeight = function() { + return this.height_; +}; + +/** + * Return an object with all the metrics required to size scrollbars for the + * flyout. The following properties are computed: + * .viewHeight: Height of the visible rectangle, + * .viewWidth: Width of the visible rectangle, + * .contentHeight: Height of the contents, + * .contentWidth: Width of the contents, + * .viewTop: Offset of top edge of visible rectangle from parent, + * .contentTop: Offset of the top-most content from the y=0 coordinate, + * .absoluteTop: Top-edge of view. + * .viewLeft: Offset of the left edge of visible rectangle from parent, + * .contentLeft: Offset of the left-most content from the x=0 coordinate, + * .absoluteLeft: Left-edge of view. + * @return {Object} Contains size and position metrics of the flyout. + * @private + */ +Blockly.Flyout.prototype.getMetrics_ = function() { + if (!this.isVisible()) { + // Flyout is hidden. + return null; + } + + try { + var optionBox = this.workspace_.getCanvas().getBBox(); + } catch (e) { + // Firefox has trouble with hidden elements (Bug 528969). + var optionBox = {height: 0, y: 0, width: 0, x: 0}; + } + + var absoluteTop = this.SCROLLBAR_PADDING; + var absoluteLeft = this.SCROLLBAR_PADDING; + if (this.horizontalLayout_) { + if (this.toolboxPosition_ == Blockly.TOOLBOX_AT_BOTTOM) { + absoluteTop = 0; + } + var viewHeight = this.height_; + if (this.toolboxPosition_ == Blockly.TOOLBOX_AT_TOP) { + viewHeight += this.MARGIN - this.SCROLLBAR_PADDING; + } + var viewWidth = this.width_ - 2 * this.SCROLLBAR_PADDING; + } else { + absoluteLeft = 0; + var viewHeight = this.height_ - 2 * this.SCROLLBAR_PADDING; + var viewWidth = this.width_; + if (!this.RTL) { + viewWidth -= this.SCROLLBAR_PADDING; + } + } + + var metrics = { + viewHeight: viewHeight, + viewWidth: viewWidth, + contentHeight: (optionBox.height + 2 * this.MARGIN) * this.workspace_.scale, + contentWidth: (optionBox.width + 2 * this.MARGIN) * this.workspace_.scale, + viewTop: -this.workspace_.scrollY, + viewLeft: -this.workspace_.scrollX, + contentTop: optionBox.y, + contentLeft: optionBox.x, + absoluteTop: absoluteTop, + absoluteLeft: absoluteLeft + }; + return metrics; +}; + +/** + * Sets the translation of the flyout to match the scrollbars. + * @param {!Object} xyRatio Contains a y property which is a float + * between 0 and 1 specifying the degree of scrolling and a + * similar x property. + * @private + */ +Blockly.Flyout.prototype.setMetrics_ = function(xyRatio) { + var metrics = this.getMetrics_(); + // This is a fix to an apparent race condition. + if (!metrics) { + return; + } + if (!this.horizontalLayout_ && goog.isNumber(xyRatio.y)) { + this.workspace_.scrollY = -metrics.contentHeight * xyRatio.y; + } else if (this.horizontalLayout_ && goog.isNumber(xyRatio.x)) { + this.workspace_.scrollX = -metrics.contentWidth * xyRatio.x; + } + + this.workspace_.translate(this.workspace_.scrollX + metrics.absoluteLeft, + this.workspace_.scrollY + metrics.absoluteTop); +}; + +/** + * Move the flyout to the edge of the workspace. + */ +Blockly.Flyout.prototype.position = function() { + if (!this.isVisible()) { + return; + } + var targetWorkspaceMetrics = this.targetWorkspace_.getMetrics(); + if (!targetWorkspaceMetrics) { + // Hidden components will return null. + return; + } + var edgeWidth = this.horizontalLayout_ ? + targetWorkspaceMetrics.viewWidth : this.width_; + edgeWidth -= this.CORNER_RADIUS; + if (this.toolboxPosition_ == Blockly.TOOLBOX_AT_RIGHT) { + edgeWidth *= -1; + } + + this.setBackgroundPath_(edgeWidth, + this.horizontalLayout_ ? this.height_ : + targetWorkspaceMetrics.viewHeight); + + var x = targetWorkspaceMetrics.absoluteLeft; + if (this.toolboxPosition_ == Blockly.TOOLBOX_AT_RIGHT) { + x += targetWorkspaceMetrics.viewWidth; + x -= this.width_; + } + + var y = targetWorkspaceMetrics.absoluteTop; + if (this.toolboxPosition_ == Blockly.TOOLBOX_AT_BOTTOM) { + y += targetWorkspaceMetrics.viewHeight; + y -= this.height_; + } + + this.svgGroup_.setAttribute('transform', 'translate(' + x + ',' + y + ')'); + + // Record the height for Blockly.Flyout.getMetrics_, or width if the layout is + // horizontal. + if (this.horizontalLayout_) { + this.width_ = targetWorkspaceMetrics.viewWidth; + } else { + this.height_ = targetWorkspaceMetrics.viewHeight; + } + + // Update the scrollbar (if one exists). + if (this.scrollbar_) { + this.scrollbar_.resize(); + } +}; + +/** + * Create and set the path for the visible boundaries of the flyout. + * @param {number} width The width of the flyout, not including the + * rounded corners. + * @param {number} height The height of the flyout, not including + * rounded corners. + * @private + */ +Blockly.Flyout.prototype.setBackgroundPath_ = function(width, height) { + if (this.horizontalLayout_) { + this.setBackgroundPathHorizontal_(width, height); + } else { + this.setBackgroundPathVertical_(width, height); + } +}; + +/** + * Create and set the path for the visible boundaries of the flyout in vertical + * mode. + * @param {number} width The width of the flyout, not including the + * rounded corners. + * @param {number} height The height of the flyout, not including + * rounded corners. + * @private + */ +Blockly.Flyout.prototype.setBackgroundPathVertical_ = function(width, height) { + var atRight = this.toolboxPosition_ == Blockly.TOOLBOX_AT_RIGHT; + // Decide whether to start on the left or right. + var path = ['M ' + (atRight ? this.width_ : 0) + ',0']; + // Top. + path.push('h', width); + // Rounded corner. + path.push('a', this.CORNER_RADIUS, this.CORNER_RADIUS, 0, 0, + atRight ? 0 : 1, + atRight ? -this.CORNER_RADIUS : this.CORNER_RADIUS, + this.CORNER_RADIUS); + // Side closest to workspace. + path.push('v', Math.max(0, height - this.CORNER_RADIUS * 2)); + // Rounded corner. + path.push('a', this.CORNER_RADIUS, this.CORNER_RADIUS, 0, 0, + atRight ? 0 : 1, + atRight ? this.CORNER_RADIUS : -this.CORNER_RADIUS, + this.CORNER_RADIUS); + // Bottom. + path.push('h', -width); + path.push('z'); + this.svgBackground_.setAttribute('d', path.join(' ')); +}; + +/** + * Create and set the path for the visible boundaries of the flyout in + * horizontal mode. + * @param {number} width The width of the flyout, not including the + * rounded corners. + * @param {number} height The height of the flyout, not including + * rounded corners. + * @private + */ +Blockly.Flyout.prototype.setBackgroundPathHorizontal_ = function(width, + height) { + var atTop = this.toolboxPosition_ == Blockly.TOOLBOX_AT_TOP; + // Start at top left. + var path = ['M 0,' + (atTop ? 0 : this.CORNER_RADIUS)]; + + if (atTop) { + // Top. + path.push('h', width + this.CORNER_RADIUS); + // Right. + path.push('v', height); + // Bottom. + path.push('a', this.CORNER_RADIUS, this.CORNER_RADIUS, 0, 0, 1, + -this.CORNER_RADIUS, this.CORNER_RADIUS); + path.push('h', -1 * (width - this.CORNER_RADIUS)); + // Left. + path.push('a', this.CORNER_RADIUS, this.CORNER_RADIUS, 0, 0, 1, + -this.CORNER_RADIUS, -this.CORNER_RADIUS); + path.push('z'); + } else { + // Top. + path.push('a', this.CORNER_RADIUS, this.CORNER_RADIUS, 0, 0, 1, + this.CORNER_RADIUS, -this.CORNER_RADIUS); + path.push('h', width - this.CORNER_RADIUS); + // Right. + path.push('a', this.CORNER_RADIUS, this.CORNER_RADIUS, 0, 0, 1, + this.CORNER_RADIUS, this.CORNER_RADIUS); + path.push('v', height - this.CORNER_RADIUS); + // Bottom. + path.push('h', -width - this.CORNER_RADIUS); + // Left. + path.push('z'); + } + this.svgBackground_.setAttribute('d', path.join(' ')); +}; + +/** + * Scroll the flyout to the top. + */ +Blockly.Flyout.prototype.scrollToStart = function() { + this.scrollbar_.set((this.horizontalLayout_ && this.RTL) ? Infinity : 0); +}; + +/** + * Scroll the flyout. + * @param {!Event} e Mouse wheel scroll event. + * @private + */ +Blockly.Flyout.prototype.wheel_ = function(e) { + var delta = this.horizontalLayout_ ? e.deltaX : e.deltaY; + + if (delta) { + if (goog.userAgent.GECKO) { + // Firefox's deltas are a tenth that of Chrome/Safari. + delta *= 10; + } + var metrics = this.getMetrics_(); + var pos = this.horizontalLayout_ ? metrics.viewLeft + delta : + metrics.viewTop + delta; + var limit = this.horizontalLayout_ ? + metrics.contentWidth - metrics.viewWidth : + metrics.contentHeight - metrics.viewHeight; + pos = Math.min(pos, limit); + pos = Math.max(pos, 0); + this.scrollbar_.set(pos); + } + + // Don't scroll the page. + e.preventDefault(); + // Don't propagate mousewheel event (zooming). + e.stopPropagation(); +}; + +/** + * Is the flyout visible? + * @return {boolean} True if visible. + */ +Blockly.Flyout.prototype.isVisible = function() { + return this.svgGroup_ && this.svgGroup_.style.display == 'block'; +}; + +/** + * Hide and empty the flyout. + */ +Blockly.Flyout.prototype.hide = function() { + if (!this.isVisible()) { + return; + } + this.svgGroup_.style.display = 'none'; + // Delete all the event listeners. + for (var x = 0, listen; listen = this.listeners_[x]; x++) { + Blockly.unbindEvent_(listen); + } + this.listeners_.length = 0; + if (this.reflowWrapper_) { + this.workspace_.removeChangeListener(this.reflowWrapper_); + this.reflowWrapper_ = null; + } + // Do NOT delete the blocks here. Wait until Flyout.show. + // https://neil.fraser.name/news/2014/08/09/ +}; + +/** + * Show and populate the flyout. + * @param {!Array|string} xmlList List of blocks to show. + * Variables and procedures have a custom set of blocks. + */ +Blockly.Flyout.prototype.show = function(xmlList) { + this.hide(); + this.clearOldBlocks_(); + + if (xmlList == Blockly.Variables.NAME_TYPE) { + // Special category for variables. + xmlList = + Blockly.Variables.flyoutCategory(this.workspace_.targetWorkspace); + } else if (xmlList == Blockly.Procedures.NAME_TYPE) { + // Special category for procedures. + xmlList = + Blockly.Procedures.flyoutCategory(this.workspace_.targetWorkspace); + } + + this.svgGroup_.style.display = 'block'; + // Create the blocks to be shown in this flyout. + var contents = []; + var gaps = []; + this.permanentlyDisabled_.length = 0; + for (var i = 0, xml; xml = xmlList[i]; i++) { + if (xml.tagName) { + var tagName = xml.tagName.toUpperCase(); + var default_gap = this.horizontalLayout_ ? this.GAP_X : this.GAP_Y; + if (tagName == 'BLOCK') { + var curBlock = Blockly.Xml.domToBlock(xml, this.workspace_); + if (curBlock.disabled) { + // Record blocks that were initially disabled. + // Do not enable these blocks as a result of capacity filtering. + this.permanentlyDisabled_.push(curBlock); + } + contents.push({type: 'block', block: curBlock}); + var gap = parseInt(xml.getAttribute('gap'), 10); + gaps.push(isNaN(gap) ? default_gap : gap); + } else if (xml.tagName.toUpperCase() == 'SEP') { + // Change the gap between two blocks. + // <sep gap="36"></sep> + // The default gap is 24, can be set larger or smaller. + // This overwrites the gap attribute on the previous block. + // Note that a deprecated method is to add a gap to a block. + // <block type="math_arithmetic" gap="8"></block> + var newGap = parseInt(xml.getAttribute('gap'), 10); + // Ignore gaps before the first block. + if (!isNaN(newGap) && gaps.length > 0) { + gaps[gaps.length - 1] = newGap; + } else { + gaps.push(default_gap); + } + } else if (tagName == 'BUTTON') { + var label = xml.getAttribute('text'); + var curButton = new Blockly.FlyoutButton(this.workspace_, + this.targetWorkspace_, label); + contents.push({type: 'button', button: curButton}); + gaps.push(default_gap); + } + } + } + + this.layout_(contents, gaps); + + // IE 11 is an incompetent browser that fails to fire mouseout events. + // When the mouse is over the background, deselect all blocks. + var deselectAll = function() { + var topBlocks = this.workspace_.getTopBlocks(false); + for (var i = 0, block; block = topBlocks[i]; i++) { + block.removeSelect(); + } + }; + + this.listeners_.push(Blockly.bindEvent_(this.svgBackground_, 'mouseover', + this, deselectAll)); + + if (this.horizontalLayout_) { + this.height_ = 0; + } else { + this.width_ = 0; + } + this.reflow(); + + this.filterForCapacity_(); + + // Correctly position the flyout's scrollbar when it opens. + this.position(); + + this.reflowWrapper_ = this.reflow.bind(this); + this.workspace_.addChangeListener(this.reflowWrapper_); +}; + +/** + * Lay out the blocks in the flyout. + * @param {!Array.<!Object>} contents The blocks and buttons to lay out. + * @param {!Array.<number>} gaps The visible gaps between blocks. + * @private + */ +Blockly.Flyout.prototype.layout_ = function(contents, gaps) { + this.workspace_.scale = this.targetWorkspace_.scale; + var margin = this.MARGIN; + var cursorX = this.RTL ? margin : margin + Blockly.BlockSvg.TAB_WIDTH; + var cursorY = margin; + if (this.horizontalLayout_ && this.RTL) { + contents = contents.reverse(); + } + + for (var i = 0, item; item = contents[i]; i++) { + if (item.type == 'block') { + var block = item.block; + var allBlocks = block.getDescendants(); + for (var j = 0, child; child = allBlocks[j]; j++) { + // Mark blocks as being inside a flyout. This is used to detect and + // prevent the closure of the flyout if the user right-clicks on such a + // block. + child.isInFlyout = true; + } + block.render(); + var root = block.getSvgRoot(); + var blockHW = block.getHeightWidth(); + var tab = block.outputConnection ? Blockly.BlockSvg.TAB_WIDTH : 0; + if (this.horizontalLayout_) { + cursorX += tab; + } + block.moveBy((this.horizontalLayout_ && this.RTL) ? + cursorX + blockHW.width - tab : cursorX, + cursorY); + if (this.horizontalLayout_) { + cursorX += (blockHW.width + gaps[i] - tab); + } else { + cursorY += blockHW.height + gaps[i]; + } + + // Create an invisible rectangle under the block to act as a button. Just + // using the block as a button is poor, since blocks have holes in them. + var rect = Blockly.createSvgElement('rect', {'fill-opacity': 0}, null); + rect.tooltip = block; + Blockly.Tooltip.bindMouseEvents(rect); + // Add the rectangles under the blocks, so that the blocks' tooltips work. + this.workspace_.getCanvas().insertBefore(rect, block.getSvgRoot()); + block.flyoutRect_ = rect; + this.backgroundButtons_[i] = rect; + + this.addBlockListeners_(root, block, rect); + } else if (item.type == 'button') { + var button = item.button; + var buttonSvg = button.createDom(); + button.moveTo(cursorX, cursorY); + button.show(); + Blockly.bindEvent_(buttonSvg, 'mouseup', button, button.onMouseUp); + + this.buttons_.push(button); + if (this.horizontalLayout_) { + cursorX += (button.width + gaps[i]); + } else { + cursorY += button.height + gaps[i]; + } + } + } +}; + +/** + * Delete blocks and background buttons from a previous showing of the flyout. + * @private + */ +Blockly.Flyout.prototype.clearOldBlocks_ = function() { + // Delete any blocks from a previous showing. + var oldBlocks = this.workspace_.getTopBlocks(false); + for (var i = 0, block; block = oldBlocks[i]; i++) { + if (block.workspace == this.workspace_) { + block.dispose(false, false); + } + } + // Delete any background buttons from a previous showing. + for (var j = 0, rect; rect = this.backgroundButtons_[j]; j++) { + goog.dom.removeNode(rect); + } + this.backgroundButtons_.length = 0; + + for (var i = 0, button; button = this.buttons_[i]; i++) { + button.dispose(); + } + this.buttons_.length = 0; +}; + +/** + * Add listeners to a block that has been added to the flyout. + * @param {!Element} root The root node of the SVG group the block is in. + * @param {!Blockly.Block} block The block to add listeners for. + * @param {!Element} rect The invisible rectangle under the block that acts as + * a button for that block. + * @private + */ +Blockly.Flyout.prototype.addBlockListeners_ = function(root, block, rect) { + this.listeners_.push(Blockly.bindEvent_(root, 'mousedown', null, + this.blockMouseDown_(block))); + this.listeners_.push(Blockly.bindEvent_(rect, 'mousedown', null, + this.blockMouseDown_(block))); + this.listeners_.push(Blockly.bindEvent_(root, 'mouseover', block, + block.addSelect)); + this.listeners_.push(Blockly.bindEvent_(root, 'mouseout', block, + block.removeSelect)); + this.listeners_.push(Blockly.bindEvent_(rect, 'mouseover', block, + block.addSelect)); + this.listeners_.push(Blockly.bindEvent_(rect, 'mouseout', block, + block.removeSelect)); +}; + +/** + * Handle a mouse-down on an SVG block in a non-closing flyout. + * @param {!Blockly.Block} block The flyout block to copy. + * @return {!Function} Function to call when block is clicked. + * @private + */ +Blockly.Flyout.prototype.blockMouseDown_ = function(block) { + var flyout = this; + return function(e) { + Blockly.terminateDrag_(); + Blockly.hideChaff(true); + if (Blockly.isRightButton(e)) { + // Right-click. + block.showContextMenu_(e); + } else { + // Left-click (or middle click) + Blockly.Css.setCursor(Blockly.Css.Cursor.CLOSED); + // Record the current mouse position. + flyout.startDragMouseY_ = e.clientY; + flyout.startDragMouseX_ = e.clientX; + Blockly.Flyout.startDownEvent_ = e; + Blockly.Flyout.startBlock_ = block; + Blockly.Flyout.startFlyout_ = flyout; + Blockly.Flyout.onMouseUpWrapper_ = Blockly.bindEvent_(document, + 'mouseup', flyout, flyout.onMouseUp_); + Blockly.Flyout.onMouseMoveBlockWrapper_ = Blockly.bindEvent_(document, + 'mousemove', flyout, flyout.onMouseMoveBlock_); + } + // This event has been handled. No need to bubble up to the document. + e.stopPropagation(); + e.preventDefault(); + }; +}; + +/** + * Mouse down on the flyout background. Start a vertical scroll drag. + * @param {!Event} e Mouse down event. + * @private + */ +Blockly.Flyout.prototype.onMouseDown_ = function(e) { + if (Blockly.isRightButton(e)) { + return; + } + Blockly.hideChaff(true); + this.dragMode_ = Blockly.DRAG_FREE; + this.startDragMouseY_ = e.clientY; + this.startDragMouseX_ = e.clientX; + Blockly.Flyout.startFlyout_ = this; + Blockly.Flyout.onMouseMoveWrapper_ = Blockly.bindEvent_(document, 'mousemove', + this, this.onMouseMove_); + Blockly.Flyout.onMouseUpWrapper_ = Blockly.bindEvent_(document, 'mouseup', + this, Blockly.Flyout.terminateDrag_); + // This event has been handled. No need to bubble up to the document. + e.preventDefault(); + e.stopPropagation(); +}; + +/** + * Handle a mouse-up anywhere in the SVG pane. Is only registered when a + * block is clicked. We can't use mouseUp on the block since a fast-moving + * cursor can briefly escape the block before it catches up. + * @param {!Event} e Mouse up event. + * @private + */ +Blockly.Flyout.prototype.onMouseUp_ = function(e) { + if (!this.workspace_.isDragging()) { + if (this.autoClose) { + this.createBlockFunc_(Blockly.Flyout.startBlock_)( + Blockly.Flyout.startDownEvent_); + } else if (!Blockly.WidgetDiv.isVisible()) { + Blockly.Events.fire( + new Blockly.Events.Ui(Blockly.Flyout.startBlock_, 'click', + undefined, undefined)); + } + } + Blockly.terminateDrag_(); +}; + +/** + * Handle a mouse-move to vertically drag the flyout. + * @param {!Event} e Mouse move event. + * @private + */ +Blockly.Flyout.prototype.onMouseMove_ = function(e) { + var metrics = this.getMetrics_(); + if (this.horizontalLayout_) { + if (metrics.contentWidth - metrics.viewWidth < 0) { + return; + } + var dx = e.clientX - this.startDragMouseX_; + this.startDragMouseX_ = e.clientX; + var x = metrics.viewLeft - dx; + x = goog.math.clamp(x, 0, metrics.contentWidth - metrics.viewWidth); + this.scrollbar_.set(x); + } else { + if (metrics.contentHeight - metrics.viewHeight < 0) { + return; + } + var dy = e.clientY - this.startDragMouseY_; + this.startDragMouseY_ = e.clientY; + var y = metrics.viewTop - dy; + y = goog.math.clamp(y, 0, metrics.contentHeight - metrics.viewHeight); + this.scrollbar_.set(y); + } +}; + +/** + * Mouse button is down on a block in a non-closing flyout. Create the block + * if the mouse moves beyond a small radius. This allows one to play with + * fields without instantiating blocks that instantly self-destruct. + * @param {!Event} e Mouse move event. + * @private + */ +Blockly.Flyout.prototype.onMouseMoveBlock_ = function(e) { + if (e.type == 'mousemove' && e.clientX <= 1 && e.clientY == 0 && + e.button == 0) { + /* HACK: + Safari Mobile 6.0 and Chrome for Android 18.0 fire rogue mousemove events + on certain touch actions. Ignore events with these signatures. + This may result in a one-pixel blind spot in other browsers, + but this shouldn't be noticeable. */ + e.stopPropagation(); + return; + } + var dx = e.clientX - Blockly.Flyout.startDownEvent_.clientX; + var dy = e.clientY - Blockly.Flyout.startDownEvent_.clientY; + + var createBlock = this.determineDragIntention_(dx, dy); + if (createBlock) { + this.createBlockFunc_(Blockly.Flyout.startBlock_)( + Blockly.Flyout.startDownEvent_); + } else if (this.dragMode_ == Blockly.DRAG_FREE) { + // Do a scroll. + this.onMouseMove_(e); + } + e.stopPropagation(); +}; + +/** + * Determine the intention of a drag. + * Updates dragMode_ based on a drag delta and the current mode, + * and returns true if we should create a new block. + * @param {number} dx X delta of the drag. + * @param {number} dy Y delta of the drag. + * @return {boolean} True if a new block should be created. + * @private + */ +Blockly.Flyout.prototype.determineDragIntention_ = function(dx, dy) { + if (this.dragMode_ == Blockly.DRAG_FREE) { + // Once in free mode, always stay in free mode and never create a block. + return false; + } + var dragDistance = Math.sqrt(dx * dx + dy * dy); + if (dragDistance < this.DRAG_RADIUS) { + // Still within the sticky drag radius. + this.dragMode_ = Blockly.DRAG_STICKY; + return false; + } else { + if (this.isDragTowardWorkspace_(dx, dy) || !this.scrollbar_.isVisible()) { + // Immediately create a block. + return true; + } else { + // Immediately move to free mode - the drag is away from the workspace. + this.dragMode_ = Blockly.DRAG_FREE; + return false; + } + } +}; + +/** + * Determine if a drag delta is toward the workspace, based on the position + * and orientation of the flyout. This is used in determineDragIntention_ to + * determine if a new block should be created or if the flyout should scroll. + * @param {number} dx X delta of the drag. + * @param {number} dy Y delta of the drag. + * @return {boolean} true if the drag is toward the workspace. + * @private + */ +Blockly.Flyout.prototype.isDragTowardWorkspace_ = function(dx, dy) { + // Direction goes from -180 to 180, with 0 toward the right and 90 on top. + var dragDirection = Math.atan2(dy, dx) / Math.PI * 180; + + var draggingTowardWorkspace = false; + var range = this.dragAngleRange_; + if (this.horizontalLayout_) { + if (this.toolboxPosition_ == Blockly.TOOLBOX_AT_TOP) { + // Horizontal at top. + if (dragDirection < 90 + range && dragDirection > 90 - range) { + draggingTowardWorkspace = true; + } + } else { + // Horizontal at bottom. + if (dragDirection > -90 - range && dragDirection < -90 + range) { + draggingTowardWorkspace = true; + } + } + } else { + if (this.toolboxPosition_ == Blockly.TOOLBOX_AT_LEFT) { + // Vertical at left. + if (dragDirection < range && dragDirection > -range) { + draggingTowardWorkspace = true; + } + } else { + // Vertical at right. + if (dragDirection < -180 + range || dragDirection > 180 - range) { + draggingTowardWorkspace = true; + } + } + } + return draggingTowardWorkspace; +}; + +/** + * Create a copy of this block on the workspace. + * @param {!Blockly.Block} originBlock The flyout block to copy. + * @return {!Function} Function to call when block is clicked. + * @private + */ +Blockly.Flyout.prototype.createBlockFunc_ = function(originBlock) { + var flyout = this; + return function(e) { + if (Blockly.isRightButton(e)) { + // Right-click. Don't create a block, let the context menu show. + return; + } + if (originBlock.disabled) { + // Beyond capacity. + return; + } + Blockly.Events.disable(); + try { + var block = flyout.placeNewBlock_(originBlock); + } finally { + Blockly.Events.enable(); + } + if (Blockly.Events.isEnabled()) { + Blockly.Events.setGroup(true); + Blockly.Events.fire(new Blockly.Events.Create(block)); + } + if (flyout.autoClose) { + flyout.hide(); + } else { + flyout.filterForCapacity_(); + } + // Start a dragging operation on the new block. + block.onMouseDown_(e); + Blockly.dragMode_ = Blockly.DRAG_FREE; + block.setDragging_(true); + }; +}; + +/** + * Copy a block from the flyout to the workspace and position it correctly. + * @param {!Blockly.Block} originBlock The flyout block to copy.. + * @return {!Blockly.Block} The new block in the main workspace. + * @private + */ +Blockly.Flyout.prototype.placeNewBlock_ = function(originBlock) { + var targetWorkspace = this.targetWorkspace_; + var svgRootOld = originBlock.getSvgRoot(); + if (!svgRootOld) { + throw 'originBlock is not rendered.'; + } + // Figure out where the original block is on the screen, relative to the upper + // left corner of the main workspace. + var xyOld = Blockly.getSvgXY_(svgRootOld, targetWorkspace); + // Take into account that the flyout might have been scrolled horizontally + // (separately from the main workspace). + // Generally a no-op in vertical mode but likely to happen in horizontal + // mode. + var scrollX = this.workspace_.scrollX; + var scale = this.workspace_.scale; + xyOld.x += scrollX / scale - scrollX; + // If the flyout is on the right side, (0, 0) in the flyout is offset to + // the right of (0, 0) in the main workspace. Add an offset to take that + // into account. + if (this.toolboxPosition_ == Blockly.TOOLBOX_AT_RIGHT) { + scrollX = targetWorkspace.getMetrics().viewWidth - this.width_; + scale = targetWorkspace.scale; + // Scale the scroll (getSvgXY_ did not do this). + xyOld.x += scrollX / scale - scrollX; + } + + // Take into account that the flyout might have been scrolled vertically + // (separately from the main workspace). + // Generally a no-op in horizontal mode but likely to happen in vertical + // mode. + var scrollY = this.workspace_.scrollY; + scale = this.workspace_.scale; + xyOld.y += scrollY / scale - scrollY; + // If the flyout is on the bottom, (0, 0) in the flyout is offset to be below + // (0, 0) in the main workspace. Add an offset to take that into account. + if (this.toolboxPosition_ == Blockly.TOOLBOX_AT_BOTTOM) { + scrollY = targetWorkspace.getMetrics().viewHeight - this.height_; + scale = targetWorkspace.scale; + xyOld.y += scrollY / scale - scrollY; + } + + // Create the new block by cloning the block in the flyout (via XML). + var xml = Blockly.Xml.blockToDom(originBlock); + var block = Blockly.Xml.domToBlock(xml, targetWorkspace); + var svgRootNew = block.getSvgRoot(); + if (!svgRootNew) { + throw 'block is not rendered.'; + } + // Figure out where the new block got placed on the screen, relative to the + // upper left corner of the workspace. This may not be the same as the + // original block because the flyout's origin may not be the same as the + // main workspace's origin. + var xyNew = Blockly.getSvgXY_(svgRootNew, targetWorkspace); + // Scale the scroll (getSvgXY_ did not do this). + xyNew.x += + targetWorkspace.scrollX / targetWorkspace.scale - targetWorkspace.scrollX; + xyNew.y += + targetWorkspace.scrollY / targetWorkspace.scale - targetWorkspace.scrollY; + // If the flyout is collapsible and the workspace can't be scrolled. + if (targetWorkspace.toolbox_ && !targetWorkspace.scrollbar) { + xyNew.x += targetWorkspace.toolbox_.getWidth() / targetWorkspace.scale; + xyNew.y += targetWorkspace.toolbox_.getHeight() / targetWorkspace.scale; + } + + // Move the new block to where the old block is. + block.moveBy(xyOld.x - xyNew.x, xyOld.y - xyNew.y); + return block; +}; + +/** + * Filter the blocks on the flyout to disable the ones that are above the + * capacity limit. + * @private + */ +Blockly.Flyout.prototype.filterForCapacity_ = function() { + var remainingCapacity = this.targetWorkspace_.remainingCapacity(); + var blocks = this.workspace_.getTopBlocks(false); + for (var i = 0, block; block = blocks[i]; i++) { + if (this.permanentlyDisabled_.indexOf(block) == -1) { + var allBlocks = block.getDescendants(); + block.setDisabled(allBlocks.length > remainingCapacity); + } + } +}; + +/** + * Return the deletion rectangle for this flyout. + * @return {goog.math.Rect} Rectangle in which to delete. + */ +Blockly.Flyout.prototype.getClientRect = function() { + if (!this.svgGroup_) { + return null; + } + + var flyoutRect = this.svgGroup_.getBoundingClientRect(); + // BIG_NUM is offscreen padding so that blocks dragged beyond the shown flyout + // area are still deleted. Must be larger than the largest screen size, + // but be smaller than half Number.MAX_SAFE_INTEGER (not available on IE). + var BIG_NUM = 1000000000; + var x = flyoutRect.left; + var y = flyoutRect.top; + var width = flyoutRect.width; + var height = flyoutRect.height; + + if (this.toolboxPosition_ == Blockly.TOOLBOX_AT_TOP) { + return new goog.math.Rect(-BIG_NUM, y - BIG_NUM, BIG_NUM * 2, + BIG_NUM + height); + } else if (this.toolboxPosition_ == Blockly.TOOLBOX_AT_BOTTOM) { + return new goog.math.Rect(-BIG_NUM, y, BIG_NUM * 2, + BIG_NUM + height); + } else if (this.toolboxPosition_ == Blockly.TOOLBOX_AT_LEFT) { + return new goog.math.Rect(x - BIG_NUM, -BIG_NUM, BIG_NUM + width, + BIG_NUM * 2); + } else { // Right + return new goog.math.Rect(x, -BIG_NUM, BIG_NUM + width, BIG_NUM * 2); + } +}; + +/** + * Stop binding to the global mouseup and mousemove events. + * @private + */ +Blockly.Flyout.terminateDrag_ = function() { + if (Blockly.Flyout.startFlyout_) { + Blockly.Flyout.startFlyout_.dragMode_ = Blockly.DRAG_NONE; + } + if (Blockly.Flyout.onMouseUpWrapper_) { + Blockly.unbindEvent_(Blockly.Flyout.onMouseUpWrapper_); + Blockly.Flyout.onMouseUpWrapper_ = null; + } + if (Blockly.Flyout.onMouseMoveBlockWrapper_) { + Blockly.unbindEvent_(Blockly.Flyout.onMouseMoveBlockWrapper_); + Blockly.Flyout.onMouseMoveBlockWrapper_ = null; + } + if (Blockly.Flyout.onMouseMoveWrapper_) { + Blockly.unbindEvent_(Blockly.Flyout.onMouseMoveWrapper_); + Blockly.Flyout.onMouseMoveWrapper_ = null; + } + Blockly.Flyout.startDownEvent_ = null; + Blockly.Flyout.startBlock_ = null; + Blockly.Flyout.startFlyout_ = null; +}; + +/** + * Compute height of flyout. Position button under each block. + * For RTL: Lay out the blocks right-aligned. + * @param {!Array<!Blockly.Block>} blocks The blocks to reflow. + */ +Blockly.Flyout.prototype.reflowHorizontal = function(blocks) { + this.workspace_.scale = this.targetWorkspace_.scale; + var flyoutHeight = 0; + for (var i = 0, block; block = blocks[i]; i++) { + flyoutHeight = Math.max(flyoutHeight, block.getHeightWidth().height); + } + flyoutHeight += this.MARGIN * 1.5; + flyoutHeight *= this.workspace_.scale; + flyoutHeight += Blockly.Scrollbar.scrollbarThickness; + if (this.height_ != flyoutHeight) { + for (var i = 0, block; block = blocks[i]; i++) { + var blockHW = block.getHeightWidth(); + if (block.flyoutRect_) { + block.flyoutRect_.setAttribute('width', blockHW.width); + block.flyoutRect_.setAttribute('height', blockHW.height); + // Rectangles behind blocks with output tabs are shifted a bit. + var tab = block.outputConnection ? Blockly.BlockSvg.TAB_WIDTH : 0; + var blockXY = block.getRelativeToSurfaceXY(); + block.flyoutRect_.setAttribute('y', blockXY.y); + block.flyoutRect_.setAttribute('x', + this.RTL ? blockXY.x - blockHW.width + tab : blockXY.x - tab); + // For hat blocks we want to shift them down by the hat height + // since the y coordinate is the corner, not the top of the hat. + var hatOffset = + block.startHat_ ? Blockly.BlockSvg.START_HAT_HEIGHT : 0; + if (hatOffset) { + block.moveBy(0, hatOffset); + } + block.flyoutRect_.setAttribute('y', blockXY.y); + } + } + // Record the height for .getMetrics_ and .position. + this.height_ = flyoutHeight; + // Call this since it is possible the trash and zoom buttons need + // to move. e.g. on a bottom positioned flyout when zoom is clicked. + this.targetWorkspace_.resize(); + } +}; + +/** + * Compute width of flyout. Position button under each block. + * For RTL: Lay out the blocks right-aligned. + * @param {!Array<!Blockly.Block>} blocks The blocks to reflow. + */ +Blockly.Flyout.prototype.reflowVertical = function(blocks) { + this.workspace_.scale = this.targetWorkspace_.scale; + var flyoutWidth = 0; + for (var i = 0, block; block = blocks[i]; i++) { + var width = block.getHeightWidth().width; + if (block.outputConnection) { + width -= Blockly.BlockSvg.TAB_WIDTH; + } + flyoutWidth = Math.max(flyoutWidth, width); + } + for (var i = 0, button; button = this.buttons_[i]; i++) { + flyoutWidth = Math.max(flyoutWidth, button.width); + } + flyoutWidth += this.MARGIN * 1.5 + Blockly.BlockSvg.TAB_WIDTH; + flyoutWidth *= this.workspace_.scale; + flyoutWidth += Blockly.Scrollbar.scrollbarThickness; + if (this.width_ != flyoutWidth) { + for (var i = 0, block; block = blocks[i]; i++) { + var blockHW = block.getHeightWidth(); + if (this.RTL) { + // With the flyoutWidth known, right-align the blocks. + var oldX = block.getRelativeToSurfaceXY().x; + var newX = flyoutWidth / this.workspace_.scale - this.MARGIN; + newX -= Blockly.BlockSvg.TAB_WIDTH; + block.moveBy(newX - oldX, 0); + } + if (block.flyoutRect_) { + block.flyoutRect_.setAttribute('width', blockHW.width); + block.flyoutRect_.setAttribute('height', blockHW.height); + // Blocks with output tabs are shifted a bit. + var tab = block.outputConnection ? Blockly.BlockSvg.TAB_WIDTH : 0; + var blockXY = block.getRelativeToSurfaceXY(); + block.flyoutRect_.setAttribute('x', + this.RTL ? blockXY.x - blockHW.width + tab : blockXY.x - tab); + // For hat blocks we want to shift them down by the hat height + // since the y coordinate is the corner, not the top of the hat. + var hatOffset = + block.startHat_ ? Blockly.BlockSvg.START_HAT_HEIGHT : 0; + if (hatOffset) { + block.moveBy(0, hatOffset); + } + block.flyoutRect_.setAttribute('y', blockXY.y); + } + } + // Record the width for .getMetrics_ and .position. + this.width_ = flyoutWidth; + // Call this since it is possible the trash and zoom buttons need + // to move. e.g. on a bottom positioned flyout when zoom is clicked. + this.targetWorkspace_.resize(); + } +}; + +/** + * Reflow blocks and their buttons. + */ +Blockly.Flyout.prototype.reflow = function() { + if (this.reflowWrapper_) { + this.workspace_.removeChangeListener(this.reflowWrapper_); + } + var blocks = this.workspace_.getTopBlocks(false); + if (this.horizontalLayout_) { + this.reflowHorizontal(blocks); + } else { + this.reflowVertical(blocks); + } + if (this.reflowWrapper_) { + this.workspace_.addChangeListener(this.reflowWrapper_); + } +}; diff --git a/src/blockly/core/flyout_button.js b/src/blockly/core/flyout_button.js new file mode 100644 index 0000000..75b7a83 --- /dev/null +++ b/src/blockly/core/flyout_button.js @@ -0,0 +1,169 @@ +/** + * @license + * Visual Blocks Editor + * + * Copyright 2016 Google Inc. + * https://developers.google.com/blockly/ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Class for a button in the flyout. + * @author fenichel@google.com (Rachel Fenichel) + */ +'use strict'; + +goog.provide('Blockly.FlyoutButton'); + +goog.require('goog.dom'); +goog.require('goog.math.Coordinate'); + + +/** + * Class for a button in the flyout. + * @param {!Blockly.Workspace} workspace The workspace in which to place this + * button. + * @param {!Blockly.Workspace} targetWorkspace The flyout's target workspace. + * @param {string} text The text to display on the button. + * @constructor + */ +Blockly.FlyoutButton = function(workspace, targetWorkspace, text) { + /** + * @type {!Blockly.Workspace} + * @private + */ + this.workspace_ = workspace; + + /** + * @type {!Blockly.Workspace} + * @private + */ + this.targetWorkspace_ = targetWorkspace; + + /** + * @type {string} + * @private + */ + this.text_ = text; + + /** + * @type {goog.math.Coordinate} + * @private + */ + this.position_ = new goog.math.Coordinate(0, 0); +}; + +/** + * The margin around the text in the button. + */ +Blockly.FlyoutButton.MARGIN = 5; + +/** + * The width of the button's rect. + * @type {number} + */ +Blockly.FlyoutButton.prototype.width = 0; + +/** + * The height of the button's rect. + * @type {number} + */ +Blockly.FlyoutButton.prototype.height = 0; + +/** + * Create the button elements. + * @return {!Element} The button's SVG group. + */ +Blockly.FlyoutButton.prototype.createDom = function() { + this.svgGroup_ = Blockly.createSvgElement('g', + {'class': 'blocklyFlyoutButton'}, this.workspace_.getCanvas()); + + // Rect with rounded corners. + var rect = Blockly.createSvgElement('rect', + {'rx': 4, 'ry': 4, + 'height': 0, 'width': 0}, + this.svgGroup_); + + var svgText = Blockly.createSvgElement('text', + {'class': 'blocklyText', 'x': 0, 'y': 0, + 'text-anchor': 'middle'}, this.svgGroup_); + svgText.textContent = this.text_; + + this.width = svgText.getComputedTextLength() + + 2 * Blockly.FlyoutButton.MARGIN; + this.height = 20; // Can't compute it :( + + rect.setAttribute('width', this.width); + rect.setAttribute('height', this.height); + + svgText.setAttribute('x', this.width / 2); + svgText.setAttribute('y', this.height - Blockly.FlyoutButton.MARGIN); + + this.updateTransform_(); + return this.svgGroup_; +}; + +/** + * Correctly position the flyout button and make it visible. + */ +Blockly.FlyoutButton.prototype.show = function() { + this.updateTransform_(); + this.svgGroup_.setAttribute('display', 'block'); +}; + +/** + * Update svg attributes to match internal state. + */ +Blockly.FlyoutButton.prototype.updateTransform_ = function() { + this.svgGroup_.setAttribute('transform', 'translate(' + this.position_.x + + ',' + this.position_.y + ')'); +}; + +/** + * Move the button to the given x, y coordinates. + * @param {number} x The new x coordinate. + * @param {number} y The new y coordinate. + */ +Blockly.FlyoutButton.prototype.moveTo = function(x, y) { + this.position_.x = x; + this.position_.y = y; + this.updateTransform_(); +}; + +/** + * Dispose of this button. + */ +Blockly.FlyoutButton.prototype.dispose = function() { + if (this.svgGroup_) { + goog.dom.removeNode(this.svgGroup_); + this.svgGroup_ = null; + } + this.workspace_ = null; + this.targetWorkspace_ = null; +}; + +/** + * Do something when the button is clicked. + * @param {!Event} e Mouse up event. + */ +Blockly.FlyoutButton.prototype.onMouseUp = function(e) { + // Don't scroll the page. + e.preventDefault(); + // Don't propagate mousewheel event (zooming). + e.stopPropagation(); + // Stop binding to mouseup and mousemove events--flyout mouseup would normally + // do this, but we're skipping that. + Blockly.Flyout.terminateDrag_(); + Blockly.Variables.createVariable(this.targetWorkspace_); +}; diff --git a/src/blockly/core/generator.js b/src/blockly/core/generator.js new file mode 100644 index 0000000..fecc355 --- /dev/null +++ b/src/blockly/core/generator.js @@ -0,0 +1,369 @@ +/** + * @license + * Visual Blocks Editor + * + * Copyright 2012 Google Inc. + * https://developers.google.com/blockly/ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Utility functions for generating executable code from + * Blockly code. + * @author fraser@google.com (Neil Fraser) + */ +'use strict'; + +goog.provide('Blockly.Generator'); + +goog.require('Blockly.Block'); +goog.require('goog.asserts'); + + +/** + * Class for a code generator that translates the blocks into a language. + * @param {string} name Language name of this generator. + * @constructor + */ +Blockly.Generator = function(name) { + this.name_ = name; + this.FUNCTION_NAME_PLACEHOLDER_REGEXP_ = + new RegExp(this.FUNCTION_NAME_PLACEHOLDER_, 'g'); +}; + +/** + * Category to separate generated function names from variables and procedures. + */ +Blockly.Generator.NAME_TYPE = 'generated_function'; + +/** + * Arbitrary code to inject into locations that risk causing infinite loops. + * Any instances of '%1' will be replaced by the block ID that failed. + * E.g. ' checkTimeout(%1);\n' + * @type {?string} + */ +Blockly.Generator.prototype.INFINITE_LOOP_TRAP = null; + +/** + * Arbitrary code to inject before every statement. + * Any instances of '%1' will be replaced by the block ID of the statement. + * E.g. 'highlight(%1);\n' + * @type {?string} + */ +Blockly.Generator.prototype.STATEMENT_PREFIX = null; + +/** + * The method of indenting. Defaults to two spaces, but language generators + * may override this to increase indent or change to tabs. + * @type {string} + */ +Blockly.Generator.prototype.INDENT = ' '; + +/** + * Maximum length for a comment before wrapping. Does not account for + * indenting level. + * @type {number} + */ +Blockly.Generator.prototype.COMMENT_WRAP = 60; + +/** + * List of outer-inner pairings that do NOT require parentheses. + * @type {!Array.<!Array.<number>>} + */ +Blockly.Generator.prototype.ORDER_OVERRIDES = []; + +/** + * Generate code for all blocks in the workspace to the specified language. + * @param {Blockly.Workspace} workspace Workspace to generate code from. + * @return {string} Generated code. + */ +Blockly.Generator.prototype.workspaceToCode = function(workspace) { + if (!workspace) { + // Backwards compatibility from before there could be multiple workspaces. + console.warn('No workspace specified in workspaceToCode call. Guessing.'); + workspace = Blockly.getMainWorkspace(); + } + var code = []; + this.init(workspace); + var blocks = workspace.getTopBlocks(true); + for (var x = 0, block; block = blocks[x]; x++) { + var line = this.blockToCode(block); + if (goog.isArray(line)) { + // Value blocks return tuples of code and operator order. + // Top-level blocks don't care about operator order. + line = line[0]; + } + if (line) { + if (block.outputConnection && this.scrubNakedValue) { + // This block is a naked value. Ask the language's code generator if + // it wants to append a semicolon, or something. + line = this.scrubNakedValue(line); + } + code.push(line); + } + } + code = code.join('\n'); // Blank line between each section. + code = this.finish(code); + // Final scrubbing of whitespace. + code = code.replace(/^\s+\n/, ''); + code = code.replace(/\n\s+$/, '\n'); + code = code.replace(/[ \t]+\n/g, '\n'); + return code; +}; + +// The following are some helpful functions which can be used by multiple +// languages. + +/** + * Prepend a common prefix onto each line of code. + * @param {string} text The lines of code. + * @param {string} prefix The common prefix. + * @return {string} The prefixed lines of code. + */ +Blockly.Generator.prototype.prefixLines = function(text, prefix) { + return prefix + text.replace(/(?!\n$)\n/g, '\n' + prefix); +}; + +/** + * Recursively spider a tree of blocks, returning all their comments. + * @param {!Blockly.Block} block The block from which to start spidering. + * @return {string} Concatenated list of comments. + */ +Blockly.Generator.prototype.allNestedComments = function(block) { + var comments = []; + var blocks = block.getDescendants(); + for (var i = 0; i < blocks.length; i++) { + var comment = blocks[i].getCommentText(); + if (comment) { + comments.push(comment); + } + } + // Append an empty string to create a trailing line break when joined. + if (comments.length) { + comments.push(''); + } + return comments.join('\n'); +}; + +/** + * Generate code for the specified block (and attached blocks). + * @param {Blockly.Block} block The block to generate code for. + * @return {string|!Array} For statement blocks, the generated code. + * For value blocks, an array containing the generated code and an + * operator order value. Returns '' if block is null. + */ +Blockly.Generator.prototype.blockToCode = function(block) { + if (!block) { + return ''; + } + if (block.disabled) { + // Skip past this block if it is disabled. + return this.blockToCode(block.getNextBlock()); + } + + var func = this[block.type]; + goog.asserts.assertFunction(func, + 'Language "%s" does not know how to generate code for block type "%s".', + this.name_, block.type); + // First argument to func.call is the value of 'this' in the generator. + // Prior to 24 September 2013 'this' was the only way to access the block. + // The current prefered method of accessing the block is through the second + // argument to func.call, which becomes the first parameter to the generator. + var code = func.call(block, block); + if (goog.isArray(code)) { + // Value blocks return tuples of code and operator order. + goog.asserts.assert(block.outputConnection, + 'Expecting string from statement block "%s".', block.type); + return [this.scrub_(block, code[0]), code[1]]; + } else if (goog.isString(code)) { + if (this.STATEMENT_PREFIX) { + code = this.STATEMENT_PREFIX.replace(/%1/g, '\'' + block.id + '\'') + + code; + } + return this.scrub_(block, code); + } else if (code === null) { + // Block has handled code generation itself. + return ''; + } else { + goog.asserts.fail('Invalid code generated: %s', code); + } +}; + +/** + * Generate code representing the specified value input. + * @param {!Blockly.Block} block The block containing the input. + * @param {string} name The name of the input. + * @param {number} outerOrder The maximum binding strength (minimum order value) + * of any operators adjacent to "block". + * @return {string} Generated code or '' if no blocks are connected or the + * specified input does not exist. + */ +Blockly.Generator.prototype.valueToCode = function(block, name, outerOrder) { + if (isNaN(outerOrder)) { + goog.asserts.fail('Expecting valid order from block "%s".', block.type); + } + var targetBlock = block.getInputTargetBlock(name); + if (!targetBlock) { + return ''; + } + var tuple = this.blockToCode(targetBlock); + if (tuple === '') { + // Disabled block. + return ''; + } + // Value blocks must return code and order of operations info. + // Statement blocks must only return code. + goog.asserts.assertArray(tuple, 'Expecting tuple from value block "%s".', + targetBlock.type); + var code = tuple[0]; + var innerOrder = tuple[1]; + if (isNaN(innerOrder)) { + goog.asserts.fail('Expecting valid order from value block "%s".', + targetBlock.type); + } + if (!code) { + return ''; + } + + // Add parentheses if needed. + var parensNeeded = false; + var outerOrderClass = Math.floor(outerOrder); + var innerOrderClass = Math.floor(innerOrder); + if (outerOrderClass <= innerOrderClass) { + if (outerOrderClass == innerOrderClass && + (outerOrderClass == 0 || outerOrderClass == 99)) { + // Don't generate parens around NONE-NONE and ATOMIC-ATOMIC pairs. + // 0 is the atomic order, 99 is the none order. No parentheses needed. + // In all known languages multiple such code blocks are not order + // sensitive. In fact in Python ('a' 'b') 'c' would fail. + } else { + // The operators outside this code are stonger than the operators + // inside this code. To prevent the code from being pulled apart, + // wrap the code in parentheses. + parensNeeded = true; + // Check for special exceptions. + for (var i = 0; i < this.ORDER_OVERRIDES.length; i++) { + if (this.ORDER_OVERRIDES[i][0] == outerOrder && + this.ORDER_OVERRIDES[i][1] == innerOrder) { + parensNeeded = false; + break; + } + } + } + } + if (parensNeeded) { + // Technically, this should be handled on a language-by-language basis. + // However all known (sane) languages use parentheses for grouping. + code = '(' + code + ')'; + } + return code; +}; + +/** + * Generate code representing the statement. Indent the code. + * @param {!Blockly.Block} block The block containing the input. + * @param {string} name The name of the input. + * @return {string} Generated code or '' if no blocks are connected. + */ +Blockly.Generator.prototype.statementToCode = function(block, name) { + var targetBlock = block.getInputTargetBlock(name); + var code = this.blockToCode(targetBlock); + // Value blocks must return code and order of operations info. + // Statement blocks must only return code. + goog.asserts.assertString(code, 'Expecting code from statement block "%s".', + targetBlock && targetBlock.type); + if (code) { + code = this.prefixLines(/** @type {string} */ (code), this.INDENT); + } + return code; +}; + +/** + * Add an infinite loop trap to the contents of a loop. + * If loop is empty, add a statment prefix for the loop block. + * @param {string} branch Code for loop contents. + * @param {string} id ID of enclosing block. + * @return {string} Loop contents, with infinite loop trap added. + */ +Blockly.Generator.prototype.addLoopTrap = function(branch, id) { + if (this.INFINITE_LOOP_TRAP) { + branch = this.INFINITE_LOOP_TRAP.replace(/%1/g, '\'' + id + '\'') + branch; + } + if (this.STATEMENT_PREFIX) { + branch += this.prefixLines(this.STATEMENT_PREFIX.replace(/%1/g, + '\'' + id + '\''), this.INDENT); + } + return branch; +}; + +/** + * Comma-separated list of reserved words. + * @type {string} + * @private + */ +Blockly.Generator.prototype.RESERVED_WORDS_ = ''; + +/** + * Add one or more words to the list of reserved words for this language. + * @param {string} words Comma-separated list of words to add to the list. + * No spaces. Duplicates are ok. + */ +Blockly.Generator.prototype.addReservedWords = function(words) { + this.RESERVED_WORDS_ += words + ','; +}; + +/** + * This is used as a placeholder in functions defined using + * Blockly.Generator.provideFunction_. It must not be legal code that could + * legitimately appear in a function definition (or comment), and it must + * not confuse the regular expression parser. + * @type {string} + * @private + */ +Blockly.Generator.prototype.FUNCTION_NAME_PLACEHOLDER_ = '{leCUI8hutHZI4480Dc}'; + +/** + * Define a function to be included in the generated code. + * The first time this is called with a given desiredName, the code is + * saved and an actual name is generated. Subsequent calls with the + * same desiredName have no effect but have the same return value. + * + * It is up to the caller to make sure the same desiredName is not + * used for different code values. + * + * The code gets output when Blockly.Generator.finish() is called. + * + * @param {string} desiredName The desired name of the function (e.g., isPrime). + * @param {!Array.<string>} code A list of statements. Use ' ' for indents. + * @return {string} The actual name of the new function. This may differ + * from desiredName if the former has already been taken by the user. + * @private + */ +Blockly.Generator.prototype.provideFunction_ = function(desiredName, code) { + if (!this.definitions_[desiredName]) { + var functionName = this.variableDB_.getDistinctName(desiredName, + Blockly.Procedures.NAME_TYPE); + this.functionNames_[desiredName] = functionName; + var codeText = code.join('\n').replace( + this.FUNCTION_NAME_PLACEHOLDER_REGEXP_, functionName); + // Change all ' ' indents into the desired indent. + var oldCodeText; + while (oldCodeText != codeText) { + oldCodeText = codeText; + codeText = codeText.replace(/^(( )*) /gm, '$1' + this.INDENT); + } + this.definitions_[desiredName] = codeText; + } + return this.functionNames_[desiredName]; +}; diff --git a/src/blockly/core/icon.js b/src/blockly/core/icon.js new file mode 100644 index 0000000..b10e181 --- /dev/null +++ b/src/blockly/core/icon.js @@ -0,0 +1,203 @@ +/** + * @license + * Visual Blocks Editor + * + * Copyright 2013 Google Inc. + * https://developers.google.com/blockly/ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Object representing an icon on a block. + * @author fraser@google.com (Neil Fraser) + */ +'use strict'; + +goog.provide('Blockly.Icon'); + +goog.require('goog.dom'); +goog.require('goog.math.Coordinate'); + + +/** + * Class for an icon. + * @param {Blockly.Block} block The block associated with this icon. + * @constructor + */ +Blockly.Icon = function(block) { + this.block_ = block; +}; + +/** + * Does this icon get hidden when the block is collapsed. + */ +Blockly.Icon.prototype.collapseHidden = true; + +/** + * Height and width of icons. + */ +Blockly.Icon.prototype.SIZE = 17; + +/** + * Bubble UI (if visible). + * @type {Blockly.Bubble} + * @private + */ +Blockly.Icon.prototype.bubble_ = null; + +/** + * Absolute coordinate of icon's center. + * @type {goog.math.Coordinate} + * @private + */ +Blockly.Icon.prototype.iconXY_ = null; + +/** + * Create the icon on the block. + */ +Blockly.Icon.prototype.createIcon = function() { + if (this.iconGroup_) { + // Icon already exists. + return; + } + /* Here's the markup that will be generated: + <g class="blocklyIconGroup"> + ... + </g> + */ + this.iconGroup_ = Blockly.createSvgElement('g', + {'class': 'blocklyIconGroup'}, null); + if (this.block_.isInFlyout) { + Blockly.addClass_(/** @type {!Element} */ (this.iconGroup_), + 'blocklyIconGroupReadonly'); + } + this.drawIcon_(this.iconGroup_); + + this.block_.getSvgRoot().appendChild(this.iconGroup_); + Blockly.bindEvent_(this.iconGroup_, 'mouseup', this, this.iconClick_); + this.updateEditable(); +}; + +/** + * Dispose of this icon. + */ +Blockly.Icon.prototype.dispose = function() { + // Dispose of and unlink the icon. + goog.dom.removeNode(this.iconGroup_); + this.iconGroup_ = null; + // Dispose of and unlink the bubble. + this.setVisible(false); + this.block_ = null; +}; + +/** + * Add or remove the UI indicating if this icon may be clicked or not. + */ +Blockly.Icon.prototype.updateEditable = function() { +}; + +/** + * Is the associated bubble visible? + * @return {boolean} True if the bubble is visible. + */ +Blockly.Icon.prototype.isVisible = function() { + return !!this.bubble_; +}; + +/** + * Clicking on the icon toggles if the bubble is visible. + * @param {!Event} e Mouse click event. + * @private + */ +Blockly.Icon.prototype.iconClick_ = function(e) { + if (this.block_.workspace.isDragging()) { + // Drag operation is concluding. Don't open the editor. + return; + } + if (!this.block_.isInFlyout && !Blockly.isRightButton(e)) { + this.setVisible(!this.isVisible()); + } +}; + +/** + * Change the colour of the associated bubble to match its block. + */ +Blockly.Icon.prototype.updateColour = function() { + if (this.isVisible()) { + this.bubble_.setColour(this.block_.getColour()); + } +}; + +/** + * Render the icon. + * @param {number} cursorX Horizontal offset at which to position the icon. + * @return {number} Horizontal offset for next item to draw. + */ +Blockly.Icon.prototype.renderIcon = function(cursorX) { + if (this.collapseHidden && this.block_.isCollapsed()) { + this.iconGroup_.setAttribute('display', 'none'); + return cursorX; + } + this.iconGroup_.setAttribute('display', 'block'); + + var TOP_MARGIN = 5; + var width = this.SIZE; + if (this.block_.RTL) { + cursorX -= width; + } + this.iconGroup_.setAttribute('transform', + 'translate(' + cursorX + ',' + TOP_MARGIN + ')'); + this.computeIconLocation(); + if (this.block_.RTL) { + cursorX -= Blockly.BlockSvg.SEP_SPACE_X; + } else { + cursorX += width + Blockly.BlockSvg.SEP_SPACE_X; + } + return cursorX; +}; + +/** + * Notification that the icon has moved. Update the arrow accordingly. + * @param {!goog.math.Coordinate} xy Absolute location. + */ +Blockly.Icon.prototype.setIconLocation = function(xy) { + this.iconXY_ = xy; + if (this.isVisible()) { + this.bubble_.setAnchorLocation(xy); + } +}; + +/** + * Notification that the icon has moved, but we don't really know where. + * Recompute the icon's location from scratch. + */ +Blockly.Icon.prototype.computeIconLocation = function() { + // Find coordinates for the centre of the icon and update the arrow. + var blockXY = this.block_.getRelativeToSurfaceXY(); + var iconXY = Blockly.getRelativeXY_(this.iconGroup_); + var newXY = new goog.math.Coordinate( + blockXY.x + iconXY.x + this.SIZE / 2, + blockXY.y + iconXY.y + this.SIZE / 2); + if (!goog.math.Coordinate.equals(this.getIconLocation(), newXY)) { + this.setIconLocation(newXY); + } +}; + +/** + * Returns the center of the block's icon relative to the surface. + * @return {!goog.math.Coordinate} Object with x and y properties. + */ +Blockly.Icon.prototype.getIconLocation = function() { + return this.iconXY_; +}; diff --git a/src/blockly/core/inject.js b/src/blockly/core/inject.js new file mode 100644 index 0000000..57e3e51 --- /dev/null +++ b/src/blockly/core/inject.js @@ -0,0 +1,378 @@ +/** + * @license + * Visual Blocks Editor + * + * Copyright 2011 Google Inc. + * https://developers.google.com/blockly/ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Functions for injecting Blockly into a web page. + * @author fraser@google.com (Neil Fraser) + */ +'use strict'; + +goog.provide('Blockly.inject'); + +goog.require('Blockly.Css'); +goog.require('Blockly.Options'); +goog.require('Blockly.WorkspaceSvg'); +goog.require('goog.dom'); +goog.require('goog.ui.Component'); +goog.require('goog.userAgent'); + + +/** + * Inject a Blockly editor into the specified container element (usually a div). + * @param {!Element|string} container Containing element, or its ID, + * or a CSS selector. + * @param {Object=} opt_options Optional dictionary of options. + * @return {!Blockly.Workspace} Newly created main workspace. + */ +Blockly.inject = function(container, opt_options) { + if (goog.isString(container)) { + container = document.getElementById(container) || + document.querySelector(container); + } + // Verify that the container is in document. + if (!goog.dom.contains(document, container)) { + throw 'Error: container is not in current document.'; + } + var options = new Blockly.Options(opt_options || {}); + var subContainer = goog.dom.createDom('div', 'injectionDiv'); + container.appendChild(subContainer); + var svg = Blockly.createDom_(subContainer, options); + var workspace = Blockly.createMainWorkspace_(svg, options); + Blockly.init_(workspace); + workspace.markFocused(); + Blockly.bindEvent_(svg, 'focus', workspace, workspace.markFocused); + Blockly.svgResize(workspace); + return workspace; +}; + +/** + * Create the SVG image. + * @param {!Element} container Containing element. + * @param {!Blockly.Options} options Dictionary of options. + * @return {!Element} Newly created SVG image. + * @private + */ +Blockly.createDom_ = function(container, options) { + // Sadly browsers (Chrome vs Firefox) are currently inconsistent in laying + // out content in RTL mode. Therefore Blockly forces the use of LTR, + // then manually positions content in RTL as needed. + container.setAttribute('dir', 'LTR'); + // Closure can be trusted to create HTML widgets with the proper direction. + goog.ui.Component.setDefaultRightToLeft(options.RTL); + + // Load CSS. + Blockly.Css.inject(options.hasCss, options.pathToMedia); + + // Build the SVG DOM. + /* + <svg + xmlns="http://www.w3.org/2000/svg" + xmlns:html="http://www.w3.org/1999/xhtml" + xmlns:xlink="http://www.w3.org/1999/xlink" + version="1.1" + class="blocklySvg"> + ... + </svg> + */ + var svg = Blockly.createSvgElement('svg', { + 'xmlns': 'http://www.w3.org/2000/svg', + 'xmlns:html': 'http://www.w3.org/1999/xhtml', + 'xmlns:xlink': 'http://www.w3.org/1999/xlink', + 'version': '1.1', + 'class': 'blocklySvg' + }, container); + /* + <defs> + ... filters go here ... + </defs> + */ + var defs = Blockly.createSvgElement('defs', {}, svg); + var rnd = String(Math.random()).substring(2); + /* + <filter id="blocklyEmbossFilter837493"> + <feGaussianBlur in="SourceAlpha" stdDeviation="1" result="blur"/> + <feSpecularLighting in="blur" surfaceScale="1" specularConstant="0.5" + specularExponent="10" lighting-color="white" + result="specOut"> + <fePointLight x="-5000" y="-10000" z="20000"/> + </feSpecularLighting> + <feComposite in="specOut" in2="SourceAlpha" operator="in" + result="specOut"/> + <feComposite in="SourceGraphic" in2="specOut" operator="arithmetic" + k1="0" k2="1" k3="1" k4="0"/> + </filter> + */ + var embossFilter = Blockly.createSvgElement('filter', + {'id': 'blocklyEmbossFilter' + rnd}, defs); + Blockly.createSvgElement('feGaussianBlur', + {'in': 'SourceAlpha', 'stdDeviation': 1, 'result': 'blur'}, embossFilter); + var feSpecularLighting = Blockly.createSvgElement('feSpecularLighting', + {'in': 'blur', 'surfaceScale': 1, 'specularConstant': 0.5, + 'specularExponent': 10, 'lighting-color': 'white', 'result': 'specOut'}, + embossFilter); + Blockly.createSvgElement('fePointLight', + {'x': -5000, 'y': -10000, 'z': 20000}, feSpecularLighting); + Blockly.createSvgElement('feComposite', + {'in': 'specOut', 'in2': 'SourceAlpha', 'operator': 'in', + 'result': 'specOut'}, embossFilter); + Blockly.createSvgElement('feComposite', + {'in': 'SourceGraphic', 'in2': 'specOut', 'operator': 'arithmetic', + 'k1': 0, 'k2': 1, 'k3': 1, 'k4': 0}, embossFilter); + options.embossFilterId = embossFilter.id; + /* + <pattern id="blocklyDisabledPattern837493" patternUnits="userSpaceOnUse" + width="10" height="10"> + <rect width="10" height="10" fill="#aaa" /> + <path d="M 0 0 L 10 10 M 10 0 L 0 10" stroke="#cc0" /> + </pattern> + */ + var disabledPattern = Blockly.createSvgElement('pattern', + {'id': 'blocklyDisabledPattern' + rnd, + 'patternUnits': 'userSpaceOnUse', + 'width': 10, 'height': 10}, defs); + Blockly.createSvgElement('rect', + {'width': 10, 'height': 10, 'fill': '#aaa'}, disabledPattern); + Blockly.createSvgElement('path', + {'d': 'M 0 0 L 10 10 M 10 0 L 0 10', 'stroke': '#cc0'}, disabledPattern); + options.disabledPatternId = disabledPattern.id; + /* + <pattern id="blocklyGridPattern837493" patternUnits="userSpaceOnUse"> + <rect stroke="#888" /> + <rect stroke="#888" /> + </pattern> + */ + var gridPattern = Blockly.createSvgElement('pattern', + {'id': 'blocklyGridPattern' + rnd, + 'patternUnits': 'userSpaceOnUse'}, defs); + if (options.gridOptions['length'] > 0 && options.gridOptions['spacing'] > 0) { + Blockly.createSvgElement('line', + {'stroke': options.gridOptions['colour']}, + gridPattern); + if (options.gridOptions['length'] > 1) { + Blockly.createSvgElement('line', + {'stroke': options.gridOptions['colour']}, + gridPattern); + } + // x1, y1, x1, x2 properties will be set later in updateGridPattern_. + } + options.gridPattern = gridPattern; + return svg; +}; + +/** + * Create a main workspace and add it to the SVG. + * @param {!Element} svg SVG element with pattern defined. + * @param {!Blockly.Options} options Dictionary of options. + * @return {!Blockly.Workspace} Newly created main workspace. + * @private + */ +Blockly.createMainWorkspace_ = function(svg, options) { + options.parentWorkspace = null; + var mainWorkspace = new Blockly.WorkspaceSvg(options); + mainWorkspace.scale = options.zoomOptions.startScale; + svg.appendChild(mainWorkspace.createDom('blocklyMainBackground')); + // A null translation will also apply the correct initial scale. + mainWorkspace.translate(0, 0); + mainWorkspace.markFocused(); + + if (!options.readOnly && !options.hasScrollbars) { + var workspaceChanged = function() { + if (Blockly.dragMode_ == Blockly.DRAG_NONE) { + var metrics = mainWorkspace.getMetrics(); + var edgeLeft = metrics.viewLeft + metrics.absoluteLeft; + var edgeTop = metrics.viewTop + metrics.absoluteTop; + if (metrics.contentTop < edgeTop || + metrics.contentTop + metrics.contentHeight > + metrics.viewHeight + edgeTop || + metrics.contentLeft < + (options.RTL ? metrics.viewLeft : edgeLeft) || + metrics.contentLeft + metrics.contentWidth > (options.RTL ? + metrics.viewWidth : metrics.viewWidth + edgeLeft)) { + // One or more blocks may be out of bounds. Bump them back in. + var MARGIN = 25; + var blocks = mainWorkspace.getTopBlocks(false); + for (var b = 0, block; block = blocks[b]; b++) { + var blockXY = block.getRelativeToSurfaceXY(); + var blockHW = block.getHeightWidth(); + // Bump any block that's above the top back inside. + var overflowTop = edgeTop + MARGIN - blockHW.height - blockXY.y; + if (overflowTop > 0) { + block.moveBy(0, overflowTop); + } + // Bump any block that's below the bottom back inside. + var overflowBottom = + edgeTop + metrics.viewHeight - MARGIN - blockXY.y; + if (overflowBottom < 0) { + block.moveBy(0, overflowBottom); + } + // Bump any block that's off the left back inside. + var overflowLeft = MARGIN + edgeLeft - + blockXY.x - (options.RTL ? 0 : blockHW.width); + if (overflowLeft > 0) { + block.moveBy(overflowLeft, 0); + } + // Bump any block that's off the right back inside. + var overflowRight = edgeLeft + metrics.viewWidth - MARGIN - + blockXY.x + (options.RTL ? blockHW.width : 0); + if (overflowRight < 0) { + block.moveBy(overflowRight, 0); + } + } + } + } + }; + mainWorkspace.addChangeListener(workspaceChanged); + } + // The SVG is now fully assembled. + Blockly.svgResize(mainWorkspace); + Blockly.WidgetDiv.createDom(); + Blockly.Tooltip.createDom(); + return mainWorkspace; +}; + +/** + * Initialize Blockly with various handlers. + * @param {!Blockly.Workspace} mainWorkspace Newly created main workspace. + * @private + */ +Blockly.init_ = function(mainWorkspace) { + var options = mainWorkspace.options; + var svg = mainWorkspace.getParentSvg(); + + // Supress the browser's context menu. + Blockly.bindEvent_(svg, 'contextmenu', null, + function(e) { + if (!Blockly.isTargetInput_(e)) { + e.preventDefault(); + } + }); + + var workspaceResizeHandler = Blockly.bindEvent_(window, 'resize', null, + function() { + Blockly.hideChaff(true); + Blockly.svgResize(mainWorkspace); + }); + mainWorkspace.setResizeHandlerWrapper(workspaceResizeHandler); + + Blockly.inject.bindDocumentEvents_(); + + if (options.languageTree) { + if (mainWorkspace.toolbox_) { + mainWorkspace.toolbox_.init(mainWorkspace); + } else if (mainWorkspace.flyout_) { + // Build a fixed flyout with the root blocks. + mainWorkspace.flyout_.init(mainWorkspace); + mainWorkspace.flyout_.show(options.languageTree.childNodes); + mainWorkspace.flyout_.scrollToStart(); + // Translate the workspace sideways to avoid the fixed flyout. + mainWorkspace.scrollX = mainWorkspace.flyout_.width_; + if (options.toolboxPosition == Blockly.TOOLBOX_AT_RIGHT) { + mainWorkspace.scrollX *= -1; + } + mainWorkspace.translate(mainWorkspace.scrollX, 0); + } + } + + if (options.hasScrollbars) { + mainWorkspace.scrollbar = new Blockly.ScrollbarPair(mainWorkspace); + mainWorkspace.scrollbar.resize(); + } + + // Load the sounds. + if (options.hasSounds) { + Blockly.inject.loadSounds_(options.pathToMedia, mainWorkspace); + } +}; + +/** + * Bind document events, but only once. Destroying and reinjecting Blockly + * should not bind again. + * Bind events for scrolling the workspace. + * Most of these events should be bound to the SVG's surface. + * However, 'mouseup' has to be on the whole document so that a block dragged + * out of bounds and released will know that it has been released. + * Also, 'keydown' has to be on the whole document since the browser doesn't + * understand a concept of focus on the SVG image. + * @private + */ +Blockly.inject.bindDocumentEvents_ = function() { + if (!Blockly.documentEventsBound_) { + Blockly.bindEvent_(document, 'keydown', null, Blockly.onKeyDown_); + Blockly.bindEvent_(document, 'touchend', null, Blockly.longStop_); + Blockly.bindEvent_(document, 'touchcancel', null, Blockly.longStop_); + // Don't use bindEvent_ for document's mouseup since that would create a + // corresponding touch handler that would squeltch the ability to interact + // with non-Blockly elements. + document.addEventListener('mouseup', Blockly.onMouseUp_, false); + // Some iPad versions don't fire resize after portrait to landscape change. + if (goog.userAgent.IPAD) { + Blockly.bindEvent_(window, 'orientationchange', document, function() { + // TODO(#397): Fix for multiple blockly workspaces. + Blockly.svgResize(Blockly.getMainWorkspace()); + }); + } + } + Blockly.documentEventsBound_ = true; +}; + +/** + * Load sounds for the given workspace. + * @param {string} pathToMedia The path to the media directory. + * @param {!Blockly.Workspace} workspace The workspace to load sounds for. + * @private + */ +Blockly.inject.loadSounds_ = function(pathToMedia, workspace) { + workspace.loadAudio_( + [pathToMedia + 'click.mp3', + pathToMedia + 'click.wav', + pathToMedia + 'click.ogg'], 'click'); + workspace.loadAudio_( + [pathToMedia + 'disconnect.wav', + pathToMedia + 'disconnect.mp3', + pathToMedia + 'disconnect.ogg'], 'disconnect'); + workspace.loadAudio_( + [pathToMedia + 'delete.mp3', + pathToMedia + 'delete.ogg', + pathToMedia + 'delete.wav'], 'delete'); + + // Bind temporary hooks that preload the sounds. + var soundBinds = []; + var unbindSounds = function() { + while (soundBinds.length) { + Blockly.unbindEvent_(soundBinds.pop()); + } + workspace.preloadAudio_(); + }; + // Android ignores any sound not loaded as a result of a user action. + soundBinds.push( + Blockly.bindEvent_(document, 'mousemove', null, unbindSounds)); + soundBinds.push( + Blockly.bindEvent_(document, 'touchstart', null, unbindSounds)); +}; + +/** + * Modify the block tree on the existing toolbox. + * @param {Node|string} tree DOM tree of blocks, or text representation of same. + */ +Blockly.updateToolbox = function(tree) { + console.warn('Deprecated call to Blockly.updateToolbox, ' + + 'use workspace.updateToolbox instead.'); + Blockly.getMainWorkspace().updateToolbox(tree); +}; diff --git a/src/blockly/core/input.js b/src/blockly/core/input.js new file mode 100644 index 0000000..4b4eb1d --- /dev/null +++ b/src/blockly/core/input.js @@ -0,0 +1,241 @@ +/** + * @license + * Visual Blocks Editor + * + * Copyright 2012 Google Inc. + * https://developers.google.com/blockly/ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Object representing an input (value, statement, or dummy). + * @author fraser@google.com (Neil Fraser) + */ +'use strict'; + +goog.provide('Blockly.Input'); + +goog.require('Blockly.Connection'); +goog.require('Blockly.FieldLabel'); +goog.require('goog.asserts'); + + +/** + * Class for an input with an optional field. + * @param {number} type The type of the input. + * @param {string} name Language-neutral identifier which may used to find this + * input again. + * @param {!Blockly.Block} block The block containing this input. + * @param {Blockly.Connection} connection Optional connection for this input. + * @constructor + */ +Blockly.Input = function(type, name, block, connection) { + /** @type {number} */ + this.type = type; + /** @type {string} */ + this.name = name; + /** + * @type {!Blockly.Block} + * @private + */ + this.sourceBlock_ = block; + /** @type {Blockly.Connection} */ + this.connection = connection; + /** @type {!Array.<!Blockly.Field>} */ + this.fieldRow = []; +}; + +/** + * Alignment of input's fields (left, right or centre). + * @type {number} + */ +Blockly.Input.prototype.align = Blockly.ALIGN_LEFT; + +/** + * Is the input visible? + * @type {boolean} + * @private + */ +Blockly.Input.prototype.visible_ = true; + +/** + * Add an item to the end of the input's field row. + * @param {string|!Blockly.Field} field Something to add as a field. + * @param {string=} opt_name Language-neutral identifier which may used to find + * this field again. Should be unique to the host block. + * @return {!Blockly.Input} The input being append to (to allow chaining). + */ +Blockly.Input.prototype.appendField = function(field, opt_name) { + // Empty string, Null or undefined generates no field, unless field is named. + if (!field && !opt_name) { + return this; + } + // Generate a FieldLabel when given a plain text field. + if (goog.isString(field)) { + field = new Blockly.FieldLabel(/** @type {string} */ (field)); + } + field.setSourceBlock(this.sourceBlock_); + if (this.sourceBlock_.rendered) { + field.init(); + } + field.name = opt_name; + + if (field.prefixField) { + // Add any prefix. + this.appendField(field.prefixField); + } + // Add the field to the field row. + this.fieldRow.push(field); + if (field.suffixField) { + // Add any suffix. + this.appendField(field.suffixField); + } + + if (this.sourceBlock_.rendered) { + this.sourceBlock_.render(); + // Adding a field will cause the block to change shape. + this.sourceBlock_.bumpNeighbours_(); + } + return this; +}; + +/** + * Add an item to the end of the input's field row. + * @param {*} field Something to add as a field. + * @param {string=} opt_name Language-neutral identifier which may used to find + * this field again. Should be unique to the host block. + * @return {!Blockly.Input} The input being append to (to allow chaining). + * @deprecated December 2013 + */ +Blockly.Input.prototype.appendTitle = function(field, opt_name) { + console.warn('Deprecated call to appendTitle, use appendField instead.'); + return this.appendField(field, opt_name); +}; + +/** + * Remove a field from this input. + * @param {string} name The name of the field. + * @throws {goog.asserts.AssertionError} if the field is not present. + */ +Blockly.Input.prototype.removeField = function(name) { + for (var i = 0, field; field = this.fieldRow[i]; i++) { + if (field.name === name) { + field.dispose(); + this.fieldRow.splice(i, 1); + if (this.sourceBlock_.rendered) { + this.sourceBlock_.render(); + // Removing a field will cause the block to change shape. + this.sourceBlock_.bumpNeighbours_(); + } + return; + } + } + goog.asserts.fail('Field "%s" not found.', name); +}; + +/** + * Gets whether this input is visible or not. + * @return {boolean} True if visible. + */ +Blockly.Input.prototype.isVisible = function() { + return this.visible_; +}; + +/** + * Sets whether this input is visible or not. + * Used to collapse/uncollapse a block. + * @param {boolean} visible True if visible. + * @return {!Array.<!Blockly.Block>} List of blocks to render. + */ +Blockly.Input.prototype.setVisible = function(visible) { + var renderList = []; + if (this.visible_ == visible) { + return renderList; + } + this.visible_ = visible; + + var display = visible ? 'block' : 'none'; + for (var y = 0, field; field = this.fieldRow[y]; y++) { + field.setVisible(visible); + } + if (this.connection) { + // Has a connection. + if (visible) { + renderList = this.connection.unhideAll(); + } else { + this.connection.hideAll(); + } + var child = this.connection.targetBlock(); + if (child) { + child.getSvgRoot().style.display = display; + if (!visible) { + child.rendered = false; + } + } + } + return renderList; +}; + +/** + * Change a connection's compatibility. + * @param {string|Array.<string>|null} check Compatible value type or + * list of value types. Null if all types are compatible. + * @return {!Blockly.Input} The input being modified (to allow chaining). + */ +Blockly.Input.prototype.setCheck = function(check) { + if (!this.connection) { + throw 'This input does not have a connection.'; + } + this.connection.setCheck(check); + return this; +}; + +/** + * Change the alignment of the connection's field(s). + * @param {number} align One of Blockly.ALIGN_LEFT, ALIGN_CENTRE, ALIGN_RIGHT. + * In RTL mode directions are reversed, and ALIGN_RIGHT aligns to the left. + * @return {!Blockly.Input} The input being modified (to allow chaining). + */ +Blockly.Input.prototype.setAlign = function(align) { + this.align = align; + if (this.sourceBlock_.rendered) { + this.sourceBlock_.render(); + } + return this; +}; + +/** + * Initialize the fields on this input. + */ +Blockly.Input.prototype.init = function() { + if (!this.sourceBlock_.workspace.rendered) { + return; // Headless blocks don't need fields initialized. + } + for (var i = 0; i < this.fieldRow.length; i++) { + this.fieldRow[i].init(); + } +}; + +/** + * Sever all links to this input. + */ +Blockly.Input.prototype.dispose = function() { + for (var i = 0, field; field = this.fieldRow[i]; i++) { + field.dispose(); + } + if (this.connection) { + this.connection.dispose(); + } + this.sourceBlock_ = null; +}; diff --git a/src/blockly/core/msg.js b/src/blockly/core/msg.js new file mode 100644 index 0000000..4ebcad1 --- /dev/null +++ b/src/blockly/core/msg.js @@ -0,0 +1,62 @@ +/** + * @license + * Visual Blocks Editor + * + * Copyright 2013 Google Inc. + * https://developers.google.com/blockly/ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Empty name space for the Message singleton. + * @author scr@google.com (Sheridan Rawlins) + */ +'use strict'; + +/** + * Name space for the Msg singleton. + * Msg gets populated in the message files. + */ +goog.provide('Blockly.Msg'); + + +/** + * Back up original getMsg function. + * @type {!Function} + */ +goog.getMsgOrig = goog.getMsg; + +/** + * Gets a localized message. + * Overrides the default Closure function to check for a Blockly.Msg first. + * Used infrequently, only known case is TODAY button in date picker. + * @param {string} str Translatable string, places holders in the form {$foo}. + * @param {Object<string, string>=} opt_values Maps place holder name to value. + * @return {string} message with placeholders filled. + * @suppress {duplicate} + */ +goog.getMsg = function(str, opt_values) { + var key = goog.getMsg.blocklyMsgMap[str]; + if (key) { + str = Blockly.Msg[key]; + } + return goog.getMsgOrig(str, opt_values); +}; + +/** + * Mapping of Closure messages to Blockly.Msg names. + */ +goog.getMsg.blocklyMsgMap = { + 'Today': 'TODAY' +}; diff --git a/src/blockly/core/mutator.js b/src/blockly/core/mutator.js new file mode 100644 index 0000000..19a0fe8 --- /dev/null +++ b/src/blockly/core/mutator.js @@ -0,0 +1,389 @@ +/** + * @license + * Visual Blocks Editor + * + * Copyright 2012 Google Inc. + * https://developers.google.com/blockly/ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Object representing a mutator dialog. A mutator allows the + * user to change the shape of a block using a nested blocks editor. + * @author fraser@google.com (Neil Fraser) + */ +'use strict'; + +goog.provide('Blockly.Mutator'); + +goog.require('Blockly.Bubble'); +goog.require('Blockly.Icon'); +goog.require('Blockly.WorkspaceSvg'); +goog.require('goog.Timer'); +goog.require('goog.dom'); + + +/** + * Class for a mutator dialog. + * @param {!Array.<string>} quarkNames List of names of sub-blocks for flyout. + * @extends {Blockly.Icon} + * @constructor + */ +Blockly.Mutator = function(quarkNames) { + Blockly.Mutator.superClass_.constructor.call(this, null); + this.quarkNames_ = quarkNames; +}; +goog.inherits(Blockly.Mutator, Blockly.Icon); + +/** + * Width of workspace. + * @private + */ +Blockly.Mutator.prototype.workspaceWidth_ = 0; + +/** + * Height of workspace. + * @private + */ +Blockly.Mutator.prototype.workspaceHeight_ = 0; + +/** + * Draw the mutator icon. + * @param {!Element} group The icon group. + * @private + */ +Blockly.Mutator.prototype.drawIcon_ = function(group) { + // Square with rounded corners. + Blockly.createSvgElement('rect', + {'class': 'blocklyIconShape', + 'rx': '4', 'ry': '4', + 'height': '16', 'width': '16'}, + group); + // Gear teeth. + Blockly.createSvgElement('path', + {'class': 'blocklyIconSymbol', + 'd': 'm4.203,7.296 0,1.368 -0.92,0.677 -0.11,0.41 0.9,1.559 0.41,0.11 1.043,-0.457 1.187,0.683 0.127,1.134 0.3,0.3 1.8,0 0.3,-0.299 0.127,-1.138 1.185,-0.682 1.046,0.458 0.409,-0.11 0.9,-1.559 -0.11,-0.41 -0.92,-0.677 0,-1.366 0.92,-0.677 0.11,-0.41 -0.9,-1.559 -0.409,-0.109 -1.046,0.458 -1.185,-0.682 -0.127,-1.138 -0.3,-0.299 -1.8,0 -0.3,0.3 -0.126,1.135 -1.187,0.682 -1.043,-0.457 -0.41,0.11 -0.899,1.559 0.108,0.409z'}, + group); + // Axle hole. + Blockly.createSvgElement('circle', + {'class': 'blocklyIconShape', 'r': '2.7', 'cx': '8', 'cy': '8'}, + group); +}; + +/** + * Clicking on the icon toggles if the mutator bubble is visible. + * Disable if block is uneditable. + * @param {!Event} e Mouse click event. + * @private + * @override + */ +Blockly.Mutator.prototype.iconClick_ = function(e) { + if (this.block_.isEditable()) { + Blockly.Icon.prototype.iconClick_.call(this, e); + } +}; + +/** + * Create the editor for the mutator's bubble. + * @return {!Element} The top-level node of the editor. + * @private + */ +Blockly.Mutator.prototype.createEditor_ = function() { + /* Create the editor. Here's the markup that will be generated: + <svg> + [Workspace] + </svg> + */ + this.svgDialog_ = Blockly.createSvgElement('svg', + {'x': Blockly.Bubble.BORDER_WIDTH, 'y': Blockly.Bubble.BORDER_WIDTH}, + null); + // Convert the list of names into a list of XML objects for the flyout. + if (this.quarkNames_.length) { + var quarkXml = goog.dom.createDom('xml'); + for (var i = 0, quarkName; quarkName = this.quarkNames_[i]; i++) { + quarkXml.appendChild(goog.dom.createDom('block', {'type': quarkName})); + } + } else { + var quarkXml = null; + } + var workspaceOptions = { + languageTree: quarkXml, + parentWorkspace: this.block_.workspace, + pathToMedia: this.block_.workspace.options.pathToMedia, + RTL: this.block_.RTL, + toolboxPosition: this.block_.RTL ? Blockly.TOOLBOX_AT_RIGHT : + Blockly.TOOLBOX_AT_LEFT, + horizontalLayout: false, + getMetrics: this.getFlyoutMetrics_.bind(this), + setMetrics: null + }; + this.workspace_ = new Blockly.WorkspaceSvg(workspaceOptions); + this.workspace_.isMutator = true; + this.svgDialog_.appendChild( + this.workspace_.createDom('blocklyMutatorBackground')); + return this.svgDialog_; +}; + +/** + * Add or remove the UI indicating if this icon may be clicked or not. + */ +Blockly.Mutator.prototype.updateEditable = function() { + if (!this.block_.isInFlyout) { + if (this.block_.isEditable()) { + if (this.iconGroup_) { + Blockly.removeClass_(/** @type {!Element} */ (this.iconGroup_), + 'blocklyIconGroupReadonly'); + } + } else { + // Close any mutator bubble. Icon is not clickable. + this.setVisible(false); + if (this.iconGroup_) { + Blockly.addClass_(/** @type {!Element} */ (this.iconGroup_), + 'blocklyIconGroupReadonly'); + } + } + } + // Default behaviour for an icon. + Blockly.Icon.prototype.updateEditable.call(this); +}; + +/** + * Callback function triggered when the bubble has resized. + * Resize the workspace accordingly. + * @private + */ +Blockly.Mutator.prototype.resizeBubble_ = function() { + var doubleBorderWidth = 2 * Blockly.Bubble.BORDER_WIDTH; + var workspaceSize = this.workspace_.getCanvas().getBBox(); + var width; + if (this.block_.RTL) { + width = -workspaceSize.x; + } else { + width = workspaceSize.width + workspaceSize.x; + } + var height = workspaceSize.height + doubleBorderWidth * 3; + if (this.workspace_.flyout_) { + var flyoutMetrics = this.workspace_.flyout_.getMetrics_(); + height = Math.max(height, flyoutMetrics.contentHeight + 20); + } + width += doubleBorderWidth * 3; + // Only resize if the size difference is significant. Eliminates shuddering. + if (Math.abs(this.workspaceWidth_ - width) > doubleBorderWidth || + Math.abs(this.workspaceHeight_ - height) > doubleBorderWidth) { + // Record some layout information for getFlyoutMetrics_. + this.workspaceWidth_ = width; + this.workspaceHeight_ = height; + // Resize the bubble. + this.bubble_.setBubbleSize(width + doubleBorderWidth, + height + doubleBorderWidth); + this.svgDialog_.setAttribute('width', this.workspaceWidth_); + this.svgDialog_.setAttribute('height', this.workspaceHeight_); + } + + if (this.block_.RTL) { + // Scroll the workspace to always left-align. + var translation = 'translate(' + this.workspaceWidth_ + ',0)'; + this.workspace_.getCanvas().setAttribute('transform', translation); + } + this.workspace_.resize(); +}; + +/** + * Show or hide the mutator bubble. + * @param {boolean} visible True if the bubble should be visible. + */ +Blockly.Mutator.prototype.setVisible = function(visible) { + if (visible == this.isVisible()) { + // No change. + return; + } + Blockly.Events.fire( + new Blockly.Events.Ui(this.block_, 'mutatorOpen', !visible, visible)); + if (visible) { + // Create the bubble. + this.bubble_ = new Blockly.Bubble( + /** @type {!Blockly.WorkspaceSvg} */ (this.block_.workspace), + this.createEditor_(), this.block_.svgPath_, this.iconXY_, null, null); + var tree = this.workspace_.options.languageTree; + if (tree) { + this.workspace_.flyout_.init(this.workspace_); + this.workspace_.flyout_.show(tree.childNodes); + } + + this.rootBlock_ = this.block_.decompose(this.workspace_); + var blocks = this.rootBlock_.getDescendants(); + for (var i = 0, child; child = blocks[i]; i++) { + child.render(); + } + // The root block should not be dragable or deletable. + this.rootBlock_.setMovable(false); + this.rootBlock_.setDeletable(false); + if (this.workspace_.flyout_) { + var margin = this.workspace_.flyout_.CORNER_RADIUS * 2; + var x = this.workspace_.flyout_.width_ + margin; + } else { + var margin = 16; + var x = margin; + } + if (this.block_.RTL) { + x = -x; + } + this.rootBlock_.moveBy(x, margin); + // Save the initial connections, then listen for further changes. + if (this.block_.saveConnections) { + var thisMutator = this; + this.block_.saveConnections(this.rootBlock_); + this.sourceListener_ = function() { + thisMutator.block_.saveConnections(thisMutator.rootBlock_); + }; + this.block_.workspace.addChangeListener(this.sourceListener_); + } + this.resizeBubble_(); + // When the mutator's workspace changes, update the source block. + this.workspace_.addChangeListener(this.workspaceChanged_.bind(this)); + this.updateColour(); + } else { + // Dispose of the bubble. + this.svgDialog_ = null; + this.workspace_.dispose(); + this.workspace_ = null; + this.rootBlock_ = null; + this.bubble_.dispose(); + this.bubble_ = null; + this.workspaceWidth_ = 0; + this.workspaceHeight_ = 0; + if (this.sourceListener_) { + this.block_.workspace.removeChangeListener(this.sourceListener_); + this.sourceListener_ = null; + } + } +}; + +/** + * Update the source block when the mutator's blocks are changed. + * Bump down any block that's too high. + * Fired whenever a change is made to the mutator's workspace. + * @private + */ +Blockly.Mutator.prototype.workspaceChanged_ = function() { + if (Blockly.dragMode_ == Blockly.DRAG_NONE) { + var blocks = this.workspace_.getTopBlocks(false); + var MARGIN = 20; + for (var b = 0, block; block = blocks[b]; b++) { + var blockXY = block.getRelativeToSurfaceXY(); + var blockHW = block.getHeightWidth(); + if (blockXY.y + blockHW.height < MARGIN) { + // Bump any block that's above the top back inside. + block.moveBy(0, MARGIN - blockHW.height - blockXY.y); + } + } + } + + // When the mutator's workspace changes, update the source block. + if (this.rootBlock_.workspace == this.workspace_) { + Blockly.Events.setGroup(true); + var block = this.block_; + var oldMutationDom = block.mutationToDom(); + var oldMutation = oldMutationDom && Blockly.Xml.domToText(oldMutationDom); + // Switch off rendering while the source block is rebuilt. + var savedRendered = block.rendered; + block.rendered = false; + // Allow the source block to rebuild itself. + block.compose(this.rootBlock_); + // Restore rendering and show the changes. + block.rendered = savedRendered; + // Mutation may have added some elements that need initalizing. + block.initSvg(); + var newMutationDom = block.mutationToDom(); + var newMutation = newMutationDom && Blockly.Xml.domToText(newMutationDom); + if (oldMutation != newMutation) { + Blockly.Events.fire(new Blockly.Events.Change( + block, 'mutation', null, oldMutation, newMutation)); + // Ensure that any bump is part of this mutation's event group. + var group = Blockly.Events.getGroup(); + setTimeout(function() { + Blockly.Events.setGroup(group); + block.bumpNeighbours_(); + Blockly.Events.setGroup(false); + }, Blockly.BUMP_DELAY); + } + if (block.rendered) { + block.render(); + } + this.resizeBubble_(); + Blockly.Events.setGroup(false); + } +}; + +/** + * Return an object with all the metrics required to size scrollbars for the + * mutator flyout. The following properties are computed: + * .viewHeight: Height of the visible rectangle, + * .viewWidth: Width of the visible rectangle, + * .absoluteTop: Top-edge of view. + * .absoluteLeft: Left-edge of view. + * @return {!Object} Contains size and position metrics of mutator dialog's + * workspace. + * @private + */ +Blockly.Mutator.prototype.getFlyoutMetrics_ = function() { + return { + viewHeight: this.workspaceHeight_, + viewWidth: this.workspaceWidth_, + absoluteTop: 0, + absoluteLeft: 0 + }; +}; + +/** + * Dispose of this mutator. + */ +Blockly.Mutator.prototype.dispose = function() { + this.block_.mutator = null; + Blockly.Icon.prototype.dispose.call(this); +}; + +/** + * Reconnect an block to a mutated input. + * @param {Blockly.Connection} connectionChild Connection on child block. + * @param {!Blockly.Block} block Parent block. + * @param {string} inputName Name of input on parent block. + * @return {boolean} True iff a reconnection was made, false otherwise. + */ +Blockly.Mutator.reconnect = function(connectionChild, block, inputName) { + if (!connectionChild || !connectionChild.getSourceBlock().workspace) { + return false; // No connection or block has been deleted. + } + var connectionParent = block.getInput(inputName).connection; + var currentParent = connectionChild.targetBlock(); + if ((!currentParent || currentParent == block) && + connectionParent.targetConnection != connectionChild) { + if (connectionParent.isConnected()) { + // There's already something connected here. Get rid of it. + connectionParent.disconnect(); + } + connectionParent.connect(connectionChild); + return true; + } + return false; +}; + +// Export symbols that would otherwise be renamed by Closure compiler. +if (!goog.global['Blockly']) { + goog.global['Blockly'] = {}; +} +if (!goog.global['Blockly']['Mutator']) { + goog.global['Blockly']['Mutator'] = {}; +} +goog.global['Blockly']['Mutator']['reconnect'] = Blockly.Mutator.reconnect; diff --git a/src/blockly/core/names.js b/src/blockly/core/names.js new file mode 100644 index 0000000..bfe942a --- /dev/null +++ b/src/blockly/core/names.js @@ -0,0 +1,143 @@ +/** + * @license + * Visual Blocks Editor + * + * Copyright 2012 Google Inc. + * https://developers.google.com/blockly/ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Utility functions for handling variables and procedure names. + * @author fraser@google.com (Neil Fraser) + */ +'use strict'; + +goog.provide('Blockly.Names'); + + +/** + * Class for a database of entity names (variables, functions, etc). + * @param {string} reservedWords A comma-separated string of words that are + * illegal for use as names in a language (e.g. 'new,if,this,...'). + * @param {string=} opt_variablePrefix Some languages need a '$' or a namespace + * before all variable names. + * @constructor + */ +Blockly.Names = function(reservedWords, opt_variablePrefix) { + this.variablePrefix_ = opt_variablePrefix || ''; + this.reservedDict_ = Object.create(null); + if (reservedWords) { + var splitWords = reservedWords.split(','); + for (var i = 0; i < splitWords.length; i++) { + this.reservedDict_[splitWords[i]] = true; + } + } + this.reset(); +}; + +/** + * When JavaScript (or most other languages) is generated, variable 'foo' and + * procedure 'foo' would collide. However, Blockly has no such problems since + * variable get 'foo' and procedure call 'foo' are unambiguous. + * Therefore, Blockly keeps a separate type name to disambiguate. + * getName('foo', 'variable') -> 'foo' + * getName('foo', 'procedure') -> 'foo2' + */ + +/** + * Empty the database and start from scratch. The reserved words are kept. + */ +Blockly.Names.prototype.reset = function() { + this.db_ = Object.create(null); + this.dbReverse_ = Object.create(null); +}; + +/** + * Convert a Blockly entity name to a legal exportable entity name. + * @param {string} name The Blockly entity name (no constraints). + * @param {string} type The type of entity in Blockly + * ('VARIABLE', 'PROCEDURE', 'BUILTIN', etc...). + * @return {string} An entity name legal for the exported language. + */ +Blockly.Names.prototype.getName = function(name, type) { + var normalized = name.toLowerCase() + '_' + type; + var prefix = (type == Blockly.Variables.NAME_TYPE) ? + this.variablePrefix_ : ''; + if (normalized in this.db_) { + return prefix + this.db_[normalized]; + } + var safeName = this.getDistinctName(name, type); + this.db_[normalized] = safeName.substr(prefix.length); + return safeName; +}; + +/** + * Convert a Blockly entity name to a legal exportable entity name. + * Ensure that this is a new name not overlapping any previously defined name. + * Also check against list of reserved words for the current language and + * ensure name doesn't collide. + * @param {string} name The Blockly entity name (no constraints). + * @param {string} type The type of entity in Blockly + * ('VARIABLE', 'PROCEDURE', 'BUILTIN', etc...). + * @return {string} An entity name legal for the exported language. + */ +Blockly.Names.prototype.getDistinctName = function(name, type) { + var safeName = this.safeName_(name); + var i = ''; + while (this.dbReverse_[safeName + i] || + (safeName + i) in this.reservedDict_) { + // Collision with existing name. Create a unique name. + i = i ? i + 1 : 2; + } + safeName += i; + this.dbReverse_[safeName] = true; + var prefix = (type == Blockly.Variables.NAME_TYPE) ? + this.variablePrefix_ : ''; + return prefix + safeName; +}; + +/** + * Given a proposed entity name, generate a name that conforms to the + * [_A-Za-z][_A-Za-z0-9]* format that most languages consider legal for + * variables. + * @param {string} name Potentially illegal entity name. + * @return {string} Safe entity name. + * @private + */ +Blockly.Names.prototype.safeName_ = function(name) { + if (!name) { + name = 'unnamed'; + } else { + // Unfortunately names in non-latin characters will look like + // _E9_9F_B3_E4_B9_90 which is pretty meaningless. + name = encodeURI(name.replace(/ /g, '_')).replace(/[^\w]/g, '_'); + // Most languages don't allow names with leading numbers. + if ('0123456789'.indexOf(name[0]) != -1) { + name = 'my_' + name; + } + } + return name; +}; + +/** + * Do the given two entity names refer to the same entity? + * Blockly names are case-insensitive. + * @param {string} name1 First name. + * @param {string} name2 Second name. + * @return {boolean} True if names are the same. + */ +Blockly.Names.equals = function(name1, name2) { + return name1.toLowerCase() == name2.toLowerCase(); +}; diff --git a/src/blockly/core/options.js b/src/blockly/core/options.js new file mode 100644 index 0000000..268affa --- /dev/null +++ b/src/blockly/core/options.js @@ -0,0 +1,231 @@ +/** + * @license + * Visual Blocks Editor + * + * Copyright 2016 Google Inc. + * https://developers.google.com/blockly/ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Object that controls settings for the workspace. + * @author fenichel@google.com (Rachel Fenichel) + */ +'use strict'; + +goog.provide('Blockly.Options'); + + +/** + * Parse the user-specified options, using reasonable defaults where behaviour + * is unspecified. + * @param {!Object} options Dictionary of options. Specification: + * https://developers.google.com/blockly/guides/get-started/web#configuration + * @constructor + */ +Blockly.Options = function(options) { + var readOnly = !!options['readOnly']; + if (readOnly) { + var languageTree = null; + var hasCategories = false; + var hasTrashcan = false; + var hasCollapse = false; + var hasComments = false; + var hasDisable = false; + var hasSounds = false; + } else { + var languageTree = Blockly.Options.parseToolboxTree(options['toolbox']); + var hasCategories = Boolean(languageTree && + languageTree.getElementsByTagName('category').length); + var hasTrashcan = options['trashcan']; + if (hasTrashcan === undefined) { + hasTrashcan = hasCategories; + } + var hasCollapse = options['collapse']; + if (hasCollapse === undefined) { + hasCollapse = hasCategories; + } + var hasComments = options['comments']; + if (hasComments === undefined) { + hasComments = hasCategories; + } + var hasDisable = options['disable']; + if (hasDisable === undefined) { + hasDisable = hasCategories; + } + var hasSounds = options['sounds']; + if (hasSounds === undefined) { + hasSounds = true; + } + } + var rtl = !!options['rtl']; + var horizontalLayout = options['horizontalLayout']; + if (horizontalLayout === undefined) { + horizontalLayout = false; + } + var toolboxAtStart = options['toolboxPosition']; + if (toolboxAtStart === 'end') { + toolboxAtStart = false; + } else { + toolboxAtStart = true; + } + + if (horizontalLayout) { + var toolboxPosition = toolboxAtStart ? + Blockly.TOOLBOX_AT_TOP : Blockly.TOOLBOX_AT_BOTTOM; + } else { + var toolboxPosition = (toolboxAtStart == rtl) ? + Blockly.TOOLBOX_AT_RIGHT : Blockly.TOOLBOX_AT_LEFT; + } + + var hasScrollbars = options['scrollbars']; + if (hasScrollbars === undefined) { + hasScrollbars = hasCategories; + } + var hasCss = options['css']; + if (hasCss === undefined) { + hasCss = true; + } + var pathToMedia = 'https://blockly-demo.appspot.com/static/media/'; + if (options['media']) { + pathToMedia = options['media']; + } else if (options['path']) { + // 'path' is a deprecated option which has been replaced by 'media'. + pathToMedia = options['path'] + 'media/'; + } + + this.RTL = rtl; + this.collapse = hasCollapse; + this.comments = hasComments; + this.disable = hasDisable; + this.readOnly = readOnly; + this.maxBlocks = options['maxBlocks'] || Infinity; + this.pathToMedia = pathToMedia; + this.hasCategories = hasCategories; + this.hasScrollbars = hasScrollbars; + this.hasTrashcan = hasTrashcan; + this.hasSounds = hasSounds; + this.hasCss = hasCss; + this.horizontalLayout = horizontalLayout; + this.languageTree = languageTree; + this.gridOptions = Blockly.Options.parseGridOptions_(options); + this.zoomOptions = Blockly.Options.parseZoomOptions_(options); + this.toolboxPosition = toolboxPosition; +}; + +/** + * @type {Blockly.Workspace} the parent of the current workspace, or null if + * there is no parent workspace. + **/ +Blockly.Options.prototype.parentWorkspace = null; + +/** + * If set, sets the translation of the workspace to match the scrollbars. + */ +Blockly.Options.prototype.setMetrics = null; + +/** + * Return an object with the metrics required to size the workspace. + * @return {Object} Contains size and position metrics, or null. + */ +Blockly.Options.prototype.getMetrics = null; + +/** + * Parse the user-specified zoom options, using reasonable defaults where + * behaviour is unspecified. See zoom documentation: + * https://developers.google.com/blockly/guides/configure/web/zoom + * @param {!Object} options Dictionary of options. + * @return {!Object} A dictionary of normalized options. + * @private + */ +Blockly.Options.parseZoomOptions_ = function(options) { + var zoom = options['zoom'] || {}; + var zoomOptions = {}; + if (zoom['controls'] === undefined) { + zoomOptions.controls = false; + } else { + zoomOptions.controls = !!zoom['controls']; + } + if (zoom['wheel'] === undefined) { + zoomOptions.wheel = false; + } else { + zoomOptions.wheel = !!zoom['wheel']; + } + if (zoom['startScale'] === undefined) { + zoomOptions.startScale = 1; + } else { + zoomOptions.startScale = parseFloat(zoom['startScale']); + } + if (zoom['maxScale'] === undefined) { + zoomOptions.maxScale = 3; + } else { + zoomOptions.maxScale = parseFloat(zoom['maxScale']); + } + if (zoom['minScale'] === undefined) { + zoomOptions.minScale = 0.3; + } else { + zoomOptions.minScale = parseFloat(zoom['minScale']); + } + if (zoom['scaleSpeed'] === undefined) { + zoomOptions.scaleSpeed = 1.2; + } else { + zoomOptions.scaleSpeed = parseFloat(zoom['scaleSpeed']); + } + return zoomOptions; +}; + +/** + * Parse the user-specified grid options, using reasonable defaults where + * behaviour is unspecified. See grid documentation: + * https://developers.google.com/blockly/guides/configure/web/grid + * @param {!Object} options Dictionary of options. + * @return {!Object} A dictionary of normalized options. + * @private + */ +Blockly.Options.parseGridOptions_ = function(options) { + var grid = options['grid'] || {}; + var gridOptions = {}; + gridOptions.spacing = parseFloat(grid['spacing']) || 0; + gridOptions.colour = grid['colour'] || '#888'; + gridOptions.length = parseFloat(grid['length']) || 1; + gridOptions.snap = gridOptions.spacing > 0 && !!grid['snap']; + return gridOptions; +}; + +/** + * Parse the provided toolbox tree into a consistent DOM format. + * @param {Node|string} tree DOM tree of blocks, or text representation of same. + * @return {Node} DOM tree of blocks, or null. + */ +Blockly.Options.parseToolboxTree = function(tree) { + if (tree) { + if (typeof tree != 'string') { + if (typeof XSLTProcessor == 'undefined' && tree.outerHTML) { + // In this case the tree will not have been properly built by the + // browser. The HTML will be contained in the element, but it will + // not have the proper DOM structure since the browser doesn't support + // XSLTProcessor (XML -> HTML). This is the case in IE 9+. + tree = tree.outerHTML; + } else if (!(tree instanceof Element)) { + tree = null; + } + } + if (typeof tree == 'string') { + tree = Blockly.Xml.textToDom(tree); + } + } else { + tree = null; + } + return tree; +}; diff --git a/src/blockly/core/procedures.js b/src/blockly/core/procedures.js new file mode 100644 index 0000000..beb4a17 --- /dev/null +++ b/src/blockly/core/procedures.js @@ -0,0 +1,287 @@ +/** + * @license + * Visual Blocks Editor + * + * Copyright 2012 Google Inc. + * https://developers.google.com/blockly/ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Utility functions for handling procedures. + * @author fraser@google.com (Neil Fraser) + */ +'use strict'; + +goog.provide('Blockly.Procedures'); + +goog.require('Blockly.Blocks'); +goog.require('Blockly.Field'); +goog.require('Blockly.Names'); +goog.require('Blockly.Workspace'); + + +/** + * Category to separate procedure names from variables and generated functions. + */ +Blockly.Procedures.NAME_TYPE = 'PROCEDURE'; + +/** + * Find all user-created procedure definitions in a workspace. + * @param {!Blockly.Workspace} root Root workspace. + * @return {!Array.<!Array.<!Array>>} Pair of arrays, the + * first contains procedures without return variables, the second with. + * Each procedure is defined by a three-element list of name, parameter + * list, and return value boolean. + */ +Blockly.Procedures.allProcedures = function(root) { + var blocks = root.getAllBlocks(); + var proceduresReturn = []; + var proceduresNoReturn = []; + for (var i = 0; i < blocks.length; i++) { + if (blocks[i].getProcedureDef) { + var tuple = blocks[i].getProcedureDef(); + if (tuple) { + if (tuple[2]) { + proceduresReturn.push(tuple); + } else { + proceduresNoReturn.push(tuple); + } + } + } + } + proceduresNoReturn.sort(Blockly.Procedures.procTupleComparator_); + proceduresReturn.sort(Blockly.Procedures.procTupleComparator_); + return [proceduresNoReturn, proceduresReturn]; +}; + +/** + * Comparison function for case-insensitive sorting of the first element of + * a tuple. + * @param {!Array} ta First tuple. + * @param {!Array} tb Second tuple. + * @return {number} -1, 0, or 1 to signify greater than, equality, or less than. + * @private + */ +Blockly.Procedures.procTupleComparator_ = function(ta, tb) { + return ta[0].toLowerCase().localeCompare(tb[0].toLowerCase()); +}; + +/** + * Ensure two identically-named procedures don't exist. + * @param {string} name Proposed procedure name. + * @param {!Blockly.Block} block Block to disambiguate. + * @return {string} Non-colliding name. + */ +Blockly.Procedures.findLegalName = function(name, block) { + if (block.isInFlyout) { + // Flyouts can have multiple procedures called 'do something'. + return name; + } + while (!Blockly.Procedures.isLegalName_(name, block.workspace, block)) { + // Collision with another procedure. + var r = name.match(/^(.*?)(\d+)$/); + if (!r) { + name += '2'; + } else { + name = r[1] + (parseInt(r[2], 10) + 1); + } + } + return name; +}; + +/** + * Does this procedure have a legal name? Illegal names include names of + * procedures already defined. + * @param {string} name The questionable name. + * @param {!Blockly.Workspace} workspace The workspace to scan for collisions. + * @param {Blockly.Block=} opt_exclude Optional block to exclude from + * comparisons (one doesn't want to collide with oneself). + * @return {boolean} True if the name is legal. + * @private + */ +Blockly.Procedures.isLegalName_ = function(name, workspace, opt_exclude) { + var blocks = workspace.getAllBlocks(); + // Iterate through every block and check the name. + for (var i = 0; i < blocks.length; i++) { + if (blocks[i] == opt_exclude) { + continue; + } + if (blocks[i].getProcedureDef) { + var procName = blocks[i].getProcedureDef(); + if (Blockly.Names.equals(procName[0], name)) { + return false; + } + } + } + return true; +}; + +/** + * Rename a procedure. Called by the editable field. + * @param {string} name The proposed new name. + * @return {string} The accepted name. + * @this {!Blockly.Field} + */ +Blockly.Procedures.rename = function(name) { + // Strip leading and trailing whitespace. Beyond this, all names are legal. + name = name.replace(/^[\s\xa0]+|[\s\xa0]+$/g, ''); + + // Ensure two identically-named procedures don't exist. + var legalName = Blockly.Procedures.findLegalName(name, this.sourceBlock_); + var oldName = this.text_; + if (oldName != name && oldName != legalName) { + // Rename any callers. + var blocks = this.sourceBlock_.workspace.getAllBlocks(); + for (var i = 0; i < blocks.length; i++) { + if (blocks[i].renameProcedure) { + blocks[i].renameProcedure(oldName, legalName); + } + } + } + return legalName; +}; + +/** + * Construct the blocks required by the flyout for the procedure category. + * @param {!Blockly.Workspace} workspace The workspace contianing procedures. + * @return {!Array.<!Element>} Array of XML block elements. + */ +Blockly.Procedures.flyoutCategory = function(workspace) { + var xmlList = []; + if (Blockly.Blocks['procedures_defnoreturn']) { + // <block type="procedures_defnoreturn" gap="16"></block> + var block = goog.dom.createDom('block'); + block.setAttribute('type', 'procedures_defnoreturn'); + block.setAttribute('gap', 16); + xmlList.push(block); + } + if (Blockly.Blocks['procedures_defreturn']) { + // <block type="procedures_defreturn" gap="16"></block> + var block = goog.dom.createDom('block'); + block.setAttribute('type', 'procedures_defreturn'); + block.setAttribute('gap', 16); + xmlList.push(block); + } + if (Blockly.Blocks['procedures_ifreturn']) { + // <block type="procedures_ifreturn" gap="16"></block> + var block = goog.dom.createDom('block'); + block.setAttribute('type', 'procedures_ifreturn'); + block.setAttribute('gap', 16); + xmlList.push(block); + } + if (xmlList.length) { + // Add slightly larger gap between system blocks and user calls. + xmlList[xmlList.length - 1].setAttribute('gap', 24); + } + + function populateProcedures(procedureList, templateName) { + for (var i = 0; i < procedureList.length; i++) { + var name = procedureList[i][0]; + var args = procedureList[i][1]; + // <block type="procedures_callnoreturn" gap="16"> + // <mutation name="do something"> + // <arg name="x"></arg> + // </mutation> + // </block> + var block = goog.dom.createDom('block'); + block.setAttribute('type', templateName); + block.setAttribute('gap', 16); + var mutation = goog.dom.createDom('mutation'); + mutation.setAttribute('name', name); + block.appendChild(mutation); + for (var j = 0; j < args.length; j++) { + var arg = goog.dom.createDom('arg'); + arg.setAttribute('name', args[j]); + mutation.appendChild(arg); + } + xmlList.push(block); + } + } + + var tuple = Blockly.Procedures.allProcedures(workspace); + populateProcedures(tuple[0], 'procedures_callnoreturn'); + populateProcedures(tuple[1], 'procedures_callreturn'); + return xmlList; +}; + +/** + * Find all the callers of a named procedure. + * @param {string} name Name of procedure. + * @param {!Blockly.Workspace} workspace The workspace to find callers in. + * @return {!Array.<!Blockly.Block>} Array of caller blocks. + */ +Blockly.Procedures.getCallers = function(name, workspace) { + var callers = []; + var blocks = workspace.getAllBlocks(); + // Iterate through every block and check the name. + for (var i = 0; i < blocks.length; i++) { + if (blocks[i].getProcedureCall) { + var procName = blocks[i].getProcedureCall(); + // Procedure name may be null if the block is only half-built. + if (procName && Blockly.Names.equals(procName, name)) { + callers.push(blocks[i]); + } + } + } + return callers; +}; + +/** + * When a procedure definition changes its parameters, find and edit all its + * callers. + * @param {!Blockly.Block} defBlock Procedure definition block. + */ +Blockly.Procedures.mutateCallers = function(defBlock) { + var oldRecordUndo = Blockly.Events.recordUndo; + var name = defBlock.getProcedureDef()[0]; + var xmlElement = defBlock.mutationToDom(true); + var callers = Blockly.Procedures.getCallers(name, defBlock.workspace); + for (var i = 0, caller; caller = callers[i]; i++) { + var oldMutationDom = caller.mutationToDom(); + var oldMutation = oldMutationDom && Blockly.Xml.domToText(oldMutationDom); + caller.domToMutation(xmlElement); + var newMutationDom = caller.mutationToDom(); + var newMutation = newMutationDom && Blockly.Xml.domToText(newMutationDom); + if (oldMutation != newMutation) { + // Fire a mutation on every caller block. But don't record this as an + // undo action since it is deterministically tied to the procedure's + // definition mutation. + Blockly.Events.recordUndo = false; + Blockly.Events.fire(new Blockly.Events.Change( + caller, 'mutation', null, oldMutation, newMutation)); + Blockly.Events.recordUndo = oldRecordUndo; + } + } +}; + +/** + * Find the definition block for the named procedure. + * @param {string} name Name of procedure. + * @param {!Blockly.Workspace} workspace The workspace to search. + * @return {Blockly.Block} The procedure definition block, or null not found. + */ +Blockly.Procedures.getDefinition = function(name, workspace) { + // Assume that a procedure definition is a top block. + var blocks = workspace.getTopBlocks(false); + for (var i = 0; i < blocks.length; i++) { + if (blocks[i].getProcedureDef) { + var tuple = blocks[i].getProcedureDef(); + if (tuple && Blockly.Names.equals(tuple[0], name)) { + return blocks[i]; + } + } + } + return null; +}; diff --git a/src/blockly/core/rendered_connection.js b/src/blockly/core/rendered_connection.js new file mode 100644 index 0000000..dcb90f1 --- /dev/null +++ b/src/blockly/core/rendered_connection.js @@ -0,0 +1,395 @@ +/** + * @license + * Visual Blocks Editor + * + * Copyright 2016 Google Inc. + * https://developers.google.com/blockly/ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Components for creating connections between blocks. + * @author fenichel@google.com (Rachel Fenichel) + */ +'use strict'; + +goog.provide('Blockly.RenderedConnection'); + +goog.require('Blockly.Connection'); + + +/** + * Class for a connection between blocks that may be rendered on screen. + * @param {!Blockly.Block} source The block establishing this connection. + * @param {number} type The type of the connection. + * @extends {Blockly.Connection} + * @constructor + */ +Blockly.RenderedConnection = function(source, type) { + Blockly.RenderedConnection.superClass_.constructor.call(this, source, type); + this.offsetInBlock_ = new goog.math.Coordinate(0, 0); +}; +goog.inherits(Blockly.RenderedConnection, Blockly.Connection); + +/** + * Returns the distance between this connection and another connection. + * @param {!Blockly.Connection} otherConnection The other connection to measure + * the distance to. + * @return {number} The distance between connections. + */ +Blockly.RenderedConnection.prototype.distanceFrom = function(otherConnection) { + var xDiff = this.x_ - otherConnection.x_; + var yDiff = this.y_ - otherConnection.y_; + return Math.sqrt(xDiff * xDiff + yDiff * yDiff); +}; + +/** + * Move the block(s) belonging to the connection to a point where they don't + * visually interfere with the specified connection. + * @param {!Blockly.Connection} staticConnection The connection to move away + * from. + * @private + */ +Blockly.RenderedConnection.prototype.bumpAwayFrom_ = function(staticConnection) { + if (Blockly.dragMode_ != Blockly.DRAG_NONE) { + // Don't move blocks around while the user is doing the same. + return; + } + // Move the root block. + var rootBlock = this.sourceBlock_.getRootBlock(); + if (rootBlock.isInFlyout) { + // Don't move blocks around in a flyout. + return; + } + var reverse = false; + if (!rootBlock.isMovable()) { + // Can't bump an uneditable block away. + // Check to see if the other block is movable. + rootBlock = staticConnection.getSourceBlock().getRootBlock(); + if (!rootBlock.isMovable()) { + return; + } + // Swap the connections and move the 'static' connection instead. + staticConnection = this; + reverse = true; + } + // Raise it to the top for extra visibility. + var selected = Blockly.selected == rootBlock; + selected || rootBlock.addSelect(); + var dx = (staticConnection.x_ + Blockly.SNAP_RADIUS) - this.x_; + var dy = (staticConnection.y_ + Blockly.SNAP_RADIUS) - this.y_; + if (reverse) { + // When reversing a bump due to an uneditable block, bump up. + dy = -dy; + } + if (rootBlock.RTL) { + dx = -dx; + } + rootBlock.moveBy(dx, dy); + selected || rootBlock.removeSelect(); +}; + +/** + * Change the connection's coordinates. + * @param {number} x New absolute x coordinate. + * @param {number} y New absolute y coordinate. + */ +Blockly.RenderedConnection.prototype.moveTo = function(x, y) { + // Remove it from its old location in the database (if already present) + if (this.inDB_) { + this.db_.removeConnection_(this); + } + this.x_ = x; + this.y_ = y; + // Insert it into its new location in the database. + if (!this.hidden_) { + this.db_.addConnection(this); + } +}; + +/** + * Change the connection's coordinates. + * @param {number} dx Change to x coordinate. + * @param {number} dy Change to y coordinate. + */ +Blockly.RenderedConnection.prototype.moveBy = function(dx, dy) { + this.moveTo(this.x_ + dx, this.y_ + dy); +}; + +/** + * Move this connection to the location given by its offset within the block and + * the coordinate of the block's top left corner. + * @param {!goog.math.Coordinate} blockTL The coordinate of the top left corner + * of the block. + */ +Blockly.RenderedConnection.prototype.moveToOffset = function(blockTL) { + this.moveTo(blockTL.x + this.offsetInBlock_.x, + blockTL.y + this.offsetInBlock_.y); +}; + +/** + * Set the offset of this connection relative to the top left of its block. + * @param {number} x The new relative x. + * @param {number} y The new relative y. + */ +Blockly.RenderedConnection.prototype.setOffsetInBlock = function(x, y) { + this.offsetInBlock_.x = x; + this.offsetInBlock_.y = y; +}; + +/** + * Move the blocks on either side of this connection right next to each other. + * @private + */ +Blockly.RenderedConnection.prototype.tighten_ = function() { + var dx = this.targetConnection.x_ - this.x_; + var dy = this.targetConnection.y_ - this.y_; + if (dx != 0 || dy != 0) { + var block = this.targetBlock(); + var svgRoot = block.getSvgRoot(); + if (!svgRoot) { + throw 'block is not rendered.'; + } + var xy = Blockly.getRelativeXY_(svgRoot); + block.getSvgRoot().setAttribute('transform', + 'translate(' + (xy.x - dx) + ',' + (xy.y - dy) + ')'); + block.moveConnections_(-dx, -dy); + } +}; + +/** + * Find the closest compatible connection to this connection. + * @param {number} maxLimit The maximum radius to another connection. + * @param {number} dx Horizontal offset between this connection's location + * in the database and the current location (as a result of dragging). + * @param {number} dy Vertical offset between this connection's location + * in the database and the current location (as a result of dragging). + * @return {!{connection: ?Blockly.Connection, radius: number}} Contains two + * properties: 'connection' which is either another connection or null, + * and 'radius' which is the distance. + */ +Blockly.RenderedConnection.prototype.closest = function(maxLimit, dx, dy) { + return this.dbOpposite_.searchForClosest(this, maxLimit, dx, dy); +}; + +/** + * Add highlighting around this connection. + */ +Blockly.RenderedConnection.prototype.highlight = function() { + var steps; + if (this.type == Blockly.INPUT_VALUE || this.type == Blockly.OUTPUT_VALUE) { + steps = 'm 0,0 ' + Blockly.BlockSvg.TAB_PATH_DOWN + ' v 5'; + } else { + steps = 'm -20,0 h 5 ' + Blockly.BlockSvg.NOTCH_PATH_LEFT + ' h 5'; + } + var xy = this.sourceBlock_.getRelativeToSurfaceXY(); + var x = this.x_ - xy.x; + var y = this.y_ - xy.y; + Blockly.Connection.highlightedPath_ = Blockly.createSvgElement('path', + {'class': 'blocklyHighlightedConnectionPath', + 'd': steps, + transform: 'translate(' + x + ',' + y + ')' + + (this.sourceBlock_.RTL ? ' scale(-1 1)' : '')}, + this.sourceBlock_.getSvgRoot()); +}; + +/** + * Unhide this connection, as well as all down-stream connections on any block + * attached to this connection. This happens when a block is expanded. + * Also unhides down-stream comments. + * @return {!Array.<!Blockly.Block>} List of blocks to render. + */ +Blockly.RenderedConnection.prototype.unhideAll = function() { + this.setHidden(false); + // All blocks that need unhiding must be unhidden before any rendering takes + // place, since rendering requires knowing the dimensions of lower blocks. + // Also, since rendering a block renders all its parents, we only need to + // render the leaf nodes. + var renderList = []; + if (this.type != Blockly.INPUT_VALUE && this.type != Blockly.NEXT_STATEMENT) { + // Only spider down. + return renderList; + } + var block = this.targetBlock(); + if (block) { + var connections; + if (block.isCollapsed()) { + // This block should only be partially revealed since it is collapsed. + connections = []; + block.outputConnection && connections.push(block.outputConnection); + block.nextConnection && connections.push(block.nextConnection); + block.previousConnection && connections.push(block.previousConnection); + } else { + // Show all connections of this block. + connections = block.getConnections_(true); + } + for (var i = 0; i < connections.length; i++) { + renderList.push.apply(renderList, connections[i].unhideAll()); + } + if (!renderList.length) { + // Leaf block. + renderList[0] = block; + } + } + return renderList; +}; + +/** + * Remove the highlighting around this connection. + */ +Blockly.RenderedConnection.prototype.unhighlight = function() { + goog.dom.removeNode(Blockly.Connection.highlightedPath_); + delete Blockly.Connection.highlightedPath_; +}; + +/** + * Set whether this connections is hidden (not tracked in a database) or not. + * @param {boolean} hidden True if connection is hidden. + */ +Blockly.RenderedConnection.prototype.setHidden = function(hidden) { + this.hidden_ = hidden; + if (hidden && this.inDB_) { + this.db_.removeConnection_(this); + } else if (!hidden && !this.inDB_) { + this.db_.addConnection(this); + } +}; + +/** + * Hide this connection, as well as all down-stream connections on any block + * attached to this connection. This happens when a block is collapsed. + * Also hides down-stream comments. + */ +Blockly.RenderedConnection.prototype.hideAll = function() { + this.setHidden(true); + if (this.targetConnection) { + var blocks = this.targetBlock().getDescendants(); + for (var i = 0; i < blocks.length; i++) { + var block = blocks[i]; + // Hide all connections of all children. + var connections = block.getConnections_(true); + for (var j = 0; j < connections.length; j++) { + connections[j].setHidden(true); + } + // Close all bubbles of all children. + var icons = block.getIcons(); + for (var j = 0; j < icons.length; j++) { + icons[j].setVisible(false); + } + } + } +}; + +/** + * Check if the two connections can be dragged to connect to each other. + * @param {!Blockly.Connection} candidate A nearby connection to check. + * @param {number} maxRadius The maximum radius allowed for connections. + * @return {boolean} True if the connection is allowed, false otherwise. + */ +Blockly.RenderedConnection.prototype.isConnectionAllowed = function(candidate, + maxRadius) { + if (this.distanceFrom(candidate) > maxRadius) { + return false; + } + + return Blockly.RenderedConnection.superClass_.isConnectionAllowed.call(this, + candidate); +}; + +/** + * Disconnect two blocks that are connected by this connection. + * @param {!Blockly.Block} parentBlock The superior block. + * @param {!Blockly.Block} childBlock The inferior block. + * @private + */ +Blockly.RenderedConnection.prototype.disconnectInternal_ = function(parentBlock, + childBlock) { + Blockly.RenderedConnection.superClass_.disconnectInternal_.call(this, + parentBlock, childBlock); + // Rerender the parent so that it may reflow. + if (parentBlock.rendered) { + parentBlock.render(); + } + if (childBlock.rendered) { + childBlock.updateDisabled(); + childBlock.render(); + } +}; + +/** + * Respawn the shadow block if there was one connected to the this connection. + * Render/rerender blocks as needed. + * @private + */ +Blockly.RenderedConnection.prototype.respawnShadow_ = function() { + var parentBlock = this.getSourceBlock(); + // Respawn the shadow block if there is one. + var shadow = this.getShadowDom(); + if (parentBlock.workspace && shadow && Blockly.Events.recordUndo) { + Blockly.RenderedConnection.superClass_.respawnShadow_.call(this); + var blockShadow = this.targetBlock(); + if (!blockShadow) { + throw 'Couldn\'t respawn the shadow block that should exist here.'; + } + blockShadow.initSvg(); + blockShadow.render(false); + if (parentBlock.rendered) { + parentBlock.render(); + } + } +}; + +/** + * Find all nearby compatible connections to this connection. + * Type checking does not apply, since this function is used for bumping. + * @param {number} maxLimit The maximum radius to another connection. + * @return {!Array.<!Blockly.Connection>} List of connections. + * @private + */ +Blockly.RenderedConnection.prototype.neighbours_ = function(maxLimit) { + return this.dbOpposite_.getNeighbours(this, maxLimit); +}; + +/** + * Connect two connections together. This is the connection on the superior + * block. Rerender blocks as needed. + * @param {!Blockly.Connection} childConnection Connection on inferior block. + * @private + */ +Blockly.RenderedConnection.prototype.connect_ = function(childConnection) { + Blockly.RenderedConnection.superClass_.connect_.call(this, childConnection); + + var parentConnection = this; + var parentBlock = parentConnection.getSourceBlock(); + var childBlock = childConnection.getSourceBlock(); + + if (parentBlock.rendered) { + parentBlock.updateDisabled(); + } + if (childBlock.rendered) { + childBlock.updateDisabled(); + } + if (parentBlock.rendered && childBlock.rendered) { + if (parentConnection.type == Blockly.NEXT_STATEMENT || + parentConnection.type == Blockly.PREVIOUS_STATEMENT) { + // Child block may need to square off its corners if it is in a stack. + // Rendering a child will render its parent. + childBlock.render(); + } else { + // Child block does not change shape. Rendering the parent node will + // move its connected children into position. + parentBlock.render(); + } + } +}; diff --git a/src/blockly/core/scrollbar.js b/src/blockly/core/scrollbar.js new file mode 100644 index 0000000..da2dd94 --- /dev/null +++ b/src/blockly/core/scrollbar.js @@ -0,0 +1,750 @@ +/** + * @license + * Visual Blocks Editor + * + * Copyright 2011 Google Inc. + * https://developers.google.com/blockly/ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Library for creating scrollbars. + * @author fraser@google.com (Neil Fraser) + */ +'use strict'; + +goog.provide('Blockly.Scrollbar'); +goog.provide('Blockly.ScrollbarPair'); + +goog.require('goog.dom'); +goog.require('goog.events'); + + +/** + * Class for a pair of scrollbars. Horizontal and vertical. + * @param {!Blockly.Workspace} workspace Workspace to bind the scrollbars to. + * @constructor + */ +Blockly.ScrollbarPair = function(workspace) { + this.workspace_ = workspace; + this.hScroll = new Blockly.Scrollbar(workspace, true, true); + this.vScroll = new Blockly.Scrollbar(workspace, false, true); + this.corner_ = Blockly.createSvgElement('rect', + {'height': Blockly.Scrollbar.scrollbarThickness, + 'width': Blockly.Scrollbar.scrollbarThickness, + 'class': 'blocklyScrollbarBackground'}, null); + Blockly.Scrollbar.insertAfter_(this.corner_, workspace.getBubbleCanvas()); +}; + +/** + * Previously recorded metrics from the workspace. + * @type {Object} + * @private + */ +Blockly.ScrollbarPair.prototype.oldHostMetrics_ = null; + +/** + * Dispose of this pair of scrollbars. + * Unlink from all DOM elements to prevent memory leaks. + */ +Blockly.ScrollbarPair.prototype.dispose = function() { + goog.dom.removeNode(this.corner_); + this.corner_ = null; + this.workspace_ = null; + this.oldHostMetrics_ = null; + this.hScroll.dispose(); + this.hScroll = null; + this.vScroll.dispose(); + this.vScroll = null; +}; + +/** + * Recalculate both of the scrollbars' locations and lengths. + * Also reposition the corner rectangle. + */ +Blockly.ScrollbarPair.prototype.resize = function() { + // Look up the host metrics once, and use for both scrollbars. + var hostMetrics = this.workspace_.getMetrics(); + if (!hostMetrics) { + // Host element is likely not visible. + return; + } + + // Only change the scrollbars if there has been a change in metrics. + var resizeH = false; + var resizeV = false; + if (!this.oldHostMetrics_ || + this.oldHostMetrics_.viewWidth != hostMetrics.viewWidth || + this.oldHostMetrics_.viewHeight != hostMetrics.viewHeight || + this.oldHostMetrics_.absoluteTop != hostMetrics.absoluteTop || + this.oldHostMetrics_.absoluteLeft != hostMetrics.absoluteLeft) { + // The window has been resized or repositioned. + resizeH = true; + resizeV = true; + } else { + // Has the content been resized or moved? + if (!this.oldHostMetrics_ || + this.oldHostMetrics_.contentWidth != hostMetrics.contentWidth || + this.oldHostMetrics_.viewLeft != hostMetrics.viewLeft || + this.oldHostMetrics_.contentLeft != hostMetrics.contentLeft) { + resizeH = true; + } + if (!this.oldHostMetrics_ || + this.oldHostMetrics_.contentHeight != hostMetrics.contentHeight || + this.oldHostMetrics_.viewTop != hostMetrics.viewTop || + this.oldHostMetrics_.contentTop != hostMetrics.contentTop) { + resizeV = true; + } + } + if (resizeH) { + this.hScroll.resize(hostMetrics); + } + if (resizeV) { + this.vScroll.resize(hostMetrics); + } + + // Reposition the corner square. + if (!this.oldHostMetrics_ || + this.oldHostMetrics_.viewWidth != hostMetrics.viewWidth || + this.oldHostMetrics_.absoluteLeft != hostMetrics.absoluteLeft) { + this.corner_.setAttribute('x', this.vScroll.position_.x); + } + if (!this.oldHostMetrics_ || + this.oldHostMetrics_.viewHeight != hostMetrics.viewHeight || + this.oldHostMetrics_.absoluteTop != hostMetrics.absoluteTop) { + this.corner_.setAttribute('y', this.hScroll.position_.y); + } + + // Cache the current metrics to potentially short-cut the next resize event. + this.oldHostMetrics_ = hostMetrics; +}; + +/** + * Set the sliders of both scrollbars to be at a certain position. + * @param {number} x Horizontal scroll value. + * @param {number} y Vertical scroll value. + */ +Blockly.ScrollbarPair.prototype.set = function(x, y) { + // This function is equivalent to: + // this.hScroll.set(x); + // this.vScroll.set(y); + // However, that calls setMetrics twice which causes a chain of + // getAttribute->setAttribute->getAttribute resulting in an extra layout pass. + // Combining them speeds up rendering. + var xyRatio = {}; + + var hHandlePosition = x * this.hScroll.ratio_; + var vHandlePosition = y * this.vScroll.ratio_; + + var hBarLength = this.hScroll.scrollViewSize_; + var vBarLength = this.vScroll.scrollViewSize_; + + xyRatio.x = this.getRatio_(hHandlePosition, hBarLength); + xyRatio.y = this.getRatio_(vHandlePosition, vBarLength); + this.workspace_.setMetrics(xyRatio); + + this.hScroll.setHandlePosition(hHandlePosition); + this.vScroll.setHandlePosition(vHandlePosition); +}; + +/** + * Helper to calculate the ratio of handle position to scrollbar view size. + * @param {number} handlePosition The value of the handle. + * @param {number} viewSize The total size of the scrollbar's view. + * @return {number} Ratio. + * @private + */ +Blockly.ScrollbarPair.prototype.getRatio_ = function(handlePosition, viewSize) { + var ratio = handlePosition / viewSize; + if (isNaN(ratio)) { + return 0; + } + return ratio; +}; + +// -------------------------------------------------------------------- + +/** + * Class for a pure SVG scrollbar. + * This technique offers a scrollbar that is guaranteed to work, but may not + * look or behave like the system's scrollbars. + * @param {!Blockly.Workspace} workspace Workspace to bind the scrollbar to. + * @param {boolean} horizontal True if horizontal, false if vertical. + * @param {boolean=} opt_pair True if scrollbar is part of a horiz/vert pair. + * @constructor + */ +Blockly.Scrollbar = function(workspace, horizontal, opt_pair) { + this.workspace_ = workspace; + this.pair_ = opt_pair || false; + this.horizontal_ = horizontal; + this.oldHostMetrics_ = null; + + this.createDom_(); + + /** + * The upper left corner of the scrollbar's svg group. + * @type {goog.math.Coordinate} + * @private + */ + this.position_ = new goog.math.Coordinate(0, 0); + + if (horizontal) { + this.svgBackground_.setAttribute('height', + Blockly.Scrollbar.scrollbarThickness); + this.svgHandle_.setAttribute('height', + Blockly.Scrollbar.scrollbarThickness - 5); + this.svgHandle_.setAttribute('y', 2.5); + + this.lengthAttribute_ = 'width'; + this.positionAttribute_ = 'x'; + } else { + this.svgBackground_.setAttribute('width', + Blockly.Scrollbar.scrollbarThickness); + this.svgHandle_.setAttribute('width', + Blockly.Scrollbar.scrollbarThickness - 5); + this.svgHandle_.setAttribute('x', 2.5); + + this.lengthAttribute_ = 'height'; + this.positionAttribute_ = 'y'; + } + var scrollbar = this; + this.onMouseDownBarWrapper_ = Blockly.bindEvent_(this.svgBackground_, + 'mousedown', scrollbar, scrollbar.onMouseDownBar_); + this.onMouseDownHandleWrapper_ = Blockly.bindEvent_(this.svgHandle_, + 'mousedown', scrollbar, scrollbar.onMouseDownHandle_); +}; + +/** + * The size of the area within which the scrollbar handle can move. + * @type {number} + * @private + */ +Blockly.Scrollbar.prototype.scrollViewSize_ = 0; + +/** + * The length of the scrollbar handle. + * @type {number} + * @private + */ +Blockly.Scrollbar.prototype.handleLength_ = 0; + +/** + * The offset of the start of the handle from the start of the scrollbar range. + * @type {number} + * @private + */ +Blockly.Scrollbar.prototype.handlePosition_ = 0; + +/** + * Whether the scrollbar handle is visible. + * @type {boolean} + * @private + */ +Blockly.Scrollbar.prototype.isVisible_ = true; + +/** + * Width of vertical scrollbar or height of horizontal scrollbar. + * Increase the size of scrollbars on touch devices. + * Don't define if there is no document object (e.g. node.js). + */ +Blockly.Scrollbar.scrollbarThickness = 15; +if (goog.events.BrowserFeature.TOUCH_ENABLED) { + Blockly.Scrollbar.scrollbarThickness = 25; +} + +/** + * @param {!Object} first An object containing computed measurements of a + * workspace. + * @param {!Object} second Another object containing computed measurements of a + * workspace. + * @return {boolean} Whether the two sets of metrics are equivalent. + * @private + */ +Blockly.Scrollbar.metricsAreEquivalent_ = function(first, second) { + if (!(first && second)) { + return false; + } + + if (first.viewWidth != second.viewWidth || + first.viewHeight != second.viewHeight || + first.viewLeft != second.viewLeft || + first.viewTop != second.viewTop || + first.absoluteTop != second.absoluteTop || + first.absoluteLeft != second.absoluteLeft || + first.contentWidth != second.contentWidth || + first.contentHeight != second.contentHeight || + first.contentLeft != second.contentLeft || + first.contentTop != second.contentTop) { + return false; + } + + return true; +}; + +/** + * Dispose of this scrollbar. + * Unlink from all DOM elements to prevent memory leaks. + */ +Blockly.Scrollbar.prototype.dispose = function() { + this.onMouseUpHandle_(); + Blockly.unbindEvent_(this.onMouseDownBarWrapper_); + this.onMouseDownBarWrapper_ = null; + Blockly.unbindEvent_(this.onMouseDownHandleWrapper_); + this.onMouseDownHandleWrapper_ = null; + + goog.dom.removeNode(this.svgGroup_); + this.svgGroup_ = null; + this.svgBackground_ = null; + this.svgHandle_ = null; + this.workspace_ = null; +}; + +/** + * Set the length of the scrollbar's handle and change the SVG attribute + * accordingly. + * @param {number} newLength The new scrollbar handle length. + */ +Blockly.Scrollbar.prototype.setHandleLength_ = function(newLength) { + this.handleLength_ = newLength; + this.svgHandle_.setAttribute(this.lengthAttribute_, this.handleLength_); +}; + +/** + * Set the offset of the scrollbar's handle and change the SVG attribute + * accordingly. + * @param {number} newPosition The new scrollbar handle offset. + */ +Blockly.Scrollbar.prototype.setHandlePosition = function(newPosition) { + this.handlePosition_ = newPosition; + this.svgHandle_.setAttribute(this.positionAttribute_, this.handlePosition_); +}; + +/** + * Set the size of the scrollbar's background and change the SVG attribute + * accordingly. + * @param {number} newSize The new scrollbar background length. + * @private + */ +Blockly.Scrollbar.prototype.setScrollViewSize_ = function(newSize) { + this.scrollViewSize_ = newSize; + this.svgBackground_.setAttribute(this.lengthAttribute_, this.scrollViewSize_); +}; + +/** + * Set the position of the scrollbar's svg group. + * @param {number} x The new x coordinate. + * @param {number} y The new y coordinate. + */ +Blockly.Scrollbar.prototype.setPosition = function(x, y) { + this.position_.x = x; + this.position_.y = y; + + this.svgGroup_.setAttribute('transform', + 'translate(' + this.position_.x + ',' + this.position_.y + ')'); +}; + +/** + * Recalculate the scrollbar's location and its length. + * @param {Object=} opt_metrics A data structure of from the describing all the + * required dimensions. If not provided, it will be fetched from the host + * object. + */ +Blockly.Scrollbar.prototype.resize = function(opt_metrics) { + // Determine the location, height and width of the host element. + var hostMetrics = opt_metrics; + if (!hostMetrics) { + hostMetrics = this.workspace_.getMetrics(); + if (!hostMetrics) { + // Host element is likely not visible. + return; + } + } + + if (Blockly.Scrollbar.metricsAreEquivalent_(hostMetrics, + this.oldHostMetrics_)) { + return; + } + this.oldHostMetrics_ = hostMetrics; + + /* hostMetrics is an object with the following properties. + * .viewHeight: Height of the visible rectangle, + * .viewWidth: Width of the visible rectangle, + * .contentHeight: Height of the contents, + * .contentWidth: Width of the content, + * .viewTop: Offset of top edge of visible rectangle from parent, + * .viewLeft: Offset of left edge of visible rectangle from parent, + * .contentTop: Offset of the top-most content from the y=0 coordinate, + * .contentLeft: Offset of the left-most content from the x=0 coordinate, + * .absoluteTop: Top-edge of view. + * .absoluteLeft: Left-edge of view. + */ + if (this.horizontal_) { + this.resizeHorizontal_(hostMetrics); + } else { + this.resizeVertical_(hostMetrics); + } + // Resizing may have caused some scrolling. + this.onScroll_(); +}; + +/** + * Recalculate a horizontal scrollbar's location and length. + * @param {!Object} hostMetrics A data structure describing all the + * required dimensions, possibly fetched from the host object. + * @private + */ +Blockly.Scrollbar.prototype.resizeHorizontal_ = function(hostMetrics) { + // TODO: Inspect metrics to determine if we can get away with just a content + // resize. + this.resizeViewHorizontal(hostMetrics); +}; + +/** + * Recalculate a horizontal scrollbar's location on the screen and path length. + * This should be called when the layout or size of the window has changed. + * @param {!Object} hostMetrics A data structure describing all the + * required dimensions, possibly fetched from the host object. + */ +Blockly.Scrollbar.prototype.resizeViewHorizontal = function(hostMetrics) { + var viewSize = hostMetrics.viewWidth - 1; + if (this.pair_) { + // Shorten the scrollbar to make room for the corner square. + viewSize -= Blockly.Scrollbar.scrollbarThickness; + } + this.setScrollViewSize_(Math.max(0, viewSize)); + + var xCoordinate = hostMetrics.absoluteLeft + 0.5; + if (this.pair_ && this.workspace_.RTL) { + xCoordinate += Blockly.Scrollbar.scrollbarThickness; + } + + // Horizontal toolbar should always be just above the bottom of the workspace. + var yCoordinate = hostMetrics.absoluteTop + hostMetrics.viewHeight - + Blockly.Scrollbar.scrollbarThickness - 0.5; + this.setPosition(xCoordinate, yCoordinate); + + // If the view has been resized, a content resize will also be necessary. The + // reverse is not true. + this.resizeContentHorizontal(hostMetrics); +}; + +/** + * Recalculate a horizontal scrollbar's location within its path and length. + * This should be called when the contents of the workspace have changed. + * @param {!Object} hostMetrics A data structure describing all the + * required dimensions, possibly fetched from the host object. + */ +Blockly.Scrollbar.prototype.resizeContentHorizontal = function(hostMetrics) { + if (!this.pair_) { + // Only show the scrollbar if needed. + // Ideally this would also apply to scrollbar pairs, but that's a bigger + // headache (due to interactions with the corner square). + this.setVisible(this.scrollViewSize_ < hostMetrics.contentWidth); + } + + this.ratio_ = this.scrollViewSize_ / hostMetrics.contentWidth; + if (this.ratio_ == -Infinity || this.ratio_ == Infinity || + isNaN(this.ratio_)) { + this.ratio_ = 0; + } + + var handleLength = hostMetrics.viewWidth * this.ratio_; + this.setHandleLength_(Math.max(0, handleLength)); + + var handlePosition = (hostMetrics.viewLeft - hostMetrics.contentLeft) * + this.ratio_; + this.setHandlePosition(this.constrainHandle_(handlePosition)); +}; + +/** + * Recalculate a vertical scrollbar's location and length. + * @param {!Object} hostMetrics A data structure describing all the + * required dimensions, possibly fetched from the host object. + * @private + */ +Blockly.Scrollbar.prototype.resizeVertical_ = function(hostMetrics) { + // TODO: Inspect metrics to determine if we can get away with just a content + // resize. + this.resizeViewVertical(hostMetrics); +}; + +/** + * Recalculate a vertical scrollbar's location on the screen and path length. + * This should be called when the layout or size of the window has changed. + * @param {!Object} hostMetrics A data structure describing all the + * required dimensions, possibly fetched from the host object. + */ +Blockly.Scrollbar.prototype.resizeViewVertical = function(hostMetrics) { + var viewSize = hostMetrics.viewHeight - 1; + if (this.pair_) { + // Shorten the scrollbar to make room for the corner square. + viewSize -= Blockly.Scrollbar.scrollbarThickness; + } + this.setScrollViewSize_(Math.max(0, viewSize)); + + var xCoordinate = hostMetrics.absoluteLeft + 0.5; + if (!this.workspace_.RTL) { + xCoordinate += hostMetrics.viewWidth - + Blockly.Scrollbar.scrollbarThickness - 1; + } + var yCoordinate = hostMetrics.absoluteTop + 0.5; + this.setPosition(xCoordinate, yCoordinate); + + // If the view has been resized, a content resize will also be necessary. The + // reverse is not true. + this.resizeContentVertical(hostMetrics); +}; + +/** + * Recalculate a vertical scrollbar's location within its path and length. + * This should be called when the contents of the workspace have changed. + * @param {!Object} hostMetrics A data structure describing all the + * required dimensions, possibly fetched from the host object. + */ +Blockly.Scrollbar.prototype.resizeContentVertical = function(hostMetrics) { + if (!this.pair_) { + // Only show the scrollbar if needed. + this.setVisible(this.scrollViewSize_ < hostMetrics.contentHeight); + } + + this.ratio_ = this.scrollViewSize_ / hostMetrics.contentHeight; + if (this.ratio_ == -Infinity || this.ratio_ == Infinity || + isNaN(this.ratio_)) { + this.ratio_ = 0; + } + + var handleLength = hostMetrics.viewHeight * this.ratio_; + this.setHandleLength_(Math.max(0, handleLength)); + + var handlePosition = (hostMetrics.viewTop - hostMetrics.contentTop) * + this.ratio_; + this.setHandlePosition(this.constrainHandle_(handlePosition)); +}; + +/** + * Create all the DOM elements required for a scrollbar. + * The resulting widget is not sized. + * @private + */ +Blockly.Scrollbar.prototype.createDom_ = function() { + /* Create the following DOM: + <g class="blocklyScrollbarHorizontal"> + <rect class="blocklyScrollbarBackground" /> + <rect class="blocklyScrollbarHandle" rx="8" ry="8" /> + </g> + */ + var className = 'blocklyScrollbar' + + (this.horizontal_ ? 'Horizontal' : 'Vertical'); + this.svgGroup_ = Blockly.createSvgElement('g', {'class': className}, null); + this.svgBackground_ = Blockly.createSvgElement('rect', + {'class': 'blocklyScrollbarBackground'}, this.svgGroup_); + var radius = Math.floor((Blockly.Scrollbar.scrollbarThickness - 5) / 2); + this.svgHandle_ = Blockly.createSvgElement('rect', + {'class': 'blocklyScrollbarHandle', 'rx': radius, 'ry': radius}, + this.svgGroup_); + Blockly.Scrollbar.insertAfter_(this.svgGroup_, + this.workspace_.getBubbleCanvas()); +}; + +/** + * Is the scrollbar visible. Non-paired scrollbars disappear when they aren't + * needed. + * @return {boolean} True if visible. + */ +Blockly.Scrollbar.prototype.isVisible = function() { + return this.isVisible_; +}; + +/** + * Set whether the scrollbar is visible. + * Only applies to non-paired scrollbars. + * @param {boolean} visible True if visible. + */ +Blockly.Scrollbar.prototype.setVisible = function(visible) { + if (visible == this.isVisible()) { + return; + } + // Ideally this would also apply to scrollbar pairs, but that's a bigger + // headache (due to interactions with the corner square). + if (this.pair_) { + throw 'Unable to toggle visibility of paired scrollbars.'; + } + + this.isVisible_ = visible; + + if (visible) { + this.svgGroup_.setAttribute('display', 'block'); + } else { + // Hide the scrollbar. + this.workspace_.setMetrics({x: 0, y: 0}); + this.svgGroup_.setAttribute('display', 'none'); + } +}; + +/** + * Scroll by one pageful. + * Called when scrollbar background is clicked. + * @param {!Event} e Mouse down event. + * @private + */ +Blockly.Scrollbar.prototype.onMouseDownBar_ = function(e) { + this.onMouseUpHandle_(); + if (Blockly.isRightButton(e)) { + // Right-click. + // Scrollbars have no context menu. + e.stopPropagation(); + return; + } + var mouseXY = Blockly.mouseToSvg(e, this.workspace_.getParentSvg(), + this.workspace_.getInverseScreenCTM()); + var mouseLocation = this.horizontal_ ? mouseXY.x : mouseXY.y; + + var handleXY = Blockly.getSvgXY_(this.svgHandle_, this.workspace_); + var handleStart = this.horizontal_ ? handleXY.x : handleXY.y; + var handlePosition = this.handlePosition_; + + var pageLength = this.handleLength_ * 0.95; + if (mouseLocation <= handleStart) { + // Decrease the scrollbar's value by a page. + handlePosition -= pageLength; + } else if (mouseLocation >= handleStart + this.handleLength_) { + // Increase the scrollbar's value by a page. + handlePosition += pageLength; + } + + this.setHandlePosition(this.constrainHandle_(handlePosition)); + + this.onScroll_(); + e.stopPropagation(); + e.preventDefault(); +}; + +/** + * Start a dragging operation. + * Called when scrollbar handle is clicked. + * @param {!Event} e Mouse down event. + * @private + */ +Blockly.Scrollbar.prototype.onMouseDownHandle_ = function(e) { + this.onMouseUpHandle_(); + if (Blockly.isRightButton(e)) { + // Right-click. + // Scrollbars have no context menu. + e.stopPropagation(); + return; + } + // Look up the current translation and record it. + this.startDragHandle = this.handlePosition_; + // Record the current mouse position. + this.startDragMouse = this.horizontal_ ? e.clientX : e.clientY; + Blockly.Scrollbar.onMouseUpWrapper_ = Blockly.bindEvent_(document, + 'mouseup', this, this.onMouseUpHandle_); + Blockly.Scrollbar.onMouseMoveWrapper_ = Blockly.bindEvent_(document, + 'mousemove', this, this.onMouseMoveHandle_); + e.stopPropagation(); + e.preventDefault(); +}; + +/** + * Drag the scrollbar's handle. + * @param {!Event} e Mouse up event. + * @private + */ +Blockly.Scrollbar.prototype.onMouseMoveHandle_ = function(e) { + var currentMouse = this.horizontal_ ? e.clientX : e.clientY; + var mouseDelta = currentMouse - this.startDragMouse; + var handlePosition = this.startDragHandle + mouseDelta; + // Position the bar. + this.setHandlePosition(this.constrainHandle_(handlePosition)); + this.onScroll_(); +}; + +/** + * Stop binding to the global mouseup and mousemove events. + * @private + */ +Blockly.Scrollbar.prototype.onMouseUpHandle_ = function() { + Blockly.hideChaff(true); + if (Blockly.Scrollbar.onMouseUpWrapper_) { + Blockly.unbindEvent_(Blockly.Scrollbar.onMouseUpWrapper_); + Blockly.Scrollbar.onMouseUpWrapper_ = null; + } + if (Blockly.Scrollbar.onMouseMoveWrapper_) { + Blockly.unbindEvent_(Blockly.Scrollbar.onMouseMoveWrapper_); + Blockly.Scrollbar.onMouseMoveWrapper_ = null; + } +}; + +/** + * Constrain the handle's position within the minimum (0) and maximum + * (length of scrollbar) values allowed for the scrollbar. + * @param {number} value Value that is potentially out of bounds. + * @return {number} Constrained value. + * @private + */ +Blockly.Scrollbar.prototype.constrainHandle_ = function(value) { + if (value <= 0 || isNaN(value) || this.scrollViewSize_ < this.handleLength_) { + value = 0; + } else { + value = Math.min(value, this.scrollViewSize_ - this.handleLength_); + } + return value; +}; + +/** + * Called when scrollbar is moved. + * @private + */ +Blockly.Scrollbar.prototype.onScroll_ = function() { + var ratio = this.handlePosition_ / this.scrollViewSize_; + if (isNaN(ratio)) { + ratio = 0; + } + var xyRatio = {}; + if (this.horizontal_) { + xyRatio.x = ratio; + } else { + xyRatio.y = ratio; + } + this.workspace_.setMetrics(xyRatio); +}; + +/** + * Set the scrollbar slider's position. + * @param {number} value The distance from the top/left end of the bar. + */ +Blockly.Scrollbar.prototype.set = function(value) { + this.setHandlePosition(this.constrainHandle_(value * this.ratio_)); + this.onScroll_(); +}; + +/** + * Insert a node after a reference node. + * Contrast with node.insertBefore function. + * @param {!Element} newNode New element to insert. + * @param {!Element} refNode Existing element to precede new node. + * @private + */ +Blockly.Scrollbar.insertAfter_ = function(newNode, refNode) { + var siblingNode = refNode.nextSibling; + var parentNode = refNode.parentNode; + if (!parentNode) { + throw 'Reference node has no parent.'; + } + if (siblingNode) { + parentNode.insertBefore(newNode, siblingNode); + } else { + parentNode.appendChild(newNode); + } +}; diff --git a/src/blockly/core/toolbox.js b/src/blockly/core/toolbox.js new file mode 100644 index 0000000..5a5096c --- /dev/null +++ b/src/blockly/core/toolbox.js @@ -0,0 +1,650 @@ +/** + * @license + * Visual Blocks Editor + * + * Copyright 2011 Google Inc. + * https://developers.google.com/blockly/ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Toolbox from whence to create blocks. + * @author fraser@google.com (Neil Fraser) + */ +'use strict'; + +goog.provide('Blockly.Toolbox'); + +goog.require('Blockly.Flyout'); +goog.require('goog.dom'); +goog.require('goog.dom.TagName'); +goog.require('goog.events'); +goog.require('goog.events.BrowserFeature'); +goog.require('goog.html.SafeHtml'); +goog.require('goog.html.SafeStyle'); +goog.require('goog.math.Rect'); +goog.require('goog.style'); +goog.require('goog.ui.tree.TreeControl'); +goog.require('goog.ui.tree.TreeNode'); + + +/** + * Class for a Toolbox. + * Creates the toolbox's DOM. + * @param {!Blockly.Workspace} workspace The workspace in which to create new + * blocks. + * @constructor + */ +Blockly.Toolbox = function(workspace) { + /** + * @type {!Blockly.Workspace} + * @private + */ + this.workspace_ = workspace; + + /** + * Is RTL vs LTR. + * @type {boolean} + */ + this.RTL = workspace.options.RTL; + + /** + * Whether the toolbox should be laid out horizontally. + * @type {boolean} + * @private + */ + this.horizontalLayout_ = workspace.options.horizontalLayout; + + /** + * Position of the toolbox and flyout relative to the workspace. + * @type {number} + */ + this.toolboxPosition = workspace.options.toolboxPosition; + + /** + * Configuration constants for Closure's tree UI. + * @type {Object.<string,*>} + * @private + */ + this.config_ = { + indentWidth: 19, + cssRoot: 'blocklyTreeRoot', + cssHideRoot: 'blocklyHidden', + cssItem: '', + cssTreeRow: 'blocklyTreeRow', + cssItemLabel: 'blocklyTreeLabel', + cssTreeIcon: 'blocklyTreeIcon', + cssExpandedFolderIcon: 'blocklyTreeIconOpen', + cssFileIcon: 'blocklyTreeIconNone', + cssSelectedRow: 'blocklyTreeSelected' + }; + + + /** + * Configuration constants for tree separator. + * @type {Object.<string,*>} + * @private + */ + this.treeSeparatorConfig_ = { + cssTreeRow: 'blocklyTreeSeparator' + }; + + if (this.horizontalLayout_) { + this.config_['cssTreeRow'] = + this.config_['cssTreeRow'] + + (workspace.RTL ? + ' blocklyHorizontalTreeRtl' : ' blocklyHorizontalTree'); + + this.treeSeparatorConfig_['cssTreeRow'] = + 'blocklyTreeSeparatorHorizontal ' + + (workspace.RTL ? + 'blocklyHorizontalTreeRtl' : 'blocklyHorizontalTree'); + this.config_['cssTreeIcon'] = ''; + } +}; + +/** + * Width of the toolbox, which changes only in vertical layout. + * @type {number} + */ +Blockly.Toolbox.prototype.width = 0; + +/** + * Height of the toolbox, which changes only in horizontal layout. + * @type {number} + */ +Blockly.Toolbox.prototype.height = 0; + +/** + * The SVG group currently selected. + * @type {SVGGElement} + * @private + */ +Blockly.Toolbox.prototype.selectedOption_ = null; + +/** + * The tree node most recently selected. + * @type {goog.ui.tree.BaseNode} + * @private + */ +Blockly.Toolbox.prototype.lastCategory_ = null; + +/** + * Initializes the toolbox. + */ +Blockly.Toolbox.prototype.init = function() { + var workspace = this.workspace_; + var svg = this.workspace_.getParentSvg(); + + // Create an HTML container for the Toolbox menu. + this.HtmlDiv = + goog.dom.createDom(goog.dom.TagName.DIV, 'blocklyToolboxDiv'); + this.HtmlDiv.setAttribute('dir', workspace.RTL ? 'RTL' : 'LTR'); + svg.parentNode.insertBefore(this.HtmlDiv, svg); + + // Clicking on toolbox closes popups. + Blockly.bindEvent_(this.HtmlDiv, 'mousedown', this, + function(e) { + if (Blockly.isRightButton(e) || e.target == this.HtmlDiv) { + // Close flyout. + Blockly.hideChaff(false); + } else { + // Just close popups. + Blockly.hideChaff(true); + } + }); + var workspaceOptions = { + disabledPatternId: workspace.options.disabledPatternId, + parentWorkspace: workspace, + RTL: workspace.RTL, + horizontalLayout: workspace.horizontalLayout, + toolboxPosition: workspace.options.toolboxPosition + }; + /** + * @type {!Blockly.Flyout} + * @private + */ + this.flyout_ = new Blockly.Flyout(workspaceOptions); + goog.dom.insertSiblingAfter(this.flyout_.createDom(), workspace.svgGroup_); + this.flyout_.init(workspace); + + this.config_['cleardotPath'] = workspace.options.pathToMedia + '1x1.gif'; + this.config_['cssCollapsedFolderIcon'] = + 'blocklyTreeIconClosed' + (workspace.RTL ? 'Rtl' : 'Ltr'); + var tree = new Blockly.Toolbox.TreeControl(this, this.config_); + this.tree_ = tree; + tree.setShowRootNode(false); + tree.setShowLines(false); + tree.setShowExpandIcons(false); + tree.setSelectedItem(null); + var openNode = this.populate_(workspace.options.languageTree); + tree.render(this.HtmlDiv); + if (openNode) { + tree.setSelectedItem(openNode); + } + this.addColour_(); + this.position(); +}; + +/** + * Dispose of this toolbox. + */ +Blockly.Toolbox.prototype.dispose = function() { + this.flyout_.dispose(); + this.tree_.dispose(); + goog.dom.removeNode(this.HtmlDiv); + this.workspace_ = null; + this.lastCategory_ = null; +}; + +/** + * Get the width of the toolbox. + * @return {number} The width of the toolbox. + */ +Blockly.Toolbox.prototype.getWidth = function() { + return this.width; +}; + +/** + * Get the height of the toolbox. + * @return {number} The width of the toolbox. + */ +Blockly.Toolbox.prototype.getHeight = function() { + return this.height; +}; + +/** + * Move the toolbox to the edge. + */ +Blockly.Toolbox.prototype.position = function() { + var treeDiv = this.HtmlDiv; + if (!treeDiv) { + // Not initialized yet. + return; + } + var svg = this.workspace_.getParentSvg(); + var svgPosition = goog.style.getPageOffset(svg); + var svgSize = Blockly.svgSize(svg); + if (this.horizontalLayout_) { + treeDiv.style.left = '0'; + treeDiv.style.height = 'auto'; + treeDiv.style.width = svgSize.width + 'px'; + this.height = treeDiv.offsetHeight; + if (this.toolboxPosition == Blockly.TOOLBOX_AT_TOP) { // Top + treeDiv.style.top = '0'; + } else { // Bottom + treeDiv.style.bottom = '0'; + } + } else { + if (this.toolboxPosition == Blockly.TOOLBOX_AT_RIGHT) { // Right + treeDiv.style.right = '0'; + } else { // Left + treeDiv.style.left = '0'; + } + treeDiv.style.height = svgSize.height + 'px'; + this.width = treeDiv.offsetWidth; + } + this.flyout_.position(); +}; + +/** + * Fill the toolbox with categories and blocks. + * @param {!Node} newTree DOM tree of blocks. + * @return {Node} Tree node to open at startup (or null). + * @private + */ +Blockly.Toolbox.prototype.populate_ = function(newTree) { + this.tree_.removeChildren(); // Delete any existing content. + this.tree_.blocks = []; + this.hasColours_ = false; + var openNode = + this.syncTrees_(newTree, this.tree_, this.workspace_.options.pathToMedia); + + if (this.tree_.blocks.length) { + throw 'Toolbox cannot have both blocks and categories in the root level.'; + } + + // Fire a resize event since the toolbox may have changed width and height. + this.workspace_.resizeContents(); + return openNode; +}; + +/** + * Sync trees of the toolbox. + * @param {!Node} treeIn DOM tree of blocks. + * @param {!Blockly.Toolbox.TreeControl} treeOut + * @param {string} pathToMedia + * @return {Node} Tree node to open at startup (or null). + * @private + */ +Blockly.Toolbox.prototype.syncTrees_ = function(treeIn, treeOut, pathToMedia) { + var openNode = null; + var lastElement = null; + for (var i = 0, childIn; childIn = treeIn.childNodes[i]; i++) { + if (!childIn.tagName) { + // Skip over text. + continue; + } + switch (childIn.tagName.toUpperCase()) { + case 'CATEGORY': + var childOut = this.tree_.createNode(childIn.getAttribute('name')); + childOut.blocks = []; + treeOut.add(childOut); + var custom = childIn.getAttribute('custom'); + if (custom) { + // Variables and procedures are special dynamic categories. + childOut.blocks = custom; + } else { + var newOpenNode = this.syncTrees_(childIn, childOut, pathToMedia); + if (newOpenNode) { + openNode = newOpenNode; + } + } + var colour = childIn.getAttribute('colour'); + if (goog.isString(colour)) { + if (colour.match(/^#[0-9a-fA-F]{6}$/)) { + childOut.hexColour = colour; + } else { + childOut.hexColour = Blockly.hueToRgb(colour); + } + this.hasColours_ = true; + } else { + childOut.hexColour = ''; + } + if (childIn.getAttribute('expanded') == 'true') { + if (childOut.blocks.length) { + // This is a category that directly contians blocks. + // After the tree is rendered, open this category and show flyout. + openNode = childOut; + } + childOut.setExpanded(true); + } else { + childOut.setExpanded(false); + } + lastElement = childIn; + break; + case 'SEP': + if (lastElement) { + if (lastElement.tagName.toUpperCase() == 'CATEGORY') { + // Separator between two categories. + // <sep></sep> + treeOut.add(new Blockly.Toolbox.TreeSeparator( + this.treeSeparatorConfig_)); + } else { + // Change the gap between two blocks. + // <sep gap="36"></sep> + // The default gap is 24, can be set larger or smaller. + // Note that a deprecated method is to add a gap to a block. + // <block type="math_arithmetic" gap="8"></block> + var newGap = parseFloat(childIn.getAttribute('gap')); + if (!isNaN(newGap) && lastElement) { + lastElement.setAttribute('gap', newGap); + } + } + } + break; + case 'BLOCK': + case 'SHADOW': + treeOut.blocks.push(childIn); + lastElement = childIn; + break; + } + } + return openNode; +}; + +/** + * Recursively add colours to this toolbox. + * @param {Blockly.Toolbox.TreeNode} opt_tree Starting point of tree. + * Defaults to the root node. + * @private + */ +Blockly.Toolbox.prototype.addColour_ = function(opt_tree) { + var tree = opt_tree || this.tree_; + var children = tree.getChildren(); + for (var i = 0, child; child = children[i]; i++) { + var element = child.getRowElement(); + if (element) { + if (this.hasColours_) { + var border = '8px solid ' + (child.hexColour || '#ddd'); + } else { + var border = 'none'; + } + if (this.workspace_.RTL) { + element.style.borderRight = border; + } else { + element.style.borderLeft = border; + } + } + this.addColour_(child); + } +}; + +/** + * Unhighlight any previously specified option. + */ +Blockly.Toolbox.prototype.clearSelection = function() { + this.tree_.setSelectedItem(null); +}; + +/** + * Return the deletion rectangle for this toolbox. + * @return {goog.math.Rect} Rectangle in which to delete. + */ +Blockly.Toolbox.prototype.getClientRect = function() { + if (!this.HtmlDiv) { + return null; + } + + // BIG_NUM is offscreen padding so that blocks dragged beyond the toolbox + // area are still deleted. Must be smaller than Infinity, but larger than + // the largest screen size. + var BIG_NUM = 10000000; + var toolboxRect = this.HtmlDiv.getBoundingClientRect(); + + var x = toolboxRect.left; + var y = toolboxRect.top; + var width = toolboxRect.width; + var height = toolboxRect.height; + + // Assumes that the toolbox is on the SVG edge. If this changes + // (e.g. toolboxes in mutators) then this code will need to be more complex. + if (this.toolboxPosition == Blockly.TOOLBOX_AT_LEFT) { + return new goog.math.Rect(-BIG_NUM, -BIG_NUM, BIG_NUM + x + width, + 2 * BIG_NUM); + } else if (this.toolboxPosition == Blockly.TOOLBOX_AT_RIGHT) { + return new goog.math.Rect(x, -BIG_NUM, BIG_NUM + width, 2 * BIG_NUM); + } else if (this.toolboxPosition == Blockly.TOOLBOX_AT_TOP) { + return new goog.math.Rect(-BIG_NUM, -BIG_NUM, 2 * BIG_NUM, + BIG_NUM + y + height); + } else { // Bottom + return new goog.math.Rect(0, y, 2 * BIG_NUM, BIG_NUM + width); + } +}; + +/** + * Update the flyout's contents without closing it. Should be used in response + * to a change in one of the dynamic categories, such as variables or + * procedures. + */ +Blockly.Toolbox.prototype.refreshSelection = function() { + var selectedItem = this.tree_.getSelectedItem(); + if (selectedItem && selectedItem.blocks) { + this.flyout_.show(selectedItem.blocks); + } +}; + +// Extending Closure's Tree UI. + +/** + * Extention of a TreeControl object that uses a custom tree node. + * @param {Blockly.Toolbox} toolbox The parent toolbox for this tree. + * @param {Object} config The configuration for the tree. See + * goog.ui.tree.TreeControl.DefaultConfig. + * @constructor + * @extends {goog.ui.tree.TreeControl} + */ +Blockly.Toolbox.TreeControl = function(toolbox, config) { + this.toolbox_ = toolbox; + goog.ui.tree.TreeControl.call(this, goog.html.SafeHtml.EMPTY, config); +}; +goog.inherits(Blockly.Toolbox.TreeControl, goog.ui.tree.TreeControl); + +/** + * Adds touch handling to TreeControl. + * @override + */ +Blockly.Toolbox.TreeControl.prototype.enterDocument = function() { + Blockly.Toolbox.TreeControl.superClass_.enterDocument.call(this); + + // Add touch handler. + if (goog.events.BrowserFeature.TOUCH_ENABLED) { + var el = this.getElement(); + Blockly.bindEvent_(el, goog.events.EventType.TOUCHSTART, this, + this.handleTouchEvent_); + } +}; + +/** + * Handles touch events. + * @param {!goog.events.BrowserEvent} e The browser event. + * @private + */ +Blockly.Toolbox.TreeControl.prototype.handleTouchEvent_ = function(e) { + e.preventDefault(); + var node = this.getNodeFromEvent_(e); + if (node && e.type === goog.events.EventType.TOUCHSTART) { + // Fire asynchronously since onMouseDown takes long enough that the browser + // would fire the default mouse event before this method returns. + setTimeout(function() { + node.onMouseDown(e); // Same behaviour for click and touch. + }, 1); + } +}; + +/** + * Creates a new tree node using a custom tree node. + * @param {string=} opt_html The HTML content of the node label. + * @return {!goog.ui.tree.TreeNode} The new item. + * @override + */ +Blockly.Toolbox.TreeControl.prototype.createNode = function(opt_html) { + return new Blockly.Toolbox.TreeNode(this.toolbox_, opt_html ? + goog.html.SafeHtml.htmlEscape(opt_html) : goog.html.SafeHtml.EMPTY, + this.getConfig(), this.getDomHelper()); +}; + +/** + * Display/hide the flyout when an item is selected. + * @param {goog.ui.tree.BaseNode} node The item to select. + * @override + */ +Blockly.Toolbox.TreeControl.prototype.setSelectedItem = function(node) { + var toolbox = this.toolbox_; + if (node == this.selectedItem_ || node == toolbox.tree_) { + return; + } + if (toolbox.lastCategory_) { + toolbox.lastCategory_.getRowElement().style.backgroundColor = ''; + } + if (node) { + var hexColour = node.hexColour || '#57e'; + node.getRowElement().style.backgroundColor = hexColour; + // Add colours to child nodes which may have been collapsed and thus + // not rendered. + toolbox.addColour_(node); + } + var oldNode = this.getSelectedItem(); + goog.ui.tree.TreeControl.prototype.setSelectedItem.call(this, node); + if (node && node.blocks && node.blocks.length) { + toolbox.flyout_.show(node.blocks); + // Scroll the flyout to the top if the category has changed. + if (toolbox.lastCategory_ != node) { + toolbox.flyout_.scrollToStart(); + } + } else { + // Hide the flyout. + toolbox.flyout_.hide(); + } + if (oldNode != node && oldNode != this) { + var event = new Blockly.Events.Ui(null, 'category', + oldNode && oldNode.getHtml(), node && node.getHtml()); + event.workspaceId = toolbox.workspace_.id; + Blockly.Events.fire(event); + } + if (node) { + toolbox.lastCategory_ = node; + } +}; + +/** + * A single node in the tree, customized for Blockly's UI. + * @param {Blockly.Toolbox} toolbox The parent toolbox for this tree. + * @param {!goog.html.SafeHtml} html The HTML content of the node label. + * @param {Object=} opt_config The configuration for the tree. See + * goog.ui.tree.TreeControl.DefaultConfig. If not specified, a default config + * will be used. + * @param {goog.dom.DomHelper=} opt_domHelper Optional DOM helper. + * @constructor + * @extends {goog.ui.tree.TreeNode} + */ +Blockly.Toolbox.TreeNode = function(toolbox, html, opt_config, opt_domHelper) { + goog.ui.tree.TreeNode.call(this, html, opt_config, opt_domHelper); + if (toolbox) { + this.horizontalLayout_ = toolbox.horizontalLayout_; + var resize = function() { + // Even though the div hasn't changed size, the visible workspace + // surface of the workspace has, so we may need to reposition everything. + Blockly.svgResize(toolbox.workspace_); + }; + // Fire a resize event since the toolbox may have changed width. + goog.events.listen(toolbox.tree_, + goog.ui.tree.BaseNode.EventType.EXPAND, resize); + goog.events.listen(toolbox.tree_, + goog.ui.tree.BaseNode.EventType.COLLAPSE, resize); + } +}; +goog.inherits(Blockly.Toolbox.TreeNode, goog.ui.tree.TreeNode); + +/** + * Supress population of the +/- icon. + * @return {!goog.html.SafeHtml} The source for the icon. + * @override + */ +Blockly.Toolbox.TreeNode.prototype.getExpandIconSafeHtml = function() { + return goog.html.SafeHtml.create('span'); +}; + +/** + * Expand or collapse the node on mouse click. + * @param {!goog.events.BrowserEvent} e The browser event. + * @override + */ +Blockly.Toolbox.TreeNode.prototype.onMouseDown = function(e) { + // Expand icon. + if (this.hasChildren() && this.isUserCollapsible_) { + this.toggle(); + this.select(); + } else if (this.isSelected()) { + this.getTree().setSelectedItem(null); + } else { + this.select(); + } + this.updateRow(); +}; + +/** + * Supress the inherited double-click behaviour. + * @param {!goog.events.BrowserEvent} e The browser event. + * @override + * @private + */ +Blockly.Toolbox.TreeNode.prototype.onDoubleClick_ = function(e) { + // NOP. +}; + +/** + * Remap event.keyCode in horizontalLayout so that arrow + * keys work properly and call original onKeyDown handler. + * @param {!goog.events.BrowserEvent} e The browser event. + * @return {boolean} The handled value. + * @override + * @private + */ +Blockly.Toolbox.TreeNode.prototype.onKeyDown = function(e) { + if (this.horizontalLayout_) { + var map = {}; + map[goog.events.KeyCodes.RIGHT] = goog.events.KeyCodes.DOWN; + map[goog.events.KeyCodes.LEFT] = goog.events.KeyCodes.UP; + map[goog.events.KeyCodes.UP] = goog.events.KeyCodes.LEFT; + map[goog.events.KeyCodes.DOWN] = goog.events.KeyCodes.RIGHT; + + var newKeyCode = map[e.keyCode]; + e.keyCode = newKeyCode || e.keyCode; + } + return Blockly.Toolbox.TreeNode.superClass_.onKeyDown.call(this, e); +}; + +/** + * A blank separator node in the tree. + * @param {Object=} config The configuration for the tree. See + * goog.ui.tree.TreeControl.DefaultConfig. If not specified, a default config + * will be used. + * @constructor + * @extends {Blockly.Toolbox.TreeNode} + */ +Blockly.Toolbox.TreeSeparator = function(config) { + Blockly.Toolbox.TreeNode.call(this, null, '', config); +}; +goog.inherits(Blockly.Toolbox.TreeSeparator, Blockly.Toolbox.TreeNode); diff --git a/src/blockly/core/tooltip.js b/src/blockly/core/tooltip.js new file mode 100644 index 0000000..2ff612b --- /dev/null +++ b/src/blockly/core/tooltip.js @@ -0,0 +1,286 @@ +/** + * @license + * Visual Blocks Editor + * + * Copyright 2011 Google Inc. + * https://developers.google.com/blockly/ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Library to create tooltips for Blockly. + * First, call Blockly.Tooltip.init() after onload. + * Second, set the 'tooltip' property on any SVG element that needs a tooltip. + * If the tooltip is a string, then that message will be displayed. + * If the tooltip is an SVG element, then that object's tooltip will be used. + * Third, call Blockly.Tooltip.bindMouseEvents(e) passing the SVG element. + * @author fraser@google.com (Neil Fraser) + */ +'use strict'; + +goog.provide('Blockly.Tooltip'); + +goog.require('goog.dom'); +goog.require('goog.dom.TagName'); + + +/** + * Is a tooltip currently showing? + */ +Blockly.Tooltip.visible = false; + +/** + * Maximum width (in characters) of a tooltip. + */ +Blockly.Tooltip.LIMIT = 50; + +/** + * PID of suspended thread to clear tooltip on mouse out. + * @private + */ +Blockly.Tooltip.mouseOutPid_ = 0; + +/** + * PID of suspended thread to show the tooltip. + * @private + */ +Blockly.Tooltip.showPid_ = 0; + +/** + * Last observed X location of the mouse pointer (freezes when tooltip appears). + * @private + */ +Blockly.Tooltip.lastX_ = 0; + +/** + * Last observed Y location of the mouse pointer (freezes when tooltip appears). + * @private + */ +Blockly.Tooltip.lastY_ = 0; + +/** + * Current element being pointed at. + * @private + */ +Blockly.Tooltip.element_ = null; + +/** + * Once a tooltip has opened for an element, that element is 'poisoned' and + * cannot respawn a tooltip until the pointer moves over a different element. + * @private + */ +Blockly.Tooltip.poisonedElement_ = null; + +/** + * Horizontal offset between mouse cursor and tooltip. + */ +Blockly.Tooltip.OFFSET_X = 0; + +/** + * Vertical offset between mouse cursor and tooltip. + */ +Blockly.Tooltip.OFFSET_Y = 10; + +/** + * Radius mouse can move before killing tooltip. + */ +Blockly.Tooltip.RADIUS_OK = 10; + +/** + * Delay before tooltip appears. + */ +Blockly.Tooltip.HOVER_MS = 750; + +/** + * Horizontal padding between tooltip and screen edge. + */ +Blockly.Tooltip.MARGINS = 5; + +/** + * The HTML container. Set once by Blockly.Tooltip.createDom. + * @type {Element} + */ +Blockly.Tooltip.DIV = null; + +/** + * Create the tooltip div and inject it onto the page. + */ +Blockly.Tooltip.createDom = function() { + if (Blockly.Tooltip.DIV) { + return; // Already created. + } + // Create an HTML container for popup overlays (e.g. editor widgets). + Blockly.Tooltip.DIV = + goog.dom.createDom(goog.dom.TagName.DIV, 'blocklyTooltipDiv'); + document.body.appendChild(Blockly.Tooltip.DIV); +}; + +/** + * Binds the required mouse events onto an SVG element. + * @param {!Element} element SVG element onto which tooltip is to be bound. + */ +Blockly.Tooltip.bindMouseEvents = function(element) { + Blockly.bindEvent_(element, 'mouseover', null, Blockly.Tooltip.onMouseOver_); + Blockly.bindEvent_(element, 'mouseout', null, Blockly.Tooltip.onMouseOut_); + Blockly.bindEvent_(element, 'mousemove', null, Blockly.Tooltip.onMouseMove_); +}; + +/** + * Hide the tooltip if the mouse is over a different object. + * Initialize the tooltip to potentially appear for this object. + * @param {!Event} e Mouse event. + * @private + */ +Blockly.Tooltip.onMouseOver_ = function(e) { + // If the tooltip is an object, treat it as a pointer to the next object in + // the chain to look at. Terminate when a string or function is found. + var element = e.target; + while (!goog.isString(element.tooltip) && !goog.isFunction(element.tooltip)) { + element = element.tooltip; + } + if (Blockly.Tooltip.element_ != element) { + Blockly.Tooltip.hide(); + Blockly.Tooltip.poisonedElement_ = null; + Blockly.Tooltip.element_ = element; + } + // Forget about any immediately preceeding mouseOut event. + clearTimeout(Blockly.Tooltip.mouseOutPid_); +}; + +/** + * Hide the tooltip if the mouse leaves the object and enters the workspace. + * @param {!Event} e Mouse event. + * @private + */ +Blockly.Tooltip.onMouseOut_ = function(e) { + // Moving from one element to another (overlapping or with no gap) generates + // a mouseOut followed instantly by a mouseOver. Fork off the mouseOut + // event and kill it if a mouseOver is received immediately. + // This way the task only fully executes if mousing into the void. + Blockly.Tooltip.mouseOutPid_ = setTimeout(function() { + Blockly.Tooltip.element_ = null; + Blockly.Tooltip.poisonedElement_ = null; + Blockly.Tooltip.hide(); + }, 1); + clearTimeout(Blockly.Tooltip.showPid_); +}; + +/** + * When hovering over an element, schedule a tooltip to be shown. If a tooltip + * is already visible, hide it if the mouse strays out of a certain radius. + * @param {!Event} e Mouse event. + * @private + */ +Blockly.Tooltip.onMouseMove_ = function(e) { + if (!Blockly.Tooltip.element_ || !Blockly.Tooltip.element_.tooltip) { + // No tooltip here to show. + return; + } else if (Blockly.dragMode_ != Blockly.DRAG_NONE) { + // Don't display a tooltip during a drag. + return; + } else if (Blockly.WidgetDiv.isVisible()) { + // Don't display a tooltip if a widget is open (tooltip would be under it). + return; + } + if (Blockly.Tooltip.visible) { + // Compute the distance between the mouse position when the tooltip was + // shown and the current mouse position. Pythagorean theorem. + var dx = Blockly.Tooltip.lastX_ - e.pageX; + var dy = Blockly.Tooltip.lastY_ - e.pageY; + if (Math.sqrt(dx * dx + dy * dy) > Blockly.Tooltip.RADIUS_OK) { + Blockly.Tooltip.hide(); + } + } else if (Blockly.Tooltip.poisonedElement_ != Blockly.Tooltip.element_) { + // The mouse moved, clear any previously scheduled tooltip. + clearTimeout(Blockly.Tooltip.showPid_); + // Maybe this time the mouse will stay put. Schedule showing of tooltip. + Blockly.Tooltip.lastX_ = e.pageX; + Blockly.Tooltip.lastY_ = e.pageY; + Blockly.Tooltip.showPid_ = + setTimeout(Blockly.Tooltip.show_, Blockly.Tooltip.HOVER_MS); + } +}; + +/** + * Hide the tooltip. + */ +Blockly.Tooltip.hide = function() { + if (Blockly.Tooltip.visible) { + Blockly.Tooltip.visible = false; + if (Blockly.Tooltip.DIV) { + Blockly.Tooltip.DIV.style.display = 'none'; + } + } + clearTimeout(Blockly.Tooltip.showPid_); +}; + +/** + * Create the tooltip and show it. + * @private + */ +Blockly.Tooltip.show_ = function() { + Blockly.Tooltip.poisonedElement_ = Blockly.Tooltip.element_; + if (!Blockly.Tooltip.DIV) { + return; + } + // Erase all existing text. + goog.dom.removeChildren(/** @type {!Element} */ (Blockly.Tooltip.DIV)); + // Get the new text. + var tip = Blockly.Tooltip.element_.tooltip; + while (goog.isFunction(tip)) { + tip = tip(); + } + tip = Blockly.utils.wrap(tip, Blockly.Tooltip.LIMIT); + // Create new text, line by line. + var lines = tip.split('\n'); + for (var i = 0; i < lines.length; i++) { + var div = document.createElement('div'); + div.appendChild(document.createTextNode(lines[i])); + Blockly.Tooltip.DIV.appendChild(div); + } + var rtl = Blockly.Tooltip.element_.RTL; + var windowSize = goog.dom.getViewportSize(); + // Display the tooltip. + Blockly.Tooltip.DIV.style.direction = rtl ? 'rtl' : 'ltr'; + Blockly.Tooltip.DIV.style.display = 'block'; + Blockly.Tooltip.visible = true; + // Move the tooltip to just below the cursor. + var anchorX = Blockly.Tooltip.lastX_; + if (rtl) { + anchorX -= Blockly.Tooltip.OFFSET_X + Blockly.Tooltip.DIV.offsetWidth; + } else { + anchorX += Blockly.Tooltip.OFFSET_X; + } + var anchorY = Blockly.Tooltip.lastY_ + Blockly.Tooltip.OFFSET_Y; + + if (anchorY + Blockly.Tooltip.DIV.offsetHeight > + windowSize.height + window.scrollY) { + // Falling off the bottom of the screen; shift the tooltip up. + anchorY -= Blockly.Tooltip.DIV.offsetHeight + 2 * Blockly.Tooltip.OFFSET_Y; + } + if (rtl) { + // Prevent falling off left edge in RTL mode. + anchorX = Math.max(Blockly.Tooltip.MARGINS - window.scrollX, anchorX); + } else { + if (anchorX + Blockly.Tooltip.DIV.offsetWidth > + windowSize.width + window.scrollX - 2 * Blockly.Tooltip.MARGINS) { + // Falling off the right edge of the screen; + // clamp the tooltip on the edge. + anchorX = windowSize.width - Blockly.Tooltip.DIV.offsetWidth - + 2 * Blockly.Tooltip.MARGINS; + } + } + Blockly.Tooltip.DIV.style.top = anchorY + 'px'; + Blockly.Tooltip.DIV.style.left = anchorX + 'px'; +}; diff --git a/src/blockly/core/trashcan.js b/src/blockly/core/trashcan.js new file mode 100644 index 0000000..28baa0f --- /dev/null +++ b/src/blockly/core/trashcan.js @@ -0,0 +1,332 @@ +/** + * @license + * Visual Blocks Editor + * + * Copyright 2011 Google Inc. + * https://developers.google.com/blockly/ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Object representing a trash can icon. + * @author fraser@google.com (Neil Fraser) + */ +'use strict'; + +goog.provide('Blockly.Trashcan'); + +goog.require('goog.Timer'); +goog.require('goog.dom'); +goog.require('goog.math'); +goog.require('goog.math.Rect'); + + +/** + * Class for a trash can. + * @param {!Blockly.Workspace} workspace The workspace to sit in. + * @constructor + */ +Blockly.Trashcan = function(workspace) { + this.workspace_ = workspace; +}; + +/** + * Width of both the trash can and lid images. + * @type {number} + * @private + */ +Blockly.Trashcan.prototype.WIDTH_ = 47; + +/** + * Height of the trashcan image (minus lid). + * @type {number} + * @private + */ +Blockly.Trashcan.prototype.BODY_HEIGHT_ = 44; + +/** + * Height of the lid image. + * @type {number} + * @private + */ +Blockly.Trashcan.prototype.LID_HEIGHT_ = 16; + +/** + * Distance between trashcan and bottom edge of workspace. + * @type {number} + * @private + */ +Blockly.Trashcan.prototype.MARGIN_BOTTOM_ = 20; + +/** + * Distance between trashcan and right edge of workspace. + * @type {number} + * @private + */ +Blockly.Trashcan.prototype.MARGIN_SIDE_ = 20; + +/** + * Extent of hotspot on all sides beyond the size of the image. + * @type {number} + * @private + */ +Blockly.Trashcan.prototype.MARGIN_HOTSPOT_ = 10; + +/** + * Location of trashcan in sprite image. + * @type {number} + * @private + */ +Blockly.Trashcan.prototype.SPRITE_LEFT_ = 0; + +/** + * Location of trashcan in sprite image. + * @type {number} + * @private + */ +Blockly.Trashcan.prototype.SPRITE_TOP_ = 32; + +/** + * Current open/close state of the lid. + * @type {boolean} + */ +Blockly.Trashcan.prototype.isOpen = false; + +/** + * The SVG group containing the trash can. + * @type {Element} + * @private + */ +Blockly.Trashcan.prototype.svgGroup_ = null; + +/** + * The SVG image element of the trash can lid. + * @type {Element} + * @private + */ +Blockly.Trashcan.prototype.svgLid_ = null; + +/** + * Task ID of opening/closing animation. + * @type {number} + * @private + */ +Blockly.Trashcan.prototype.lidTask_ = 0; + +/** + * Current state of lid opening (0.0 = closed, 1.0 = open). + * @type {number} + * @private + */ +Blockly.Trashcan.prototype.lidOpen_ = 0; + +/** + * Left coordinate of the trash can. + * @type {number} + * @private + */ +Blockly.Trashcan.prototype.left_ = 0; + +/** + * Top coordinate of the trash can. + * @type {number} + * @private + */ +Blockly.Trashcan.prototype.top_ = 0; + +/** + * Create the trash can elements. + * @return {!Element} The trash can's SVG group. + */ +Blockly.Trashcan.prototype.createDom = function() { + /* Here's the markup that will be generated: + <g class="blocklyTrash"> + <clippath id="blocklyTrashBodyClipPath837493"> + <rect width="47" height="45" y="15"></rect> + </clippath> + <image width="64" height="92" y="-32" xlink:href="media/sprites.png" + clip-path="url(#blocklyTrashBodyClipPath837493)"></image> + <clippath id="blocklyTrashLidClipPath837493"> + <rect width="47" height="15"></rect> + </clippath> + <image width="84" height="92" y="-32" xlink:href="media/sprites.png" + clip-path="url(#blocklyTrashLidClipPath837493)"></image> + </g> + */ + this.svgGroup_ = Blockly.createSvgElement('g', + {'class': 'blocklyTrash'}, null); + var rnd = String(Math.random()).substring(2); + var clip = Blockly.createSvgElement('clipPath', + {'id': 'blocklyTrashBodyClipPath' + rnd}, + this.svgGroup_); + Blockly.createSvgElement('rect', + {'width': this.WIDTH_, 'height': this.BODY_HEIGHT_, + 'y': this.LID_HEIGHT_}, + clip); + var body = Blockly.createSvgElement('image', + {'width': Blockly.SPRITE.width, 'x': -this.SPRITE_LEFT_, + 'height': Blockly.SPRITE.height, 'y': -this.SPRITE_TOP_, + 'clip-path': 'url(#blocklyTrashBodyClipPath' + rnd + ')'}, + this.svgGroup_); + body.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href', + this.workspace_.options.pathToMedia + Blockly.SPRITE.url); + + var clip = Blockly.createSvgElement('clipPath', + {'id': 'blocklyTrashLidClipPath' + rnd}, + this.svgGroup_); + Blockly.createSvgElement('rect', + {'width': this.WIDTH_, 'height': this.LID_HEIGHT_}, clip); + this.svgLid_ = Blockly.createSvgElement('image', + {'width': Blockly.SPRITE.width, 'x': -this.SPRITE_LEFT_, + 'height': Blockly.SPRITE.height, 'y': -this.SPRITE_TOP_, + 'clip-path': 'url(#blocklyTrashLidClipPath' + rnd + ')'}, + this.svgGroup_); + this.svgLid_.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href', + this.workspace_.options.pathToMedia + Blockly.SPRITE.url); + + Blockly.bindEvent_(this.svgGroup_, 'mouseup', this, this.click); + this.animateLid_(); + return this.svgGroup_; +}; + +/** + * Initialize the trash can. + * @param {number} bottom Distance from workspace bottom to bottom of trashcan. + * @return {number} Distance from workspace bottom to the top of trashcan. + */ +Blockly.Trashcan.prototype.init = function(bottom) { + this.bottom_ = this.MARGIN_BOTTOM_ + bottom; + this.setOpen_(false); + return this.bottom_ + this.BODY_HEIGHT_ + this.LID_HEIGHT_; +}; + +/** + * Dispose of this trash can. + * Unlink from all DOM elements to prevent memory leaks. + */ +Blockly.Trashcan.prototype.dispose = function() { + if (this.svgGroup_) { + goog.dom.removeNode(this.svgGroup_); + this.svgGroup_ = null; + } + this.svgLid_ = null; + this.workspace_ = null; + goog.Timer.clear(this.lidTask_); +}; + +/** + * Move the trash can to the bottom-right corner. + */ +Blockly.Trashcan.prototype.position = function() { + var metrics = this.workspace_.getMetrics(); + if (!metrics) { + // There are no metrics available (workspace is probably not visible). + return; + } + if (this.workspace_.RTL) { + this.left_ = this.MARGIN_SIDE_ + Blockly.Scrollbar.scrollbarThickness; + if (metrics.toolboxPosition == Blockly.TOOLBOX_AT_LEFT) { + this.left_ += metrics.flyoutWidth; + if (this.workspace_.toolbox_) { + this.left_ += metrics.absoluteLeft; + } + } + } else { + this.left_ = metrics.viewWidth + metrics.absoluteLeft - + this.WIDTH_ - this.MARGIN_SIDE_ - Blockly.Scrollbar.scrollbarThickness; + + if (metrics.toolboxPosition == Blockly.TOOLBOX_AT_RIGHT) { + this.left_ -= metrics.flyoutWidth; + } + } + this.top_ = metrics.viewHeight + metrics.absoluteTop - + (this.BODY_HEIGHT_ + this.LID_HEIGHT_) - this.bottom_; + + if (metrics.toolboxPosition == Blockly.TOOLBOX_AT_BOTTOM) { + this.top_ -= metrics.flyoutHeight; + } + this.svgGroup_.setAttribute('transform', + 'translate(' + this.left_ + ',' + this.top_ + ')'); +}; + +/** + * Return the deletion rectangle for this trash can. + * @return {goog.math.Rect} Rectangle in which to delete. + */ +Blockly.Trashcan.prototype.getClientRect = function() { + if (!this.svgGroup_) { + return null; + } + + var trashRect = this.svgGroup_.getBoundingClientRect(); + var left = trashRect.left + this.SPRITE_LEFT_ - this.MARGIN_HOTSPOT_; + var top = trashRect.top + this.SPRITE_TOP_ - this.MARGIN_HOTSPOT_; + var width = this.WIDTH_ + 2 * this.MARGIN_HOTSPOT_; + var height = this.LID_HEIGHT_ + this.BODY_HEIGHT_ + 2 * this.MARGIN_HOTSPOT_; + return new goog.math.Rect(left, top, width, height); + +}; + +/** + * Flip the lid open or shut. + * @param {boolean} state True if open. + * @private + */ +Blockly.Trashcan.prototype.setOpen_ = function(state) { + if (this.isOpen == state) { + return; + } + goog.Timer.clear(this.lidTask_); + this.isOpen = state; + this.animateLid_(); +}; + +/** + * Rotate the lid open or closed by one step. Then wait and recurse. + * @private + */ +Blockly.Trashcan.prototype.animateLid_ = function() { + this.lidOpen_ += this.isOpen ? 0.2 : -0.2; + this.lidOpen_ = goog.math.clamp(this.lidOpen_, 0, 1); + var lidAngle = this.lidOpen_ * 45; + this.svgLid_.setAttribute('transform', 'rotate(' + + (this.workspace_.RTL ? -lidAngle : lidAngle) + ',' + + (this.workspace_.RTL ? 4 : this.WIDTH_ - 4) + ',' + + (this.LID_HEIGHT_ - 2) + ')'); + var opacity = goog.math.lerp(0.4, 0.8, this.lidOpen_); + this.svgGroup_.style.opacity = opacity; + if (this.lidOpen_ > 0 && this.lidOpen_ < 1) { + this.lidTask_ = goog.Timer.callOnce(this.animateLid_, 20, this); + } +}; + +/** + * Flip the lid shut. + * Called externally after a drag. + */ +Blockly.Trashcan.prototype.close = function() { + this.setOpen_(false); +}; + +/** + * Inspect the contents of the trash. + */ +Blockly.Trashcan.prototype.click = function() { + var dx = this.workspace_.startScrollX - this.workspace_.scrollX; + var dy = this.workspace_.startScrollY - this.workspace_.scrollY; + if (Math.sqrt(dx * dx + dy * dy) > Blockly.DRAG_RADIUS) { + return; + } + console.log('TODO: Inspect trash.'); +}; diff --git a/src/blockly/core/utils.js b/src/blockly/core/utils.js new file mode 100644 index 0000000..fe13572 --- /dev/null +++ b/src/blockly/core/utils.js @@ -0,0 +1,668 @@ +/** + * @license + * Visual Blocks Editor + * + * Copyright 2012 Google Inc. + * https://developers.google.com/blockly/ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Utility methods. + * These methods are not specific to Blockly, and could be factored out into + * a JavaScript framework such as Closure. + * @author fraser@google.com (Neil Fraser) + */ +'use strict'; + +goog.provide('Blockly.utils'); + +goog.require('goog.dom'); +goog.require('goog.events.BrowserFeature'); +goog.require('goog.math.Coordinate'); +goog.require('goog.userAgent'); + + +/** + * Add a CSS class to a element. + * Similar to Closure's goog.dom.classes.add, except it handles SVG elements. + * @param {!Element} element DOM element to add class to. + * @param {string} className Name of class to add. + * @private + */ +Blockly.addClass_ = function(element, className) { + var classes = element.getAttribute('class') || ''; + if ((' ' + classes + ' ').indexOf(' ' + className + ' ') == -1) { + if (classes) { + classes += ' '; + } + element.setAttribute('class', classes + className); + } +}; + +/** + * Remove a CSS class from a element. + * Similar to Closure's goog.dom.classes.remove, except it handles SVG elements. + * @param {!Element} element DOM element to remove class from. + * @param {string} className Name of class to remove. + * @private + */ +Blockly.removeClass_ = function(element, className) { + var classes = element.getAttribute('class'); + if ((' ' + classes + ' ').indexOf(' ' + className + ' ') != -1) { + var classList = classes.split(/\s+/); + for (var i = 0; i < classList.length; i++) { + if (!classList[i] || classList[i] == className) { + classList.splice(i, 1); + i--; + } + } + if (classList.length) { + element.setAttribute('class', classList.join(' ')); + } else { + element.removeAttribute('class'); + } + } +}; + +/** + * Checks if an element has the specified CSS class. + * Similar to Closure's goog.dom.classes.has, except it handles SVG elements. + * @param {!Element} element DOM element to check. + * @param {string} className Name of class to check. + * @return {boolean} True if class exists, false otherwise. + * @private + */ +Blockly.hasClass_ = function(element, className) { + var classes = element.getAttribute('class'); + return (' ' + classes + ' ').indexOf(' ' + className + ' ') != -1; +}; + +/** + * Bind an event to a function call. + * @param {!Node} node Node upon which to listen. + * @param {string} name Event name to listen to (e.g. 'mousedown'). + * @param {Object} thisObject The value of 'this' in the function. + * @param {!Function} func Function to call when event is triggered. + * @return {!Array.<!Array>} Opaque data that can be passed to unbindEvent_. + * @private + */ +Blockly.bindEvent_ = function(node, name, thisObject, func) { + if (thisObject) { + var wrapFunc = function(e) { + func.call(thisObject, e); + }; + } else { + var wrapFunc = func; + } + node.addEventListener(name, wrapFunc, false); + var bindData = [[node, name, wrapFunc]]; + // Add equivalent touch event. + if (name in Blockly.bindEvent_.TOUCH_MAP) { + wrapFunc = function(e) { + // Punt on multitouch events. + if (e.changedTouches.length == 1) { + // Map the touch event's properties to the event. + var touchPoint = e.changedTouches[0]; + e.clientX = touchPoint.clientX; + e.clientY = touchPoint.clientY; + } + func.call(thisObject, e); + // Stop the browser from scrolling/zooming the page. + e.preventDefault(); + }; + for (var i = 0, eventName; + eventName = Blockly.bindEvent_.TOUCH_MAP[name][i]; i++) { + node.addEventListener(eventName, wrapFunc, false); + bindData.push([node, eventName, wrapFunc]); + } + } + return bindData; +}; + +/** + * The TOUCH_MAP lookup dictionary specifies additional touch events to fire, + * in conjunction with mouse events. + * @type {Object} + */ +Blockly.bindEvent_.TOUCH_MAP = {}; +if (goog.events.BrowserFeature.TOUCH_ENABLED) { + Blockly.bindEvent_.TOUCH_MAP = { + 'mousedown': ['touchstart'], + 'mousemove': ['touchmove'], + 'mouseup': ['touchend', 'touchcancel'] + }; +} + +/** + * Unbind one or more events event from a function call. + * @param {!Array.<!Array>} bindData Opaque data from bindEvent_. This list is + * emptied during the course of calling this function. + * @return {!Function} The function call. + * @private + */ +Blockly.unbindEvent_ = function(bindData) { + while (bindData.length) { + var bindDatum = bindData.pop(); + var node = bindDatum[0]; + var name = bindDatum[1]; + var func = bindDatum[2]; + node.removeEventListener(name, func, false); + } + return func; +}; + +/** + * Don't do anything for this event, just halt propagation. + * @param {!Event} e An event. + */ +Blockly.noEvent = function(e) { + // This event has been handled. No need to bubble up to the document. + e.preventDefault(); + e.stopPropagation(); +}; + +/** + * Is this event targeting a text input widget? + * @param {!Event} e An event. + * @return {boolean} True if text input. + * @private + */ +Blockly.isTargetInput_ = function(e) { + return e.target.type == 'textarea' || e.target.type == 'text' || + e.target.type == 'number' || e.target.type == 'email' || + e.target.type == 'password' || e.target.type == 'search' || + e.target.type == 'tel' || e.target.type == 'url' || + e.target.isContentEditable; +}; + +/** + * Return the coordinates of the top-left corner of this element relative to + * its parent. Only for SVG elements and children (e.g. rect, g, path). + * @param {!Element} element SVG element to find the coordinates of. + * @return {!goog.math.Coordinate} Object with .x and .y properties. + * @private + */ +Blockly.getRelativeXY_ = function(element) { + var xy = new goog.math.Coordinate(0, 0); + // First, check for x and y attributes. + var x = element.getAttribute('x'); + if (x) { + xy.x = parseInt(x, 10); + } + var y = element.getAttribute('y'); + if (y) { + xy.y = parseInt(y, 10); + } + // Second, check for transform="translate(...)" attribute. + var transform = element.getAttribute('transform'); + var r = transform && transform.match(Blockly.getRelativeXY_.XY_REGEXP_); + if (r) { + xy.x += parseFloat(r[1]); + if (r[3]) { + xy.y += parseFloat(r[3]); + } + } + return xy; +}; + +/** + * Static regex to pull the x,y values out of an SVG translate() directive. + * Note that Firefox and IE (9,10) return 'translate(12)' instead of + * 'translate(12, 0)'. + * Note that IE (9,10) returns 'translate(16 8)' instead of 'translate(16, 8)'. + * Note that IE has been reported to return scientific notation (0.123456e-42). + * @type {!RegExp} + * @private + */ +Blockly.getRelativeXY_.XY_REGEXP_ = + /translate\(\s*([-+\d.e]+)([ ,]\s*([-+\d.e]+)\s*\))?/; + +/** + * Return the absolute coordinates of the top-left corner of this element, + * scales that after canvas SVG element, if it's a descendant. + * The origin (0,0) is the top-left corner of the Blockly SVG. + * @param {!Element} element Element to find the coordinates of. + * @param {!Blockly.Workspace} workspace Element must be in this workspace. + * @return {!goog.math.Coordinate} Object with .x and .y properties. + * @private + */ +Blockly.getSvgXY_ = function(element, workspace) { + var x = 0; + var y = 0; + var scale = 1; + if (goog.dom.contains(workspace.getCanvas(), element) || + goog.dom.contains(workspace.getBubbleCanvas(), element)) { + // Before the SVG canvas, scale the coordinates. + scale = workspace.scale; + } + do { + // Loop through this block and every parent. + var xy = Blockly.getRelativeXY_(element); + if (element == workspace.getCanvas() || + element == workspace.getBubbleCanvas()) { + // After the SVG canvas, don't scale the coordinates. + scale = 1; + } + x += xy.x * scale; + y += xy.y * scale; + element = element.parentNode; + } while (element && element != workspace.getParentSvg()); + return new goog.math.Coordinate(x, y); +}; + +/** + * Helper method for creating SVG elements. + * @param {string} name Element's tag name. + * @param {!Object} attrs Dictionary of attribute names and values. + * @param {Element} parent Optional parent on which to append the element. + * @param {Blockly.Workspace=} opt_workspace Optional workspace for access to + * context (scale...). + * @return {!SVGElement} Newly created SVG element. + */ +Blockly.createSvgElement = function(name, attrs, parent, opt_workspace) { + var e = /** @type {!SVGElement} */ ( + document.createElementNS(Blockly.SVG_NS, name)); + for (var key in attrs) { + e.setAttribute(key, attrs[key]); + } + // IE defines a unique attribute "runtimeStyle", it is NOT applied to + // elements created with createElementNS. However, Closure checks for IE + // and assumes the presence of the attribute and crashes. + if (document.body.runtimeStyle) { // Indicates presence of IE-only attr. + e.runtimeStyle = e.currentStyle = e.style; + } + if (parent) { + parent.appendChild(e); + } + return e; +}; + +/** + * Is this event a right-click? + * @param {!Event} e Mouse event. + * @return {boolean} True if right-click. + */ +Blockly.isRightButton = function(e) { + if (e.ctrlKey && goog.userAgent.MAC) { + // Control-clicking on Mac OS X is treated as a right-click. + // WebKit on Mac OS X fails to change button to 2 (but Gecko does). + return true; + } + return e.button == 2; +}; + +/** + * Return the converted coordinates of the given mouse event. + * The origin (0,0) is the top-left corner of the Blockly svg. + * @param {!Event} e Mouse event. + * @param {!Element} svg SVG element. + * @param {SVGMatrix} matrix Inverted screen CTM to use. + * @return {!Object} Object with .x and .y properties. + */ +Blockly.mouseToSvg = function(e, svg, matrix) { + var svgPoint = svg.createSVGPoint(); + svgPoint.x = e.clientX; + svgPoint.y = e.clientY; + + if (!matrix) { + matrix = svg.getScreenCTM().inverse(); + } + return svgPoint.matrixTransform(matrix); +}; + +/** + * Given an array of strings, return the length of the shortest one. + * @param {!Array.<string>} array Array of strings. + * @return {number} Length of shortest string. + */ +Blockly.shortestStringLength = function(array) { + if (!array.length) { + return 0; + } + var len = array[0].length; + for (var i = 1; i < array.length; i++) { + len = Math.min(len, array[i].length); + } + return len; +}; + +/** + * Given an array of strings, return the length of the common prefix. + * Words may not be split. Any space after a word is included in the length. + * @param {!Array.<string>} array Array of strings. + * @param {number=} opt_shortest Length of shortest string. + * @return {number} Length of common prefix. + */ +Blockly.commonWordPrefix = function(array, opt_shortest) { + if (!array.length) { + return 0; + } else if (array.length == 1) { + return array[0].length; + } + var wordPrefix = 0; + var max = opt_shortest || Blockly.shortestStringLength(array); + for (var len = 0; len < max; len++) { + var letter = array[0][len]; + for (var i = 1; i < array.length; i++) { + if (letter != array[i][len]) { + return wordPrefix; + } + } + if (letter == ' ') { + wordPrefix = len + 1; + } + } + for (var i = 1; i < array.length; i++) { + var letter = array[i][len]; + if (letter && letter != ' ') { + return wordPrefix; + } + } + return max; +}; + +/** + * Given an array of strings, return the length of the common suffix. + * Words may not be split. Any space after a word is included in the length. + * @param {!Array.<string>} array Array of strings. + * @param {number=} opt_shortest Length of shortest string. + * @return {number} Length of common suffix. + */ +Blockly.commonWordSuffix = function(array, opt_shortest) { + if (!array.length) { + return 0; + } else if (array.length == 1) { + return array[0].length; + } + var wordPrefix = 0; + var max = opt_shortest || Blockly.shortestStringLength(array); + for (var len = 0; len < max; len++) { + var letter = array[0].substr(-len - 1, 1); + for (var i = 1; i < array.length; i++) { + if (letter != array[i].substr(-len - 1, 1)) { + return wordPrefix; + } + } + if (letter == ' ') { + wordPrefix = len + 1; + } + } + for (var i = 1; i < array.length; i++) { + var letter = array[i].charAt(array[i].length - len - 1); + if (letter && letter != ' ') { + return wordPrefix; + } + } + return max; +}; + +/** + * Is the given string a number (includes negative and decimals). + * @param {string} str Input string. + * @return {boolean} True if number, false otherwise. + */ +Blockly.isNumber = function(str) { + return !!str.match(/^\s*-?\d+(\.\d+)?\s*$/); +}; + +/** + * Parse a string with any number of interpolation tokens (%1, %2, ...). + * '%' characters may be self-escaped (%%). + * @param {string} message Text containing interpolation tokens. + * @return {!Array.<string|number>} Array of strings and numbers. + */ +Blockly.utils.tokenizeInterpolation = function(message) { + var tokens = []; + var chars = message.split(''); + chars.push(''); // End marker. + // Parse the message with a finite state machine. + // 0 - Base case. + // 1 - % found. + // 2 - Digit found. + var state = 0; + var buffer = []; + var number = null; + for (var i = 0; i < chars.length; i++) { + var c = chars[i]; + if (state == 0) { + if (c == '%') { + state = 1; // Start escape. + } else { + buffer.push(c); // Regular char. + } + } else if (state == 1) { + if (c == '%') { + buffer.push(c); // Escaped %: %% + state = 0; + } else if ('0' <= c && c <= '9') { + state = 2; + number = c; + var text = buffer.join(''); + if (text) { + tokens.push(text); + } + buffer.length = 0; + } else { + buffer.push('%', c); // Not an escape: %a + state = 0; + } + } else if (state == 2) { + if ('0' <= c && c <= '9') { + number += c; // Multi-digit number. + } else { + tokens.push(parseInt(number, 10)); + i--; // Parse this char again. + state = 0; + } + } + } + var text = buffer.join(''); + if (text) { + tokens.push(text); + } + return tokens; +}; + +/** + * Generate a unique ID. This should be globally unique. + * 87 characters ^ 20 length > 128 bits (better than a UUID). + * @return {string} A globally unique ID string. + */ +Blockly.genUid = function() { + var length = 20; + var soupLength = Blockly.genUid.soup_.length; + var id = []; + for (var i = 0; i < length; i++) { + id[i] = Blockly.genUid.soup_.charAt(Math.random() * soupLength); + } + return id.join(''); +}; + +/** + * Legal characters for the unique ID. + * Should be all on a US keyboard. No XML special characters or control codes. + * Removed $ due to issue 251. + * @private + */ +Blockly.genUid.soup_ = '!#%()*+,-./:;=?@[]^_`{|}~' + + 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + +/** + * Wrap text to the specified width. + * @param {string} text Text to wrap. + * @param {number} limit Width to wrap each line. + * @return {string} Wrapped text. + */ +Blockly.utils.wrap = function(text, limit) { + var lines = text.split('\n'); + for (var i = 0; i < lines.length; i++) { + lines[i] = Blockly.utils.wrap_line_(lines[i], limit); + } + return lines.join('\n'); +}; + +/** + * Wrap single line of text to the specified width. + * @param {string} text Text to wrap. + * @param {number} limit Width to wrap each line. + * @return {string} Wrapped text. + * @private + */ +Blockly.utils.wrap_line_ = function(text, limit) { + if (text.length <= limit) { + // Short text, no need to wrap. + return text; + } + // Split the text into words. + var words = text.trim().split(/\s+/); + // Set limit to be the length of the largest word. + for (var i = 0; i < words.length; i++) { + if (words[i].length > limit) { + limit = words[i].length; + } + } + + var lastScore; + var score = -Infinity; + var lastText; + var lineCount = 1; + do { + lastScore = score; + lastText = text; + // Create a list of booleans representing if a space (false) or + // a break (true) appears after each word. + var wordBreaks = []; + // Seed the list with evenly spaced linebreaks. + var steps = words.length / lineCount; + var insertedBreaks = 1; + for (var i = 0; i < words.length - 1; i++) { + if (insertedBreaks < (i + 1.5) / steps) { + insertedBreaks++; + wordBreaks[i] = true; + } else { + wordBreaks[i] = false; + } + } + wordBreaks = Blockly.utils.wrapMutate_(words, wordBreaks, limit); + score = Blockly.utils.wrapScore_(words, wordBreaks, limit); + text = Blockly.utils.wrapToText_(words, wordBreaks); + lineCount++; + } while (score > lastScore); + return lastText; +}; + +/** + * Compute a score for how good the wrapping is. + * @param {!Array.<string>} words Array of each word. + * @param {!Array.<boolean>} wordBreaks Array of line breaks. + * @param {number} limit Width to wrap each line. + * @return {number} Larger the better. + * @private + */ +Blockly.utils.wrapScore_ = function(words, wordBreaks, limit) { + // If this function becomes a performance liability, add caching. + // Compute the length of each line. + var lineLengths = [0]; + var linePunctuation = []; + for (var i = 0; i < words.length; i++) { + lineLengths[lineLengths.length - 1] += words[i].length; + if (wordBreaks[i] === true) { + lineLengths.push(0); + linePunctuation.push(words[i].charAt(words[i].length - 1)); + } else if (wordBreaks[i] === false) { + lineLengths[lineLengths.length - 1]++; + } + } + var maxLength = Math.max.apply(Math, lineLengths); + + var score = 0; + for (var i = 0; i < lineLengths.length; i++) { + // Optimize for width. + // -2 points per char over limit (scaled to the power of 1.5). + score -= Math.pow(Math.abs(limit - lineLengths[i]), 1.5) * 2; + // Optimize for even lines. + // -1 point per char smaller than max (scaled to the power of 1.5). + score -= Math.pow(maxLength - lineLengths[i], 1.5); + // Optimize for structure. + // Add score to line endings after punctuation. + if ('.?!'.indexOf(linePunctuation[i]) != -1) { + score += limit / 3; + } else if (',;)]}'.indexOf(linePunctuation[i]) != -1) { + score += limit / 4; + } + } + // All else being equal, the last line should not be longer than the + // previous line. For example, this looks wrong: + // aaa bbb + // ccc ddd eee + if (lineLengths.length > 1 && lineLengths[lineLengths.length - 1] <= + lineLengths[lineLengths.length - 2]) { + score += 0.5; + } + return score; +}; + +/** + * Mutate the array of line break locations until an optimal solution is found. + * No line breaks are added or deleted, they are simply moved around. + * @param {!Array.<string>} words Array of each word. + * @param {!Array.<boolean>} wordBreaks Array of line breaks. + * @param {number} limit Width to wrap each line. + * @return {!Array.<boolean>} New array of optimal line breaks. + * @private + */ +Blockly.utils.wrapMutate_ = function(words, wordBreaks, limit) { + var bestScore = Blockly.utils.wrapScore_(words, wordBreaks, limit); + var bestBreaks; + // Try shifting every line break forward or backward. + for (var i = 0; i < wordBreaks.length - 1; i++) { + if (wordBreaks[i] == wordBreaks[i + 1]) { + continue; + } + var mutatedWordBreaks = [].concat(wordBreaks); + mutatedWordBreaks[i] = !mutatedWordBreaks[i]; + mutatedWordBreaks[i + 1] = !mutatedWordBreaks[i + 1]; + var mutatedScore = + Blockly.utils.wrapScore_(words, mutatedWordBreaks, limit); + if (mutatedScore > bestScore) { + bestScore = mutatedScore; + bestBreaks = mutatedWordBreaks; + } + } + if (bestBreaks) { + // Found an improvement. See if it may be improved further. + return Blockly.utils.wrapMutate_(words, bestBreaks, limit); + } + // No improvements found. Done. + return wordBreaks; +}; + +/** + * Reassemble the array of words into text, with the specified line breaks. + * @param {!Array.<string>} words Array of each word. + * @param {!Array.<boolean>} wordBreaks Array of line breaks. + * @return {string} Plain text. + * @private + */ +Blockly.utils.wrapToText_ = function(words, wordBreaks) { + var text = []; + for (var i = 0; i < words.length; i++) { + text.push(words[i]); + if (wordBreaks[i] !== undefined) { + text.push(wordBreaks[i] ? '\n' : ' '); + } + } + return text.join(''); +}; diff --git a/src/blockly/core/variables.js b/src/blockly/core/variables.js new file mode 100644 index 0000000..00c38ad --- /dev/null +++ b/src/blockly/core/variables.js @@ -0,0 +1,273 @@ +/** + * @license + * Visual Blocks Editor + * + * Copyright 2012 Google Inc. + * https://developers.google.com/blockly/ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Utility functions for handling variables. + * @author fraser@google.com (Neil Fraser) + */ +'use strict'; + +goog.provide('Blockly.Variables'); + +goog.require('Blockly.Blocks'); +goog.require('Blockly.Workspace'); +goog.require('goog.string'); + + +/** + * Category to separate variable names from procedures and generated functions. + */ +Blockly.Variables.NAME_TYPE = 'VARIABLE'; + +/** + * Find all user-created variables that are in use in the workspace. + * For use by generators. + * @param {!Blockly.Block|!Blockly.Workspace} root Root block or workspace. + * @return {!Array.<string>} Array of variable names. + */ +Blockly.Variables.allUsedVariables = function(root) { + var blocks; + if (root instanceof Blockly.Block) { + // Root is Block. + blocks = root.getDescendants(); + } else if (root.getAllBlocks) { + // Root is Workspace. + blocks = root.getAllBlocks(); + } else { + throw 'Not Block or Workspace: ' + root; + } + var variableHash = Object.create(null); + // Iterate through every block and add each variable to the hash. + for (var x = 0; x < blocks.length; x++) { + var blockVariables = blocks[x].getVars(); + if (blockVariables) { + for (var y = 0; y < blockVariables.length; y++) { + var varName = blockVariables[y]; + // Variable name may be null if the block is only half-built. + if (varName) { + variableHash[varName.toLowerCase()] = varName; + } + } + } + } + // Flatten the hash into a list. + var variableList = []; + for (var name in variableHash) { + variableList.push(variableHash[name]); + } + return variableList; +}; + +/** + * Find all variables that the user has created through the workspace or + * toolbox. For use by generators. + * @param {!Blockly.Workspace} root The workspace to inspect. + * @return {!Array.<string>} Array of variable names. + */ +Blockly.Variables.allVariables = function(root) { + if (root instanceof Blockly.Block) { + // Root is Block. + console.warn('Deprecated call to Blockly.Variables.allVariables ' + + 'with a block instead of a workspace. You may want ' + + 'Blockly.Variables.allUsedVariables'); + } + return root.variableList; +}; + +/** + * Construct the blocks required by the flyout for the variable category. + * @param {!Blockly.Workspace} workspace The workspace contianing variables. + * @return {!Array.<!Element>} Array of XML block elements. + */ +Blockly.Variables.flyoutCategory = function(workspace) { + var variableList = workspace.variableList; + variableList.sort(goog.string.caseInsensitiveCompare); + + var xmlList = []; + var button = goog.dom.createDom('button'); + button.setAttribute('text', Blockly.Msg.NEW_VARIABLE); + xmlList.push(button); + + if (variableList.length > 0) { + if (Blockly.Blocks['variables_set']) { + // <block type="variables_set" gap="20"> + // <field name="VAR">item</field> + // </block> + var block = goog.dom.createDom('block'); + block.setAttribute('type', 'variables_set'); + if (Blockly.Blocks['math_change']) { + block.setAttribute('gap', 8); + } else { + block.setAttribute('gap', 24); + } + var field = goog.dom.createDom('field', null, variableList[0]); + field.setAttribute('name', 'VAR'); + block.appendChild(field); + xmlList.push(block); + } + if (Blockly.Blocks['math_change']) { + // <block type="math_change"> + // <value name="DELTA"> + // <shadow type="math_number"> + // <field name="NUM">1</field> + // </shadow> + // </value> + // </block> + var block = goog.dom.createDom('block'); + block.setAttribute('type', 'math_change'); + if (Blockly.Blocks['variables_get']) { + block.setAttribute('gap', 20); + } + var value = goog.dom.createDom('value'); + value.setAttribute('name', 'DELTA'); + block.appendChild(value); + + var field = goog.dom.createDom('field', null, variableList[0]); + field.setAttribute('name', 'VAR'); + block.appendChild(field); + + var shadowBlock = goog.dom.createDom('shadow'); + shadowBlock.setAttribute('type', 'math_number'); + value.appendChild(shadowBlock); + + var numberField = goog.dom.createDom('field', null, '1'); + numberField.setAttribute('name', 'NUM'); + shadowBlock.appendChild(numberField); + + xmlList.push(block); + } + + for (var i = 0; i < variableList.length; i++) { + if (Blockly.Blocks['variables_get']) { + // <block type="variables_get" gap="8"> + // <field name="VAR">item</field> + // </block> + var block = goog.dom.createDom('block'); + block.setAttribute('type', 'variables_get'); + if (Blockly.Blocks['variables_set']) { + block.setAttribute('gap', 8); + } + var field = goog.dom.createDom('field', null, variableList[i]); + field.setAttribute('name', 'VAR'); + block.appendChild(field); + xmlList.push(block); + } + } + } + return xmlList; +}; + +/** +* Return a new variable name that is not yet being used. This will try to +* generate single letter variable names in the range 'i' to 'z' to start with. +* If no unique name is located it will try 'i' to 'z', 'a' to 'h', +* then 'i2' to 'z2' etc. Skip 'l'. + * @param {!Blockly.Workspace} workspace The workspace to be unique in. +* @return {string} New variable name. +*/ +Blockly.Variables.generateUniqueName = function(workspace) { + var variableList = workspace.variableList; + var newName = ''; + if (variableList.length) { + var nameSuffix = 1; + var letters = 'ijkmnopqrstuvwxyzabcdefgh'; // No 'l'. + var letterIndex = 0; + var potName = letters.charAt(letterIndex); + while (!newName) { + var inUse = false; + for (var i = 0; i < variableList.length; i++) { + if (variableList[i].toLowerCase() == potName) { + // This potential name is already used. + inUse = true; + break; + } + } + if (inUse) { + // Try the next potential name. + letterIndex++; + if (letterIndex == letters.length) { + // Reached the end of the character sequence so back to 'i'. + // a new suffix. + letterIndex = 0; + nameSuffix++; + } + potName = letters.charAt(letterIndex); + if (nameSuffix > 1) { + potName += nameSuffix; + } + } else { + // We can use the current potential name. + newName = potName; + } + } + } else { + newName = 'i'; + } + return newName; +}; + +/** + * Create a new variable on the given workspace. + * @param {!Blockly.Workspace} workspace The workspace on which to create the + * variable. + * @return {null|undefined|string} An acceptable new variable name, or null if + * change is to be aborted (cancel button), or undefined if an existing + * variable was chosen. + */ +Blockly.Variables.createVariable = function(workspace) { + while (true) { + var text = Blockly.Variables.promptName(Blockly.Msg.NEW_VARIABLE_TITLE, ''); + if (text) { + if (workspace.variableIndexOf(text) != -1) { + window.alert(Blockly.Msg.VARIABLE_ALREADY_EXISTS.replace('%1', + text.toLowerCase())); + } else { + workspace.createVariable(text); + break; + } + } else { + text = null; + break; + } + } + return text; +}; + +/** + * Prompt the user for a new variable name. + * @param {string} promptText The string of the prompt. + * @param {string} defaultText The default value to show in the prompt's field. + * @return {?string} The new variable name, or null if the user picked + * something illegal. + */ +Blockly.Variables.promptName = function(promptText, defaultText) { + var newVar = window.prompt(promptText, defaultText); + // Merge runs of whitespace. Strip leading and trailing whitespace. + // Beyond this, all names are legal. + if (newVar) { + newVar = newVar.replace(/[\s\xa0]+/g, ' ').replace(/^ | $/g, ''); + if (newVar == Blockly.Msg.RENAME_VARIABLE || + newVar == Blockly.Msg.NEW_VARIABLE) { + // Ok, not ALL names are legal... + newVar = null; + } + } + return newVar; +}; diff --git a/src/blockly/core/warning.js b/src/blockly/core/warning.js new file mode 100644 index 0000000..bffbf06 --- /dev/null +++ b/src/blockly/core/warning.js @@ -0,0 +1,185 @@ +/** + * @license + * Visual Blocks Editor + * + * Copyright 2012 Google Inc. + * https://developers.google.com/blockly/ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Object representing a warning. + * @author fraser@google.com (Neil Fraser) + */ +'use strict'; + +goog.provide('Blockly.Warning'); + +goog.require('Blockly.Bubble'); +goog.require('Blockly.Icon'); + + +/** + * Class for a warning. + * @param {!Blockly.Block} block The block associated with this warning. + * @extends {Blockly.Icon} + * @constructor + */ +Blockly.Warning = function(block) { + Blockly.Warning.superClass_.constructor.call(this, block); + this.createIcon(); + // The text_ object can contain multiple warnings. + this.text_ = {}; +}; +goog.inherits(Blockly.Warning, Blockly.Icon); + +/** + * Does this icon get hidden when the block is collapsed. + */ +Blockly.Warning.prototype.collapseHidden = false; + +/** + * Draw the warning icon. + * @param {!Element} group The icon group. + * @private + */ +Blockly.Warning.prototype.drawIcon_ = function(group) { + // Triangle with rounded corners. + Blockly.createSvgElement('path', + {'class': 'blocklyIconShape', + 'd': 'M2,15Q-1,15 0.5,12L6.5,1.7Q8,-1 9.5,1.7L15.5,12Q17,15 14,15z'}, + group); + // Can't use a real '!' text character since different browsers and operating + // systems render it differently. + // Body of exclamation point. + Blockly.createSvgElement('path', + {'class': 'blocklyIconSymbol', + 'd': 'm7,4.8v3.16l0.27,2.27h1.46l0.27,-2.27v-3.16z'}, + group); + // Dot of exclamation point. + Blockly.createSvgElement('rect', + {'class': 'blocklyIconSymbol', + 'x': '7', 'y': '11', 'height': '2', 'width': '2'}, + group); +}; + +/** + * Create the text for the warning's bubble. + * @param {string} text The text to display. + * @return {!SVGTextElement} The top-level node of the text. + * @private + */ +Blockly.Warning.textToDom_ = function(text) { + var paragraph = /** @type {!SVGTextElement} */ ( + Blockly.createSvgElement('text', + {'class': 'blocklyText blocklyBubbleText', + 'y': Blockly.Bubble.BORDER_WIDTH}, + null)); + var lines = text.split('\n'); + for (var i = 0; i < lines.length; i++) { + var tspanElement = Blockly.createSvgElement('tspan', + {'dy': '1em', 'x': Blockly.Bubble.BORDER_WIDTH}, paragraph); + var textNode = document.createTextNode(lines[i]); + tspanElement.appendChild(textNode); + } + return paragraph; +}; + +/** + * Show or hide the warning bubble. + * @param {boolean} visible True if the bubble should be visible. + */ +Blockly.Warning.prototype.setVisible = function(visible) { + if (visible == this.isVisible()) { + // No change. + return; + } + Blockly.Events.fire( + new Blockly.Events.Ui(this.block_, 'warningOpen', !visible, visible)); + if (visible) { + // Create the bubble to display all warnings. + var paragraph = Blockly.Warning.textToDom_(this.getText()); + this.bubble_ = new Blockly.Bubble( + /** @type {!Blockly.WorkspaceSvg} */ (this.block_.workspace), + paragraph, this.block_.svgPath_, this.iconXY_, null, null); + if (this.block_.RTL) { + // Right-align the paragraph. + // This cannot be done until the bubble is rendered on screen. + var maxWidth = paragraph.getBBox().width; + for (var i = 0, textElement; textElement = paragraph.childNodes[i]; i++) { + textElement.setAttribute('text-anchor', 'end'); + textElement.setAttribute('x', maxWidth + Blockly.Bubble.BORDER_WIDTH); + } + } + this.updateColour(); + // Bump the warning into the right location. + var size = this.bubble_.getBubbleSize(); + this.bubble_.setBubbleSize(size.width, size.height); + } else { + // Dispose of the bubble. + this.bubble_.dispose(); + this.bubble_ = null; + this.body_ = null; + } +}; + +/** + * Bring the warning to the top of the stack when clicked on. + * @param {!Event} e Mouse up event. + * @private + */ +Blockly.Warning.prototype.bodyFocus_ = function(e) { + this.bubble_.promote_(); +}; + +/** + * Set this warning's text. + * @param {string} text Warning text (or '' to delete). + * @param {string} id An ID for this text entry to be able to maintain + * multiple warnings. + */ +Blockly.Warning.prototype.setText = function(text, id) { + if (this.text_[id] == text) { + return; + } + if (text) { + this.text_[id] = text; + } else { + delete this.text_[id]; + } + if (this.isVisible()) { + this.setVisible(false); + this.setVisible(true); + } +}; + +/** + * Get this warning's texts. + * @return {string} All texts concatenated into one string. + */ +Blockly.Warning.prototype.getText = function() { + var allWarnings = []; + for (var id in this.text_) { + allWarnings.push(this.text_[id]); + } + return allWarnings.join('\n'); +}; + +/** + * Dispose of this warning. + */ +Blockly.Warning.prototype.dispose = function() { + this.block_.warning = null; + Blockly.Icon.prototype.dispose.call(this); +}; diff --git a/src/blockly/core/widgetdiv.js b/src/blockly/core/widgetdiv.js new file mode 100644 index 0000000..a811339 --- /dev/null +++ b/src/blockly/core/widgetdiv.js @@ -0,0 +1,152 @@ +/** + * @license + * Visual Blocks Editor + * + * Copyright 2013 Google Inc. + * https://developers.google.com/blockly/ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview A div that floats on top of Blockly. This singleton contains + * temporary HTML UI widgets that the user is currently interacting with. + * E.g. text input areas, colour pickers, context menus. + * @author fraser@google.com (Neil Fraser) + */ +'use strict'; + +goog.provide('Blockly.WidgetDiv'); + +goog.require('Blockly.Css'); +goog.require('goog.dom'); +goog.require('goog.dom.TagName'); +goog.require('goog.style'); + + +/** + * The HTML container. Set once by Blockly.WidgetDiv.createDom. + * @type {Element} + */ +Blockly.WidgetDiv.DIV = null; + +/** + * The object currently using this container. + * @type {Object} + * @private + */ +Blockly.WidgetDiv.owner_ = null; + +/** + * Optional cleanup function set by whichever object uses the widget. + * @type {Function} + * @private + */ +Blockly.WidgetDiv.dispose_ = null; + +/** + * Create the widget div and inject it onto the page. + */ +Blockly.WidgetDiv.createDom = function() { + if (Blockly.WidgetDiv.DIV) { + return; // Already created. + } + // Create an HTML container for popup overlays (e.g. editor widgets). + Blockly.WidgetDiv.DIV = + goog.dom.createDom(goog.dom.TagName.DIV, 'blocklyWidgetDiv'); + document.body.appendChild(Blockly.WidgetDiv.DIV); +}; + +/** + * Initialize and display the widget div. Close the old one if needed. + * @param {!Object} newOwner The object that will be using this container. + * @param {boolean} rtl Right-to-left (true) or left-to-right (false). + * @param {Function} dispose Optional cleanup function to be run when the widget + * is closed. + */ +Blockly.WidgetDiv.show = function(newOwner, rtl, dispose) { + Blockly.WidgetDiv.hide(); + Blockly.WidgetDiv.owner_ = newOwner; + Blockly.WidgetDiv.dispose_ = dispose; + // Temporarily move the widget to the top of the screen so that it does not + // cause a scrollbar jump in Firefox when displayed. + var xy = goog.style.getViewportPageOffset(document); + Blockly.WidgetDiv.DIV.style.top = xy.y + 'px'; + Blockly.WidgetDiv.DIV.style.direction = rtl ? 'rtl' : 'ltr'; + Blockly.WidgetDiv.DIV.style.display = 'block'; +}; + +/** + * Destroy the widget and hide the div. + */ +Blockly.WidgetDiv.hide = function() { + if (Blockly.WidgetDiv.owner_) { + Blockly.WidgetDiv.owner_ = null; + Blockly.WidgetDiv.DIV.style.display = 'none'; + Blockly.WidgetDiv.DIV.style.left = ''; + Blockly.WidgetDiv.DIV.style.top = ''; + Blockly.WidgetDiv.dispose_ && Blockly.WidgetDiv.dispose_(); + Blockly.WidgetDiv.dispose_ = null; + goog.dom.removeChildren(Blockly.WidgetDiv.DIV); + } +}; + +/** + * Is the container visible? + * @return {boolean} True if visible. + */ +Blockly.WidgetDiv.isVisible = function() { + return !!Blockly.WidgetDiv.owner_; +}; + +/** + * Destroy the widget and hide the div if it is being used by the specified + * object. + * @param {!Object} oldOwner The object that was using this container. + */ +Blockly.WidgetDiv.hideIfOwner = function(oldOwner) { + if (Blockly.WidgetDiv.owner_ == oldOwner) { + Blockly.WidgetDiv.hide(); + } +}; + +/** + * Position the widget at a given location. Prevent the widget from going + * offscreen top or left (right in RTL). + * @param {number} anchorX Horizontal location (window coorditates, not body). + * @param {number} anchorY Vertical location (window coorditates, not body). + * @param {!goog.math.Size} windowSize Height/width of window. + * @param {!goog.math.Coordinate} scrollOffset X/y of window scrollbars. + * @param {boolean} rtl True if RTL, false if LTR. + */ +Blockly.WidgetDiv.position = function(anchorX, anchorY, windowSize, + scrollOffset, rtl) { + // Don't let the widget go above the top edge of the window. + if (anchorY < scrollOffset.y) { + anchorY = scrollOffset.y; + } + if (rtl) { + // Don't let the widget go right of the right edge of the window. + if (anchorX > windowSize.width + scrollOffset.x) { + anchorX = windowSize.width + scrollOffset.x; + } + } else { + // Don't let the widget go left of the left edge of the window. + if (anchorX < scrollOffset.x) { + anchorX = scrollOffset.x; + } + } + Blockly.WidgetDiv.DIV.style.left = anchorX + 'px'; + Blockly.WidgetDiv.DIV.style.top = anchorY + 'px'; + Blockly.WidgetDiv.DIV.style.height = windowSize.height + 'px'; +}; diff --git a/src/blockly/core/workspace.js b/src/blockly/core/workspace.js new file mode 100644 index 0000000..a8e97c0 --- /dev/null +++ b/src/blockly/core/workspace.js @@ -0,0 +1,501 @@ +/** + * @license + * Visual Blocks Editor + * + * Copyright 2012 Google Inc. + * https://developers.google.com/blockly/ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Object representing a workspace. + * @author fraser@google.com (Neil Fraser) + */ +'use strict'; + +goog.provide('Blockly.Workspace'); + +goog.require('goog.math'); + + +/** + * Class for a workspace. This is a data structure that contains blocks. + * There is no UI, and can be created headlessly. + * @param {Blockly.Options} opt_options Dictionary of options. + * @constructor + */ +Blockly.Workspace = function(opt_options) { + /** @type {string} */ + this.id = Blockly.genUid(); + Blockly.Workspace.WorkspaceDB_[this.id] = this; + /** @type {!Blockly.Options} */ + this.options = opt_options || {}; + /** @type {boolean} */ + this.RTL = !!this.options.RTL; + /** @type {boolean} */ + this.horizontalLayout = !!this.options.horizontalLayout; + /** @type {number} */ + this.toolboxPosition = this.options.toolboxPosition; + + /** + * @type {!Array.<!Blockly.Block>} + * @private + */ + this.topBlocks_ = []; + /** + * @type {!Array.<!Function>} + * @private + */ + this.listeners_ = []; + /** + * @type {!Array.<!Blockly.Events.Abstract>} + * @private + */ + this.undoStack_ = []; + /** + * @type {!Array.<!Blockly.Events.Abstract>} + * @private + */ + this.redoStack_ = []; + /** + * @type {!Object} + * @private + */ + this.blockDB_ = Object.create(null); + /* + * @type {!Array.<!string>} + * A list of all of the named variables in the workspace, including variables + * that are not currently in use. + */ + this.variableList = []; +}; + +/** + * Workspaces may be headless. + * @type {boolean} True if visible. False if headless. + */ +Blockly.Workspace.prototype.rendered = false; + +/** + * Maximum number of undo events in stack. + * @type {number} 0 to turn off undo, Infinity for unlimited. + */ +Blockly.Workspace.prototype.MAX_UNDO = 1024; + +/** + * Dispose of this workspace. + * Unlink from all DOM elements to prevent memory leaks. + */ +Blockly.Workspace.prototype.dispose = function() { + this.listeners_.length = 0; + this.clear(); + // Remove from workspace database. + delete Blockly.Workspace.WorkspaceDB_[this.id]; +}; + +/** + * Angle away from the horizontal to sweep for blocks. Order of execution is + * generally top to bottom, but a small angle changes the scan to give a bit of + * a left to right bias (reversed in RTL). Units are in degrees. + * See: http://tvtropes.org/pmwiki/pmwiki.php/Main/DiagonalBilling. + */ +Blockly.Workspace.SCAN_ANGLE = 3; + +/** + * Add a block to the list of top blocks. + * @param {!Blockly.Block} block Block to remove. + */ +Blockly.Workspace.prototype.addTopBlock = function(block) { + this.topBlocks_.push(block); + if (this.isFlyout) { + // This is for the (unlikely) case where you have a variable in a block in + // an always-open flyout. It needs to be possible to edit the block in the + // flyout, so the contents of the dropdown need to be correct. + var variables = Blockly.Variables.allUsedVariables(block); + for (var i = 0; i < variables.length; i++) { + if (this.variableList.indexOf(variables[i]) == -1) { + this.variableList.push(variables[i]); + } + } + } +}; + +/** + * Remove a block from the list of top blocks. + * @param {!Blockly.Block} block Block to remove. + */ +Blockly.Workspace.prototype.removeTopBlock = function(block) { + var found = false; + for (var child, i = 0; child = this.topBlocks_[i]; i++) { + if (child == block) { + this.topBlocks_.splice(i, 1); + found = true; + break; + } + } + if (!found) { + throw 'Block not present in workspace\'s list of top-most blocks.'; + } +}; + +/** + * Finds the top-level blocks and returns them. Blocks are optionally sorted + * by position; top to bottom (with slight LTR or RTL bias). + * @param {boolean} ordered Sort the list if true. + * @return {!Array.<!Blockly.Block>} The top-level block objects. + */ +Blockly.Workspace.prototype.getTopBlocks = function(ordered) { + // Copy the topBlocks_ list. + var blocks = [].concat(this.topBlocks_); + if (ordered && blocks.length > 1) { + var offset = Math.sin(goog.math.toRadians(Blockly.Workspace.SCAN_ANGLE)); + if (this.RTL) { + offset *= -1; + } + blocks.sort(function(a, b) { + var aXY = a.getRelativeToSurfaceXY(); + var bXY = b.getRelativeToSurfaceXY(); + return (aXY.y + offset * aXY.x) - (bXY.y + offset * bXY.x); + }); + } + return blocks; +}; + +/** + * Find all blocks in workspace. No particular order. + * @return {!Array.<!Blockly.Block>} Array of blocks. + */ +Blockly.Workspace.prototype.getAllBlocks = function() { + var blocks = this.getTopBlocks(false); + for (var i = 0; i < blocks.length; i++) { + blocks.push.apply(blocks, blocks[i].getChildren()); + } + return blocks; +}; + +/** + * Dispose of all blocks in workspace. + */ +Blockly.Workspace.prototype.clear = function() { + var existingGroup = Blockly.Events.getGroup(); + if (!existingGroup) { + Blockly.Events.setGroup(true); + } + while (this.topBlocks_.length) { + this.topBlocks_[0].dispose(); + } + if (!existingGroup) { + Blockly.Events.setGroup(false); + } + + this.variableList.length = 0; +}; + +/** + * Walk the workspace and update the list of variables to only contain ones in + * use on the workspace. Use when loading new workspaces from disk. + * @param {boolean} clearList True if the old variable list should be cleared. + */ +Blockly.Workspace.prototype.updateVariableList = function(clearList) { + // TODO: Sort + if (!this.isFlyout) { + // Update the list in place so that the flyout's references stay correct. + if (clearList) { + this.variableList.length = 0; + } + var allVariables = Blockly.Variables.allUsedVariables(this); + for (var i = 0; i < allVariables.length; i++) { + this.createVariable(allVariables[i]); + } + } +}; + +/** + * Rename a variable by updating its name in the variable list. + * TODO: #468 + * @param {string} oldName Variable to rename. + * @param {string} newName New variable name. + */ +Blockly.Workspace.prototype.renameVariable = function(oldName, newName) { + // Find the old name in the list. + var variableIndex = this.variableIndexOf(oldName); + var newVariableIndex = this.variableIndexOf(newName); + + // We might be renaming to an existing name but with different case. If so, + // we will also update all of the blocks using the new name to have the + // correct case. + if (newVariableIndex != -1 && + this.variableList[newVariableIndex] != newName) { + var oldCase = this.variableList[newVariableIndex]; + } + + Blockly.Events.setGroup(true); + var blocks = this.getAllBlocks(); + // Iterate through every block. + for (var i = 0; i < blocks.length; i++) { + blocks[i].renameVar(oldName, newName); + if (oldCase) { + blocks[i].renameVar(oldCase, newName); + } + } + Blockly.Events.setGroup(false); + + + if (variableIndex == newVariableIndex || + variableIndex != -1 && newVariableIndex == -1) { + // Only changing case, or renaming to a completely novel name. + this.variableList[variableIndex] = newName; + } else if (variableIndex != -1 && newVariableIndex != -1) { + // Renaming one existing variable to another existing variable. + this.variableList.splice(variableIndex, 1); + // The case might have changed. + this.variableList[newVariableIndex] = newName; + } else { + this.variableList.push(newName); + console.log('Tried to rename an non-existent variable.'); + } +}; + +/** + * Create a variable with the given name. + * TODO: #468 + * @param {string} name The new variable's name. + */ +Blockly.Workspace.prototype.createVariable = function(name) { + var index = this.variableIndexOf(name); + if (index == -1) { + this.variableList.push(name); + } +}; + +/** + * Find all the uses of a named variable. + * @param {string} name Name of variable. + * @return {!Array.<!Blockly.Block>} Array of block usages. + */ +Blockly.Workspace.prototype.getVariableUses = function(name) { + var uses = []; + var blocks = this.getAllBlocks(); + // Iterate through every block and check the name. + for (var i = 0; i < blocks.length; i++) { + var blockVariables = blocks[i].getVars(); + if (blockVariables) { + for (var j = 0; j < blockVariables.length; j++) { + var varName = blockVariables[j]; + // Variable name may be null if the block is only half-built. + if (varName && Blockly.Names.equals(varName, name)) { + uses.push(blocks[i]); + } + } + } + } + return uses; +}; + +/** + * Delete a variables and all of its uses from this workspace. + * @param {string} name Name of variable to delete. + */ +Blockly.Workspace.prototype.deleteVariable = function(name) { + var variableIndex = this.variableIndexOf(name); + if (variableIndex != -1) { + var uses = this.getVariableUses(name); + if (uses.length > 1) { + for (var i = 0, block; block = uses[i]; i++) { + if (block.type == 'procedures_defnoreturn' || + block.type == 'procedures_defreturn') { + var procedureName = block.getFieldValue('NAME'); + window.alert( + Blockly.Msg.CANNOT_DELETE_VARIABLE_PROCEDURE.replace('%1', name). + replace('%2', procedureName)); + return; + } + } + var ok = window.confirm( + Blockly.Msg.DELETE_VARIABLE_CONFIRMATION.replace('%1', uses.length). + replace('%2', name)); + if (!ok) { + return; + } + } + + Blockly.Events.setGroup(true); + for (var i = 0; i < uses.length; i++) { + uses[i].dispose(true, false); + } + Blockly.Events.setGroup(false); + this.variableList.splice(variableIndex, 1); + } +}; + +/** + * Check whether a variable exists with the given name. The check is + * case-insensitive. + * @param {string} name The name to check for. + * @return {number} The index of the name in the variable list, or -1 if it is + * not present. + */ +Blockly.Workspace.prototype.variableIndexOf = function(name) { + for (var i = 0, varname; varname = this.variableList[i]; i++) { + if (Blockly.Names.equals(varname, name)) { + return i; + } + } + return -1; +}; + +/** + * Returns the horizontal offset of the workspace. + * Intended for LTR/RTL compatibility in XML. + * Not relevant for a headless workspace. + * @return {number} Width. + */ +Blockly.Workspace.prototype.getWidth = function() { + return 0; +}; + +/** + * Obtain a newly created block. + * @param {?string} prototypeName Name of the language object containing + * type-specific functions for this block. + * @param {=string} opt_id Optional ID. Use this ID if provided, otherwise + * create a new id. + * @return {!Blockly.Block} The created block. + */ +Blockly.Workspace.prototype.newBlock = function(prototypeName, opt_id) { + return new Blockly.Block(this, prototypeName, opt_id); +}; + +/** + * The number of blocks that may be added to the workspace before reaching + * the maxBlocks. + * @return {number} Number of blocks left. + */ +Blockly.Workspace.prototype.remainingCapacity = function() { + if (isNaN(this.options.maxBlocks)) { + return Infinity; + } + return this.options.maxBlocks - this.getAllBlocks().length; +}; + +/** + * Undo or redo the previous action. + * @param {boolean} redo False if undo, true if redo. + */ +Blockly.Workspace.prototype.undo = function(redo) { + var inputStack = redo ? this.redoStack_ : this.undoStack_; + var outputStack = redo ? this.undoStack_ : this.redoStack_; + var inputEvent = inputStack.pop(); + if (!inputEvent) { + return; + } + var events = [inputEvent]; + // Do another undo/redo if the next one is of the same group. + while (inputStack.length && inputEvent.group && + inputEvent.group == inputStack[inputStack.length - 1].group) { + events.push(inputStack.pop()); + } + // Push these popped events on the opposite stack. + for (var i = 0, event; event = events[i]; i++) { + outputStack.push(event); + } + events = Blockly.Events.filter(events, redo); + Blockly.Events.recordUndo = false; + for (var i = 0, event; event = events[i]; i++) { + event.run(redo); + } + Blockly.Events.recordUndo = true; +}; + +/** + * Clear the undo/redo stacks. + */ +Blockly.Workspace.prototype.clearUndo = function() { + this.undoStack_.length = 0; + this.redoStack_.length = 0; + // Stop any events already in the firing queue from being undoable. + Blockly.Events.clearPendingUndo(); +}; + +/** + * When something in this workspace changes, call a function. + * @param {!Function} func Function to call. + * @return {!Function} Function that can be passed to + * removeChangeListener. + */ +Blockly.Workspace.prototype.addChangeListener = function(func) { + this.listeners_.push(func); + return func; +}; + +/** + * Stop listening for this workspace's changes. + * @param {Function} func Function to stop calling. + */ +Blockly.Workspace.prototype.removeChangeListener = function(func) { + var i = this.listeners_.indexOf(func); + if (i != -1) { + this.listeners_.splice(i, 1); + } +}; + +/** + * Fire a change event. + * @param {!Blockly.Events.Abstract} event Event to fire. + */ +Blockly.Workspace.prototype.fireChangeListener = function(event) { + if (event.recordUndo) { + this.undoStack_.push(event); + this.redoStack_.length = 0; + if (this.undoStack_.length > this.MAX_UNDO) { + this.undoStack_.unshift(); + } + } + for (var i = 0, func; func = this.listeners_[i]; i++) { + func(event); + } +}; + +/** + * Find the block on this workspace with the specified ID. + * @param {string} id ID of block to find. + * @return {Blockly.Block} The sought after block or null if not found. + */ +Blockly.Workspace.prototype.getBlockById = function(id) { + return this.blockDB_[id] || null; +}; + +/** + * Database of all workspaces. + * @private + */ +Blockly.Workspace.WorkspaceDB_ = Object.create(null); + +/** + * Find the workspace with the specified ID. + * @param {string} id ID of workspace to find. + * @return {Blockly.Workspace} The sought after workspace or null if not found. + */ +Blockly.Workspace.getById = function(id) { + return Blockly.Workspace.WorkspaceDB_[id] || null; +}; + +// Export symbols that would otherwise be renamed by Closure compiler. +Blockly.Workspace.prototype['clear'] = Blockly.Workspace.prototype.clear; +Blockly.Workspace.prototype['clearUndo'] = + Blockly.Workspace.prototype.clearUndo; +Blockly.Workspace.prototype['addChangeListener'] = + Blockly.Workspace.prototype.addChangeListener; +Blockly.Workspace.prototype['removeChangeListener'] = + Blockly.Workspace.prototype.removeChangeListener; diff --git a/src/blockly/core/workspace_svg.js b/src/blockly/core/workspace_svg.js new file mode 100644 index 0000000..a3c0b53 --- /dev/null +++ b/src/blockly/core/workspace_svg.js @@ -0,0 +1,1401 @@ +/** + * @license + * Visual Blocks Editor + * + * Copyright 2014 Google Inc. + * https://developers.google.com/blockly/ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Object representing a workspace rendered as SVG. + * @author fraser@google.com (Neil Fraser) + */ +'use strict'; + +goog.provide('Blockly.WorkspaceSvg'); + +// TODO(scr): Fix circular dependencies +//goog.require('Blockly.BlockSvg'); +goog.require('Blockly.ConnectionDB'); +goog.require('Blockly.constants'); +goog.require('Blockly.Options'); +goog.require('Blockly.ScrollbarPair'); +goog.require('Blockly.Trashcan'); +goog.require('Blockly.Workspace'); +goog.require('Blockly.Xml'); +goog.require('Blockly.ZoomControls'); + +goog.require('goog.dom'); +goog.require('goog.math.Coordinate'); +goog.require('goog.userAgent'); + + +/** + * Class for a workspace. This is an onscreen area with optional trashcan, + * scrollbars, bubbles, and dragging. + * @param {!Blockly.Options} options Dictionary of options. + * @extends {Blockly.Workspace} + * @constructor + */ +Blockly.WorkspaceSvg = function(options) { + Blockly.WorkspaceSvg.superClass_.constructor.call(this, options); + this.getMetrics = + options.getMetrics || Blockly.WorkspaceSvg.getTopLevelWorkspaceMetrics_; + this.setMetrics = + options.setMetrics || Blockly.WorkspaceSvg.setTopLevelWorkspaceMetrics_; + + Blockly.ConnectionDB.init(this); + + /** + * Database of pre-loaded sounds. + * @private + * @const + */ + this.SOUNDS_ = Object.create(null); +}; +goog.inherits(Blockly.WorkspaceSvg, Blockly.Workspace); + +/** + * Wrapper function called when a resize event occurs. + * @type {Array.<!Array>} Data that can be passed to unbindEvent_ + */ +Blockly.WorkspaceSvg.prototype.resizeHandlerWrapper_ = null; + +/** + * Svg workspaces are user-visible (as opposed to a headless workspace). + * @type {boolean} True if visible. False if headless. + */ +Blockly.WorkspaceSvg.prototype.rendered = true; + +/** + * Is this workspace the surface for a flyout? + * @type {boolean} + */ +Blockly.WorkspaceSvg.prototype.isFlyout = false; + +/** + * Is this workspace the surface for a mutator? + * @type {boolean} + * @package + */ +Blockly.WorkspaceSvg.prototype.isMutator = false; + +/** + * Is this workspace currently being dragged around? + * DRAG_NONE - No drag operation. + * DRAG_BEGIN - Still inside the initial DRAG_RADIUS. + * DRAG_FREE - Workspace has been dragged further than DRAG_RADIUS. + * @private + */ +Blockly.WorkspaceSvg.prototype.dragMode_ = Blockly.DRAG_NONE; + +/** + * Current horizontal scrolling offset. + * @type {number} + */ +Blockly.WorkspaceSvg.prototype.scrollX = 0; + +/** + * Current vertical scrolling offset. + * @type {number} + */ +Blockly.WorkspaceSvg.prototype.scrollY = 0; + +/** + * Horizontal scroll value when scrolling started. + * @type {number} + */ +Blockly.WorkspaceSvg.prototype.startScrollX = 0; + +/** + * Vertical scroll value when scrolling started. + * @type {number} + */ +Blockly.WorkspaceSvg.prototype.startScrollY = 0; + +/** + * Distance from mouse to object being dragged. + * @type {goog.math.Coordinate} + * @private + */ +Blockly.WorkspaceSvg.prototype.dragDeltaXY_ = null; + +/** + * Current scale. + * @type {number} + */ +Blockly.WorkspaceSvg.prototype.scale = 1; + +/** + * The workspace's trashcan (if any). + * @type {Blockly.Trashcan} + */ +Blockly.WorkspaceSvg.prototype.trashcan = null; + +/** + * This workspace's scrollbars, if they exist. + * @type {Blockly.ScrollbarPair} + */ +Blockly.WorkspaceSvg.prototype.scrollbar = null; + +/** + * Time that the last sound was played. + * @type {Date} + * @private + */ +Blockly.WorkspaceSvg.prototype.lastSound_ = null; + +/** + * Last known position of the page scroll. + * This is used to determine whether we have recalculated screen coordinate + * stuff since the page scrolled. + * @type {!goog.math.Coordinate} + * @private + */ +Blockly.WorkspaceSvg.prototype.lastRecordedPageScroll_ = null; + +/** + * Inverted screen CTM, for use in mouseToSvg. + * @type {SVGMatrix} + * @private + */ +Blockly.WorkspaceSvg.prototype.inverseScreenCTM_ = null; + +/** + * Getter for the inverted screen CTM. + * @return {SVGMatrix} The matrix to use in mouseToSvg + */ +Blockly.WorkspaceSvg.prototype.getInverseScreenCTM = function() { + return this.inverseScreenCTM_; +}; + +/** + * Update the inverted screen CTM. + */ +Blockly.WorkspaceSvg.prototype.updateInverseScreenCTM = function() { + this.inverseScreenCTM_ = this.getParentSvg().getScreenCTM().inverse(); +}; + +/** + * Save resize handler data so we can delete it later in dispose. + * @param {!Array.<!Array>} handler Data that can be passed to unbindEvent_. + */ +Blockly.WorkspaceSvg.prototype.setResizeHandlerWrapper = function(handler) { + this.resizeHandlerWrapper_ = handler; +}; + +/** + * Create the workspace DOM elements. + * @param {string=} opt_backgroundClass Either 'blocklyMainBackground' or + * 'blocklyMutatorBackground'. + * @return {!Element} The workspace's SVG group. + */ +Blockly.WorkspaceSvg.prototype.createDom = function(opt_backgroundClass) { + /** + * <g class="blocklyWorkspace"> + * <rect class="blocklyMainBackground" height="100%" width="100%"></rect> + * [Trashcan and/or flyout may go here] + * <g class="blocklyBlockCanvas"></g> + * <g class="blocklyBubbleCanvas"></g> + * [Scrollbars may go here] + * </g> + * @type {SVGElement} + */ + this.svgGroup_ = Blockly.createSvgElement('g', + {'class': 'blocklyWorkspace'}, null); + if (opt_backgroundClass) { + /** @type {SVGElement} */ + this.svgBackground_ = Blockly.createSvgElement('rect', + {'height': '100%', 'width': '100%', 'class': opt_backgroundClass}, + this.svgGroup_); + if (opt_backgroundClass == 'blocklyMainBackground') { + this.svgBackground_.style.fill = + 'url(#' + this.options.gridPattern.id + ')'; + } + } + /** @type {SVGElement} */ + this.svgBlockCanvas_ = Blockly.createSvgElement('g', + {'class': 'blocklyBlockCanvas'}, this.svgGroup_, this); + /** @type {SVGElement} */ + this.svgBubbleCanvas_ = Blockly.createSvgElement('g', + {'class': 'blocklyBubbleCanvas'}, this.svgGroup_, this); + var bottom = Blockly.Scrollbar.scrollbarThickness; + if (this.options.hasTrashcan) { + bottom = this.addTrashcan_(bottom); + } + if (this.options.zoomOptions && this.options.zoomOptions.controls) { + bottom = this.addZoomControls_(bottom); + } + + if (!this.isFlyout) { + Blockly.bindEvent_(this.svgGroup_, 'mousedown', this, this.onMouseDown_); + var thisWorkspace = this; + Blockly.bindEvent_(this.svgGroup_, 'touchstart', null, + function(e) {Blockly.longStart_(e, thisWorkspace);}); + if (this.options.zoomOptions && this.options.zoomOptions.wheel) { + // Mouse-wheel. + Blockly.bindEvent_(this.svgGroup_, 'wheel', this, this.onMouseWheel_); + } + } + + // Determine if there needs to be a category tree, or a simple list of + // blocks. This cannot be changed later, since the UI is very different. + if (this.options.hasCategories) { + this.toolbox_ = new Blockly.Toolbox(this); + } else if (this.options.languageTree) { + this.addFlyout_(); + } + this.updateGridPattern_(); + this.recordDeleteAreas(); + return this.svgGroup_; +}; + +/** + * Dispose of this workspace. + * Unlink from all DOM elements to prevent memory leaks. + */ +Blockly.WorkspaceSvg.prototype.dispose = function() { + // Stop rerendering. + this.rendered = false; + Blockly.WorkspaceSvg.superClass_.dispose.call(this); + if (this.svgGroup_) { + goog.dom.removeNode(this.svgGroup_); + this.svgGroup_ = null; + } + this.svgBlockCanvas_ = null; + this.svgBubbleCanvas_ = null; + if (this.toolbox_) { + this.toolbox_.dispose(); + this.toolbox_ = null; + } + if (this.flyout_) { + this.flyout_.dispose(); + this.flyout_ = null; + } + if (this.trashcan) { + this.trashcan.dispose(); + this.trashcan = null; + } + if (this.scrollbar) { + this.scrollbar.dispose(); + this.scrollbar = null; + } + if (this.zoomControls_) { + this.zoomControls_.dispose(); + this.zoomControls_ = null; + } + if (!this.options.parentWorkspace) { + // Top-most workspace. Dispose of the div that the + // svg is injected into (i.e. injectionDiv). + goog.dom.removeNode(this.getParentSvg().parentNode); + } + if (this.resizeHandlerWrapper_) { + Blockly.unbindEvent_(this.resizeHandlerWrapper_); + this.resizeHandlerWrapper_ = null; + } +}; + +/** + * Obtain a newly created block. + * @param {?string} prototypeName Name of the language object containing + * type-specific functions for this block. + * @param {=string} opt_id Optional ID. Use this ID if provided, otherwise + * create a new id. + * @return {!Blockly.BlockSvg} The created block. + */ +Blockly.WorkspaceSvg.prototype.newBlock = function(prototypeName, opt_id) { + return new Blockly.BlockSvg(this, prototypeName, opt_id); +}; + +/** + * Add a trashcan. + * @param {number} bottom Distance from workspace bottom to bottom of trashcan. + * @return {number} Distance from workspace bottom to the top of trashcan. + * @private + */ +Blockly.WorkspaceSvg.prototype.addTrashcan_ = function(bottom) { + /** @type {Blockly.Trashcan} */ + this.trashcan = new Blockly.Trashcan(this); + var svgTrashcan = this.trashcan.createDom(); + this.svgGroup_.insertBefore(svgTrashcan, this.svgBlockCanvas_); + return this.trashcan.init(bottom); +}; + +/** + * Add zoom controls. + * @param {number} bottom Distance from workspace bottom to bottom of controls. + * @return {number} Distance from workspace bottom to the top of controls. + * @private + */ +Blockly.WorkspaceSvg.prototype.addZoomControls_ = function(bottom) { + /** @type {Blockly.ZoomControls} */ + this.zoomControls_ = new Blockly.ZoomControls(this); + var svgZoomControls = this.zoomControls_.createDom(); + this.svgGroup_.appendChild(svgZoomControls); + return this.zoomControls_.init(bottom); +}; + +/** + * Add a flyout. + * @private + */ +Blockly.WorkspaceSvg.prototype.addFlyout_ = function() { + var workspaceOptions = { + disabledPatternId: this.options.disabledPatternId, + parentWorkspace: this, + RTL: this.RTL, + horizontalLayout: this.horizontalLayout, + toolboxPosition: this.options.toolboxPosition + }; + /** @type {Blockly.Flyout} */ + this.flyout_ = new Blockly.Flyout(workspaceOptions); + this.flyout_.autoClose = false; + var svgFlyout = this.flyout_.createDom(); + this.svgGroup_.insertBefore(svgFlyout, this.svgBlockCanvas_); +}; + +/** + * Update items that use screen coordinate calculations + * because something has changed (e.g. scroll position, window size). + * @private + */ +Blockly.WorkspaceSvg.prototype.updateScreenCalculations_ = function() { + this.updateInverseScreenCTM(); + this.recordDeleteAreas(); +}; + +/** + * Resize the parts of the workspace that change when the workspace + * contents (e.g. block positions) change. This will also scroll the + * workspace contents if needed. + * @package + */ +Blockly.WorkspaceSvg.prototype.resizeContents = function() { + if (this.scrollbar) { + // TODO(picklesrus): Once rachel-fenichel's scrollbar refactoring + // is complete, call the method that only resizes scrollbar + // based on contents. + this.scrollbar.resize(); + } + this.updateInverseScreenCTM(); +}; + +/** + * Resize and reposition all of the workspace chrome (toolbox, + * trash, scrollbars etc.) + * This should be called when something changes that + * requires recalculating dimensions and positions of the + * trash, zoom, toolbox, etc. (e.g. window resize). + */ +Blockly.WorkspaceSvg.prototype.resize = function() { + if (this.toolbox_) { + this.toolbox_.position(); + } + if (this.flyout_) { + this.flyout_.position(); + } + if (this.trashcan) { + this.trashcan.position(); + } + if (this.zoomControls_) { + this.zoomControls_.position(); + } + if (this.scrollbar) { + this.scrollbar.resize(); + } + this.updateScreenCalculations_(); +}; + +/** + * Resizes and repositions workspace chrome if the page has a new + * scroll position. + * @package + */ +Blockly.WorkspaceSvg.prototype.updateScreenCalculationsIfScrolled + = function() { + /* eslint-disable indent */ + var currScroll = goog.dom.getDocumentScroll(); + if (!goog.math.Coordinate.equals(this.lastRecordedPageScroll_, + currScroll)) { + this.lastRecordedPageScroll_ = currScroll; + this.updateScreenCalculations_(); + } +}; /* eslint-enable indent */ + +/** + * Get the SVG element that forms the drawing surface. + * @return {!Element} SVG element. + */ +Blockly.WorkspaceSvg.prototype.getCanvas = function() { + return this.svgBlockCanvas_; +}; + +/** + * Get the SVG element that forms the bubble surface. + * @return {!SVGGElement} SVG element. + */ +Blockly.WorkspaceSvg.prototype.getBubbleCanvas = function() { + return this.svgBubbleCanvas_; +}; + +/** + * Get the SVG element that contains this workspace. + * @return {!Element} SVG element. + */ +Blockly.WorkspaceSvg.prototype.getParentSvg = function() { + if (this.cachedParentSvg_) { + return this.cachedParentSvg_; + } + var element = this.svgGroup_; + while (element) { + if (element.tagName == 'svg') { + this.cachedParentSvg_ = element; + return element; + } + element = element.parentNode; + } + return null; +}; + +/** + * Translate this workspace to new coordinates. + * @param {number} x Horizontal translation. + * @param {number} y Vertical translation. + */ +Blockly.WorkspaceSvg.prototype.translate = function(x, y) { + var translation = 'translate(' + x + ',' + y + ') ' + + 'scale(' + this.scale + ')'; + this.svgBlockCanvas_.setAttribute('transform', translation); + this.svgBubbleCanvas_.setAttribute('transform', translation); +}; + +/** + * Returns the horizontal offset of the workspace. + * Intended for LTR/RTL compatibility in XML. + * @return {number} Width. + */ +Blockly.WorkspaceSvg.prototype.getWidth = function() { + var metrics = this.getMetrics(); + return metrics ? metrics.viewWidth / this.scale : 0; +}; + +/** + * Toggles the visibility of the workspace. + * Currently only intended for main workspace. + * @param {boolean} isVisible True if workspace should be visible. + */ +Blockly.WorkspaceSvg.prototype.setVisible = function(isVisible) { + this.getParentSvg().style.display = isVisible ? 'block' : 'none'; + if (this.toolbox_) { + // Currently does not support toolboxes in mutators. + this.toolbox_.HtmlDiv.style.display = isVisible ? 'block' : 'none'; + } + if (isVisible) { + this.render(); + if (this.toolbox_) { + this.toolbox_.position(); + } + } else { + Blockly.hideChaff(true); + } +}; + +/** + * Render all blocks in workspace. + */ +Blockly.WorkspaceSvg.prototype.render = function() { + // Generate list of all blocks. + var blocks = this.getAllBlocks(); + // Render each block. + for (var i = blocks.length - 1; i >= 0; i--) { + blocks[i].render(false); + } +}; + +/** + * Turn the visual trace functionality on or off. + * @param {boolean} armed True if the trace should be on. + */ +Blockly.WorkspaceSvg.prototype.traceOn = function(armed) { + this.traceOn_ = armed; + if (this.traceWrapper_) { + Blockly.unbindEvent_(this.traceWrapper_); + this.traceWrapper_ = null; + } + if (armed) { + this.traceWrapper_ = Blockly.bindEvent_(this.svgBlockCanvas_, + 'blocklySelectChange', this, function() {this.traceOn_ = false;}); + } +}; + +/** + * Highlight a block in the workspace. + * @param {?string} id ID of block to find. + */ +Blockly.WorkspaceSvg.prototype.highlightBlock = function(id) { + if (this.traceOn_ && Blockly.dragMode_ != Blockly.DRAG_NONE) { + // The blocklySelectChange event normally prevents this, but sometimes + // there is a race condition on fast-executing apps. + this.traceOn(false); + } + if (!this.traceOn_) { + return; + } + var block = null; + if (id) { + block = this.getBlockById(id); + if (!block) { + return; + } + } + // Temporary turn off the listener for selection changes, so that we don't + // trip the monitor for detecting user activity. + this.traceOn(false); + // Select the current block. + if (block) { + block.select(); + } else if (Blockly.selected) { + Blockly.selected.unselect(); + } + // Restore the monitor for user activity after the selection event has fired. + var thisWorkspace = this; + setTimeout(function() {thisWorkspace.traceOn(true);}, 1); +}; + +/** + * Paste the provided block onto the workspace. + * @param {!Element} xmlBlock XML block element. + */ +Blockly.WorkspaceSvg.prototype.paste = function(xmlBlock) { + if (!this.rendered || xmlBlock.getElementsByTagName('block').length >= + this.remainingCapacity()) { + return; + } + Blockly.terminateDrag_(); // Dragging while pasting? No. + Blockly.Events.disable(); + try { + var block = Blockly.Xml.domToBlock(xmlBlock, this); + // Move the duplicate to original position. + var blockX = parseInt(xmlBlock.getAttribute('x'), 10); + var blockY = parseInt(xmlBlock.getAttribute('y'), 10); + if (!isNaN(blockX) && !isNaN(blockY)) { + if (this.RTL) { + blockX = -blockX; + } + // Offset block until not clobbering another block and not in connection + // distance with neighbouring blocks. + do { + var collide = false; + var allBlocks = this.getAllBlocks(); + for (var i = 0, otherBlock; otherBlock = allBlocks[i]; i++) { + var otherXY = otherBlock.getRelativeToSurfaceXY(); + if (Math.abs(blockX - otherXY.x) <= 1 && + Math.abs(blockY - otherXY.y) <= 1) { + collide = true; + break; + } + } + if (!collide) { + // Check for blocks in snap range to any of its connections. + var connections = block.getConnections_(false); + for (var i = 0, connection; connection = connections[i]; i++) { + var neighbour = connection.closest(Blockly.SNAP_RADIUS, + new goog.math.Coordinate(blockX, blockY)); + if (neighbour.connection) { + collide = true; + break; + } + } + } + if (collide) { + if (this.RTL) { + blockX -= Blockly.SNAP_RADIUS; + } else { + blockX += Blockly.SNAP_RADIUS; + } + blockY += Blockly.SNAP_RADIUS * 2; + } + } while (collide); + block.moveBy(blockX, blockY); + } + } finally { + Blockly.Events.enable(); + } + if (Blockly.Events.isEnabled() && !block.isShadow()) { + Blockly.Events.fire(new Blockly.Events.Create(block)); + } + block.select(); +}; + +/** + * Create a new variable with the given name. Update the flyout to show the new + * variable immediately. + * TODO: #468 + * @param {string} name The new variable's name. + */ +Blockly.WorkspaceSvg.prototype.createVariable = function(name) { + Blockly.WorkspaceSvg.superClass_.createVariable.call(this, name); + if (this.toolbox_ && this.toolbox_.flyout_) { + this.toolbox_.refreshSelection(); + } +}; + +/** + * Make a list of all the delete areas for this workspace. + */ +Blockly.WorkspaceSvg.prototype.recordDeleteAreas = function() { + if (this.trashcan) { + this.deleteAreaTrash_ = this.trashcan.getClientRect(); + } else { + this.deleteAreaTrash_ = null; + } + if (this.flyout_) { + this.deleteAreaToolbox_ = this.flyout_.getClientRect(); + } else if (this.toolbox_) { + this.deleteAreaToolbox_ = this.toolbox_.getClientRect(); + } else { + this.deleteAreaToolbox_ = null; + } +}; + +/** + * Is the mouse event over a delete area (toolbox or non-closing flyout)? + * Opens or closes the trashcan and sets the cursor as a side effect. + * @param {!Event} e Mouse move event. + * @return {boolean} True if event is in a delete area. + */ +Blockly.WorkspaceSvg.prototype.isDeleteArea = function(e) { + var xy = new goog.math.Coordinate(e.clientX, e.clientY); + if (this.deleteAreaTrash_) { + if (this.deleteAreaTrash_.contains(xy)) { + this.trashcan.setOpen_(true); + Blockly.Css.setCursor(Blockly.Css.Cursor.DELETE); + return true; + } + this.trashcan.setOpen_(false); + } + if (this.deleteAreaToolbox_) { + if (this.deleteAreaToolbox_.contains(xy)) { + Blockly.Css.setCursor(Blockly.Css.Cursor.DELETE); + return true; + } + } + Blockly.Css.setCursor(Blockly.Css.Cursor.CLOSED); + return false; +}; + +/** + * Handle a mouse-down on SVG drawing surface. + * @param {!Event} e Mouse down event. + * @private + */ +Blockly.WorkspaceSvg.prototype.onMouseDown_ = function(e) { + this.markFocused(); + if (Blockly.isTargetInput_(e)) { + return; + } + Blockly.terminateDrag_(); // In case mouse-up event was lost. + Blockly.hideChaff(); + var isTargetWorkspace = e.target && e.target.nodeName && + (e.target.nodeName.toLowerCase() == 'svg' || + e.target == this.svgBackground_); + if (isTargetWorkspace && Blockly.selected && !this.options.readOnly) { + // Clicking on the document clears the selection. + Blockly.selected.unselect(); + } + if (Blockly.isRightButton(e)) { + // Right-click. + this.showContextMenu_(e); + } else if (this.scrollbar) { + this.dragMode_ = Blockly.DRAG_BEGIN; + // Record the current mouse position. + this.startDragMouseX = e.clientX; + this.startDragMouseY = e.clientY; + this.startDragMetrics = this.getMetrics(); + this.startScrollX = this.scrollX; + this.startScrollY = this.scrollY; + + // If this is a touch event then bind to the mouseup so workspace drag mode + // is turned off and double move events are not performed on a block. + // See comment in inject.js Blockly.init_ as to why mouseup events are + // bound to the document instead of the SVG's surface. + if ('mouseup' in Blockly.bindEvent_.TOUCH_MAP) { + Blockly.onTouchUpWrapper_ = Blockly.onTouchUpWrapper_ || []; + Blockly.onTouchUpWrapper_ = Blockly.onTouchUpWrapper_.concat( + Blockly.bindEvent_(document, 'mouseup', null, Blockly.onMouseUp_)); + } + Blockly.onMouseMoveWrapper_ = Blockly.onMouseMoveWrapper_ || []; + Blockly.onMouseMoveWrapper_ = Blockly.onMouseMoveWrapper_.concat( + Blockly.bindEvent_(document, 'mousemove', null, Blockly.onMouseMove_)); + } + // This event has been handled. No need to bubble up to the document. + e.stopPropagation(); + e.preventDefault(); +}; + +/** + * Start tracking a drag of an object on this workspace. + * @param {!Event} e Mouse down event. + * @param {!goog.math.Coordinate} xy Starting location of object. + */ +Blockly.WorkspaceSvg.prototype.startDrag = function(e, xy) { + // Record the starting offset between the bubble's location and the mouse. + var point = Blockly.mouseToSvg(e, this.getParentSvg(), + this.getInverseScreenCTM()); + // Fix scale of mouse event. + point.x /= this.scale; + point.y /= this.scale; + this.dragDeltaXY_ = goog.math.Coordinate.difference(xy, point); +}; + +/** + * Track a drag of an object on this workspace. + * @param {!Event} e Mouse move event. + * @return {!goog.math.Coordinate} New location of object. + */ +Blockly.WorkspaceSvg.prototype.moveDrag = function(e) { + var point = Blockly.mouseToSvg(e, this.getParentSvg(), + this.getInverseScreenCTM()); + // Fix scale of mouse event. + point.x /= this.scale; + point.y /= this.scale; + return goog.math.Coordinate.sum(this.dragDeltaXY_, point); +}; + +/** + * Is the user currently dragging a block or scrolling the flyout/workspace? + * @return {boolean} True if currently dragging or scrolling. + */ +Blockly.WorkspaceSvg.prototype.isDragging = function() { + return Blockly.dragMode_ == Blockly.DRAG_FREE || + (Blockly.Flyout.startFlyout_ && + Blockly.Flyout.startFlyout_.dragMode_ == Blockly.DRAG_FREE) || + this.dragMode_ == Blockly.DRAG_FREE; +}; + +/** + * Handle a mouse-wheel on SVG drawing surface. + * @param {!Event} e Mouse wheel event. + * @private + */ +Blockly.WorkspaceSvg.prototype.onMouseWheel_ = function(e) { + // TODO: Remove terminateDrag and compensate for coordinate skew during zoom. + Blockly.terminateDrag_(); + var delta = e.deltaY > 0 ? -1 : 1; + var position = Blockly.mouseToSvg(e, this.getParentSvg(), + this.getInverseScreenCTM()); + this.zoom(position.x, position.y, delta); + e.preventDefault(); +}; + +/** + * Calculate the bounding box for the blocks on the workspace. + * + * @return {Object} Contains the position and size of the bounding box + * containing the blocks on the workspace. + */ +Blockly.WorkspaceSvg.prototype.getBlocksBoundingBox = function() { + var topBlocks = this.getTopBlocks(false); + // There are no blocks, return empty rectangle. + if (!topBlocks.length) { + return {x: 0, y: 0, width: 0, height: 0}; + } + + // Initialize boundary using the first block. + var boundary = topBlocks[0].getBoundingRectangle(); + + // Start at 1 since the 0th block was used for initialization + for (var i = 1; i < topBlocks.length; i++) { + var blockBoundary = topBlocks[i].getBoundingRectangle(); + if (blockBoundary.topLeft.x < boundary.topLeft.x) { + boundary.topLeft.x = blockBoundary.topLeft.x; + } + if (blockBoundary.bottomRight.x > boundary.bottomRight.x) { + boundary.bottomRight.x = blockBoundary.bottomRight.x; + } + if (blockBoundary.topLeft.y < boundary.topLeft.y) { + boundary.topLeft.y = blockBoundary.topLeft.y; + } + if (blockBoundary.bottomRight.y > boundary.bottomRight.y) { + boundary.bottomRight.y = blockBoundary.bottomRight.y; + } + } + return { + x: boundary.topLeft.x, + y: boundary.topLeft.y, + width: boundary.bottomRight.x - boundary.topLeft.x, + height: boundary.bottomRight.y - boundary.topLeft.y + }; +}; + +/** + * Clean up the workspace by ordering all the blocks in a column. + */ +Blockly.WorkspaceSvg.prototype.cleanUp = function() { + Blockly.Events.setGroup(true); + var topBlocks = this.getTopBlocks(true); + var cursorY = 0; + for (var i = 0, block; block = topBlocks[i]; i++) { + var xy = block.getRelativeToSurfaceXY(); + block.moveBy(-xy.x, cursorY - xy.y); + block.snapToGrid(); + cursorY = block.getRelativeToSurfaceXY().y + + block.getHeightWidth().height + Blockly.BlockSvg.MIN_BLOCK_Y; + } + Blockly.Events.setGroup(false); + // Fire an event to allow scrollbars to resize. + this.resizeContents(); +}; + +/** + * Show the context menu for the workspace. + * @param {!Event} e Mouse event. + * @private + */ +Blockly.WorkspaceSvg.prototype.showContextMenu_ = function(e) { + if (this.options.readOnly || this.isFlyout) { + return; + } + var menuOptions = []; + var topBlocks = this.getTopBlocks(true); + var eventGroup = Blockly.genUid(); + + // Options to undo/redo previous action. + var undoOption = {}; + undoOption.text = Blockly.Msg.UNDO; + undoOption.enabled = this.undoStack_.length > 0; + undoOption.callback = this.undo.bind(this, false); + menuOptions.push(undoOption); + var redoOption = {}; + redoOption.text = Blockly.Msg.REDO; + redoOption.enabled = this.redoStack_.length > 0; + redoOption.callback = this.undo.bind(this, true); + menuOptions.push(redoOption); + + // Option to clean up blocks. + if (this.scrollbar) { + var cleanOption = {}; + cleanOption.text = Blockly.Msg.CLEAN_UP; + cleanOption.enabled = topBlocks.length > 1; + cleanOption.callback = this.cleanUp.bind(this); + menuOptions.push(cleanOption); + } + + // Add a little animation to collapsing and expanding. + var DELAY = 10; + if (this.options.collapse) { + var hasCollapsedBlocks = false; + var hasExpandedBlocks = false; + for (var i = 0; i < topBlocks.length; i++) { + var block = topBlocks[i]; + while (block) { + if (block.isCollapsed()) { + hasCollapsedBlocks = true; + } else { + hasExpandedBlocks = true; + } + block = block.getNextBlock(); + } + } + + /** + * Option to collapse or expand top blocks. + * @param {boolean} shouldCollapse Whether a block should collapse. + * @private + */ + var toggleOption = function(shouldCollapse) { + var ms = 0; + for (var i = 0; i < topBlocks.length; i++) { + var block = topBlocks[i]; + while (block) { + setTimeout(block.setCollapsed.bind(block, shouldCollapse), ms); + block = block.getNextBlock(); + ms += DELAY; + } + } + }; + + // Option to collapse top blocks. + var collapseOption = {enabled: hasExpandedBlocks}; + collapseOption.text = Blockly.Msg.COLLAPSE_ALL; + collapseOption.callback = function() { + toggleOption(true); + }; + menuOptions.push(collapseOption); + + // Option to expand top blocks. + var expandOption = {enabled: hasCollapsedBlocks}; + expandOption.text = Blockly.Msg.EXPAND_ALL; + expandOption.callback = function() { + toggleOption(false); + }; + menuOptions.push(expandOption); + } + + // Option to delete all blocks. + // Count the number of blocks that are deletable. + var deleteList = []; + function addDeletableBlocks(block) { + if (block.isDeletable()) { + deleteList = deleteList.concat(block.getDescendants()); + } else { + var children = block.getChildren(); + for (var i = 0; i < children.length; i++) { + addDeletableBlocks(children[i]); + } + } + } + for (var i = 0; i < topBlocks.length; i++) { + addDeletableBlocks(topBlocks[i]); + } + + function deleteNext() { + Blockly.Events.setGroup(eventGroup); + var block = deleteList.shift(); + if (block) { + if (block.workspace) { + block.dispose(false, true); + setTimeout(deleteNext, DELAY); + } else { + deleteNext(); + } + } + Blockly.Events.setGroup(false); + } + + var deleteOption = { + text: deleteList.length == 1 ? Blockly.Msg.DELETE_BLOCK : + Blockly.Msg.DELETE_X_BLOCKS.replace('%1', String(deleteList.length)), + enabled: deleteList.length > 0, + callback: function() { + if (deleteList.length < 2 || + window.confirm(Blockly.Msg.DELETE_ALL_BLOCKS.replace('%1', + String(deleteList.length)))) { + deleteNext(); + } + } + }; + menuOptions.push(deleteOption); + + Blockly.ContextMenu.show(e, menuOptions, this.RTL); +}; + +/** + * Load an audio file. Cache it, ready for instantaneous playing. + * @param {!Array.<string>} filenames List of file types in decreasing order of + * preference (i.e. increasing size). E.g. ['media/go.mp3', 'media/go.wav'] + * Filenames include path from Blockly's root. File extensions matter. + * @param {string} name Name of sound. + * @private + */ +Blockly.WorkspaceSvg.prototype.loadAudio_ = function(filenames, name) { + if (!filenames.length) { + return; + } + try { + var audioTest = new window['Audio'](); + } catch (e) { + // No browser support for Audio. + // IE can throw an error even if the Audio object exists. + return; + } + var sound; + for (var i = 0; i < filenames.length; i++) { + var filename = filenames[i]; + var ext = filename.match(/\.(\w+)$/); + if (ext && audioTest.canPlayType('audio/' + ext[1])) { + // Found an audio format we can play. + sound = new window['Audio'](filename); + break; + } + } + if (sound && sound.play) { + this.SOUNDS_[name] = sound; + } +}; + +/** + * Preload all the audio files so that they play quickly when asked for. + * @private + */ +Blockly.WorkspaceSvg.prototype.preloadAudio_ = function() { + for (var name in this.SOUNDS_) { + var sound = this.SOUNDS_[name]; + sound.volume = .01; + sound.play(); + sound.pause(); + // iOS can only process one sound at a time. Trying to load more than one + // corrupts the earlier ones. Just load one and leave the others uncached. + if (goog.userAgent.IPAD || goog.userAgent.IPHONE) { + break; + } + } +}; + +/** + * Play a named sound at specified volume. If volume is not specified, + * use full volume (1). + * @param {string} name Name of sound. + * @param {number=} opt_volume Volume of sound (0-1). + */ +Blockly.WorkspaceSvg.prototype.playAudio = function(name, opt_volume) { + var sound = this.SOUNDS_[name]; + if (sound) { + // Don't play one sound on top of another. + var now = new Date; + if (now - this.lastSound_ < Blockly.SOUND_LIMIT) { + return; + } + this.lastSound_ = now; + var mySound; + var ie9 = goog.userAgent.DOCUMENT_MODE && + goog.userAgent.DOCUMENT_MODE === 9; + if (ie9 || goog.userAgent.IPAD || goog.userAgent.ANDROID) { + // Creating a new audio node causes lag in IE9, Android and iPad. Android + // and IE9 refetch the file from the server, iPad uses a singleton audio + // node which must be deleted and recreated for each new audio tag. + mySound = sound; + } else { + mySound = sound.cloneNode(); + } + mySound.volume = (opt_volume === undefined ? 1 : opt_volume); + mySound.play(); + } else if (this.options.parentWorkspace) { + // Maybe a workspace on a lower level knows about this sound. + this.options.parentWorkspace.playAudio(name, opt_volume); + } +}; + +/** + * Modify the block tree on the existing toolbox. + * @param {Node|string} tree DOM tree of blocks, or text representation of same. + */ +Blockly.WorkspaceSvg.prototype.updateToolbox = function(tree) { + tree = Blockly.Options.parseToolboxTree(tree); + if (!tree) { + if (this.options.languageTree) { + throw 'Can\'t nullify an existing toolbox.'; + } + return; // No change (null to null). + } + if (!this.options.languageTree) { + throw 'Existing toolbox is null. Can\'t create new toolbox.'; + } + if (tree.getElementsByTagName('category').length) { + if (!this.toolbox_) { + throw 'Existing toolbox has no categories. Can\'t change mode.'; + } + this.options.languageTree = tree; + this.toolbox_.populate_(tree); + this.toolbox_.addColour_(); + } else { + if (!this.flyout_) { + throw 'Existing toolbox has categories. Can\'t change mode.'; + } + this.options.languageTree = tree; + this.flyout_.show(tree.childNodes); + } +}; + +/** + * Mark this workspace as the currently focused main workspace. + */ +Blockly.WorkspaceSvg.prototype.markFocused = function() { + if (this.options.parentWorkspace) { + this.options.parentWorkspace.markFocused(); + } else { + Blockly.mainWorkspace = this; + } +}; + +/** + * Zooming the blocks centered in (x, y) coordinate with zooming in or out. + * @param {number} x X coordinate of center. + * @param {number} y Y coordinate of center. + * @param {number} type Type of zooming (-1 zooming out and 1 zooming in). + */ +Blockly.WorkspaceSvg.prototype.zoom = function(x, y, type) { + var speed = this.options.zoomOptions.scaleSpeed; + var metrics = this.getMetrics(); + var center = this.getParentSvg().createSVGPoint(); + center.x = x; + center.y = y; + center = center.matrixTransform(this.getCanvas().getCTM().inverse()); + x = center.x; + y = center.y; + var canvas = this.getCanvas(); + // Scale factor. + var scaleChange = (type == 1) ? speed : 1 / speed; + // Clamp scale within valid range. + var newScale = this.scale * scaleChange; + if (newScale > this.options.zoomOptions.maxScale) { + scaleChange = this.options.zoomOptions.maxScale / this.scale; + } else if (newScale < this.options.zoomOptions.minScale) { + scaleChange = this.options.zoomOptions.minScale / this.scale; + } + if (this.scale == newScale) { + return; // No change in zoom. + } + if (this.scrollbar) { + var matrix = canvas.getCTM() + .translate(x * (1 - scaleChange), y * (1 - scaleChange)) + .scale(scaleChange); + // newScale and matrix.a should be identical (within a rounding error). + this.scrollX = matrix.e - metrics.absoluteLeft; + this.scrollY = matrix.f - metrics.absoluteTop; + } + this.setScale(newScale); +}; + +/** + * Zooming the blocks centered in the center of view with zooming in or out. + * @param {number} type Type of zooming (-1 zooming out and 1 zooming in). + */ +Blockly.WorkspaceSvg.prototype.zoomCenter = function(type) { + var metrics = this.getMetrics(); + var x = metrics.viewWidth / 2; + var y = metrics.viewHeight / 2; + this.zoom(x, y, type); +}; + +/** + * Zoom the blocks to fit in the workspace if possible. + */ +Blockly.WorkspaceSvg.prototype.zoomToFit = function() { + var metrics = this.getMetrics(); + var blocksBox = this.getBlocksBoundingBox(); + var blocksWidth = blocksBox.width; + var blocksHeight = blocksBox.height; + if (!blocksWidth) { + return; // Prevents zooming to infinity. + } + var workspaceWidth = metrics.viewWidth; + var workspaceHeight = metrics.viewHeight; + if (this.flyout_) { + workspaceWidth -= this.flyout_.width_; + } + if (!this.scrollbar) { + // Orgin point of 0,0 is fixed, blocks will not scroll to center. + blocksWidth += metrics.contentLeft; + blocksHeight += metrics.contentTop; + } + var ratioX = workspaceWidth / blocksWidth; + var ratioY = workspaceHeight / blocksHeight; + this.setScale(Math.min(ratioX, ratioY)); + this.scrollCenter(); +}; + +/** + * Center the workspace. + */ +Blockly.WorkspaceSvg.prototype.scrollCenter = function() { + if (!this.scrollbar) { + // Can't center a non-scrolling workspace. + return; + } + var metrics = this.getMetrics(); + var x = (metrics.contentWidth - metrics.viewWidth) / 2; + if (this.flyout_) { + x -= this.flyout_.width_ / 2; + } + var y = (metrics.contentHeight - metrics.viewHeight) / 2; + this.scrollbar.set(x, y); +}; + +/** + * Set the workspace's zoom factor. + * @param {number} newScale Zoom factor. + */ +Blockly.WorkspaceSvg.prototype.setScale = function(newScale) { + if (this.options.zoomOptions.maxScale && + newScale > this.options.zoomOptions.maxScale) { + newScale = this.options.zoomOptions.maxScale; + } else if (this.options.zoomOptions.minScale && + newScale < this.options.zoomOptions.minScale) { + newScale = this.options.zoomOptions.minScale; + } + this.scale = newScale; + this.updateGridPattern_(); + if (this.scrollbar) { + this.scrollbar.resize(); + } else { + this.translate(this.scrollX, this.scrollY); + } + Blockly.hideChaff(false); + if (this.flyout_) { + // No toolbox, resize flyout. + this.flyout_.reflow(); + } +}; + +/** + * Updates the grid pattern. + * @private + */ +Blockly.WorkspaceSvg.prototype.updateGridPattern_ = function() { + if (!this.options.gridPattern) { + return; // No grid. + } + // MSIE freaks if it sees a 0x0 pattern, so set empty patterns to 100x100. + var safeSpacing = (this.options.gridOptions['spacing'] * this.scale) || 100; + this.options.gridPattern.setAttribute('width', safeSpacing); + this.options.gridPattern.setAttribute('height', safeSpacing); + var half = Math.floor(this.options.gridOptions['spacing'] / 2) + 0.5; + var start = half - this.options.gridOptions['length'] / 2; + var end = half + this.options.gridOptions['length'] / 2; + var line1 = this.options.gridPattern.firstChild; + var line2 = line1 && line1.nextSibling; + half *= this.scale; + start *= this.scale; + end *= this.scale; + if (line1) { + line1.setAttribute('stroke-width', this.scale); + line1.setAttribute('x1', start); + line1.setAttribute('y1', half); + line1.setAttribute('x2', end); + line1.setAttribute('y2', half); + } + if (line2) { + line2.setAttribute('stroke-width', this.scale); + line2.setAttribute('x1', half); + line2.setAttribute('y1', start); + line2.setAttribute('x2', half); + line2.setAttribute('y2', end); + } +}; + + +/** + * Return an object with all the metrics required to size scrollbars for a + * top level workspace. The following properties are computed: + * .viewHeight: Height of the visible rectangle, + * .viewWidth: Width of the visible rectangle, + * .contentHeight: Height of the contents, + * .contentWidth: Width of the content, + * .viewTop: Offset of top edge of visible rectangle from parent, + * .viewLeft: Offset of left edge of visible rectangle from parent, + * .contentTop: Offset of the top-most content from the y=0 coordinate, + * .contentLeft: Offset of the left-most content from the x=0 coordinate. + * .absoluteTop: Top-edge of view. + * .absoluteLeft: Left-edge of view. + * .toolboxWidth: Width of toolbox, if it exists. Otherwise zero. + * .toolboxHeight: Height of toolbox, if it exists. Otherwise zero. + * .flyoutWidth: Width of the flyout if it is always open. Otherwise zero. + * .flyoutHeight: Height of flyout if it is always open. Otherwise zero. + * .toolboxPosition: Top, bottom, left or right. + * @return {Object} Contains size and position metrics of a top level workspace. + * @private + */ +Blockly.WorkspaceSvg.getTopLevelWorkspaceMetrics_ = function() { + var svgSize = Blockly.svgSize(this.getParentSvg()); + if (this.toolbox_) { + if (this.toolboxPosition == Blockly.TOOLBOX_AT_TOP || + this.toolboxPosition == Blockly.TOOLBOX_AT_BOTTOM) { + svgSize.height -= this.toolbox_.getHeight(); + } else if (this.toolboxPosition == Blockly.TOOLBOX_AT_LEFT || + this.toolboxPosition == Blockly.TOOLBOX_AT_RIGHT) { + svgSize.width -= this.toolbox_.getWidth(); + } + } + // Set the margin to match the flyout's margin so that the workspace does + // not jump as blocks are added. + var MARGIN = Blockly.Flyout.prototype.CORNER_RADIUS - 1; + var viewWidth = svgSize.width - MARGIN; + var viewHeight = svgSize.height - MARGIN; + var blockBox = this.getBlocksBoundingBox(); + + // Fix scale. + var contentWidth = blockBox.width * this.scale; + var contentHeight = blockBox.height * this.scale; + var contentX = blockBox.x * this.scale; + var contentY = blockBox.y * this.scale; + if (this.scrollbar) { + // Add a border around the content that is at least half a screenful wide. + // Ensure border is wide enough that blocks can scroll over entire screen. + var leftEdge = Math.min(contentX - viewWidth / 2, + contentX + contentWidth - viewWidth); + var rightEdge = Math.max(contentX + contentWidth + viewWidth / 2, + contentX + viewWidth); + var topEdge = Math.min(contentY - viewHeight / 2, + contentY + contentHeight - viewHeight); + var bottomEdge = Math.max(contentY + contentHeight + viewHeight / 2, + contentY + viewHeight); + } else { + var leftEdge = blockBox.x; + var rightEdge = leftEdge + blockBox.width; + var topEdge = blockBox.y; + var bottomEdge = topEdge + blockBox.height; + } + var absoluteLeft = 0; + if (this.toolbox_ && this.toolboxPosition == Blockly.TOOLBOX_AT_LEFT) { + absoluteLeft = this.toolbox_.getWidth(); + } + var absoluteTop = 0; + if (this.toolbox_ && this.toolboxPosition == Blockly.TOOLBOX_AT_TOP) { + absoluteTop = this.toolbox_.getHeight(); + } + + var metrics = { + viewHeight: svgSize.height, + viewWidth: svgSize.width, + contentHeight: bottomEdge - topEdge, + contentWidth: rightEdge - leftEdge, + viewTop: -this.scrollY, + viewLeft: -this.scrollX, + contentTop: topEdge, + contentLeft: leftEdge, + absoluteTop: absoluteTop, + absoluteLeft: absoluteLeft, + toolboxWidth: this.toolbox_ ? this.toolbox_.getWidth() : 0, + toolboxHeight: this.toolbox_ ? this.toolbox_.getHeight() : 0, + flyoutWidth: this.flyout_ ? this.flyout_.getWidth() : 0, + flyoutHeight: this.flyout_ ? this.flyout_.getHeight() : 0, + toolboxPosition: this.toolboxPosition + }; + return metrics; +}; + +/** + * Sets the X/Y translations of a top level workspace to match the scrollbars. + * @param {!Object} xyRatio Contains an x and/or y property which is a float + * between 0 and 1 specifying the degree of scrolling. + * @private + */ +Blockly.WorkspaceSvg.setTopLevelWorkspaceMetrics_ = function(xyRatio) { + if (!this.scrollbar) { + throw 'Attempt to set top level workspace scroll without scrollbars.'; + } + var metrics = this.getMetrics(); + if (goog.isNumber(xyRatio.x)) { + this.scrollX = -metrics.contentWidth * xyRatio.x - metrics.contentLeft; + } + if (goog.isNumber(xyRatio.y)) { + this.scrollY = -metrics.contentHeight * xyRatio.y - metrics.contentTop; + } + var x = this.scrollX + metrics.absoluteLeft; + var y = this.scrollY + metrics.absoluteTop; + this.translate(x, y); + if (this.options.gridPattern) { + this.options.gridPattern.setAttribute('x', x); + this.options.gridPattern.setAttribute('y', y); + if (goog.userAgent.IE) { + // IE doesn't notice that the x/y offsets have changed. Force an update. + this.updateGridPattern_(); + } + } +}; +// Export symbols that would otherwise be renamed by Closure compiler. +Blockly.WorkspaceSvg.prototype['setVisible'] = + Blockly.WorkspaceSvg.prototype.setVisible; diff --git a/src/blockly/core/xml.js b/src/blockly/core/xml.js new file mode 100644 index 0000000..2567560 --- /dev/null +++ b/src/blockly/core/xml.js @@ -0,0 +1,566 @@ +/** + * @license + * Visual Blocks Editor + * + * Copyright 2012 Google Inc. + * https://developers.google.com/blockly/ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview XML reader and writer. + * @author fraser@google.com (Neil Fraser) + */ +'use strict'; + +goog.provide('Blockly.Xml'); + +goog.require('goog.asserts'); +goog.require('goog.dom'); + + +/** + * Encode a block tree as XML. + * @param {!Blockly.Workspace} workspace The workspace containing blocks. + * @return {!Element} XML document. + */ +Blockly.Xml.workspaceToDom = function(workspace) { + var xml = goog.dom.createDom('xml'); + var blocks = workspace.getTopBlocks(true); + for (var i = 0, block; block = blocks[i]; i++) { + xml.appendChild(Blockly.Xml.blockToDomWithXY(block)); + } + return xml; +}; + +/** + * Encode a block subtree as XML with XY coordinates. + * @param {!Blockly.Block} block The root block to encode. + * @return {!Element} Tree of XML elements. + */ +Blockly.Xml.blockToDomWithXY = function(block) { + var width; // Not used in LTR. + if (block.workspace.RTL) { + width = block.workspace.getWidth(); + } + var element = Blockly.Xml.blockToDom(block); + var xy = block.getRelativeToSurfaceXY(); + element.setAttribute('x', + Math.round(block.workspace.RTL ? width - xy.x : xy.x)); + element.setAttribute('y', Math.round(xy.y)); + return element; +}; + +/** + * Encode a block subtree as XML. + * @param {!Blockly.Block} block The root block to encode. + * @return {!Element} Tree of XML elements. + */ +Blockly.Xml.blockToDom = function(block) { + var element = goog.dom.createDom(block.isShadow() ? 'shadow' : 'block'); + element.setAttribute('type', block.type); + element.setAttribute('id', block.id); + if (block.mutationToDom) { + // Custom data for an advanced block. + var mutation = block.mutationToDom(); + if (mutation && (mutation.hasChildNodes() || mutation.hasAttributes())) { + element.appendChild(mutation); + } + } + function fieldToDom(field) { + if (field.name && field.EDITABLE) { + var container = goog.dom.createDom('field', null, field.getValue()); + container.setAttribute('name', field.name); + element.appendChild(container); + } + } + for (var i = 0, input; input = block.inputList[i]; i++) { + for (var j = 0, field; field = input.fieldRow[j]; j++) { + fieldToDom(field); + } + } + + var commentText = block.getCommentText(); + if (commentText) { + var commentElement = goog.dom.createDom('comment', null, commentText); + if (typeof block.comment == 'object') { + commentElement.setAttribute('pinned', block.comment.isVisible()); + var hw = block.comment.getBubbleSize(); + commentElement.setAttribute('h', hw.height); + commentElement.setAttribute('w', hw.width); + } + element.appendChild(commentElement); + } + + if (block.data) { + var dataElement = goog.dom.createDom('data', null, block.data); + element.appendChild(dataElement); + } + + for (var i = 0, input; input = block.inputList[i]; i++) { + var container; + var empty = true; + if (input.type == Blockly.DUMMY_INPUT) { + continue; + } else { + var childBlock = input.connection.targetBlock(); + if (input.type == Blockly.INPUT_VALUE) { + container = goog.dom.createDom('value'); + } else if (input.type == Blockly.NEXT_STATEMENT) { + container = goog.dom.createDom('statement'); + } + var shadow = input.connection.getShadowDom(); + if (shadow && (!childBlock || !childBlock.isShadow())) { + container.appendChild(Blockly.Xml.cloneShadow_(shadow)); + } + if (childBlock) { + container.appendChild(Blockly.Xml.blockToDom(childBlock)); + empty = false; + } + } + container.setAttribute('name', input.name); + if (!empty) { + element.appendChild(container); + } + } + if (block.inputsInlineDefault != block.inputsInline) { + element.setAttribute('inline', block.inputsInline); + } + if (block.isCollapsed()) { + element.setAttribute('collapsed', true); + } + if (block.disabled) { + element.setAttribute('disabled', true); + } + if (!block.isDeletable() && !block.isShadow()) { + element.setAttribute('deletable', false); + } + if (!block.isMovable() && !block.isShadow()) { + element.setAttribute('movable', false); + } + if (!block.isEditable()) { + element.setAttribute('editable', false); + } + + var nextBlock = block.getNextBlock(); + if (nextBlock) { + var container = goog.dom.createDom('next', null, + Blockly.Xml.blockToDom(nextBlock)); + element.appendChild(container); + } + var shadow = block.nextConnection && block.nextConnection.getShadowDom(); + if (shadow && (!nextBlock || !nextBlock.isShadow())) { + container.appendChild(Blockly.Xml.cloneShadow_(shadow)); + } + + return element; +}; + +/** + * Deeply clone the shadow's DOM so that changes don't back-wash to the block. + * @param {!Element} shadow A tree of XML elements. + * @return {!Element} A tree of XML elements. + * @private + */ +Blockly.Xml.cloneShadow_ = function(shadow) { + shadow = shadow.cloneNode(true); + // Walk the tree looking for whitespace. Don't prune whitespace in a tag. + var node = shadow; + var textNode; + while (node) { + if (node.firstChild) { + node = node.firstChild; + } else { + while (node && !node.nextSibling) { + textNode = node; + node = node.parentNode; + if (textNode.nodeType == 3 && textNode.data.trim() == '' && + node.firstChild != textNode) { + // Prune whitespace after a tag. + goog.dom.removeNode(textNode); + } + } + if (node) { + textNode = node; + node = node.nextSibling; + if (textNode.nodeType == 3 && textNode.data.trim() == '') { + // Prune whitespace before a tag. + goog.dom.removeNode(textNode); + } + } + } + } + return shadow; +}; + +/** + * Converts a DOM structure into plain text. + * Currently the text format is fairly ugly: all one line with no whitespace. + * @param {!Element} dom A tree of XML elements. + * @return {string} Text representation. + */ +Blockly.Xml.domToText = function(dom) { + var oSerializer = new XMLSerializer(); + return oSerializer.serializeToString(dom); +}; + +/** + * Converts a DOM structure into properly indented text. + * @param {!Element} dom A tree of XML elements. + * @return {string} Text representation. + */ +Blockly.Xml.domToPrettyText = function(dom) { + // This function is not guaranteed to be correct for all XML. + // But it handles the XML that Blockly generates. + var blob = Blockly.Xml.domToText(dom); + // Place every open and close tag on its own line. + var lines = blob.split('<'); + // Indent every line. + var indent = ''; + for (var i = 1; i < lines.length; i++) { + var line = lines[i]; + if (line[0] == '/') { + indent = indent.substring(2); + } + lines[i] = indent + '<' + line; + if (line[0] != '/' && line.slice(-2) != '/>') { + indent += ' '; + } + } + // Pull simple tags back together. + // E.g. <foo></foo> + var text = lines.join('\n'); + text = text.replace(/(<(\w+)\b[^>]*>[^\n]*)\n *<\/\2>/g, '$1</$2>'); + // Trim leading blank line. + return text.replace(/^\n/, ''); +}; + +/** + * Converts plain text into a DOM structure. + * Throws an error if XML doesn't parse. + * @param {string} text Text representation. + * @return {!Element} A tree of XML elements. + */ +Blockly.Xml.textToDom = function(text) { + var oParser = new DOMParser(); + var dom = oParser.parseFromString(text, 'text/xml'); + // The DOM should have one and only one top-level node, an XML tag. + if (!dom || !dom.firstChild || + dom.firstChild.nodeName.toLowerCase() != 'xml' || + dom.firstChild !== dom.lastChild) { + // Whatever we got back from the parser is not XML. + goog.asserts.fail('Blockly.Xml.textToDom did not obtain a valid XML tree.'); + } + return dom.firstChild; +}; + +/** + * Decode an XML DOM and create blocks on the workspace. + * @param {!Element} xml XML DOM. + * @param {!Blockly.Workspace} workspace The workspace. + */ +Blockly.Xml.domToWorkspace = function(xml, workspace) { + if (xml instanceof Blockly.Workspace) { + var swap = xml; + xml = workspace; + workspace = swap; + console.warn('Deprecated call to Blockly.Xml.domToWorkspace, ' + + 'swap the arguments.'); + } + var width; // Not used in LTR. + if (workspace.RTL) { + width = workspace.getWidth(); + } + Blockly.Field.startCache(); + // Safari 7.1.3 is known to provide node lists with extra references to + // children beyond the lists' length. Trust the length, do not use the + // looping pattern of checking the index for an object. + var childCount = xml.childNodes.length; + var existingGroup = Blockly.Events.getGroup(); + if (!existingGroup) { + Blockly.Events.setGroup(true); + } + for (var i = 0; i < childCount; i++) { + var xmlChild = xml.childNodes[i]; + var name = xmlChild.nodeName.toLowerCase(); + if (name == 'block' || + (name == 'shadow' && !Blockly.Events.recordUndo)) { + // Allow top-level shadow blocks if recordUndo is disabled since + // that means an undo is in progress. Such a block is expected + // to be moved to a nested destination in the next operation. + var block = Blockly.Xml.domToBlock(xmlChild, workspace); + var blockX = parseInt(xmlChild.getAttribute('x'), 10); + var blockY = parseInt(xmlChild.getAttribute('y'), 10); + if (!isNaN(blockX) && !isNaN(blockY)) { + block.moveBy(workspace.RTL ? width - blockX : blockX, blockY); + } + } else if (name == 'shadow') { + goog.asserts.fail('Shadow block cannot be a top-level block.'); + } + } + if (!existingGroup) { + Blockly.Events.setGroup(false); + } + Blockly.Field.stopCache(); + + workspace.updateVariableList(false); +}; + +/** + * Decode an XML block tag and create a block (and possibly sub blocks) on the + * workspace. + * @param {!Element} xmlBlock XML block element. + * @param {!Blockly.Workspace} workspace The workspace. + * @return {!Blockly.Block} The root block created. + */ +Blockly.Xml.domToBlock = function(xmlBlock, workspace) { + if (xmlBlock instanceof Blockly.Workspace) { + var swap = xmlBlock; + xmlBlock = workspace; + workspace = swap; + console.warn('Deprecated call to Blockly.Xml.domToBlock, ' + + 'swap the arguments.'); + } + // Create top-level block. + Blockly.Events.disable(); + try { + var topBlock = Blockly.Xml.domToBlockHeadless_(xmlBlock, workspace); + if (workspace.rendered) { + // Hide connections to speed up assembly. + topBlock.setConnectionsHidden(true); + // Generate list of all blocks. + var blocks = topBlock.getDescendants(); + // Render each block. + for (var i = blocks.length - 1; i >= 0; i--) { + blocks[i].initSvg(); + } + for (var i = blocks.length - 1; i >= 0; i--) { + blocks[i].render(false); + } + // Populating the connection database may be defered until after the + // blocks have rendered. + setTimeout(function() { + if (topBlock.workspace) { // Check that the block hasn't been deleted. + topBlock.setConnectionsHidden(false); + } + }, 1); + topBlock.updateDisabled(); + // Allow the scrollbars to resize and move based on the new contents. + // TODO(@picklesrus): #387. Remove when domToBlock avoids resizing. + workspace.resizeContents(); + } + } finally { + Blockly.Events.enable(); + } + if (Blockly.Events.isEnabled()) { + Blockly.Events.fire(new Blockly.Events.Create(topBlock)); + } + return topBlock; +}; + +/** + * Decode an XML block tag and create a block (and possibly sub blocks) on the + * workspace. + * @param {!Element} xmlBlock XML block element. + * @param {!Blockly.Workspace} workspace The workspace. + * @return {!Blockly.Block} The root block created. + * @private + */ +Blockly.Xml.domToBlockHeadless_ = function(xmlBlock, workspace) { + var block = null; + var prototypeName = xmlBlock.getAttribute('type'); + goog.asserts.assert(prototypeName, 'Block type unspecified: %s', + xmlBlock.outerHTML); + var id = xmlBlock.getAttribute('id'); + block = workspace.newBlock(prototypeName, id); + + var blockChild = null; + for (var i = 0, xmlChild; xmlChild = xmlBlock.childNodes[i]; i++) { + if (xmlChild.nodeType == 3) { + // Ignore any text at the <block> level. It's all whitespace anyway. + continue; + } + var input; + + // Find any enclosed blocks or shadows in this tag. + var childBlockNode = null; + var childShadowNode = null; + for (var j = 0, grandchildNode; grandchildNode = xmlChild.childNodes[j]; + j++) { + if (grandchildNode.nodeType == 1) { + if (grandchildNode.nodeName.toLowerCase() == 'block') { + childBlockNode = grandchildNode; + } else if (grandchildNode.nodeName.toLowerCase() == 'shadow') { + childShadowNode = grandchildNode; + } + } + } + // Use the shadow block if there is no child block. + if (!childBlockNode && childShadowNode) { + childBlockNode = childShadowNode; + } + + var name = xmlChild.getAttribute('name'); + switch (xmlChild.nodeName.toLowerCase()) { + case 'mutation': + // Custom data for an advanced block. + if (block.domToMutation) { + block.domToMutation(xmlChild); + if (block.initSvg) { + // Mutation may have added some elements that need initalizing. + block.initSvg(); + } + } + break; + case 'comment': + block.setCommentText(xmlChild.textContent); + var visible = xmlChild.getAttribute('pinned'); + if (visible && !block.isInFlyout) { + // Give the renderer a millisecond to render and position the block + // before positioning the comment bubble. + setTimeout(function() { + if (block.comment && block.comment.setVisible) { + block.comment.setVisible(visible == 'true'); + } + }, 1); + } + var bubbleW = parseInt(xmlChild.getAttribute('w'), 10); + var bubbleH = parseInt(xmlChild.getAttribute('h'), 10); + if (!isNaN(bubbleW) && !isNaN(bubbleH) && + block.comment && block.comment.setVisible) { + block.comment.setBubbleSize(bubbleW, bubbleH); + } + break; + case 'data': + block.data = xmlChild.textContent; + break; + case 'title': + // Titles were renamed to field in December 2013. + // Fall through. + case 'field': + var field = block.getField(name); + if (!field) { + console.warn('Ignoring non-existent field ' + name + ' in block ' + + prototypeName); + break; + } + field.setValue(xmlChild.textContent); + break; + case 'value': + case 'statement': + input = block.getInput(name); + if (!input) { + console.warn('Ignoring non-existent input ' + name + ' in block ' + + prototypeName); + break; + } + if (childShadowNode) { + input.connection.setShadowDom(childShadowNode); + } + if (childBlockNode) { + blockChild = Blockly.Xml.domToBlockHeadless_(childBlockNode, + workspace); + if (blockChild.outputConnection) { + input.connection.connect(blockChild.outputConnection); + } else if (blockChild.previousConnection) { + input.connection.connect(blockChild.previousConnection); + } else { + goog.asserts.fail( + 'Child block does not have output or previous statement.'); + } + } + break; + case 'next': + if (childShadowNode && block.nextConnection) { + block.nextConnection.setShadowDom(childShadowNode); + } + if (childBlockNode) { + goog.asserts.assert(block.nextConnection, + 'Next statement does not exist.'); + // If there is more than one XML 'next' tag. + goog.asserts.assert(!block.nextConnection.isConnected(), + 'Next statement is already connected.'); + blockChild = Blockly.Xml.domToBlockHeadless_(childBlockNode, + workspace); + goog.asserts.assert(blockChild.previousConnection, + 'Next block does not have previous statement.'); + block.nextConnection.connect(blockChild.previousConnection); + } + break; + default: + // Unknown tag; ignore. Same principle as HTML parsers. + console.warn('Ignoring unknown tag: ' + xmlChild.nodeName); + } + } + + var inline = xmlBlock.getAttribute('inline'); + if (inline) { + block.setInputsInline(inline == 'true'); + } + var disabled = xmlBlock.getAttribute('disabled'); + if (disabled) { + block.setDisabled(disabled == 'true'); + } + var deletable = xmlBlock.getAttribute('deletable'); + if (deletable) { + block.setDeletable(deletable == 'true'); + } + var movable = xmlBlock.getAttribute('movable'); + if (movable) { + block.setMovable(movable == 'true'); + } + var editable = xmlBlock.getAttribute('editable'); + if (editable) { + block.setEditable(editable == 'true'); + } + var collapsed = xmlBlock.getAttribute('collapsed'); + if (collapsed) { + block.setCollapsed(collapsed == 'true'); + } + if (xmlBlock.nodeName.toLowerCase() == 'shadow') { + // Ensure all children are also shadows. + var children = block.getChildren(); + for (var i = 0, child; child = children[i]; i++) { + goog.asserts.assert(child.isShadow(), + 'Shadow block not allowed non-shadow child.'); + } + block.setShadow(true); + } + return block; +}; + +/** + * Remove any 'next' block (statements in a stack). + * @param {!Element} xmlBlock XML block element. + */ +Blockly.Xml.deleteNext = function(xmlBlock) { + for (var i = 0, child; child = xmlBlock.childNodes[i]; i++) { + if (child.nodeName.toLowerCase() == 'next') { + xmlBlock.removeChild(child); + break; + } + } +}; + +// Export symbols that would otherwise be renamed by Closure compiler. +if (!goog.global['Blockly']) { + goog.global['Blockly'] = {}; +} +if (!goog.global['Blockly']['Xml']) { + goog.global['Blockly']['Xml'] = {}; +} +goog.global['Blockly']['Xml']['domToText'] = Blockly.Xml.domToText; +goog.global['Blockly']['Xml']['domToWorkspace'] = Blockly.Xml.domToWorkspace; +goog.global['Blockly']['Xml']['textToDom'] = Blockly.Xml.textToDom; +goog.global['Blockly']['Xml']['workspaceToDom'] = Blockly.Xml.workspaceToDom; diff --git a/src/blockly/core/zoom_controls.js b/src/blockly/core/zoom_controls.js new file mode 100644 index 0000000..48d8ce5 --- /dev/null +++ b/src/blockly/core/zoom_controls.js @@ -0,0 +1,239 @@ +/** + * @license + * Visual Blocks Editor + * + * Copyright 2015 Google Inc. + * https://developers.google.com/blockly/ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Object representing a zoom icons. + * @author carloslfu@gmail.com (Carlos Galarza) + */ +'use strict'; + +goog.provide('Blockly.ZoomControls'); + +goog.require('goog.dom'); + + +/** + * Class for a zoom controls. + * @param {!Blockly.Workspace} workspace The workspace to sit in. + * @constructor + */ +Blockly.ZoomControls = function(workspace) { + this.workspace_ = workspace; +}; + +/** + * Width of the zoom controls. + * @type {number} + * @private + */ +Blockly.ZoomControls.prototype.WIDTH_ = 32; + +/** + * Height of the zoom controls. + * @type {number} + * @private + */ +Blockly.ZoomControls.prototype.HEIGHT_ = 110; + +/** + * Distance between zoom controls and bottom edge of workspace. + * @type {number} + * @private + */ +Blockly.ZoomControls.prototype.MARGIN_BOTTOM_ = 20; + +/** + * Distance between zoom controls and right edge of workspace. + * @type {number} + * @private + */ +Blockly.ZoomControls.prototype.MARGIN_SIDE_ = 20; + +/** + * The SVG group containing the zoom controls. + * @type {Element} + * @private + */ +Blockly.ZoomControls.prototype.svgGroup_ = null; + +/** + * Left coordinate of the zoom controls. + * @type {number} + * @private + */ +Blockly.ZoomControls.prototype.left_ = 0; + +/** + * Top coordinate of the zoom controls. + * @type {number} + * @private + */ +Blockly.ZoomControls.prototype.top_ = 0; + +/** + * Create the zoom controls. + * @return {!Element} The zoom controls SVG group. + */ +Blockly.ZoomControls.prototype.createDom = function() { + var workspace = this.workspace_; + /* Here's the markup that will be generated: + <g class="blocklyZoom"> + <clippath id="blocklyZoomoutClipPath837493"> + <rect width="32" height="32" y="77"></rect> + </clippath> + <image width="96" height="124" x="-64" y="-15" xlink:href="media/sprites.png" + clip-path="url(#blocklyZoomoutClipPath837493)"></image> + <clippath id="blocklyZoominClipPath837493"> + <rect width="32" height="32" y="43"></rect> + </clippath> + <image width="96" height="124" x="-32" y="-49" xlink:href="media/sprites.png" + clip-path="url(#blocklyZoominClipPath837493)"></image> + <clippath id="blocklyZoomresetClipPath837493"> + <rect width="32" height="32"></rect> + </clippath> + <image width="96" height="124" y="-92" xlink:href="media/sprites.png" + clip-path="url(#blocklyZoomresetClipPath837493)"></image> + </g> + */ + this.svgGroup_ = Blockly.createSvgElement('g', + {'class': 'blocklyZoom'}, null); + var rnd = String(Math.random()).substring(2); + + var clip = Blockly.createSvgElement('clipPath', + {'id': 'blocklyZoomoutClipPath' + rnd}, + this.svgGroup_); + Blockly.createSvgElement('rect', + {'width': 32, 'height': 32, 'y': 77}, + clip); + var zoomoutSvg = Blockly.createSvgElement('image', + {'width': Blockly.SPRITE.width, + 'height': Blockly.SPRITE.height, 'x': -64, + 'y': -15, + 'clip-path': 'url(#blocklyZoomoutClipPath' + rnd + ')'}, + this.svgGroup_); + zoomoutSvg.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href', + workspace.options.pathToMedia + Blockly.SPRITE.url); + + var clip = Blockly.createSvgElement('clipPath', + {'id': 'blocklyZoominClipPath' + rnd}, + this.svgGroup_); + Blockly.createSvgElement('rect', + {'width': 32, 'height': 32, 'y': 43}, + clip); + var zoominSvg = Blockly.createSvgElement('image', + {'width': Blockly.SPRITE.width, + 'height': Blockly.SPRITE.height, + 'x': -32, + 'y': -49, + 'clip-path': 'url(#blocklyZoominClipPath' + rnd + ')'}, + this.svgGroup_); + zoominSvg.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href', + workspace.options.pathToMedia + Blockly.SPRITE.url); + + var clip = Blockly.createSvgElement('clipPath', + {'id': 'blocklyZoomresetClipPath' + rnd}, + this.svgGroup_); + Blockly.createSvgElement('rect', + {'width': 32, 'height': 32}, + clip); + var zoomresetSvg = Blockly.createSvgElement('image', + {'width': Blockly.SPRITE.width, + 'height': Blockly.SPRITE.height, 'y': -92, + 'clip-path': 'url(#blocklyZoomresetClipPath' + rnd + ')'}, + this.svgGroup_); + zoomresetSvg.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href', + workspace.options.pathToMedia + Blockly.SPRITE.url); + + // Attach event listeners. + Blockly.bindEvent_(zoomresetSvg, 'mousedown', null, function(e) { + workspace.setScale(1); + workspace.scrollCenter(); + e.stopPropagation(); // Don't start a workspace scroll. + e.preventDefault(); // Stop double-clicking from selecting text. + }); + Blockly.bindEvent_(zoominSvg, 'mousedown', null, function(e) { + workspace.zoomCenter(1); + e.stopPropagation(); // Don't start a workspace scroll. + e.preventDefault(); // Stop double-clicking from selecting text. + }); + Blockly.bindEvent_(zoomoutSvg, 'mousedown', null, function(e) { + workspace.zoomCenter(-1); + e.stopPropagation(); // Don't start a workspace scroll. + e.preventDefault(); // Stop double-clicking from selecting text. + }); + + return this.svgGroup_; +}; + +/** + * Initialize the zoom controls. + * @param {number} bottom Distance from workspace bottom to bottom of controls. + * @return {number} Distance from workspace bottom to the top of controls. + */ +Blockly.ZoomControls.prototype.init = function(bottom) { + this.bottom_ = this.MARGIN_BOTTOM_ + bottom; + return this.bottom_ + this.HEIGHT_; +}; + +/** + * Dispose of this zoom controls. + * Unlink from all DOM elements to prevent memory leaks. + */ +Blockly.ZoomControls.prototype.dispose = function() { + if (this.svgGroup_) { + goog.dom.removeNode(this.svgGroup_); + this.svgGroup_ = null; + } + this.workspace_ = null; +}; + +/** + * Move the zoom controls to the bottom-right corner. + */ +Blockly.ZoomControls.prototype.position = function() { + var metrics = this.workspace_.getMetrics(); + if (!metrics) { + // There are no metrics available (workspace is probably not visible). + return; + } + if (this.workspace_.RTL) { + this.left_ = this.MARGIN_SIDE_ + Blockly.Scrollbar.scrollbarThickness; + if (metrics.toolboxPosition == Blockly.TOOLBOX_AT_LEFT) { + this.left_ += metrics.flyoutWidth; + if (this.workspace_.toolbox_) { + this.left_ += metrics.absoluteLeft; + } + } + } else { + this.left_ = metrics.viewWidth + metrics.absoluteLeft - + this.WIDTH_ - this.MARGIN_SIDE_ - Blockly.Scrollbar.scrollbarThickness; + + if (metrics.toolboxPosition == Blockly.TOOLBOX_AT_RIGHT) { + this.left_ -= metrics.flyoutWidth; + } + } + this.top_ = metrics.viewHeight + metrics.absoluteTop - + this.HEIGHT_ - this.bottom_; + if (metrics.toolboxPosition == Blockly.TOOLBOX_AT_BOTTOM) { + this.top_ -= metrics.flyoutHeight; + } + this.svgGroup_.setAttribute('transform', + 'translate(' + this.left_ + ',' + this.top_ + ')'); +}; diff --git a/src/deps.cljs b/src/deps.cljs new file mode 100644 index 0000000..15f1415 --- /dev/null +++ b/src/deps.cljs @@ -0,0 +1,150 @@ +{:foreign-libs [{:file "semantic.js" + :file-min "semantic.min.js" + :provides ["semantic-ui"] + :requires ["cljsjs.jquery"]} +;; generated by generate_deps.py +{:file "blockly/core/warning.js" + :provides ["Blockly.Warning"] + :requires ["Blockly.Bubble" "Blockly.Icon"]} +{:file "blockly/core/field.js" + :provides ["Blockly.Field"] + :requires ["goog.asserts" "goog.dom" "goog.math.Size" "goog.style" "goog.userAgent"]} +{:file "blockly/core/scrollbar.js" + :provides ["Blockly.Scrollbar" "Blockly.ScrollbarPair"] + :requires ["goog.dom" "goog.events"]} +{:file "blockly/core/comment.js" + :provides ["Blockly.Comment"] + :requires ["Blockly.Bubble" "Blockly.Icon" "goog.userAgent"]} +{:file "blockly/core/events.js" + :provides ["Blockly.Events"] + :requires ["goog.math.Coordinate"]} +{:file "blockly/core/trashcan.js" + :provides ["Blockly.Trashcan"] + :requires ["goog.Timer" "goog.dom" "goog.math" "goog.math.Rect"]} +{:file "blockly/core/connection.js" + :provides ["Blockly.Connection"] + :requires ["goog.asserts" "goog.dom"]} +{:file "blockly/core/widgetdiv.js" + :provides ["Blockly.WidgetDiv"] + :requires ["Blockly.Css" "goog.dom" "goog.dom.TagName" "goog.style"]} +{:file "blockly/core/blockly.js" + :provides ["Blockly"] + :requires ["Blockly.BlockSvg.render" "Blockly.Events" "Blockly.FieldAngle" "Blockly.FieldCheckbox" "Blockly.FieldColour" "Blockly.FieldDropdown" "Blockly.FieldImage" "Blockly.FieldTextInput" "Blockly.FieldNumber" "Blockly.FieldVariable" "Blockly.Generator" "Blockly.Msg" "Blockly.Procedures" "Blockly.Toolbox" "Blockly.WidgetDiv" "Blockly.WorkspaceSvg" "Blockly.constants" "Blockly.inject" "Blockly.utils" "goog.color" "goog.userAgent"]} +{:file "blockly/core/flyout_button.js" + :provides ["Blockly.FlyoutButton"] + :requires ["goog.dom" "goog.math.Coordinate"]} +{:file "blockly/core/block_render_svg.js" + :provides ["Blockly.BlockSvg.render"] + :requires ["Blockly.BlockSvg"]} +{:file "blockly/core/utils.js" + :provides ["Blockly.utils"] + :requires ["goog.dom" "goog.events.BrowserFeature" "goog.math.Coordinate" "goog.userAgent"]} +{:file "blockly/core/msg.js" + :provides ["Blockly.Msg"] + :requires []} +{:file "blockly/core/contextmenu.js" + :provides ["Blockly.ContextMenu"] + :requires ["goog.dom" "goog.events" "goog.style" "goog.ui.Menu" "goog.ui.MenuItem"]} +{:file "blockly/core/icon.js" + :provides ["Blockly.Icon"] + :requires ["goog.dom" "goog.math.Coordinate"]} +{:file "blockly/core/field_textinput.js" + :provides ["Blockly.FieldTextInput"] + :requires ["Blockly.Field" "Blockly.Msg" "goog.asserts" "goog.dom" "goog.dom.TagName" "goog.userAgent"]} +{:file "blockly/core/toolbox.js" + :provides ["Blockly.Toolbox"] + :requires ["Blockly.Flyout" "goog.dom" "goog.dom.TagName" "goog.events" "goog.events.BrowserFeature" "goog.html.SafeHtml" "goog.html.SafeStyle" "goog.math.Rect" "goog.style" "goog.ui.tree.TreeControl" "goog.ui.tree.TreeNode"]} +{:file "blockly/core/options.js" + :provides ["Blockly.Options"] + :requires []} +{:file "blockly/core/block.js" + :provides ["Blockly.Block"] + :requires ["Blockly.Blocks" "Blockly.Comment" "Blockly.Connection" "Blockly.Input" "Blockly.Mutator" "Blockly.Warning" "Blockly.Workspace" "Blockly.Xml" "goog.array" "goog.asserts" "goog.math.Coordinate" "goog.string"]} +{:file "blockly/core/block_svg.js" + :provides ["Blockly.BlockSvg"] + :requires ["Blockly.Block" "Blockly.ContextMenu" "Blockly.RenderedConnection" "goog.Timer" "goog.asserts" "goog.dom" "goog.math.Coordinate" "goog.userAgent"]} +{:file "blockly/core/field_dropdown.js" + :provides ["Blockly.FieldDropdown"] + :requires ["Blockly.Field" "goog.dom" "goog.events" "goog.style" "goog.ui.Menu" "goog.ui.MenuItem" "goog.userAgent"]} +{:file "blockly/core/css.js" + :provides ["Blockly.Css"] + :requires []} +{:file "blockly/core/field_checkbox.js" + :provides ["Blockly.FieldCheckbox"] + :requires ["Blockly.Field"]} +{:file "blockly/core/field_label.js" + :provides ["Blockly.FieldLabel"] + :requires ["Blockly.Field" "Blockly.Tooltip" "goog.dom" "goog.math.Size"]} +{:file "blockly/core/names.js" + :provides ["Blockly.Names"] + :requires []} +{:file "blockly/core/mutator.js" + :provides ["Blockly.Mutator"] + :requires ["Blockly.Bubble" "Blockly.Icon" "Blockly.WorkspaceSvg" "goog.Timer" "goog.dom"]} +{:file "blockly/core/constants.js" + :provides ["Blockly.constants"] + :requires []} +{:file "blockly/core/rendered_connection.js" + :provides ["Blockly.RenderedConnection"] + :requires ["Blockly.Connection"]} +{:file "blockly/core/field_colour.js" + :provides ["Blockly.FieldColour"] + :requires ["Blockly.Field" "goog.dom" "goog.events" "goog.style" "goog.ui.ColorPicker"]} +{:file "blockly/core/field_image.js" + :provides ["Blockly.FieldImage"] + :requires ["Blockly.Field" "goog.dom" "goog.math.Size" "goog.userAgent"]} +{:file "blockly/core/field_variable.js" + :provides ["Blockly.FieldVariable"] + :requires ["Blockly.FieldDropdown" "Blockly.Msg" "Blockly.Variables" "goog.string"]} +{:file "blockly/core/input.js" + :provides ["Blockly.Input"] + :requires ["Blockly.Connection" "Blockly.FieldLabel" "goog.asserts"]} +{:file "blockly/core/field_number.js" + :provides ["Blockly.FieldNumber"] + :requires ["Blockly.FieldTextInput" "goog.math"]} +{:file "blockly/core/variables.js" + :provides ["Blockly.Variables"] + :requires ["Blockly.Blocks" "Blockly.Workspace" "goog.string"]} +{:file "blockly/core/workspace_svg.js" + :provides ["Blockly.WorkspaceSvg"] + :requires ["Blockly.ConnectionDB" "Blockly.constants" "Blockly.Options" "Blockly.ScrollbarPair" "Blockly.Trashcan" "Blockly.Workspace" "Blockly.Xml" "Blockly.ZoomControls" "goog.dom" "goog.math.Coordinate" "goog.userAgent"]} +{:file "blockly/core/bubble.js" + :provides ["Blockly.Bubble"] + :requires ["Blockly.Workspace" "goog.dom" "goog.math" "goog.math.Coordinate" "goog.userAgent"]} +{:file "blockly/core/procedures.js" + :provides ["Blockly.Procedures"] + :requires ["Blockly.Blocks" "Blockly.Field" "Blockly.Names" "Blockly.Workspace"]} +{:file "blockly/core/flyout.js" + :provides ["Blockly.Flyout"] + :requires ["Blockly.Block" "Blockly.Comment" "Blockly.Events" "Blockly.FlyoutButton" "Blockly.WorkspaceSvg" "goog.dom" "goog.events" "goog.math.Rect" "goog.userAgent"]} +{:file "blockly/core/xml.js" + :provides ["Blockly.Xml"] + :requires ["goog.asserts" "goog.dom"]} +{:file "blockly/core/blocks.js" + :provides ["Blockly.Blocks"] + :requires []} +{:file "blockly/core/tooltip.js" + :provides ["Blockly.Tooltip"] + :requires ["goog.dom" "goog.dom.TagName"]} +{:file "blockly/core/field_angle.js" + :provides ["Blockly.FieldAngle"] + :requires ["Blockly.FieldTextInput" "goog.math" "goog.userAgent"]} +{:file "blockly/core/zoom_controls.js" + :provides ["Blockly.ZoomControls"] + :requires ["goog.dom"]} +{:file "blockly/core/workspace.js" + :provides ["Blockly.Workspace"] + :requires ["goog.math"]} +{:file "blockly/core/field_date.js" + :provides ["Blockly.FieldDate"] + :requires ["Blockly.Field" "goog.date" "goog.dom" "goog.events" "goog.i18n.DateTimeSymbols" "goog.i18n.DateTimeSymbols_he" "goog.style" "goog.ui.DatePicker"]} +{:file "blockly/core/generator.js" + :provides ["Blockly.Generator"] + :requires ["Blockly.Block" "goog.asserts"]} +{:file "blockly/core/inject.js" + :provides ["Blockly.inject"] + :requires ["Blockly.Css" "Blockly.Options" "Blockly.WorkspaceSvg" "goog.dom" "goog.ui.Component" "goog.userAgent"]} +{:file "blockly/core/connection_db.js" + :provides ["Blockly.ConnectionDB"] + :requires ["Blockly.Connection"]} +]} diff --git a/src/index.cljs.hl b/src/index.cljs.hl index c8be06d..2472649 100644 --- a/src/index.cljs.hl +++ b/src/index.cljs.hl @@ -1,10 +1,36 @@ (page "index.html" - (:require [app.rpc :as rpc])) + (:require [app.rpc :as rpc] + [Blockly])) + +(def toolbox + "<xml> + <block type=\"controls_if\"></block> + <block type=\"logic_compare\"></block> + <block type=\"controls_repeat_ext\"></block> + <block type=\"math_number\"></block> + <block type=\"math_arithmetic\"></block> + <block type=\"text\"></block> + <block type=\"text_print\"></block> + </xml>") + +(defelem blockly-workspace + [{:keys [options] :as attr} kids] + (let [elem (div (dissoc attr :options) kids)] + (with-init! + (set! (.-workspace elem) + (.inject js/Blockly elem (clj->js options)))) + elem)) + (html (head + (link :rel "stylesheet" :type "text/css" :href "css/semantic.min.css") + (script :src "blocks_compressed.js") + (script :src "en.js") (title "Tankputer")) (body - (h1 "Tankputer"))) + (h1 "Tankputer") + (blockly-workspace :css {:height "480px" :width "600px"} + :options {:media "media/" :toolbox toolbox}))) ;; vim: set expandtab ts=2 sw=2 filetype=clojure : diff --git a/src/semantic.js b/src/semantic.js new file mode 100644 index 0000000..1817530 --- /dev/null +++ b/src/semantic.js @@ -0,0 +1,22500 @@ + /* + * # Semantic UI - 2.2.6 + * https://github.com/Semantic-Org/Semantic-UI + * http://www.semantic-ui.com/ + * + * Copyright 2014 Contributors + * Released under the MIT license + * http://opensource.org/licenses/MIT + * + */ +/*! + * # Semantic UI 2.2.6 - Site + * http://github.com/semantic-org/semantic-ui/ + * + * + * Released under the MIT license + * http://opensource.org/licenses/MIT + * + */ + +;(function ($, window, document, undefined) { + +$.site = $.fn.site = function(parameters) { + var + time = new Date().getTime(), + performance = [], + + query = arguments[0], + methodInvoked = (typeof query == 'string'), + queryArguments = [].slice.call(arguments, 1), + + settings = ( $.isPlainObject(parameters) ) + ? $.extend(true, {}, $.site.settings, parameters) + : $.extend({}, $.site.settings), + + namespace = settings.namespace, + error = settings.error, + + eventNamespace = '.' + namespace, + moduleNamespace = 'module-' + namespace, + + $document = $(document), + $module = $document, + element = this, + instance = $module.data(moduleNamespace), + + module, + returnedValue + ; + module = { + + initialize: function() { + module.instantiate(); + }, + + instantiate: function() { + module.verbose('Storing instance of site', module); + instance = module; + $module + .data(moduleNamespace, module) + ; + }, + + normalize: function() { + module.fix.console(); + module.fix.requestAnimationFrame(); + }, + + fix: { + console: function() { + module.debug('Normalizing window.console'); + if (console === undefined || console.log === undefined) { + module.verbose('Console not available, normalizing events'); + module.disable.console(); + } + if (typeof console.group == 'undefined' || typeof console.groupEnd == 'undefined' || typeof console.groupCollapsed == 'undefined') { + module.verbose('Console group not available, normalizing events'); + window.console.group = function() {}; + window.console.groupEnd = function() {}; + window.console.groupCollapsed = function() {}; + } + if (typeof console.markTimeline == 'undefined') { + module.verbose('Mark timeline not available, normalizing events'); + window.console.markTimeline = function() {}; + } + }, + consoleClear: function() { + module.debug('Disabling programmatic console clearing'); + window.console.clear = function() {}; + }, + requestAnimationFrame: function() { + module.debug('Normalizing requestAnimationFrame'); + if(window.requestAnimationFrame === undefined) { + module.debug('RequestAnimationFrame not available, normalizing event'); + window.requestAnimationFrame = window.requestAnimationFrame + || window.mozRequestAnimationFrame + || window.webkitRequestAnimationFrame + || window.msRequestAnimationFrame + || function(callback) { setTimeout(callback, 0); } + ; + } + } + }, + + moduleExists: function(name) { + return ($.fn[name] !== undefined && $.fn[name].settings !== undefined); + }, + + enabled: { + modules: function(modules) { + var + enabledModules = [] + ; + modules = modules || settings.modules; + $.each(modules, function(index, name) { + if(module.moduleExists(name)) { + enabledModules.push(name); + } + }); + return enabledModules; + } + }, + + disabled: { + modules: function(modules) { + var + disabledModules = [] + ; + modules = modules || settings.modules; + $.each(modules, function(index, name) { + if(!module.moduleExists(name)) { + disabledModules.push(name); + } + }); + return disabledModules; + } + }, + + change: { + setting: function(setting, value, modules, modifyExisting) { + modules = (typeof modules === 'string') + ? (modules === 'all') + ? settings.modules + : [modules] + : modules || settings.modules + ; + modifyExisting = (modifyExisting !== undefined) + ? modifyExisting + : true + ; + $.each(modules, function(index, name) { + var + namespace = (module.moduleExists(name)) + ? $.fn[name].settings.namespace || false + : true, + $existingModules + ; + if(module.moduleExists(name)) { + module.verbose('Changing default setting', setting, value, name); + $.fn[name].settings[setting] = value; + if(modifyExisting && namespace) { + $existingModules = $(':data(module-' + namespace + ')'); + if($existingModules.length > 0) { + module.verbose('Modifying existing settings', $existingModules); + $existingModules[name]('setting', setting, value); + } + } + } + }); + }, + settings: function(newSettings, modules, modifyExisting) { + modules = (typeof modules === 'string') + ? [modules] + : modules || settings.modules + ; + modifyExisting = (modifyExisting !== undefined) + ? modifyExisting + : true + ; + $.each(modules, function(index, name) { + var + $existingModules + ; + if(module.moduleExists(name)) { + module.verbose('Changing default setting', newSettings, name); + $.extend(true, $.fn[name].settings, newSettings); + if(modifyExisting && namespace) { + $existingModules = $(':data(module-' + namespace + ')'); + if($existingModules.length > 0) { + module.verbose('Modifying existing settings', $existingModules); + $existingModules[name]('setting', newSettings); + } + } + } + }); + } + }, + + enable: { + console: function() { + module.console(true); + }, + debug: function(modules, modifyExisting) { + modules = modules || settings.modules; + module.debug('Enabling debug for modules', modules); + module.change.setting('debug', true, modules, modifyExisting); + }, + verbose: function(modules, modifyExisting) { + modules = modules || settings.modules; + module.debug('Enabling verbose debug for modules', modules); + module.change.setting('verbose', true, modules, modifyExisting); + } + }, + disable: { + console: function() { + module.console(false); + }, + debug: function(modules, modifyExisting) { + modules = modules || settings.modules; + module.debug('Disabling debug for modules', modules); + module.change.setting('debug', false, modules, modifyExisting); + }, + verbose: function(modules, modifyExisting) { + modules = modules || settings.modules; + module.debug('Disabling verbose debug for modules', modules); + module.change.setting('verbose', false, modules, modifyExisting); + } + }, + + console: function(enable) { + if(enable) { + if(instance.cache.console === undefined) { + module.error(error.console); + return; + } + module.debug('Restoring console function'); + window.console = instance.cache.console; + } + else { + module.debug('Disabling console function'); + instance.cache.console = window.console; + window.console = { + clear : function(){}, + error : function(){}, + group : function(){}, + groupCollapsed : function(){}, + groupEnd : function(){}, + info : function(){}, + log : function(){}, + markTimeline : function(){}, + warn : function(){} + }; + } + }, + + destroy: function() { + module.verbose('Destroying previous site for', $module); + $module + .removeData(moduleNamespace) + ; + }, + + cache: {}, + + setting: function(name, value) { + if( $.isPlainObject(name) ) { + $.extend(true, settings, name); + } + else if(value !== undefined) { + settings[name] = value; + } + else { + return settings[name]; + } + }, + internal: function(name, value) { + if( $.isPlainObject(name) ) { + $.extend(true, module, name); + } + else if(value !== undefined) { + module[name] = value; + } + else { + return module[name]; + } + }, + debug: function() { + if(settings.debug) { + if(settings.performance) { + module.performance.log(arguments); + } + else { + module.debug = Function.prototype.bind.call(console.info, console, settings.name + ':'); + module.debug.apply(console, arguments); + } + } + }, + verbose: function() { + if(settings.verbose && settings.debug) { + if(settings.performance) { + module.performance.log(arguments); + } + else { + module.verbose = Function.prototype.bind.call(console.info, console, settings.name + ':'); + module.verbose.apply(console, arguments); + } + } + }, + error: function() { + module.error = Function.prototype.bind.call(console.error, console, settings.name + ':'); + module.error.apply(console, arguments); + }, + performance: { + log: function(message) { + var + currentTime, + executionTime, + previousTime + ; + if(settings.performance) { + currentTime = new Date().getTime(); + previousTime = time || currentTime; + executionTime = currentTime - previousTime; + time = currentTime; + performance.push({ + 'Element' : element, + 'Name' : message[0], + 'Arguments' : [].slice.call(message, 1) || '', + 'Execution Time' : executionTime + }); + } + clearTimeout(module.performance.timer); + module.performance.timer = setTimeout(module.performance.display, 500); + }, + display: function() { + var + title = settings.name + ':', + totalTime = 0 + ; + time = false; + clearTimeout(module.performance.timer); + $.each(performance, function(index, data) { + totalTime += data['Execution Time']; + }); + title += ' ' + totalTime + 'ms'; + if( (console.group !== undefined || console.table !== undefined) && performance.length > 0) { + console.groupCollapsed(title); + if(console.table) { + console.table(performance); + } + else { + $.each(performance, function(index, data) { + console.log(data['Name'] + ': ' + data['Execution Time']+'ms'); + }); + } + console.groupEnd(); + } + performance = []; + } + }, + invoke: function(query, passedArguments, context) { + var + object = instance, + maxDepth, + found, + response + ; + passedArguments = passedArguments || queryArguments; + context = element || context; + if(typeof query == 'string' && object !== undefined) { + query = query.split(/[\. ]/); + maxDepth = query.length - 1; + $.each(query, function(depth, value) { + var camelCaseValue = (depth != maxDepth) + ? value + query[depth + 1].charAt(0).toUpperCase() + query[depth + 1].slice(1) + : query + ; + if( $.isPlainObject( object[camelCaseValue] ) && (depth != maxDepth) ) { + object = object[camelCaseValue]; + } + else if( object[camelCaseValue] !== undefined ) { + found = object[camelCaseValue]; + return false; + } + else if( $.isPlainObject( object[value] ) && (depth != maxDepth) ) { + object = object[value]; + } + else if( object[value] !== undefined ) { + found = object[value]; + return false; + } + else { + module.error(error.method, query); + return false; + } + }); + } + if ( $.isFunction( found ) ) { + response = found.apply(context, passedArguments); + } + else if(found !== undefined) { + response = found; + } + if($.isArray(returnedValue)) { + returnedValue.push(response); + } + else if(returnedValue !== undefined) { + returnedValue = [returnedValue, response]; + } + else if(response !== undefined) { + returnedValue = response; + } + return found; + } + }; + + if(methodInvoked) { + if(instance === undefined) { + module.initialize(); + } + module.invoke(query); + } + else { + if(instance !== undefined) { + module.destroy(); + } + module.initialize(); + } + return (returnedValue !== undefined) + ? returnedValue + : this + ; +}; + +$.site.settings = { + + name : 'Site', + namespace : 'site', + + error : { + console : 'Console cannot be restored, most likely it was overwritten outside of module', + method : 'The method you called is not defined.' + }, + + debug : false, + verbose : false, + performance : true, + + modules: [ + 'accordion', + 'api', + 'checkbox', + 'dimmer', + 'dropdown', + 'embed', + 'form', + 'modal', + 'nag', + 'popup', + 'rating', + 'shape', + 'sidebar', + 'state', + 'sticky', + 'tab', + 'transition', + 'visit', + 'visibility' + ], + + siteNamespace : 'site', + namespaceStub : { + cache : {}, + config : {}, + sections : {}, + section : {}, + utilities : {} + } + +}; + +// allows for selection of elements with data attributes +$.extend($.expr[ ":" ], { + data: ($.expr.createPseudo) + ? $.expr.createPseudo(function(dataName) { + return function(elem) { + return !!$.data(elem, dataName); + }; + }) + : function(elem, i, match) { + // support: jQuery < 1.8 + return !!$.data(elem, match[ 3 ]); + } +}); + + +})( jQuery, window, document ); + +/*! + * # Semantic UI 2.2.6 - Form Validation + * http://github.com/semantic-org/semantic-ui/ + * + * + * Released under the MIT license + * http://opensource.org/licenses/MIT + * + */ + +;(function ($, window, document, undefined) { + +"use strict"; + +window = (typeof window != 'undefined' && window.Math == Math) + ? window + : (typeof self != 'undefined' && self.Math == Math) + ? self + : Function('return this')() +; + +$.fn.form = function(parameters) { + var + $allModules = $(this), + moduleSelector = $allModules.selector || '', + + time = new Date().getTime(), + performance = [], + + query = arguments[0], + legacyParameters = arguments[1], + methodInvoked = (typeof query == 'string'), + queryArguments = [].slice.call(arguments, 1), + returnedValue + ; + $allModules + .each(function() { + var + $module = $(this), + element = this, + + formErrors = [], + keyHeldDown = false, + + // set at run-time + $field, + $group, + $message, + $prompt, + $submit, + $clear, + $reset, + + settings, + validation, + + metadata, + selector, + className, + error, + + namespace, + moduleNamespace, + eventNamespace, + + instance, + module + ; + + module = { + + initialize: function() { + + // settings grabbed at run time + module.get.settings(); + if(methodInvoked) { + if(instance === undefined) { + module.instantiate(); + } + module.invoke(query); + } + else { + if(instance !== undefined) { + instance.invoke('destroy'); + } + module.verbose('Initializing form validation', $module, settings); + module.bindEvents(); + module.set.defaults(); + module.instantiate(); + } + }, + + instantiate: function() { + module.verbose('Storing instance of module', module); + instance = module; + $module + .data(moduleNamespace, module) + ; + }, + + destroy: function() { + module.verbose('Destroying previous module', instance); + module.removeEvents(); + $module + .removeData(moduleNamespace) + ; + }, + + refresh: function() { + module.verbose('Refreshing selector cache'); + $field = $module.find(selector.field); + $group = $module.find(selector.group); + $message = $module.find(selector.message); + $prompt = $module.find(selector.prompt); + + $submit = $module.find(selector.submit); + $clear = $module.find(selector.clear); + $reset = $module.find(selector.reset); + }, + + submit: function() { + module.verbose('Submitting form', $module); + $module + .submit() + ; + }, + + attachEvents: function(selector, action) { + action = action || 'submit'; + $(selector) + .on('click' + eventNamespace, function(event) { + module[action](); + event.preventDefault(); + }) + ; + }, + + bindEvents: function() { + module.verbose('Attaching form events'); + $module + .on('submit' + eventNamespace, module.validate.form) + .on('blur' + eventNamespace, selector.field, module.event.field.blur) + .on('click' + eventNamespace, selector.submit, module.submit) + .on('click' + eventNamespace, selector.reset, module.reset) + .on('click' + eventNamespace, selector.clear, module.clear) + ; + if(settings.keyboardShortcuts) { + $module + .on('keydown' + eventNamespace, selector.field, module.event.field.keydown) + ; + } + $field + .each(function() { + var + $input = $(this), + type = $input.prop('type'), + inputEvent = module.get.changeEvent(type, $input) + ; + $(this) + .on(inputEvent + eventNamespace, module.event.field.change) + ; + }) + ; + }, + + clear: function() { + $field + .each(function () { + var + $field = $(this), + $element = $field.parent(), + $fieldGroup = $field.closest($group), + $prompt = $fieldGroup.find(selector.prompt), + defaultValue = $field.data(metadata.defaultValue) || '', + isCheckbox = $element.is(selector.uiCheckbox), + isDropdown = $element.is(selector.uiDropdown), + isErrored = $fieldGroup.hasClass(className.error) + ; + if(isErrored) { + module.verbose('Resetting error on field', $fieldGroup); + $fieldGroup.removeClass(className.error); + $prompt.remove(); + } + if(isDropdown) { + module.verbose('Resetting dropdown value', $element, defaultValue); + $element.dropdown('clear'); + } + else if(isCheckbox) { + $field.prop('checked', false); + } + else { + module.verbose('Resetting field value', $field, defaultValue); + $field.val(''); + } + }) + ; + }, + + reset: function() { + $field + .each(function () { + var + $field = $(this), + $element = $field.parent(), + $fieldGroup = $field.closest($group), + $prompt = $fieldGroup.find(selector.prompt), + defaultValue = $field.data(metadata.defaultValue), + isCheckbox = $element.is(selector.uiCheckbox), + isDropdown = $element.is(selector.uiDropdown), + isErrored = $fieldGroup.hasClass(className.error) + ; + if(defaultValue === undefined) { + return; + } + if(isErrored) { + module.verbose('Resetting error on field', $fieldGroup); + $fieldGroup.removeClass(className.error); + $prompt.remove(); + } + if(isDropdown) { + module.verbose('Resetting dropdown value', $element, defaultValue); + $element.dropdown('restore defaults'); + } + else if(isCheckbox) { + module.verbose('Resetting checkbox value', $element, defaultValue); + $field.prop('checked', defaultValue); + } + else { + module.verbose('Resetting field value', $field, defaultValue); + $field.val(defaultValue); + } + }) + ; + }, + + is: { + bracketedRule: function(rule) { + return (rule.type && rule.type.match(settings.regExp.bracket)); + }, + empty: function($field) { + if(!$field || $field.length === 0) { + return true; + } + else if($field.is('input[type="checkbox"]')) { + return !$field.is(':checked'); + } + else { + return module.is.blank($field); + } + }, + blank: function($field) { + return $.trim($field.val()) === ''; + }, + valid: function() { + var + allValid = true + ; + module.verbose('Checking if form is valid'); + $.each(validation, function(fieldName, field) { + if( !( module.validate.field(field, fieldName) ) ) { + allValid = false; + } + }); + return allValid; + } + }, + + removeEvents: function() { + $module + .off(eventNamespace) + ; + $field + .off(eventNamespace) + ; + $submit + .off(eventNamespace) + ; + $field + .off(eventNamespace) + ; + }, + + event: { + field: { + keydown: function(event) { + var + $field = $(this), + key = event.which, + isInput = $field.is(selector.input), + isCheckbox = $field.is(selector.checkbox), + isInDropdown = ($field.closest(selector.uiDropdown).length > 0), + keyCode = { + enter : 13, + escape : 27 + } + ; + if( key == keyCode.escape) { + module.verbose('Escape key pressed blurring field'); + $field + .blur() + ; + } + if(!event.ctrlKey && key == keyCode.enter && isInput && !isInDropdown && !isCheckbox) { + if(!keyHeldDown) { + $field + .one('keyup' + eventNamespace, module.event.field.keyup) + ; + module.submit(); + module.debug('Enter pressed on input submitting form'); + } + keyHeldDown = true; + } + }, + keyup: function() { + keyHeldDown = false; + }, + blur: function(event) { + var + $field = $(this), + $fieldGroup = $field.closest($group), + validationRules = module.get.validation($field) + ; + if( $fieldGroup.hasClass(className.error) ) { + module.debug('Revalidating field', $field, validationRules); + if(validationRules) { + module.validate.field( validationRules ); + } + } + else if(settings.on == 'blur' || settings.on == 'change') { + if(validationRules) { + module.validate.field( validationRules ); + } + } + }, + change: function(event) { + var + $field = $(this), + $fieldGroup = $field.closest($group), + validationRules = module.get.validation($field) + ; + if(settings.on == 'change' || ( $fieldGroup.hasClass(className.error) && settings.revalidate) ) { + clearTimeout(module.timer); + module.timer = setTimeout(function() { + module.debug('Revalidating field', $field, module.get.validation($field)); + module.validate.field( validationRules ); + }, settings.delay); + } + } + } + + }, + + get: { + ancillaryValue: function(rule) { + if(!rule.type || (!rule.value && !module.is.bracketedRule(rule))) { + return false; + } + return (rule.value !== undefined) + ? rule.value + : rule.type.match(settings.regExp.bracket)[1] + '' + ; + }, + ruleName: function(rule) { + if( module.is.bracketedRule(rule) ) { + return rule.type.replace(rule.type.match(settings.regExp.bracket)[0], ''); + } + return rule.type; + }, + changeEvent: function(type, $input) { + if(type == 'checkbox' || type == 'radio' || type == 'hidden' || $input.is('select')) { + return 'change'; + } + else { + return module.get.inputEvent(); + } + }, + inputEvent: function() { + return (document.createElement('input').oninput !== undefined) + ? 'input' + : (document.createElement('input').onpropertychange !== undefined) + ? 'propertychange' + : 'keyup' + ; + }, + prompt: function(rule, field) { + var + ruleName = module.get.ruleName(rule), + ancillary = module.get.ancillaryValue(rule), + prompt = rule.prompt || settings.prompt[ruleName] || settings.text.unspecifiedRule, + requiresValue = (prompt.search('{value}') !== -1), + requiresName = (prompt.search('{name}') !== -1), + $label, + $field, + name + ; + if(requiresName || requiresValue) { + $field = module.get.field(field.identifier); + } + if(requiresValue) { + prompt = prompt.replace('{value}', $field.val()); + } + if(requiresName) { + $label = $field.closest(selector.group).find('label').eq(0); + name = ($label.length == 1) + ? $label.text() + : $field.prop('placeholder') || settings.text.unspecifiedField + ; + prompt = prompt.replace('{name}', name); + } + prompt = prompt.replace('{identifier}', field.identifier); + prompt = prompt.replace('{ruleValue}', ancillary); + if(!rule.prompt) { + module.verbose('Using default validation prompt for type', prompt, ruleName); + } + return prompt; + }, + settings: function() { + if($.isPlainObject(parameters)) { + var + keys = Object.keys(parameters), + isLegacySettings = (keys.length > 0) + ? (parameters[keys[0]].identifier !== undefined && parameters[keys[0]].rules !== undefined) + : false, + ruleKeys + ; + if(isLegacySettings) { + // 1.x (ducktyped) + settings = $.extend(true, {}, $.fn.form.settings, legacyParameters); + validation = $.extend({}, $.fn.form.settings.defaults, parameters); + module.error(settings.error.oldSyntax, element); + module.verbose('Extending settings from legacy parameters', validation, settings); + } + else { + // 2.x + if(parameters.fields) { + ruleKeys = Object.keys(parameters.fields); + if( typeof parameters.fields[ruleKeys[0]] == 'string' || $.isArray(parameters.fields[ruleKeys[0]]) ) { + $.each(parameters.fields, function(name, rules) { + if(typeof rules == 'string') { + rules = [rules]; + } + parameters.fields[name] = { + rules: [] + }; + $.each(rules, function(index, rule) { + parameters.fields[name].rules.push({ type: rule }); + }); + }); + } + } + + settings = $.extend(true, {}, $.fn.form.settings, parameters); + validation = $.extend({}, $.fn.form.settings.defaults, settings.fields); + module.verbose('Extending settings', validation, settings); + } + } + else { + settings = $.fn.form.settings; + validation = $.fn.form.settings.defaults; + module.verbose('Using default form validation', validation, settings); + } + + // shorthand + namespace = settings.namespace; + metadata = settings.metadata; + selector = settings.selector; + className = settings.className; + error = settings.error; + moduleNamespace = 'module-' + namespace; + eventNamespace = '.' + namespace; + + // grab instance + instance = $module.data(moduleNamespace); + + // refresh selector cache + module.refresh(); + }, + field: function(identifier) { + module.verbose('Finding field with identifier', identifier); + if( $field.filter('#' + identifier).length > 0 ) { + return $field.filter('#' + identifier); + } + else if( $field.filter('[name="' + identifier +'"]').length > 0 ) { + return $field.filter('[name="' + identifier +'"]'); + } + else if( $field.filter('[name="' + identifier +'[]"]').length > 0 ) { + return $field.filter('[name="' + identifier +'[]"]'); + } + else if( $field.filter('[data-' + metadata.validate + '="'+ identifier +'"]').length > 0 ) { + return $field.filter('[data-' + metadata.validate + '="'+ identifier +'"]'); + } + return $('<input/>'); + }, + fields: function(fields) { + var + $fields = $() + ; + $.each(fields, function(index, name) { + $fields = $fields.add( module.get.field(name) ); + }); + return $fields; + }, + validation: function($field) { + var + fieldValidation, + identifier + ; + if(!validation) { + return false; + } + $.each(validation, function(fieldName, field) { + identifier = field.identifier || fieldName; + if( module.get.field(identifier)[0] == $field[0] ) { + field.identifier = identifier; + fieldValidation = field; + } + }); + return fieldValidation || false; + }, + value: function (field) { + var + fields = [], + results + ; + fields.push(field); + results = module.get.values.call(element, fields); + return results[field]; + }, + values: function (fields) { + var + $fields = $.isArray(fields) + ? module.get.fields(fields) + : $field, + values = {} + ; + $fields.each(function(index, field) { + var + $field = $(field), + type = $field.prop('type'), + name = $field.prop('name'), + value = $field.val(), + isCheckbox = $field.is(selector.checkbox), + isRadio = $field.is(selector.radio), + isMultiple = (name.indexOf('[]') !== -1), + isChecked = (isCheckbox) + ? $field.is(':checked') + : false + ; + if(name) { + if(isMultiple) { + name = name.replace('[]', ''); + if(!values[name]) { + values[name] = []; + } + if(isCheckbox) { + if(isChecked) { + values[name].push(value || true); + } + else { + values[name].push(false); + } + } + else { + values[name].push(value); + } + } + else { + if(isRadio) { + if(isChecked) { + values[name] = value; + } + } + else if(isCheckbox) { + if(isChecked) { + values[name] = value || true; + } + else { + values[name] = false; + } + } + else { + values[name] = value; + } + } + } + }); + return values; + } + }, + + has: { + + field: function(identifier) { + module.verbose('Checking for existence of a field with identifier', identifier); + if(typeof identifier !== 'string') { + module.error(error.identifier, identifier); + } + if( $field.filter('#' + identifier).length > 0 ) { + return true; + } + else if( $field.filter('[name="' + identifier +'"]').length > 0 ) { + return true; + } + else if( $field.filter('[data-' + metadata.validate + '="'+ identifier +'"]').length > 0 ) { + return true; + } + return false; + } + + }, + + add: { + prompt: function(identifier, errors) { + var + $field = module.get.field(identifier), + $fieldGroup = $field.closest($group), + $prompt = $fieldGroup.children(selector.prompt), + promptExists = ($prompt.length !== 0) + ; + errors = (typeof errors == 'string') + ? [errors] + : errors + ; + module.verbose('Adding field error state', identifier); + $fieldGroup + .addClass(className.error) + ; + if(settings.inline) { + if(!promptExists) { + $prompt = settings.templates.prompt(errors); + $prompt + .appendTo($fieldGroup) + ; + } + $prompt + .html(errors[0]) + ; + if(!promptExists) { + if(settings.transition && $.fn.transition !== undefined && $module.transition('is supported')) { + module.verbose('Displaying error with css transition', settings.transition); + $prompt.transition(settings.transition + ' in', settings.duration); + } + else { + module.verbose('Displaying error with fallback javascript animation'); + $prompt + .fadeIn(settings.duration) + ; + } + } + else { + module.verbose('Inline errors are disabled, no inline error added', identifier); + } + } + }, + errors: function(errors) { + module.debug('Adding form error messages', errors); + module.set.error(); + $message + .html( settings.templates.error(errors) ) + ; + } + }, + + remove: { + prompt: function(identifier) { + var + $field = module.get.field(identifier), + $fieldGroup = $field.closest($group), + $prompt = $fieldGroup.children(selector.prompt) + ; + $fieldGroup + .removeClass(className.error) + ; + if(settings.inline && $prompt.is(':visible')) { + module.verbose('Removing prompt for field', identifier); + if(settings.transition && $.fn.transition !== undefined && $module.transition('is supported')) { + $prompt.transition(settings.transition + ' out', settings.duration, function() { + $prompt.remove(); + }); + } + else { + $prompt + .fadeOut(settings.duration, function(){ + $prompt.remove(); + }) + ; + } + } + } + }, + + set: { + success: function() { + $module + .removeClass(className.error) + .addClass(className.success) + ; + }, + defaults: function () { + $field + .each(function () { + var + $field = $(this), + isCheckbox = ($field.filter(selector.checkbox).length > 0), + value = (isCheckbox) + ? $field.is(':checked') + : $field.val() + ; + $field.data(metadata.defaultValue, value); + }) + ; + }, + error: function() { + $module + .removeClass(className.success) + .addClass(className.error) + ; + }, + value: function (field, value) { + var + fields = {} + ; + fields[field] = value; + return module.set.values.call(element, fields); + }, + values: function (fields) { + if($.isEmptyObject(fields)) { + return; + } + $.each(fields, function(key, value) { + var + $field = module.get.field(key), + $element = $field.parent(), + isMultiple = $.isArray(value), + isCheckbox = $element.is(selector.uiCheckbox), + isDropdown = $element.is(selector.uiDropdown), + isRadio = ($field.is(selector.radio) && isCheckbox), + fieldExists = ($field.length > 0), + $multipleField + ; + if(fieldExists) { + if(isMultiple && isCheckbox) { + module.verbose('Selecting multiple', value, $field); + $element.checkbox('uncheck'); + $.each(value, function(index, value) { + $multipleField = $field.filter('[value="' + value + '"]'); + $element = $multipleField.parent(); + if($multipleField.length > 0) { + $element.checkbox('check'); + } + }); + } + else if(isRadio) { + module.verbose('Selecting radio value', value, $field); + $field.filter('[value="' + value + '"]') + .parent(selector.uiCheckbox) + .checkbox('check') + ; + } + else if(isCheckbox) { + module.verbose('Setting checkbox value', value, $element); + if(value === true) { + $element.checkbox('check'); + } + else { + $element.checkbox('uncheck'); + } + } + else if(isDropdown) { + module.verbose('Setting dropdown value', value, $element); + $element.dropdown('set selected', value); + } + else { + module.verbose('Setting field value', value, $field); + $field.val(value); + } + } + }); + } + }, + + validate: { + + form: function(event, ignoreCallbacks) { + var + values = module.get.values(), + apiRequest + ; + + // input keydown event will fire submit repeatedly by browser default + if(keyHeldDown) { + return false; + } + + // reset errors + formErrors = []; + if( module.is.valid() ) { + module.debug('Form has no validation errors, submitting'); + module.set.success(); + if(ignoreCallbacks !== true) { + return settings.onSuccess.call(element, event, values); + } + } + else { + module.debug('Form has errors'); + module.set.error(); + if(!settings.inline) { + module.add.errors(formErrors); + } + // prevent ajax submit + if($module.data('moduleApi') !== undefined) { + event.stopImmediatePropagation(); + } + if(ignoreCallbacks !== true) { + return settings.onFailure.call(element, formErrors, values); + } + } + }, + + // takes a validation object and returns whether field passes validation + field: function(field, fieldName) { + var + identifier = field.identifier || fieldName, + $field = module.get.field(identifier), + $dependsField = (field.depends) + ? module.get.field(field.depends) + : false, + fieldValid = true, + fieldErrors = [] + ; + if(!field.identifier) { + module.debug('Using field name as identifier', identifier); + field.identifier = identifier; + } + if($field.prop('disabled')) { + module.debug('Field is disabled. Skipping', identifier); + fieldValid = true; + } + else if(field.optional && module.is.blank($field)){ + module.debug('Field is optional and blank. Skipping', identifier); + fieldValid = true; + } + else if(field.depends && module.is.empty($dependsField)) { + module.debug('Field depends on another value that is not present or empty. Skipping', $dependsField); + fieldValid = true; + } + else if(field.rules !== undefined) { + $.each(field.rules, function(index, rule) { + if( module.has.field(identifier) && !( module.validate.rule(field, rule) ) ) { + module.debug('Field is invalid', identifier, rule.type); + fieldErrors.push(module.get.prompt(rule, field)); + fieldValid = false; + } + }); + } + if(fieldValid) { + module.remove.prompt(identifier, fieldErrors); + settings.onValid.call($field); + } + else { + formErrors = formErrors.concat(fieldErrors); + module.add.prompt(identifier, fieldErrors); + settings.onInvalid.call($field, fieldErrors); + return false; + } + return true; + }, + + // takes validation rule and returns whether field passes rule + rule: function(field, rule) { + var + $field = module.get.field(field.identifier), + type = rule.type, + value = $field.val(), + isValid = true, + ancillary = module.get.ancillaryValue(rule), + ruleName = module.get.ruleName(rule), + ruleFunction = settings.rules[ruleName] + ; + if( !$.isFunction(ruleFunction) ) { + module.error(error.noRule, ruleName); + return; + } + // cast to string avoiding encoding special values + value = (value === undefined || value === '' || value === null) + ? '' + : $.trim(value + '') + ; + return ruleFunction.call($field, value, ancillary); + } + }, + + setting: function(name, value) { + if( $.isPlainObject(name) ) { + $.extend(true, settings, name); + } + else if(value !== undefined) { + settings[name] = value; + } + else { + return settings[name]; + } + }, + internal: function(name, value) { + if( $.isPlainObject(name) ) { + $.extend(true, module, name); + } + else if(value !== undefined) { + module[name] = value; + } + else { + return module[name]; + } + }, + debug: function() { + if(!settings.silent && settings.debug) { + if(settings.performance) { + module.performance.log(arguments); + } + else { + module.debug = Function.prototype.bind.call(console.info, console, settings.name + ':'); + module.debug.apply(console, arguments); + } + } + }, + verbose: function() { + if(!settings.silent && settings.verbose && settings.debug) { + if(settings.performance) { + module.performance.log(arguments); + } + else { + module.verbose = Function.prototype.bind.call(console.info, console, settings.name + ':'); + module.verbose.apply(console, arguments); + } + } + }, + error: function() { + if(!settings.silent) { + module.error = Function.prototype.bind.call(console.error, console, settings.name + ':'); + module.error.apply(console, arguments); + } + }, + performance: { + log: function(message) { + var + currentTime, + executionTime, + previousTime + ; + if(settings.performance) { + currentTime = new Date().getTime(); + previousTime = time || currentTime; + executionTime = currentTime - previousTime; + time = currentTime; + performance.push({ + 'Name' : message[0], + 'Arguments' : [].slice.call(message, 1) || '', + 'Element' : element, + 'Execution Time' : executionTime + }); + } + clearTimeout(module.performance.timer); + module.performance.timer = setTimeout(module.performance.display, 500); + }, + display: function() { + var + title = settings.name + ':', + totalTime = 0 + ; + time = false; + clearTimeout(module.performance.timer); + $.each(performance, function(index, data) { + totalTime += data['Execution Time']; + }); + title += ' ' + totalTime + 'ms'; + if(moduleSelector) { + title += ' \'' + moduleSelector + '\''; + } + if($allModules.length > 1) { + title += ' ' + '(' + $allModules.length + ')'; + } + if( (console.group !== undefined || console.table !== undefined) && performance.length > 0) { + console.groupCollapsed(title); + if(console.table) { + console.table(performance); + } + else { + $.each(performance, function(index, data) { + console.log(data['Name'] + ': ' + data['Execution Time']+'ms'); + }); + } + console.groupEnd(); + } + performance = []; + } + }, + invoke: function(query, passedArguments, context) { + var + object = instance, + maxDepth, + found, + response + ; + passedArguments = passedArguments || queryArguments; + context = element || context; + if(typeof query == 'string' && object !== undefined) { + query = query.split(/[\. ]/); + maxDepth = query.length - 1; + $.each(query, function(depth, value) { + var camelCaseValue = (depth != maxDepth) + ? value + query[depth + 1].charAt(0).toUpperCase() + query[depth + 1].slice(1) + : query + ; + if( $.isPlainObject( object[camelCaseValue] ) && (depth != maxDepth) ) { + object = object[camelCaseValue]; + } + else if( object[camelCaseValue] !== undefined ) { + found = object[camelCaseValue]; + return false; + } + else if( $.isPlainObject( object[value] ) && (depth != maxDepth) ) { + object = object[value]; + } + else if( object[value] !== undefined ) { + found = object[value]; + return false; + } + else { + return false; + } + }); + } + if( $.isFunction( found ) ) { + response = found.apply(context, passedArguments); + } + else if(found !== undefined) { + response = found; + } + if($.isArray(returnedValue)) { + returnedValue.push(response); + } + else if(returnedValue !== undefined) { + returnedValue = [returnedValue, response]; + } + else if(response !== undefined) { + returnedValue = response; + } + return found; + } + }; + module.initialize(); + }) + ; + + return (returnedValue !== undefined) + ? returnedValue + : this + ; +}; + +$.fn.form.settings = { + + name : 'Form', + namespace : 'form', + + debug : false, + verbose : false, + performance : true, + + fields : false, + + keyboardShortcuts : true, + on : 'submit', + inline : false, + + delay : 200, + revalidate : true, + + transition : 'scale', + duration : 200, + + onValid : function() {}, + onInvalid : function() {}, + onSuccess : function() { return true; }, + onFailure : function() { return false; }, + + metadata : { + defaultValue : 'default', + validate : 'validate' + }, + + regExp: { + bracket : /\[(.*)\]/i, + decimal : /^\d*(\.)\d+/, + email : /^[a-z0-9!#$%&'*+\/=?^_`{|}~.-]+@[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/i, + escape : /[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, + flags : /^\/(.*)\/(.*)?/, + integer : /^\-?\d+$/, + number : /^\-?\d*(\.\d+)?$/, + url : /(https?:\/\/(?:www\.|(?!www))[^\s\.]+\.[^\s]{2,}|www\.[^\s]+\.[^\s]{2,})/i + }, + + text: { + unspecifiedRule : 'Please enter a valid value', + unspecifiedField : 'This field' + }, + + prompt: { + empty : '{name} must have a value', + checked : '{name} must be checked', + email : '{name} must be a valid e-mail', + url : '{name} must be a valid url', + regExp : '{name} is not formatted correctly', + integer : '{name} must be an integer', + decimal : '{name} must be a decimal number', + number : '{name} must be set to a number', + is : '{name} must be "{ruleValue}"', + isExactly : '{name} must be exactly "{ruleValue}"', + not : '{name} cannot be set to "{ruleValue}"', + notExactly : '{name} cannot be set to exactly "{ruleValue}"', + contain : '{name} cannot contain "{ruleValue}"', + containExactly : '{name} cannot contain exactly "{ruleValue}"', + doesntContain : '{name} must contain "{ruleValue}"', + doesntContainExactly : '{name} must contain exactly "{ruleValue}"', + minLength : '{name} must be at least {ruleValue} characters', + length : '{name} must be at least {ruleValue} characters', + exactLength : '{name} must be exactly {ruleValue} characters', + maxLength : '{name} cannot be longer than {ruleValue} characters', + match : '{name} must match {ruleValue} field', + different : '{name} must have a different value than {ruleValue} field', + creditCard : '{name} must be a valid credit card number', + minCount : '{name} must have at least {ruleValue} choices', + exactCount : '{name} must have exactly {ruleValue} choices', + maxCount : '{name} must have {ruleValue} or less choices' + }, + + selector : { + checkbox : 'input[type="checkbox"], input[type="radio"]', + clear : '.clear', + field : 'input, textarea, select', + group : '.field', + input : 'input', + message : '.error.message', + prompt : '.prompt.label', + radio : 'input[type="radio"]', + reset : '.reset:not([type="reset"])', + submit : '.submit:not([type="submit"])', + uiCheckbox : '.ui.checkbox', + uiDropdown : '.ui.dropdown' + }, + + className : { + error : 'error', + label : 'ui prompt label', + pressed : 'down', + success : 'success' + }, + + error: { + identifier : 'You must specify a string identifier for each field', + method : 'The method you called is not defined.', + noRule : 'There is no rule matching the one you specified', + oldSyntax : 'Starting in 2.0 forms now only take a single settings object. Validation settings converted to new syntax automatically.' + }, + + templates: { + + // template that produces error message + error: function(errors) { + var + html = '<ul class="list">' + ; + $.each(errors, function(index, value) { + html += '<li>' + value + '</li>'; + }); + html += '</ul>'; + return $(html); + }, + + // template that produces label + prompt: function(errors) { + return $('<div/>') + .addClass('ui basic red pointing prompt label') + .html(errors[0]) + ; + } + }, + + rules: { + + // is not empty or blank string + empty: function(value) { + return !(value === undefined || '' === value || $.isArray(value) && value.length === 0); + }, + + // checkbox checked + checked: function() { + return ($(this).filter(':checked').length > 0); + }, + + // is most likely an email + email: function(value){ + return $.fn.form.settings.regExp.email.test(value); + }, + + // value is most likely url + url: function(value) { + return $.fn.form.settings.regExp.url.test(value); + }, + + // matches specified regExp + regExp: function(value, regExp) { + if(regExp instanceof RegExp) { + return value.match(regExp); + } + var + regExpParts = regExp.match($.fn.form.settings.regExp.flags), + flags + ; + // regular expression specified as /baz/gi (flags) + if(regExpParts) { + regExp = (regExpParts.length >= 2) + ? regExpParts[1] + : regExp + ; + flags = (regExpParts.length >= 3) + ? regExpParts[2] + : '' + ; + } + return value.match( new RegExp(regExp, flags) ); + }, + + // is valid integer or matches range + integer: function(value, range) { + var + intRegExp = $.fn.form.settings.regExp.integer, + min, + max, + parts + ; + if( !range || ['', '..'].indexOf(range) !== -1) { + // do nothing + } + else if(range.indexOf('..') == -1) { + if(intRegExp.test(range)) { + min = max = range - 0; + } + } + else { + parts = range.split('..', 2); + if(intRegExp.test(parts[0])) { + min = parts[0] - 0; + } + if(intRegExp.test(parts[1])) { + max = parts[1] - 0; + } + } + return ( + intRegExp.test(value) && + (min === undefined || value >= min) && + (max === undefined || value <= max) + ); + }, + + // is valid number (with decimal) + decimal: function(value) { + return $.fn.form.settings.regExp.decimal.test(value); + }, + + // is valid number + number: function(value) { + return $.fn.form.settings.regExp.number.test(value); + }, + + // is value (case insensitive) + is: function(value, text) { + text = (typeof text == 'string') + ? text.toLowerCase() + : text + ; + value = (typeof value == 'string') + ? value.toLowerCase() + : value + ; + return (value == text); + }, + + // is value + isExactly: function(value, text) { + return (value == text); + }, + + // value is not another value (case insensitive) + not: function(value, notValue) { + value = (typeof value == 'string') + ? value.toLowerCase() + : value + ; + notValue = (typeof notValue == 'string') + ? notValue.toLowerCase() + : notValue + ; + return (value != notValue); + }, + + // value is not another value (case sensitive) + notExactly: function(value, notValue) { + return (value != notValue); + }, + + // value contains text (insensitive) + contains: function(value, text) { + // escape regex characters + text = text.replace($.fn.form.settings.regExp.escape, "\\$&"); + return (value.search( new RegExp(text, 'i') ) !== -1); + }, + + // value contains text (case sensitive) + containsExactly: function(value, text) { + // escape regex characters + text = text.replace($.fn.form.settings.regExp.escape, "\\$&"); + return (value.search( new RegExp(text) ) !== -1); + }, + + // value contains text (insensitive) + doesntContain: function(value, text) { + // escape regex characters + text = text.replace($.fn.form.settings.regExp.escape, "\\$&"); + return (value.search( new RegExp(text, 'i') ) === -1); + }, + + // value contains text (case sensitive) + doesntContainExactly: function(value, text) { + // escape regex characters + text = text.replace($.fn.form.settings.regExp.escape, "\\$&"); + return (value.search( new RegExp(text) ) === -1); + }, + + // is at least string length + minLength: function(value, requiredLength) { + return (value !== undefined) + ? (value.length >= requiredLength) + : false + ; + }, + + // see rls notes for 2.0.6 (this is a duplicate of minLength) + length: function(value, requiredLength) { + return (value !== undefined) + ? (value.length >= requiredLength) + : false + ; + }, + + // is exactly length + exactLength: function(value, requiredLength) { + return (value !== undefined) + ? (value.length == requiredLength) + : false + ; + }, + + // is less than length + maxLength: function(value, maxLength) { + return (value !== undefined) + ? (value.length <= maxLength) + : false + ; + }, + + // matches another field + match: function(value, identifier) { + var + $form = $(this), + matchingValue + ; + if( $('[data-validate="'+ identifier +'"]').length > 0 ) { + matchingValue = $('[data-validate="'+ identifier +'"]').val(); + } + else if($('#' + identifier).length > 0) { + matchingValue = $('#' + identifier).val(); + } + else if($('[name="' + identifier +'"]').length > 0) { + matchingValue = $('[name="' + identifier + '"]').val(); + } + else if( $('[name="' + identifier +'[]"]').length > 0 ) { + matchingValue = $('[name="' + identifier +'[]"]'); + } + return (matchingValue !== undefined) + ? ( value.toString() == matchingValue.toString() ) + : false + ; + }, + + // different than another field + different: function(value, identifier) { + // use either id or name of field + var + $form = $(this), + matchingValue + ; + if( $('[data-validate="'+ identifier +'"]').length > 0 ) { + matchingValue = $('[data-validate="'+ identifier +'"]').val(); + } + else if($('#' + identifier).length > 0) { + matchingValue = $('#' + identifier).val(); + } + else if($('[name="' + identifier +'"]').length > 0) { + matchingValue = $('[name="' + identifier + '"]').val(); + } + else if( $('[name="' + identifier +'[]"]').length > 0 ) { + matchingValue = $('[name="' + identifier +'[]"]'); + } + return (matchingValue !== undefined) + ? ( value.toString() !== matchingValue.toString() ) + : false + ; + }, + + creditCard: function(cardNumber, cardTypes) { + var + cards = { + visa: { + pattern : /^4/, + length : [16] + }, + amex: { + pattern : /^3[47]/, + length : [15] + }, + mastercard: { + pattern : /^5[1-5]/, + length : [16] + }, + discover: { + pattern : /^(6011|622(12[6-9]|1[3-9][0-9]|[2-8][0-9]{2}|9[0-1][0-9]|92[0-5]|64[4-9])|65)/, + length : [16] + }, + unionPay: { + pattern : /^(62|88)/, + length : [16, 17, 18, 19] + }, + jcb: { + pattern : /^35(2[89]|[3-8][0-9])/, + length : [16] + }, + maestro: { + pattern : /^(5018|5020|5038|6304|6759|676[1-3])/, + length : [12, 13, 14, 15, 16, 17, 18, 19] + }, + dinersClub: { + pattern : /^(30[0-5]|^36)/, + length : [14] + }, + laser: { + pattern : /^(6304|670[69]|6771)/, + length : [16, 17, 18, 19] + }, + visaElectron: { + pattern : /^(4026|417500|4508|4844|491(3|7))/, + length : [16] + } + }, + valid = {}, + validCard = false, + requiredTypes = (typeof cardTypes == 'string') + ? cardTypes.split(',') + : false, + unionPay, + validation + ; + + if(typeof cardNumber !== 'string' || cardNumber.length === 0) { + return; + } + + // verify card types + if(requiredTypes) { + $.each(requiredTypes, function(index, type){ + // verify each card type + validation = cards[type]; + if(validation) { + valid = { + length : ($.inArray(cardNumber.length, validation.length) !== -1), + pattern : (cardNumber.search(validation.pattern) !== -1) + }; + if(valid.length && valid.pattern) { + validCard = true; + } + } + }); + + if(!validCard) { + return false; + } + } + + // skip luhn for UnionPay + unionPay = { + number : ($.inArray(cardNumber.length, cards.unionPay.length) !== -1), + pattern : (cardNumber.search(cards.unionPay.pattern) !== -1) + }; + if(unionPay.number && unionPay.pattern) { + return true; + } + + // verify luhn, adapted from <https://gist.github.com/2134376> + var + length = cardNumber.length, + multiple = 0, + producedValue = [ + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + [0, 2, 4, 6, 8, 1, 3, 5, 7, 9] + ], + sum = 0 + ; + while (length--) { + sum += producedValue[multiple][parseInt(cardNumber.charAt(length), 10)]; + multiple ^= 1; + } + return (sum % 10 === 0 && sum > 0); + }, + + minCount: function(value, minCount) { + if(minCount == 0) { + return true; + } + if(minCount == 1) { + return (value !== ''); + } + return (value.split(',').length >= minCount); + }, + + exactCount: function(value, exactCount) { + if(exactCount == 0) { + return (value === ''); + } + if(exactCount == 1) { + return (value !== '' && value.search(',') === -1); + } + return (value.split(',').length == exactCount); + }, + + maxCount: function(value, maxCount) { + if(maxCount == 0) { + return false; + } + if(maxCount == 1) { + return (value.search(',') === -1); + } + return (value.split(',').length <= maxCount); + } + } + +}; + +})( jQuery, window, document ); + +/*! + * # Semantic UI 2.2.6 - Accordion + * http://github.com/semantic-org/semantic-ui/ + * + * + * Released under the MIT license + * http://opensource.org/licenses/MIT + * + */ + +;(function ($, window, document, undefined) { + +"use strict"; + +window = (typeof window != 'undefined' && window.Math == Math) + ? window + : (typeof self != 'undefined' && self.Math == Math) + ? self + : Function('return this')() +; + +$.fn.accordion = function(parameters) { + var + $allModules = $(this), + + time = new Date().getTime(), + performance = [], + + query = arguments[0], + methodInvoked = (typeof query == 'string'), + queryArguments = [].slice.call(arguments, 1), + + requestAnimationFrame = window.requestAnimationFrame + || window.mozRequestAnimationFrame + || window.webkitRequestAnimationFrame + || window.msRequestAnimationFrame + || function(callback) { setTimeout(callback, 0); }, + + returnedValue + ; + $allModules + .each(function() { + var + settings = ( $.isPlainObject(parameters) ) + ? $.extend(true, {}, $.fn.accordion.settings, parameters) + : $.extend({}, $.fn.accordion.settings), + + className = settings.className, + namespace = settings.namespace, + selector = settings.selector, + error = settings.error, + + eventNamespace = '.' + namespace, + moduleNamespace = 'module-' + namespace, + moduleSelector = $allModules.selector || '', + + $module = $(this), + $title = $module.find(selector.title), + $content = $module.find(selector.content), + + element = this, + instance = $module.data(moduleNamespace), + observer, + module + ; + + module = { + + initialize: function() { + module.debug('Initializing', $module); + module.bind.events(); + if(settings.observeChanges) { + module.observeChanges(); + } + module.instantiate(); + }, + + instantiate: function() { + instance = module; + $module + .data(moduleNamespace, module) + ; + }, + + destroy: function() { + module.debug('Destroying previous instance', $module); + $module + .off(eventNamespace) + .removeData(moduleNamespace) + ; + }, + + refresh: function() { + $title = $module.find(selector.title); + $content = $module.find(selector.content); + }, + + observeChanges: function() { + if('MutationObserver' in window) { + observer = new MutationObserver(function(mutations) { + module.debug('DOM tree modified, updating selector cache'); + module.refresh(); + }); + observer.observe(element, { + childList : true, + subtree : true + }); + module.debug('Setting up mutation observer', observer); + } + }, + + bind: { + events: function() { + module.debug('Binding delegated events'); + $module + .on(settings.on + eventNamespace, selector.trigger, module.event.click) + ; + } + }, + + event: { + click: function() { + module.toggle.call(this); + } + }, + + toggle: function(query) { + var + $activeTitle = (query !== undefined) + ? (typeof query === 'number') + ? $title.eq(query) + : $(query).closest(selector.title) + : $(this).closest(selector.title), + $activeContent = $activeTitle.next($content), + isAnimating = $activeContent.hasClass(className.animating), + isActive = $activeContent.hasClass(className.active), + isOpen = (isActive && !isAnimating), + isOpening = (!isActive && isAnimating) + ; + module.debug('Toggling visibility of content', $activeTitle); + if(isOpen || isOpening) { + if(settings.collapsible) { + module.close.call($activeTitle); + } + else { + module.debug('Cannot close accordion content collapsing is disabled'); + } + } + else { + module.open.call($activeTitle); + } + }, + + open: function(query) { + var + $activeTitle = (query !== undefined) + ? (typeof query === 'number') + ? $title.eq(query) + : $(query).closest(selector.title) + : $(this).closest(selector.title), + $activeContent = $activeTitle.next($content), + isAnimating = $activeContent.hasClass(className.animating), + isActive = $activeContent.hasClass(className.active), + isOpen = (isActive || isAnimating) + ; + if(isOpen) { + module.debug('Accordion already open, skipping', $activeContent); + return; + } + module.debug('Opening accordion content', $activeTitle); + settings.onOpening.call($activeContent); + if(settings.exclusive) { + module.closeOthers.call($activeTitle); + } + $activeTitle + .addClass(className.active) + ; + $activeContent + .stop(true, true) + .addClass(className.animating) + ; + if(settings.animateChildren) { + if($.fn.transition !== undefined && $module.transition('is supported')) { + $activeContent + .children() + .transition({ + animation : 'fade in', + queue : false, + useFailSafe : true, + debug : settings.debug, + verbose : settings.verbose, + duration : settings.duration + }) + ; + } + else { + $activeContent + .children() + .stop(true, true) + .animate({ + opacity: 1 + }, settings.duration, module.resetOpacity) + ; + } + } + $activeContent + .slideDown(settings.duration, settings.easing, function() { + $activeContent + .removeClass(className.animating) + .addClass(className.active) + ; + module.reset.display.call(this); + settings.onOpen.call(this); + settings.onChange.call(this); + }) + ; + }, + + close: function(query) { + var + $activeTitle = (query !== undefined) + ? (typeof query === 'number') + ? $title.eq(query) + : $(query).closest(selector.title) + : $(this).closest(selector.title), + $activeContent = $activeTitle.next($content), + isAnimating = $activeContent.hasClass(className.animating), + isActive = $activeContent.hasClass(className.active), + isOpening = (!isActive && isAnimating), + isClosing = (isActive && isAnimating) + ; + if((isActive || isOpening) && !isClosing) { + module.debug('Closing accordion content', $activeContent); + settings.onClosing.call($activeContent); + $activeTitle + .removeClass(className.active) + ; + $activeContent + .stop(true, true) + .addClass(className.animating) + ; + if(settings.animateChildren) { + if($.fn.transition !== undefined && $module.transition('is supported')) { + $activeContent + .children() + .transition({ + animation : 'fade out', + queue : false, + useFailSafe : true, + debug : settings.debug, + verbose : settings.verbose, + duration : settings.duration + }) + ; + } + else { + $activeContent + .children() + .stop(true, true) + .animate({ + opacity: 0 + }, settings.duration, module.resetOpacity) + ; + } + } + $activeContent + .slideUp(settings.duration, settings.easing, function() { + $activeContent + .removeClass(className.animating) + .removeClass(className.active) + ; + module.reset.display.call(this); + settings.onClose.call(this); + settings.onChange.call(this); + }) + ; + } + }, + + closeOthers: function(index) { + var + $activeTitle = (index !== undefined) + ? $title.eq(index) + : $(this).closest(selector.title), + $parentTitles = $activeTitle.parents(selector.content).prev(selector.title), + $activeAccordion = $activeTitle.closest(selector.accordion), + activeSelector = selector.title + '.' + className.active + ':visible', + activeContent = selector.content + '.' + className.active + ':visible', + $openTitles, + $nestedTitles, + $openContents + ; + if(settings.closeNested) { + $openTitles = $activeAccordion.find(activeSelector).not($parentTitles); + $openContents = $openTitles.next($content); + } + else { + $openTitles = $activeAccordion.find(activeSelector).not($parentTitles); + $nestedTitles = $activeAccordion.find(activeContent).find(activeSelector).not($parentTitles); + $openTitles = $openTitles.not($nestedTitles); + $openContents = $openTitles.next($content); + } + if( ($openTitles.length > 0) ) { + module.debug('Exclusive enabled, closing other content', $openTitles); + $openTitles + .removeClass(className.active) + ; + $openContents + .removeClass(className.animating) + .stop(true, true) + ; + if(settings.animateChildren) { + if($.fn.transition !== undefined && $module.transition('is supported')) { + $openContents + .children() + .transition({ + animation : 'fade out', + useFailSafe : true, + debug : settings.debug, + verbose : settings.verbose, + duration : settings.duration + }) + ; + } + else { + $openContents + .children() + .stop(true, true) + .animate({ + opacity: 0 + }, settings.duration, module.resetOpacity) + ; + } + } + $openContents + .slideUp(settings.duration , settings.easing, function() { + $(this).removeClass(className.active); + module.reset.display.call(this); + }) + ; + } + }, + + reset: { + + display: function() { + module.verbose('Removing inline display from element', this); + $(this).css('display', ''); + if( $(this).attr('style') === '') { + $(this) + .attr('style', '') + .removeAttr('style') + ; + } + }, + + opacity: function() { + module.verbose('Removing inline opacity from element', this); + $(this).css('opacity', ''); + if( $(this).attr('style') === '') { + $(this) + .attr('style', '') + .removeAttr('style') + ; + } + }, + + }, + + setting: function(name, value) { + module.debug('Changing setting', name, value); + if( $.isPlainObject(name) ) { + $.extend(true, settings, name); + } + else if(value !== undefined) { + if($.isPlainObject(settings[name])) { + $.extend(true, settings[name], value); + } + else { + settings[name] = value; + } + } + else { + return settings[name]; + } + }, + internal: function(name, value) { + module.debug('Changing internal', name, value); + if(value !== undefined) { + if( $.isPlainObject(name) ) { + $.extend(true, module, name); + } + else { + module[name] = value; + } + } + else { + return module[name]; + } + }, + debug: function() { + if(!settings.silent && settings.debug) { + if(settings.performance) { + module.performance.log(arguments); + } + else { + module.debug = Function.prototype.bind.call(console.info, console, settings.name + ':'); + module.debug.apply(console, arguments); + } + } + }, + verbose: function() { + if(!settings.silent && settings.verbose && settings.debug) { + if(settings.performance) { + module.performance.log(arguments); + } + else { + module.verbose = Function.prototype.bind.call(console.info, console, settings.name + ':'); + module.verbose.apply(console, arguments); + } + } + }, + error: function() { + if(!settings.silent) { + module.error = Function.prototype.bind.call(console.error, console, settings.name + ':'); + module.error.apply(console, arguments); + } + }, + performance: { + log: function(message) { + var + currentTime, + executionTime, + previousTime + ; + if(settings.performance) { + currentTime = new Date().getTime(); + previousTime = time || currentTime; + executionTime = currentTime - previousTime; + time = currentTime; + performance.push({ + 'Name' : message[0], + 'Arguments' : [].slice.call(message, 1) || '', + 'Element' : element, + 'Execution Time' : executionTime + }); + } + clearTimeout(module.performance.timer); + module.performance.timer = setTimeout(module.performance.display, 500); + }, + display: function() { + var + title = settings.name + ':', + totalTime = 0 + ; + time = false; + clearTimeout(module.performance.timer); + $.each(performance, function(index, data) { + totalTime += data['Execution Time']; + }); + title += ' ' + totalTime + 'ms'; + if(moduleSelector) { + title += ' \'' + moduleSelector + '\''; + } + if( (console.group !== undefined || console.table !== undefined) && performance.length > 0) { + console.groupCollapsed(title); + if(console.table) { + console.table(performance); + } + else { + $.each(performance, function(index, data) { + console.log(data['Name'] + ': ' + data['Execution Time']+'ms'); + }); + } + console.groupEnd(); + } + performance = []; + } + }, + invoke: function(query, passedArguments, context) { + var + object = instance, + maxDepth, + found, + response + ; + passedArguments = passedArguments || queryArguments; + context = element || context; + if(typeof query == 'string' && object !== undefined) { + query = query.split(/[\. ]/); + maxDepth = query.length - 1; + $.each(query, function(depth, value) { + var camelCaseValue = (depth != maxDepth) + ? value + query[depth + 1].charAt(0).toUpperCase() + query[depth + 1].slice(1) + : query + ; + if( $.isPlainObject( object[camelCaseValue] ) && (depth != maxDepth) ) { + object = object[camelCaseValue]; + } + else if( object[camelCaseValue] !== undefined ) { + found = object[camelCaseValue]; + return false; + } + else if( $.isPlainObject( object[value] ) && (depth != maxDepth) ) { + object = object[value]; + } + else if( object[value] !== undefined ) { + found = object[value]; + return false; + } + else { + module.error(error.method, query); + return false; + } + }); + } + if ( $.isFunction( found ) ) { + response = found.apply(context, passedArguments); + } + else if(found !== undefined) { + response = found; + } + if($.isArray(returnedValue)) { + returnedValue.push(response); + } + else if(returnedValue !== undefined) { + returnedValue = [returnedValue, response]; + } + else if(response !== undefined) { + returnedValue = response; + } + return found; + } + }; + if(methodInvoked) { + if(instance === undefined) { + module.initialize(); + } + module.invoke(query); + } + else { + if(instance !== undefined) { + instance.invoke('destroy'); + } + module.initialize(); + } + }) + ; + return (returnedValue !== undefined) + ? returnedValue + : this + ; +}; + +$.fn.accordion.settings = { + + name : 'Accordion', + namespace : 'accordion', + + silent : false, + debug : false, + verbose : false, + performance : true, + + on : 'click', // event on title that opens accordion + + observeChanges : true, // whether accordion should automatically refresh on DOM insertion + + exclusive : true, // whether a single accordion content panel should be open at once + collapsible : true, // whether accordion content can be closed + closeNested : false, // whether nested content should be closed when a panel is closed + animateChildren : true, // whether children opacity should be animated + + duration : 350, // duration of animation + easing : 'easeOutQuad', // easing equation for animation + + + onOpening : function(){}, // callback before open animation + onOpen : function(){}, // callback after open animation + onClosing : function(){}, // callback before closing animation + onClose : function(){}, // callback after closing animation + onChange : function(){}, // callback after closing or opening animation + + error: { + method : 'The method you called is not defined' + }, + + className : { + active : 'active', + animating : 'animating' + }, + + selector : { + accordion : '.accordion', + title : '.title', + trigger : '.title', + content : '.content' + } + +}; + +// Adds easing +$.extend( $.easing, { + easeOutQuad: function (x, t, b, c, d) { + return -c *(t/=d)*(t-2) + b; + } +}); + +})( jQuery, window, document ); + + +/*! + * # Semantic UI 2.2.6 - Checkbox + * http://github.com/semantic-org/semantic-ui/ + * + * + * Released under the MIT license + * http://opensource.org/licenses/MIT + * + */ + +;(function ($, window, document, undefined) { + +"use strict"; + +window = (typeof window != 'undefined' && window.Math == Math) + ? window + : (typeof self != 'undefined' && self.Math == Math) + ? self + : Function('return this')() +; + +$.fn.checkbox = function(parameters) { + var + $allModules = $(this), + moduleSelector = $allModules.selector || '', + + time = new Date().getTime(), + performance = [], + + query = arguments[0], + methodInvoked = (typeof query == 'string'), + queryArguments = [].slice.call(arguments, 1), + returnedValue + ; + + $allModules + .each(function() { + var + settings = $.extend(true, {}, $.fn.checkbox.settings, parameters), + + className = settings.className, + namespace = settings.namespace, + selector = settings.selector, + error = settings.error, + + eventNamespace = '.' + namespace, + moduleNamespace = 'module-' + namespace, + + $module = $(this), + $label = $(this).children(selector.label), + $input = $(this).children(selector.input), + input = $input[0], + + initialLoad = false, + shortcutPressed = false, + instance = $module.data(moduleNamespace), + + observer, + element = this, + module + ; + + module = { + + initialize: function() { + module.verbose('Initializing checkbox', settings); + + module.create.label(); + module.bind.events(); + + module.set.tabbable(); + module.hide.input(); + + module.observeChanges(); + module.instantiate(); + module.setup(); + }, + + instantiate: function() { + module.verbose('Storing instance of module', module); + instance = module; + $module + .data(moduleNamespace, module) + ; + }, + + destroy: function() { + module.verbose('Destroying module'); + module.unbind.events(); + module.show.input(); + $module.removeData(moduleNamespace); + }, + + fix: { + reference: function() { + if( $module.is(selector.input) ) { + module.debug('Behavior called on <input> adjusting invoked element'); + $module = $module.closest(selector.checkbox); + module.refresh(); + } + } + }, + + setup: function() { + module.set.initialLoad(); + if( module.is.indeterminate() ) { + module.debug('Initial value is indeterminate'); + module.indeterminate(); + } + else if( module.is.checked() ) { + module.debug('Initial value is checked'); + module.check(); + } + else { + module.debug('Initial value is unchecked'); + module.uncheck(); + } + module.remove.initialLoad(); + }, + + refresh: function() { + $label = $module.children(selector.label); + $input = $module.children(selector.input); + input = $input[0]; + }, + + hide: { + input: function() { + module.verbose('Modifying <input> z-index to be unselectable'); + $input.addClass(className.hidden); + } + }, + show: { + input: function() { + module.verbose('Modifying <input> z-index to be selectable'); + $input.removeClass(className.hidden); + } + }, + + observeChanges: function() { + if('MutationObserver' in window) { + observer = new MutationObserver(function(mutations) { + module.debug('DOM tree modified, updating selector cache'); + module.refresh(); + }); + observer.observe(element, { + childList : true, + subtree : true + }); + module.debug('Setting up mutation observer', observer); + } + }, + + attachEvents: function(selector, event) { + var + $element = $(selector) + ; + event = $.isFunction(module[event]) + ? module[event] + : module.toggle + ; + if($element.length > 0) { + module.debug('Attaching checkbox events to element', selector, event); + $element + .on('click' + eventNamespace, event) + ; + } + else { + module.error(error.notFound); + } + }, + + event: { + click: function(event) { + var + $target = $(event.target) + ; + if( $target.is(selector.input) ) { + module.verbose('Using default check action on initialized checkbox'); + return; + } + if( $target.is(selector.link) ) { + module.debug('Clicking link inside checkbox, skipping toggle'); + return; + } + module.toggle(); + $input.focus(); + event.preventDefault(); + }, + keydown: function(event) { + var + key = event.which, + keyCode = { + enter : 13, + space : 32, + escape : 27 + } + ; + if(key == keyCode.escape) { + module.verbose('Escape key pressed blurring field'); + $input.blur(); + shortcutPressed = true; + } + else if(!event.ctrlKey && ( key == keyCode.space || key == keyCode.enter) ) { + module.verbose('Enter/space key pressed, toggling checkbox'); + module.toggle(); + shortcutPressed = true; + } + else { + shortcutPressed = false; + } + }, + keyup: function(event) { + if(shortcutPressed) { + event.preventDefault(); + } + } + }, + + check: function() { + if( !module.should.allowCheck() ) { + return; + } + module.debug('Checking checkbox', $input); + module.set.checked(); + if( !module.should.ignoreCallbacks() ) { + settings.onChecked.call(input); + settings.onChange.call(input); + } + }, + + uncheck: function() { + if( !module.should.allowUncheck() ) { + return; + } + module.debug('Unchecking checkbox'); + module.set.unchecked(); + if( !module.should.ignoreCallbacks() ) { + settings.onUnchecked.call(input); + settings.onChange.call(input); + } + }, + + indeterminate: function() { + if( module.should.allowIndeterminate() ) { + module.debug('Checkbox is already indeterminate'); + return; + } + module.debug('Making checkbox indeterminate'); + module.set.indeterminate(); + if( !module.should.ignoreCallbacks() ) { + settings.onIndeterminate.call(input); + settings.onChange.call(input); + } + }, + + determinate: function() { + if( module.should.allowDeterminate() ) { + module.debug('Checkbox is already determinate'); + return; + } + module.debug('Making checkbox determinate'); + module.set.determinate(); + if( !module.should.ignoreCallbacks() ) { + settings.onDeterminate.call(input); + settings.onChange.call(input); + } + }, + + enable: function() { + if( module.is.enabled() ) { + module.debug('Checkbox is already enabled'); + return; + } + module.debug('Enabling checkbox'); + module.set.enabled(); + settings.onEnable.call(input); + // preserve legacy callbacks + settings.onEnabled.call(input); + }, + + disable: function() { + if( module.is.disabled() ) { + module.debug('Checkbox is already disabled'); + return; + } + module.debug('Disabling checkbox'); + module.set.disabled(); + settings.onDisable.call(input); + // preserve legacy callbacks + settings.onDisabled.call(input); + }, + + get: { + radios: function() { + var + name = module.get.name() + ; + return $('input[name="' + name + '"]').closest(selector.checkbox); + }, + otherRadios: function() { + return module.get.radios().not($module); + }, + name: function() { + return $input.attr('name'); + } + }, + + is: { + initialLoad: function() { + return initialLoad; + }, + radio: function() { + return ($input.hasClass(className.radio) || $input.attr('type') == 'radio'); + }, + indeterminate: function() { + return $input.prop('indeterminate') !== undefined && $input.prop('indeterminate'); + }, + checked: function() { + return $input.prop('checked') !== undefined && $input.prop('checked'); + }, + disabled: function() { + return $input.prop('disabled') !== undefined && $input.prop('disabled'); + }, + enabled: function() { + return !module.is.disabled(); + }, + determinate: function() { + return !module.is.indeterminate(); + }, + unchecked: function() { + return !module.is.checked(); + } + }, + + should: { + allowCheck: function() { + if(module.is.determinate() && module.is.checked() && !module.should.forceCallbacks() ) { + module.debug('Should not allow check, checkbox is already checked'); + return false; + } + if(settings.beforeChecked.apply(input) === false) { + module.debug('Should not allow check, beforeChecked cancelled'); + return false; + } + return true; + }, + allowUncheck: function() { + if(module.is.determinate() && module.is.unchecked() && !module.should.forceCallbacks() ) { + module.debug('Should not allow uncheck, checkbox is already unchecked'); + return false; + } + if(settings.beforeUnchecked.apply(input) === false) { + module.debug('Should not allow uncheck, beforeUnchecked cancelled'); + return false; + } + return true; + }, + allowIndeterminate: function() { + if(module.is.indeterminate() && !module.should.forceCallbacks() ) { + module.debug('Should not allow indeterminate, checkbox is already indeterminate'); + return false; + } + if(settings.beforeIndeterminate.apply(input) === false) { + module.debug('Should not allow indeterminate, beforeIndeterminate cancelled'); + return false; + } + return true; + }, + allowDeterminate: function() { + if(module.is.determinate() && !module.should.forceCallbacks() ) { + module.debug('Should not allow determinate, checkbox is already determinate'); + return false; + } + if(settings.beforeDeterminate.apply(input) === false) { + module.debug('Should not allow determinate, beforeDeterminate cancelled'); + return false; + } + return true; + }, + forceCallbacks: function() { + return (module.is.initialLoad() && settings.fireOnInit); + }, + ignoreCallbacks: function() { + return (initialLoad && !settings.fireOnInit); + } + }, + + can: { + change: function() { + return !( $module.hasClass(className.disabled) || $module.hasClass(className.readOnly) || $input.prop('disabled') || $input.prop('readonly') ); + }, + uncheck: function() { + return (typeof settings.uncheckable === 'boolean') + ? settings.uncheckable + : !module.is.radio() + ; + } + }, + + set: { + initialLoad: function() { + initialLoad = true; + }, + checked: function() { + module.verbose('Setting class to checked'); + $module + .removeClass(className.indeterminate) + .addClass(className.checked) + ; + if( module.is.radio() ) { + module.uncheckOthers(); + } + if(!module.is.indeterminate() && module.is.checked()) { + module.debug('Input is already checked, skipping input property change'); + return; + } + module.verbose('Setting state to checked', input); + $input + .prop('indeterminate', false) + .prop('checked', true) + ; + module.trigger.change(); + }, + unchecked: function() { + module.verbose('Removing checked class'); + $module + .removeClass(className.indeterminate) + .removeClass(className.checked) + ; + if(!module.is.indeterminate() && module.is.unchecked() ) { + module.debug('Input is already unchecked'); + return; + } + module.debug('Setting state to unchecked'); + $input + .prop('indeterminate', false) + .prop('checked', false) + ; + module.trigger.change(); + }, + indeterminate: function() { + module.verbose('Setting class to indeterminate'); + $module + .addClass(className.indeterminate) + ; + if( module.is.indeterminate() ) { + module.debug('Input is already indeterminate, skipping input property change'); + return; + } + module.debug('Setting state to indeterminate'); + $input + .prop('indeterminate', true) + ; + module.trigger.change(); + }, + determinate: function() { + module.verbose('Removing indeterminate class'); + $module + .removeClass(className.indeterminate) + ; + if( module.is.determinate() ) { + module.debug('Input is already determinate, skipping input property change'); + return; + } + module.debug('Setting state to determinate'); + $input + .prop('indeterminate', false) + ; + }, + disabled: function() { + module.verbose('Setting class to disabled'); + $module + .addClass(className.disabled) + ; + if( module.is.disabled() ) { + module.debug('Input is already disabled, skipping input property change'); + return; + } + module.debug('Setting state to disabled'); + $input + .prop('disabled', 'disabled') + ; + module.trigger.change(); + }, + enabled: function() { + module.verbose('Removing disabled class'); + $module.removeClass(className.disabled); + if( module.is.enabled() ) { + module.debug('Input is already enabled, skipping input property change'); + return; + } + module.debug('Setting state to enabled'); + $input + .prop('disabled', false) + ; + module.trigger.change(); + }, + tabbable: function() { + module.verbose('Adding tabindex to checkbox'); + if( $input.attr('tabindex') === undefined) { + $input.attr('tabindex', 0); + } + } + }, + + remove: { + initialLoad: function() { + initialLoad = false; + } + }, + + trigger: { + change: function() { + var + events = document.createEvent('HTMLEvents'), + inputElement = $input[0] + ; + if(inputElement) { + module.verbose('Triggering native change event'); + events.initEvent('change', true, false); + inputElement.dispatchEvent(events); + } + } + }, + + + create: { + label: function() { + if($input.prevAll(selector.label).length > 0) { + $input.prev(selector.label).detach().insertAfter($input); + module.debug('Moving existing label', $label); + } + else if( !module.has.label() ) { + $label = $('<label>').insertAfter($input); + module.debug('Creating label', $label); + } + } + }, + + has: { + label: function() { + return ($label.length > 0); + } + }, + + bind: { + events: function() { + module.verbose('Attaching checkbox events'); + $module + .on('click' + eventNamespace, module.event.click) + .on('keydown' + eventNamespace, selector.input, module.event.keydown) + .on('keyup' + eventNamespace, selector.input, module.event.keyup) + ; + } + }, + + unbind: { + events: function() { + module.debug('Removing events'); + $module + .off(eventNamespace) + ; + } + }, + + uncheckOthers: function() { + var + $radios = module.get.otherRadios() + ; + module.debug('Unchecking other radios', $radios); + $radios.removeClass(className.checked); + }, + + toggle: function() { + if( !module.can.change() ) { + if(!module.is.radio()) { + module.debug('Checkbox is read-only or disabled, ignoring toggle'); + } + return; + } + if( module.is.indeterminate() || module.is.unchecked() ) { + module.debug('Currently unchecked'); + module.check(); + } + else if( module.is.checked() && module.can.uncheck() ) { + module.debug('Currently checked'); + module.uncheck(); + } + }, + setting: function(name, value) { + module.debug('Changing setting', name, value); + if( $.isPlainObject(name) ) { + $.extend(true, settings, name); + } + else if(value !== undefined) { + if($.isPlainObject(settings[name])) { + $.extend(true, settings[name], value); + } + else { + settings[name] = value; + } + } + else { + return settings[name]; + } + }, + internal: function(name, value) { + if( $.isPlainObject(name) ) { + $.extend(true, module, name); + } + else if(value !== undefined) { + module[name] = value; + } + else { + return module[name]; + } + }, + debug: function() { + if(!settings.silent && settings.debug) { + if(settings.performance) { + module.performance.log(arguments); + } + else { + module.debug = Function.prototype.bind.call(console.info, console, settings.name + ':'); + module.debug.apply(console, arguments); + } + } + }, + verbose: function() { + if(!settings.silent && settings.verbose && settings.debug) { + if(settings.performance) { + module.performance.log(arguments); + } + else { + module.verbose = Function.prototype.bind.call(console.info, console, settings.name + ':'); + module.verbose.apply(console, arguments); + } + } + }, + error: function() { + if(!settings.silent) { + module.error = Function.prototype.bind.call(console.error, console, settings.name + ':'); + module.error.apply(console, arguments); + } + }, + performance: { + log: function(message) { + var + currentTime, + executionTime, + previousTime + ; + if(settings.performance) { + currentTime = new Date().getTime(); + previousTime = time || currentTime; + executionTime = currentTime - previousTime; + time = currentTime; + performance.push({ + 'Name' : message[0], + 'Arguments' : [].slice.call(message, 1) || '', + 'Element' : element, + 'Execution Time' : executionTime + }); + } + clearTimeout(module.performance.timer); + module.performance.timer = setTimeout(module.performance.display, 500); + }, + display: function() { + var + title = settings.name + ':', + totalTime = 0 + ; + time = false; + clearTimeout(module.performance.timer); + $.each(performance, function(index, data) { + totalTime += data['Execution Time']; + }); + title += ' ' + totalTime + 'ms'; + if(moduleSelector) { + title += ' \'' + moduleSelector + '\''; + } + if( (console.group !== undefined || console.table !== undefined) && performance.length > 0) { + console.groupCollapsed(title); + if(console.table) { + console.table(performance); + } + else { + $.each(performance, function(index, data) { + console.log(data['Name'] + ': ' + data['Execution Time']+'ms'); + }); + } + console.groupEnd(); + } + performance = []; + } + }, + invoke: function(query, passedArguments, context) { + var + object = instance, + maxDepth, + found, + response + ; + passedArguments = passedArguments || queryArguments; + context = element || context; + if(typeof query == 'string' && object !== undefined) { + query = query.split(/[\. ]/); + maxDepth = query.length - 1; + $.each(query, function(depth, value) { + var camelCaseValue = (depth != maxDepth) + ? value + query[depth + 1].charAt(0).toUpperCase() + query[depth + 1].slice(1) + : query + ; + if( $.isPlainObject( object[camelCaseValue] ) && (depth != maxDepth) ) { + object = object[camelCaseValue]; + } + else if( object[camelCaseValue] !== undefined ) { + found = object[camelCaseValue]; + return false; + } + else if( $.isPlainObject( object[value] ) && (depth != maxDepth) ) { + object = object[value]; + } + else if( object[value] !== undefined ) { + found = object[value]; + return false; + } + else { + module.error(error.method, query); + return false; + } + }); + } + if ( $.isFunction( found ) ) { + response = found.apply(context, passedArguments); + } + else if(found !== undefined) { + response = found; + } + if($.isArray(returnedValue)) { + returnedValue.push(response); + } + else if(returnedValue !== undefined) { + returnedValue = [returnedValue, response]; + } + else if(response !== undefined) { + returnedValue = response; + } + return found; + } + }; + + if(methodInvoked) { + if(instance === undefined) { + module.initialize(); + } + module.invoke(query); + } + else { + if(instance !== undefined) { + instance.invoke('destroy'); + } + module.initialize(); + } + }) + ; + + return (returnedValue !== undefined) + ? returnedValue + : this + ; +}; + +$.fn.checkbox.settings = { + + name : 'Checkbox', + namespace : 'checkbox', + + silent : false, + debug : false, + verbose : true, + performance : true, + + // delegated event context + uncheckable : 'auto', + fireOnInit : false, + + onChange : function(){}, + + beforeChecked : function(){}, + beforeUnchecked : function(){}, + beforeDeterminate : function(){}, + beforeIndeterminate : function(){}, + + onChecked : function(){}, + onUnchecked : function(){}, + + onDeterminate : function() {}, + onIndeterminate : function() {}, + + onEnable : function(){}, + onDisable : function(){}, + + // preserve misspelled callbacks (will be removed in 3.0) + onEnabled : function(){}, + onDisabled : function(){}, + + className : { + checked : 'checked', + indeterminate : 'indeterminate', + disabled : 'disabled', + hidden : 'hidden', + radio : 'radio', + readOnly : 'read-only' + }, + + error : { + method : 'The method you called is not defined' + }, + + selector : { + checkbox : '.ui.checkbox', + label : 'label, .box', + input : 'input[type="checkbox"], input[type="radio"]', + link : 'a[href]' + } + +}; + +})( jQuery, window, document ); + +/*! + * # Semantic UI 2.2.6 - Dimmer + * http://github.com/semantic-org/semantic-ui/ + * + * + * Released under the MIT license + * http://opensource.org/licenses/MIT + * + */ + +;(function ($, window, document, undefined) { + +"use strict"; + +window = (typeof window != 'undefined' && window.Math == Math) + ? window + : (typeof self != 'undefined' && self.Math == Math) + ? self + : Function('return this')() +; + +$.fn.dimmer = function(parameters) { + var + $allModules = $(this), + + time = new Date().getTime(), + performance = [], + + query = arguments[0], + methodInvoked = (typeof query == 'string'), + queryArguments = [].slice.call(arguments, 1), + + returnedValue + ; + + $allModules + .each(function() { + var + settings = ( $.isPlainObject(parameters) ) + ? $.extend(true, {}, $.fn.dimmer.settings, parameters) + : $.extend({}, $.fn.dimmer.settings), + + selector = settings.selector, + namespace = settings.namespace, + className = settings.className, + error = settings.error, + + eventNamespace = '.' + namespace, + moduleNamespace = 'module-' + namespace, + moduleSelector = $allModules.selector || '', + + clickEvent = ('ontouchstart' in document.documentElement) + ? 'touchstart' + : 'click', + + $module = $(this), + $dimmer, + $dimmable, + + element = this, + instance = $module.data(moduleNamespace), + module + ; + + module = { + + preinitialize: function() { + if( module.is.dimmer() ) { + + $dimmable = $module.parent(); + $dimmer = $module; + } + else { + $dimmable = $module; + if( module.has.dimmer() ) { + if(settings.dimmerName) { + $dimmer = $dimmable.find(selector.dimmer).filter('.' + settings.dimmerName); + } + else { + $dimmer = $dimmable.find(selector.dimmer); + } + } + else { + $dimmer = module.create(); + } + module.set.variation(); + } + }, + + initialize: function() { + module.debug('Initializing dimmer', settings); + + module.bind.events(); + module.set.dimmable(); + module.instantiate(); + }, + + instantiate: function() { + module.verbose('Storing instance of module', module); + instance = module; + $module + .data(moduleNamespace, instance) + ; + }, + + destroy: function() { + module.verbose('Destroying previous module', $dimmer); + module.unbind.events(); + module.remove.variation(); + $dimmable + .off(eventNamespace) + ; + }, + + bind: { + events: function() { + if(settings.on == 'hover') { + $dimmable + .on('mouseenter' + eventNamespace, module.show) + .on('mouseleave' + eventNamespace, module.hide) + ; + } + else if(settings.on == 'click') { + $dimmable + .on(clickEvent + eventNamespace, module.toggle) + ; + } + if( module.is.page() ) { + module.debug('Setting as a page dimmer', $dimmable); + module.set.pageDimmer(); + } + + if( module.is.closable() ) { + module.verbose('Adding dimmer close event', $dimmer); + $dimmable + .on(clickEvent + eventNamespace, selector.dimmer, module.event.click) + ; + } + } + }, + + unbind: { + events: function() { + $module + .removeData(moduleNamespace) + ; + $dimmable + .off(eventNamespace) + ; + } + }, + + event: { + click: function(event) { + module.verbose('Determining if event occured on dimmer', event); + if( $dimmer.find(event.target).length === 0 || $(event.target).is(selector.content) ) { + module.hide(); + event.stopImmediatePropagation(); + } + } + }, + + addContent: function(element) { + var + $content = $(element) + ; + module.debug('Add content to dimmer', $content); + if($content.parent()[0] !== $dimmer[0]) { + $content.detach().appendTo($dimmer); + } + }, + + create: function() { + var + $element = $( settings.template.dimmer() ) + ; + if(settings.dimmerName) { + module.debug('Creating named dimmer', settings.dimmerName); + $element.addClass(settings.dimmerName); + } + $element + .appendTo($dimmable) + ; + return $element; + }, + + show: function(callback) { + callback = $.isFunction(callback) + ? callback + : function(){} + ; + module.debug('Showing dimmer', $dimmer, settings); + if( (!module.is.dimmed() || module.is.animating()) && module.is.enabled() ) { + module.animate.show(callback); + settings.onShow.call(element); + settings.onChange.call(element); + } + else { + module.debug('Dimmer is already shown or disabled'); + } + }, + + hide: function(callback) { + callback = $.isFunction(callback) + ? callback + : function(){} + ; + if( module.is.dimmed() || module.is.animating() ) { + module.debug('Hiding dimmer', $dimmer); + module.animate.hide(callback); + settings.onHide.call(element); + settings.onChange.call(element); + } + else { + module.debug('Dimmer is not visible'); + } + }, + + toggle: function() { + module.verbose('Toggling dimmer visibility', $dimmer); + if( !module.is.dimmed() ) { + module.show(); + } + else { + module.hide(); + } + }, + + animate: { + show: function(callback) { + callback = $.isFunction(callback) + ? callback + : function(){} + ; + if(settings.useCSS && $.fn.transition !== undefined && $dimmer.transition('is supported')) { + if(settings.opacity !== 'auto') { + module.set.opacity(); + } + $dimmer + .transition({ + animation : settings.transition + ' in', + queue : false, + duration : module.get.duration(), + useFailSafe : true, + onStart : function() { + module.set.dimmed(); + }, + onComplete : function() { + module.set.active(); + callback(); + } + }) + ; + } + else { + module.verbose('Showing dimmer animation with javascript'); + module.set.dimmed(); + if(settings.opacity == 'auto') { + settings.opacity = 0.8; + } + $dimmer + .stop() + .css({ + opacity : 0, + width : '100%', + height : '100%' + }) + .fadeTo(module.get.duration(), settings.opacity, function() { + $dimmer.removeAttr('style'); + module.set.active(); + callback(); + }) + ; + } + }, + hide: function(callback) { + callback = $.isFunction(callback) + ? callback + : function(){} + ; + if(settings.useCSS && $.fn.transition !== undefined && $dimmer.transition('is supported')) { + module.verbose('Hiding dimmer with css'); + $dimmer + .transition({ + animation : settings.transition + ' out', + queue : false, + duration : module.get.duration(), + useFailSafe : true, + onStart : function() { + module.remove.dimmed(); + }, + onComplete : function() { + module.remove.active(); + callback(); + } + }) + ; + } + else { + module.verbose('Hiding dimmer with javascript'); + module.remove.dimmed(); + $dimmer + .stop() + .fadeOut(module.get.duration(), function() { + module.remove.active(); + $dimmer.removeAttr('style'); + callback(); + }) + ; + } + } + }, + + get: { + dimmer: function() { + return $dimmer; + }, + duration: function() { + if(typeof settings.duration == 'object') { + if( module.is.active() ) { + return settings.duration.hide; + } + else { + return settings.duration.show; + } + } + return settings.duration; + } + }, + + has: { + dimmer: function() { + if(settings.dimmerName) { + return ($module.find(selector.dimmer).filter('.' + settings.dimmerName).length > 0); + } + else { + return ( $module.find(selector.dimmer).length > 0 ); + } + } + }, + + is: { + active: function() { + return $dimmer.hasClass(className.active); + }, + animating: function() { + return ( $dimmer.is(':animated') || $dimmer.hasClass(className.animating) ); + }, + closable: function() { + if(settings.closable == 'auto') { + if(settings.on == 'hover') { + return false; + } + return true; + } + return settings.closable; + }, + dimmer: function() { + return $module.hasClass(className.dimmer); + }, + dimmable: function() { + return $module.hasClass(className.dimmable); + }, + dimmed: function() { + return $dimmable.hasClass(className.dimmed); + }, + disabled: function() { + return $dimmable.hasClass(className.disabled); + }, + enabled: function() { + return !module.is.disabled(); + }, + page: function () { + return $dimmable.is('body'); + }, + pageDimmer: function() { + return $dimmer.hasClass(className.pageDimmer); + } + }, + + can: { + show: function() { + return !$dimmer.hasClass(className.disabled); + } + }, + + set: { + opacity: function(opacity) { + var + color = $dimmer.css('background-color'), + colorArray = color.split(','), + isRGB = (colorArray && colorArray.length == 3), + isRGBA = (colorArray && colorArray.length == 4) + ; + opacity = settings.opacity === 0 ? 0 : settings.opacity || opacity; + if(isRGB || isRGBA) { + colorArray[3] = opacity + ')'; + color = colorArray.join(','); + } + else { + color = 'rgba(0, 0, 0, ' + opacity + ')'; + } + module.debug('Setting opacity to', opacity); + $dimmer.css('background-color', color); + }, + active: function() { + $dimmer.addClass(className.active); + }, + dimmable: function() { + $dimmable.addClass(className.dimmable); + }, + dimmed: function() { + $dimmable.addClass(className.dimmed); + }, + pageDimmer: function() { + $dimmer.addClass(className.pageDimmer); + }, + disabled: function() { + $dimmer.addClass(className.disabled); + }, + variation: function(variation) { + variation = variation || settings.variation; + if(variation) { + $dimmer.addClass(variation); + } + } + }, + + remove: { + active: function() { + $dimmer + .removeClass(className.active) + ; + }, + dimmed: function() { + $dimmable.removeClass(className.dimmed); + }, + disabled: function() { + $dimmer.removeClass(className.disabled); + }, + variation: function(variation) { + variation = variation || settings.variation; + if(variation) { + $dimmer.removeClass(variation); + } + } + }, + + setting: function(name, value) { + module.debug('Changing setting', name, value); + if( $.isPlainObject(name) ) { + $.extend(true, settings, name); + } + else if(value !== undefined) { + if($.isPlainObject(settings[name])) { + $.extend(true, settings[name], value); + } + else { + settings[name] = value; + } + } + else { + return settings[name]; + } + }, + internal: function(name, value) { + if( $.isPlainObject(name) ) { + $.extend(true, module, name); + } + else if(value !== undefined) { + module[name] = value; + } + else { + return module[name]; + } + }, + debug: function() { + if(!settings.silent && settings.debug) { + if(settings.performance) { + module.performance.log(arguments); + } + else { + module.debug = Function.prototype.bind.call(console.info, console, settings.name + ':'); + module.debug.apply(console, arguments); + } + } + }, + verbose: function() { + if(!settings.silent && settings.verbose && settings.debug) { + if(settings.performance) { + module.performance.log(arguments); + } + else { + module.verbose = Function.prototype.bind.call(console.info, console, settings.name + ':'); + module.verbose.apply(console, arguments); + } + } + }, + error: function() { + if(!settings.silent) { + module.error = Function.prototype.bind.call(console.error, console, settings.name + ':'); + module.error.apply(console, arguments); + } + }, + performance: { + log: function(message) { + var + currentTime, + executionTime, + previousTime + ; + if(settings.performance) { + currentTime = new Date().getTime(); + previousTime = time || currentTime; + executionTime = currentTime - previousTime; + time = currentTime; + performance.push({ + 'Name' : message[0], + 'Arguments' : [].slice.call(message, 1) || '', + 'Element' : element, + 'Execution Time' : executionTime + }); + } + clearTimeout(module.performance.timer); + module.performance.timer = setTimeout(module.performance.display, 500); + }, + display: function() { + var + title = settings.name + ':', + totalTime = 0 + ; + time = false; + clearTimeout(module.performance.timer); + $.each(performance, function(index, data) { + totalTime += data['Execution Time']; + }); + title += ' ' + totalTime + 'ms'; + if(moduleSelector) { + title += ' \'' + moduleSelector + '\''; + } + if($allModules.length > 1) { + title += ' ' + '(' + $allModules.length + ')'; + } + if( (console.group !== undefined || console.table !== undefined) && performance.length > 0) { + console.groupCollapsed(title); + if(console.table) { + console.table(performance); + } + else { + $.each(performance, function(index, data) { + console.log(data['Name'] + ': ' + data['Execution Time']+'ms'); + }); + } + console.groupEnd(); + } + performance = []; + } + }, + invoke: function(query, passedArguments, context) { + var + object = instance, + maxDepth, + found, + response + ; + passedArguments = passedArguments || queryArguments; + context = element || context; + if(typeof query == 'string' && object !== undefined) { + query = query.split(/[\. ]/); + maxDepth = query.length - 1; + $.each(query, function(depth, value) { + var camelCaseValue = (depth != maxDepth) + ? value + query[depth + 1].charAt(0).toUpperCase() + query[depth + 1].slice(1) + : query + ; + if( $.isPlainObject( object[camelCaseValue] ) && (depth != maxDepth) ) { + object = object[camelCaseValue]; + } + else if( object[camelCaseValue] !== undefined ) { + found = object[camelCaseValue]; + return false; + } + else if( $.isPlainObject( object[value] ) && (depth != maxDepth) ) { + object = object[value]; + } + else if( object[value] !== undefined ) { + found = object[value]; + return false; + } + else { + module.error(error.method, query); + return false; + } + }); + } + if ( $.isFunction( found ) ) { + response = found.apply(context, passedArguments); + } + else if(found !== undefined) { + response = found; + } + if($.isArray(returnedValue)) { + returnedValue.push(response); + } + else if(returnedValue !== undefined) { + returnedValue = [returnedValue, response]; + } + else if(response !== undefined) { + returnedValue = response; + } + return found; + } + }; + + module.preinitialize(); + + if(methodInvoked) { + if(instance === undefined) { + module.initialize(); + } + module.invoke(query); + } + else { + if(instance !== undefined) { + instance.invoke('destroy'); + } + module.initialize(); + } + }) + ; + + return (returnedValue !== undefined) + ? returnedValue + : this + ; +}; + +$.fn.dimmer.settings = { + + name : 'Dimmer', + namespace : 'dimmer', + + silent : false, + debug : false, + verbose : false, + performance : true, + + // name to distinguish between multiple dimmers in context + dimmerName : false, + + // whether to add a variation type + variation : false, + + // whether to bind close events + closable : 'auto', + + // whether to use css animations + useCSS : true, + + // css animation to use + transition : 'fade', + + // event to bind to + on : false, + + // overriding opacity value + opacity : 'auto', + + // transition durations + duration : { + show : 500, + hide : 500 + }, + + onChange : function(){}, + onShow : function(){}, + onHide : function(){}, + + error : { + method : 'The method you called is not defined.' + }, + + className : { + active : 'active', + animating : 'animating', + dimmable : 'dimmable', + dimmed : 'dimmed', + dimmer : 'dimmer', + disabled : 'disabled', + hide : 'hide', + pageDimmer : 'page', + show : 'show' + }, + + selector: { + dimmer : '> .ui.dimmer', + content : '.ui.dimmer > .content, .ui.dimmer > .content > .center' + }, + + template: { + dimmer: function() { + return $('<div />').attr('class', 'ui dimmer'); + } + } + +}; + +})( jQuery, window, document ); + +/*! + * # Semantic UI 2.2.6 - Dropdown + * http://github.com/semantic-org/semantic-ui/ + * + * + * Released under the MIT license + * http://opensource.org/licenses/MIT + * + */ + +;(function ($, window, document, undefined) { + +"use strict"; + +window = (typeof window != 'undefined' && window.Math == Math) + ? window + : (typeof self != 'undefined' && self.Math == Math) + ? self + : Function('return this')() +; + +$.fn.dropdown = function(parameters) { + var + $allModules = $(this), + $document = $(document), + + moduleSelector = $allModules.selector || '', + + hasTouch = ('ontouchstart' in document.documentElement), + time = new Date().getTime(), + performance = [], + + query = arguments[0], + methodInvoked = (typeof query == 'string'), + queryArguments = [].slice.call(arguments, 1), + returnedValue + ; + + $allModules + .each(function(elementIndex) { + var + settings = ( $.isPlainObject(parameters) ) + ? $.extend(true, {}, $.fn.dropdown.settings, parameters) + : $.extend({}, $.fn.dropdown.settings), + + className = settings.className, + message = settings.message, + fields = settings.fields, + keys = settings.keys, + metadata = settings.metadata, + namespace = settings.namespace, + regExp = settings.regExp, + selector = settings.selector, + error = settings.error, + templates = settings.templates, + + eventNamespace = '.' + namespace, + moduleNamespace = 'module-' + namespace, + + $module = $(this), + $context = $(settings.context), + $text = $module.find(selector.text), + $search = $module.find(selector.search), + $sizer = $module.find(selector.sizer), + $input = $module.find(selector.input), + $icon = $module.find(selector.icon), + + $combo = ($module.prev().find(selector.text).length > 0) + ? $module.prev().find(selector.text) + : $module.prev(), + + $menu = $module.children(selector.menu), + $item = $menu.find(selector.item), + + activated = false, + itemActivated = false, + internalChange = false, + element = this, + instance = $module.data(moduleNamespace), + + initialLoad, + pageLostFocus, + willRefocus, + elementNamespace, + id, + selectObserver, + menuObserver, + module + ; + + module = { + + initialize: function() { + module.debug('Initializing dropdown', settings); + + if( module.is.alreadySetup() ) { + module.setup.reference(); + } + else { + module.setup.layout(); + module.refreshData(); + + module.save.defaults(); + module.restore.selected(); + + module.create.id(); + module.bind.events(); + + module.observeChanges(); + module.instantiate(); + } + + }, + + instantiate: function() { + module.verbose('Storing instance of dropdown', module); + instance = module; + $module + .data(moduleNamespace, module) + ; + }, + + destroy: function() { + module.verbose('Destroying previous dropdown', $module); + module.remove.tabbable(); + $module + .off(eventNamespace) + .removeData(moduleNamespace) + ; + $menu + .off(eventNamespace) + ; + $document + .off(elementNamespace) + ; + module.disconnect.menuObserver(); + module.disconnect.selectObserver(); + }, + + observeChanges: function() { + if('MutationObserver' in window) { + selectObserver = new MutationObserver(module.event.select.mutation); + menuObserver = new MutationObserver(module.event.menu.mutation); + module.debug('Setting up mutation observer', selectObserver, menuObserver); + module.observe.select(); + module.observe.menu(); + } + }, + + disconnect: { + menuObserver: function() { + if(menuObserver) { + menuObserver.disconnect(); + } + }, + selectObserver: function() { + if(selectObserver) { + selectObserver.disconnect(); + } + } + }, + observe: { + select: function() { + if(module.has.input()) { + selectObserver.observe($input[0], { + childList : true, + subtree : true + }); + } + }, + menu: function() { + if(module.has.menu()) { + menuObserver.observe($menu[0], { + childList : true, + subtree : true + }); + } + } + }, + + create: { + id: function() { + id = (Math.random().toString(16) + '000000000').substr(2, 8); + elementNamespace = '.' + id; + module.verbose('Creating unique id for element', id); + }, + userChoice: function(values) { + var + $userChoices, + $userChoice, + isUserValue, + html + ; + values = values || module.get.userValues(); + if(!values) { + return false; + } + values = $.isArray(values) + ? values + : [values] + ; + $.each(values, function(index, value) { + if(module.get.item(value) === false) { + html = settings.templates.addition( module.add.variables(message.addResult, value) ); + $userChoice = $('<div />') + .html(html) + .attr('data-' + metadata.value, value) + .attr('data-' + metadata.text, value) + .addClass(className.addition) + .addClass(className.item) + ; + if(settings.hideAdditions) { + $userChoice.addClass(className.hidden); + } + $userChoices = ($userChoices === undefined) + ? $userChoice + : $userChoices.add($userChoice) + ; + module.verbose('Creating user choices for value', value, $userChoice); + } + }); + return $userChoices; + }, + userLabels: function(value) { + var + userValues = module.get.userValues() + ; + if(userValues) { + module.debug('Adding user labels', userValues); + $.each(userValues, function(index, value) { + module.verbose('Adding custom user value'); + module.add.label(value, value); + }); + } + }, + menu: function() { + $menu = $('<div />') + .addClass(className.menu) + .appendTo($module) + ; + }, + sizer: function() { + $sizer = $('<span />') + .addClass(className.sizer) + .insertAfter($search) + ; + } + }, + + search: function(query) { + query = (query !== undefined) + ? query + : module.get.query() + ; + module.verbose('Searching for query', query); + if(module.has.minCharacters(query)) { + module.filter(query); + } + else { + module.hide(); + } + }, + + select: { + firstUnfiltered: function() { + module.verbose('Selecting first non-filtered element'); + module.remove.selectedItem(); + $item + .not(selector.unselectable) + .not(selector.addition + selector.hidden) + .eq(0) + .addClass(className.selected) + ; + }, + nextAvailable: function($selected) { + $selected = $selected.eq(0); + var + $nextAvailable = $selected.nextAll(selector.item).not(selector.unselectable).eq(0), + $prevAvailable = $selected.prevAll(selector.item).not(selector.unselectable).eq(0), + hasNext = ($nextAvailable.length > 0) + ; + if(hasNext) { + module.verbose('Moving selection to', $nextAvailable); + $nextAvailable.addClass(className.selected); + } + else { + module.verbose('Moving selection to', $prevAvailable); + $prevAvailable.addClass(className.selected); + } + } + }, + + setup: { + api: function() { + var + apiSettings = { + debug : settings.debug, + urlData : { + value : module.get.value(), + query : module.get.query() + }, + on : false + } + ; + module.verbose('First request, initializing API'); + $module + .api(apiSettings) + ; + }, + layout: function() { + if( $module.is('select') ) { + module.setup.select(); + module.setup.returnedObject(); + } + if( !module.has.menu() ) { + module.create.menu(); + } + if( module.is.search() && !module.has.search() ) { + module.verbose('Adding search input'); + $search = $('<input />') + .addClass(className.search) + .prop('autocomplete', 'off') + .insertBefore($text) + ; + } + if( module.is.multiple() && module.is.searchSelection() && !module.has.sizer()) { + module.create.sizer(); + } + if(settings.allowTab) { + module.set.tabbable(); + } + }, + select: function() { + var + selectValues = module.get.selectValues() + ; + module.debug('Dropdown initialized on a select', selectValues); + if( $module.is('select') ) { + $input = $module; + } + // see if select is placed correctly already + if($input.parent(selector.dropdown).length > 0) { + module.debug('UI dropdown already exists. Creating dropdown menu only'); + $module = $input.closest(selector.dropdown); + if( !module.has.menu() ) { + module.create.menu(); + } + $menu = $module.children(selector.menu); + module.setup.menu(selectValues); + } + else { + module.debug('Creating entire dropdown from select'); + $module = $('<div />') + .attr('class', $input.attr('class') ) + .addClass(className.selection) + .addClass(className.dropdown) + .html( templates.dropdown(selectValues) ) + .insertBefore($input) + ; + if($input.hasClass(className.multiple) && $input.prop('multiple') === false) { + module.error(error.missingMultiple); + $input.prop('multiple', true); + } + if($input.is('[multiple]')) { + module.set.multiple(); + } + if ($input.prop('disabled')) { + module.debug('Disabling dropdown'); + $module.addClass(className.disabled); + } + $input + .removeAttr('class') + .detach() + .prependTo($module) + ; + } + module.refresh(); + }, + menu: function(values) { + $menu.html( templates.menu(values, fields)); + $item = $menu.find(selector.item); + }, + reference: function() { + module.debug('Dropdown behavior was called on select, replacing with closest dropdown'); + // replace module reference + $module = $module.parent(selector.dropdown); + module.refresh(); + module.setup.returnedObject(); + // invoke method in context of current instance + if(methodInvoked) { + instance = module; + module.invoke(query); + } + }, + returnedObject: function() { + var + $firstModules = $allModules.slice(0, elementIndex), + $lastModules = $allModules.slice(elementIndex + 1) + ; + // adjust all modules to use correct reference + $allModules = $firstModules.add($module).add($lastModules); + } + }, + + refresh: function() { + module.refreshSelectors(); + module.refreshData(); + }, + + refreshItems: function() { + $item = $menu.find(selector.item); + }, + + refreshSelectors: function() { + module.verbose('Refreshing selector cache'); + $text = $module.find(selector.text); + $search = $module.find(selector.search); + $input = $module.find(selector.input); + $icon = $module.find(selector.icon); + $combo = ($module.prev().find(selector.text).length > 0) + ? $module.prev().find(selector.text) + : $module.prev() + ; + $menu = $module.children(selector.menu); + $item = $menu.find(selector.item); + }, + + refreshData: function() { + module.verbose('Refreshing cached metadata'); + $item + .removeData(metadata.text) + .removeData(metadata.value) + ; + }, + + clearData: function() { + module.verbose('Clearing metadata'); + $item + .removeData(metadata.text) + .removeData(metadata.value) + ; + $module + .removeData(metadata.defaultText) + .removeData(metadata.defaultValue) + .removeData(metadata.placeholderText) + ; + }, + + toggle: function() { + module.verbose('Toggling menu visibility'); + if( !module.is.active() ) { + module.show(); + } + else { + module.hide(); + } + }, + + show: function(callback) { + callback = $.isFunction(callback) + ? callback + : function(){} + ; + if( module.can.show() && !module.is.active() ) { + module.debug('Showing dropdown'); + if(module.has.message() && !(module.has.maxSelections() || module.has.allResultsFiltered()) ) { + module.remove.message(); + } + if(module.is.allFiltered()) { + return true; + } + if(settings.onShow.call(element) !== false) { + module.animate.show(function() { + if( module.can.click() ) { + module.bind.intent(); + } + if(module.has.menuSearch()) { + module.focusSearch(); + } + module.set.visible(); + callback.call(element); + }); + } + } + }, + + hide: function(callback) { + callback = $.isFunction(callback) + ? callback + : function(){} + ; + if( module.is.active() ) { + module.debug('Hiding dropdown'); + if(settings.onHide.call(element) !== false) { + module.animate.hide(function() { + module.remove.visible(); + callback.call(element); + }); + } + } + }, + + hideOthers: function() { + module.verbose('Finding other dropdowns to hide'); + $allModules + .not($module) + .has(selector.menu + '.' + className.visible) + .dropdown('hide') + ; + }, + + hideMenu: function() { + module.verbose('Hiding menu instantaneously'); + module.remove.active(); + module.remove.visible(); + $menu.transition('hide'); + }, + + hideSubMenus: function() { + var + $subMenus = $menu.children(selector.item).find(selector.menu) + ; + module.verbose('Hiding sub menus', $subMenus); + $subMenus.transition('hide'); + }, + + bind: { + events: function() { + if(hasTouch) { + module.bind.touchEvents(); + } + module.bind.keyboardEvents(); + module.bind.inputEvents(); + module.bind.mouseEvents(); + }, + touchEvents: function() { + module.debug('Touch device detected binding additional touch events'); + if( module.is.searchSelection() ) { + // do nothing special yet + } + else if( module.is.single() ) { + $module + .on('touchstart' + eventNamespace, module.event.test.toggle) + ; + } + $menu + .on('touchstart' + eventNamespace, selector.item, module.event.item.mouseenter) + ; + }, + keyboardEvents: function() { + module.verbose('Binding keyboard events'); + $module + .on('keydown' + eventNamespace, module.event.keydown) + ; + if( module.has.search() ) { + $module + .on(module.get.inputEvent() + eventNamespace, selector.search, module.event.input) + ; + } + if( module.is.multiple() ) { + $document + .on('keydown' + elementNamespace, module.event.document.keydown) + ; + } + }, + inputEvents: function() { + module.verbose('Binding input change events'); + $module + .on('change' + eventNamespace, selector.input, module.event.change) + ; + }, + mouseEvents: function() { + module.verbose('Binding mouse events'); + if(module.is.multiple()) { + $module + .on('click' + eventNamespace, selector.label, module.event.label.click) + .on('click' + eventNamespace, selector.remove, module.event.remove.click) + ; + } + if( module.is.searchSelection() ) { + $module + .on('mousedown' + eventNamespace, module.event.mousedown) + .on('mouseup' + eventNamespace, module.event.mouseup) + .on('mousedown' + eventNamespace, selector.menu, module.event.menu.mousedown) + .on('mouseup' + eventNamespace, selector.menu, module.event.menu.mouseup) + .on('click' + eventNamespace, selector.icon, module.event.icon.click) + .on('focus' + eventNamespace, selector.search, module.event.search.focus) + .on('click' + eventNamespace, selector.search, module.event.search.focus) + .on('blur' + eventNamespace, selector.search, module.event.search.blur) + .on('click' + eventNamespace, selector.text, module.event.text.focus) + ; + if(module.is.multiple()) { + $module + .on('click' + eventNamespace, module.event.click) + ; + } + } + else { + if(settings.on == 'click') { + $module + .on('click' + eventNamespace, selector.icon, module.event.icon.click) + .on('click' + eventNamespace, module.event.test.toggle) + ; + } + else if(settings.on == 'hover') { + $module + .on('mouseenter' + eventNamespace, module.delay.show) + .on('mouseleave' + eventNamespace, module.delay.hide) + ; + } + else { + $module + .on(settings.on + eventNamespace, module.toggle) + ; + } + $module + .on('mousedown' + eventNamespace, module.event.mousedown) + .on('mouseup' + eventNamespace, module.event.mouseup) + .on('focus' + eventNamespace, module.event.focus) + .on('blur' + eventNamespace, module.event.blur) + ; + } + $menu + .on('mouseenter' + eventNamespace, selector.item, module.event.item.mouseenter) + .on('mouseleave' + eventNamespace, selector.item, module.event.item.mouseleave) + .on('click' + eventNamespace, selector.item, module.event.item.click) + ; + }, + intent: function() { + module.verbose('Binding hide intent event to document'); + if(hasTouch) { + $document + .on('touchstart' + elementNamespace, module.event.test.touch) + .on('touchmove' + elementNamespace, module.event.test.touch) + ; + } + $document + .on('click' + elementNamespace, module.event.test.hide) + ; + } + }, + + unbind: { + intent: function() { + module.verbose('Removing hide intent event from document'); + if(hasTouch) { + $document + .off('touchstart' + elementNamespace) + .off('touchmove' + elementNamespace) + ; + } + $document + .off('click' + elementNamespace) + ; + } + }, + + filter: function(query) { + var + searchTerm = (query !== undefined) + ? query + : module.get.query(), + afterFiltered = function() { + if(module.is.multiple()) { + module.filterActive(); + } + module.select.firstUnfiltered(); + if( module.has.allResultsFiltered() ) { + if( settings.onNoResults.call(element, searchTerm) ) { + if(settings.allowAdditions) { + if(settings.hideAdditions) { + module.verbose('User addition with no menu, setting empty style'); + module.set.empty(); + module.hideMenu(); + } + } + else { + module.verbose('All items filtered, showing message', searchTerm); + module.add.message(message.noResults); + } + } + else { + module.verbose('All items filtered, hiding dropdown', searchTerm); + module.hideMenu(); + } + } + else { + module.remove.empty(); + module.remove.message(); + } + if(settings.allowAdditions) { + module.add.userSuggestion(query); + } + if(module.is.searchSelection() && module.can.show() && module.is.focusedOnSearch() ) { + module.show(); + } + } + ; + if(settings.useLabels && module.has.maxSelections()) { + return; + } + if(settings.apiSettings) { + if( module.can.useAPI() ) { + module.queryRemote(searchTerm, function() { + afterFiltered(); + }); + } + else { + module.error(error.noAPI); + } + } + else { + module.filterItems(searchTerm); + afterFiltered(); + } + }, + + queryRemote: function(query, callback) { + var + apiSettings = { + errorDuration : false, + cache : 'local', + throttle : settings.throttle, + urlData : { + query: query + }, + onError: function() { + module.add.message(message.serverError); + callback(); + }, + onFailure: function() { + module.add.message(message.serverError); + callback(); + }, + onSuccess : function(response) { + module.remove.message(); + module.setup.menu({ + values: response[fields.remoteValues] + }); + callback(); + } + } + ; + if( !$module.api('get request') ) { + module.setup.api(); + } + apiSettings = $.extend(true, {}, apiSettings, settings.apiSettings); + $module + .api('setting', apiSettings) + .api('query') + ; + }, + + filterItems: function(query) { + var + searchTerm = (query !== undefined) + ? query + : module.get.query(), + results = null, + escapedTerm = module.escape.regExp(searchTerm), + beginsWithRegExp = new RegExp('^' + escapedTerm, 'igm') + ; + // avoid loop if we're matching nothing + if( module.has.query() ) { + results = []; + + module.verbose('Searching for matching values', searchTerm); + $item + .each(function(){ + var + $choice = $(this), + text, + value + ; + if(settings.match == 'both' || settings.match == 'text') { + text = String(module.get.choiceText($choice, false)); + if(text.search(beginsWithRegExp) !== -1) { + results.push(this); + return true; + } + else if (settings.fullTextSearch === 'exact' && module.exactSearch(searchTerm, text)) { + results.push(this); + return true; + } + else if (settings.fullTextSearch === true && module.fuzzySearch(searchTerm, text)) { + results.push(this); + return true; + } + } + if(settings.match == 'both' || settings.match == 'value') { + value = String(module.get.choiceValue($choice, text)); + + if(value.search(beginsWithRegExp) !== -1) { + results.push(this); + return true; + } + else if(settings.fullTextSearch && module.fuzzySearch(searchTerm, value)) { + results.push(this); + return true; + } + } + }) + ; + } + module.debug('Showing only matched items', searchTerm); + module.remove.filteredItem(); + if(results) { + $item + .not(results) + .addClass(className.filtered) + ; + } + }, + + fuzzySearch: function(query, term) { + var + termLength = term.length, + queryLength = query.length + ; + query = query.toLowerCase(); + term = term.toLowerCase(); + if(queryLength > termLength) { + return false; + } + if(queryLength === termLength) { + return (query === term); + } + search: for (var characterIndex = 0, nextCharacterIndex = 0; characterIndex < queryLength; characterIndex++) { + var + queryCharacter = query.charCodeAt(characterIndex) + ; + while(nextCharacterIndex < termLength) { + if(term.charCodeAt(nextCharacterIndex++) === queryCharacter) { + continue search; + } + } + return false; + } + return true; + }, + exactSearch: function (query, term) { + query = query.toLowerCase(); + term = term.toLowerCase(); + if(term.indexOf(query) > -1) { + return true; + } + return false; + }, + filterActive: function() { + if(settings.useLabels) { + $item.filter('.' + className.active) + .addClass(className.filtered) + ; + } + }, + + focusSearch: function(skipHandler) { + if( module.has.search() && !module.is.focusedOnSearch() ) { + if(skipHandler) { + $module.off('focus' + eventNamespace, selector.search); + $search.focus(); + $module.on('focus' + eventNamespace, selector.search, module.event.search.focus); + } + else { + $search.focus(); + } + } + }, + + forceSelection: function() { + var + $currentlySelected = $item.not(className.filtered).filter('.' + className.selected).eq(0), + $activeItem = $item.not(className.filtered).filter('.' + className.active).eq(0), + $selectedItem = ($currentlySelected.length > 0) + ? $currentlySelected + : $activeItem, + hasSelected = ($selectedItem.length > 0) + ; + if(hasSelected) { + module.debug('Forcing partial selection to selected item', $selectedItem); + module.event.item.click.call($selectedItem, {}, true); + return; + } + else { + if(settings.allowAdditions) { + module.set.selected(module.get.query()); + module.remove.searchTerm(); + } + else { + module.remove.searchTerm(); + } + } + }, + + event: { + change: function() { + if(!internalChange) { + module.debug('Input changed, updating selection'); + module.set.selected(); + } + }, + focus: function() { + if(settings.showOnFocus && !activated && module.is.hidden() && !pageLostFocus) { + module.show(); + } + }, + blur: function(event) { + pageLostFocus = (document.activeElement === this); + if(!activated && !pageLostFocus) { + module.remove.activeLabel(); + module.hide(); + } + }, + mousedown: function() { + if(module.is.searchSelection()) { + // prevent menu hiding on immediate re-focus + willRefocus = true; + } + else { + // prevents focus callback from occurring on mousedown + activated = true; + } + }, + mouseup: function() { + if(module.is.searchSelection()) { + // prevent menu hiding on immediate re-focus + willRefocus = false; + } + else { + activated = false; + } + }, + click: function(event) { + var + $target = $(event.target) + ; + // focus search + if($target.is($module)) { + if(!module.is.focusedOnSearch()) { + module.focusSearch(); + } + else { + module.show(); + } + } + }, + search: { + focus: function() { + activated = true; + if(module.is.multiple()) { + module.remove.activeLabel(); + } + if(settings.showOnFocus) { + module.search(); + } + }, + blur: function(event) { + pageLostFocus = (document.activeElement === this); + if(!willRefocus) { + if(!itemActivated && !pageLostFocus) { + if(settings.forceSelection) { + module.forceSelection(); + } + module.hide(); + } + } + willRefocus = false; + } + }, + icon: { + click: function(event) { + module.toggle(); + } + }, + text: { + focus: function(event) { + activated = true; + module.focusSearch(); + } + }, + input: function(event) { + if(module.is.multiple() || module.is.searchSelection()) { + module.set.filtered(); + } + clearTimeout(module.timer); + module.timer = setTimeout(module.search, settings.delay.search); + }, + label: { + click: function(event) { + var + $label = $(this), + $labels = $module.find(selector.label), + $activeLabels = $labels.filter('.' + className.active), + $nextActive = $label.nextAll('.' + className.active), + $prevActive = $label.prevAll('.' + className.active), + $range = ($nextActive.length > 0) + ? $label.nextUntil($nextActive).add($activeLabels).add($label) + : $label.prevUntil($prevActive).add($activeLabels).add($label) + ; + if(event.shiftKey) { + $activeLabels.removeClass(className.active); + $range.addClass(className.active); + } + else if(event.ctrlKey) { + $label.toggleClass(className.active); + } + else { + $activeLabels.removeClass(className.active); + $label.addClass(className.active); + } + settings.onLabelSelect.apply(this, $labels.filter('.' + className.active)); + } + }, + remove: { + click: function() { + var + $label = $(this).parent() + ; + if( $label.hasClass(className.active) ) { + // remove all selected labels + module.remove.activeLabels(); + } + else { + // remove this label only + module.remove.activeLabels( $label ); + } + } + }, + test: { + toggle: function(event) { + var + toggleBehavior = (module.is.multiple()) + ? module.show + : module.toggle + ; + if(module.is.bubbledLabelClick(event) || module.is.bubbledIconClick(event)) { + return; + } + if( module.determine.eventOnElement(event, toggleBehavior) ) { + event.preventDefault(); + } + }, + touch: function(event) { + module.determine.eventOnElement(event, function() { + if(event.type == 'touchstart') { + module.timer = setTimeout(function() { + module.hide(); + }, settings.delay.touch); + } + else if(event.type == 'touchmove') { + clearTimeout(module.timer); + } + }); + event.stopPropagation(); + }, + hide: function(event) { + module.determine.eventInModule(event, module.hide); + } + }, + select: { + mutation: function(mutations) { + module.debug('<select> modified, recreating menu'); + module.setup.select(); + } + }, + menu: { + mutation: function(mutations) { + var + mutation = mutations[0], + $addedNode = mutation.addedNodes + ? $(mutation.addedNodes[0]) + : $(false), + $removedNode = mutation.removedNodes + ? $(mutation.removedNodes[0]) + : $(false), + $changedNodes = $addedNode.add($removedNode), + isUserAddition = $changedNodes.is(selector.addition) || $changedNodes.closest(selector.addition).length > 0, + isMessage = $changedNodes.is(selector.message) || $changedNodes.closest(selector.message).length > 0 + ; + if(isUserAddition || isMessage) { + module.debug('Updating item selector cache'); + module.refreshItems(); + } + else { + module.debug('Menu modified, updating selector cache'); + module.refresh(); + } + }, + mousedown: function() { + itemActivated = true; + }, + mouseup: function() { + itemActivated = false; + } + }, + item: { + mouseenter: function(event) { + var + $target = $(event.target), + $item = $(this), + $subMenu = $item.children(selector.menu), + $otherMenus = $item.siblings(selector.item).children(selector.menu), + hasSubMenu = ($subMenu.length > 0), + isBubbledEvent = ($subMenu.find($target).length > 0) + ; + if( !isBubbledEvent && hasSubMenu ) { + clearTimeout(module.itemTimer); + module.itemTimer = setTimeout(function() { + module.verbose('Showing sub-menu', $subMenu); + $.each($otherMenus, function() { + module.animate.hide(false, $(this)); + }); + module.animate.show(false, $subMenu); + }, settings.delay.show); + event.preventDefault(); + } + }, + mouseleave: function(event) { + var + $subMenu = $(this).children(selector.menu) + ; + if($subMenu.length > 0) { + clearTimeout(module.itemTimer); + module.itemTimer = setTimeout(function() { + module.verbose('Hiding sub-menu', $subMenu); + module.animate.hide(false, $subMenu); + }, settings.delay.hide); + } + }, + click: function (event, skipRefocus) { + var + $choice = $(this), + $target = (event) + ? $(event.target) + : $(''), + $subMenu = $choice.find(selector.menu), + text = module.get.choiceText($choice), + value = module.get.choiceValue($choice, text), + hasSubMenu = ($subMenu.length > 0), + isBubbledEvent = ($subMenu.find($target).length > 0) + ; + if(!isBubbledEvent && (!hasSubMenu || settings.allowCategorySelection)) { + if(module.is.searchSelection()) { + if(settings.allowAdditions) { + module.remove.userAddition(); + } + module.remove.searchTerm(); + if(!module.is.focusedOnSearch() && !(skipRefocus == true)) { + module.focusSearch(true); + } + } + if(!settings.useLabels) { + module.remove.filteredItem(); + module.set.scrollPosition($choice); + } + module.determine.selectAction.call(this, text, value); + } + } + }, + + document: { + // label selection should occur even when element has no focus + keydown: function(event) { + var + pressedKey = event.which, + isShortcutKey = module.is.inObject(pressedKey, keys) + ; + if(isShortcutKey) { + var + $label = $module.find(selector.label), + $activeLabel = $label.filter('.' + className.active), + activeValue = $activeLabel.data(metadata.value), + labelIndex = $label.index($activeLabel), + labelCount = $label.length, + hasActiveLabel = ($activeLabel.length > 0), + hasMultipleActive = ($activeLabel.length > 1), + isFirstLabel = (labelIndex === 0), + isLastLabel = (labelIndex + 1 == labelCount), + isSearch = module.is.searchSelection(), + isFocusedOnSearch = module.is.focusedOnSearch(), + isFocused = module.is.focused(), + caretAtStart = (isFocusedOnSearch && module.get.caretPosition() === 0), + $nextLabel + ; + if(isSearch && !hasActiveLabel && !isFocusedOnSearch) { + return; + } + + if(pressedKey == keys.leftArrow) { + // activate previous label + if((isFocused || caretAtStart) && !hasActiveLabel) { + module.verbose('Selecting previous label'); + $label.last().addClass(className.active); + } + else if(hasActiveLabel) { + if(!event.shiftKey) { + module.verbose('Selecting previous label'); + $label.removeClass(className.active); + } + else { + module.verbose('Adding previous label to selection'); + } + if(isFirstLabel && !hasMultipleActive) { + $activeLabel.addClass(className.active); + } + else { + $activeLabel.prev(selector.siblingLabel) + .addClass(className.active) + .end() + ; + } + event.preventDefault(); + } + } + else if(pressedKey == keys.rightArrow) { + // activate first label + if(isFocused && !hasActiveLabel) { + $label.first().addClass(className.active); + } + // activate next label + if(hasActiveLabel) { + if(!event.shiftKey) { + module.verbose('Selecting next label'); + $label.removeClass(className.active); + } + else { + module.verbose('Adding next label to selection'); + } + if(isLastLabel) { + if(isSearch) { + if(!isFocusedOnSearch) { + module.focusSearch(); + } + else { + $label.removeClass(className.active); + } + } + else if(hasMultipleActive) { + $activeLabel.next(selector.siblingLabel).addClass(className.active); + } + else { + $activeLabel.addClass(className.active); + } + } + else { + $activeLabel.next(selector.siblingLabel).addClass(className.active); + } + event.preventDefault(); + } + } + else if(pressedKey == keys.deleteKey || pressedKey == keys.backspace) { + if(hasActiveLabel) { + module.verbose('Removing active labels'); + if(isLastLabel) { + if(isSearch && !isFocusedOnSearch) { + module.focusSearch(); + } + } + $activeLabel.last().next(selector.siblingLabel).addClass(className.active); + module.remove.activeLabels($activeLabel); + event.preventDefault(); + } + else if(caretAtStart && !hasActiveLabel && pressedKey == keys.backspace) { + module.verbose('Removing last label on input backspace'); + $activeLabel = $label.last().addClass(className.active); + module.remove.activeLabels($activeLabel); + } + } + else { + $activeLabel.removeClass(className.active); + } + } + } + }, + + keydown: function(event) { + var + pressedKey = event.which, + isShortcutKey = module.is.inObject(pressedKey, keys) + ; + if(isShortcutKey) { + var + $currentlySelected = $item.not(selector.unselectable).filter('.' + className.selected).eq(0), + $activeItem = $menu.children('.' + className.active).eq(0), + $selectedItem = ($currentlySelected.length > 0) + ? $currentlySelected + : $activeItem, + $visibleItems = ($selectedItem.length > 0) + ? $selectedItem.siblings(':not(.' + className.filtered +')').addBack() + : $menu.children(':not(.' + className.filtered +')'), + $subMenu = $selectedItem.children(selector.menu), + $parentMenu = $selectedItem.closest(selector.menu), + inVisibleMenu = ($parentMenu.hasClass(className.visible) || $parentMenu.hasClass(className.animating) || $parentMenu.parent(selector.menu).length > 0), + hasSubMenu = ($subMenu.length> 0), + hasSelectedItem = ($selectedItem.length > 0), + selectedIsSelectable = ($selectedItem.not(selector.unselectable).length > 0), + delimiterPressed = (pressedKey == keys.delimiter && settings.allowAdditions && module.is.multiple()), + isAdditionWithoutMenu = (settings.allowAdditions && settings.hideAdditions && (pressedKey == keys.enter || delimiterPressed) && selectedIsSelectable), + $nextItem, + isSubMenuItem, + newIndex + ; + // allow selection with menu closed + if(isAdditionWithoutMenu) { + module.verbose('Selecting item from keyboard shortcut', $selectedItem); + module.event.item.click.call($selectedItem, event); + if(module.is.searchSelection()) { + module.remove.searchTerm(); + } + } + + // visible menu keyboard shortcuts + if( module.is.visible() ) { + + // enter (select or open sub-menu) + if(pressedKey == keys.enter || delimiterPressed) { + if(pressedKey == keys.enter && hasSelectedItem && hasSubMenu && !settings.allowCategorySelection) { + module.verbose('Pressed enter on unselectable category, opening sub menu'); + pressedKey = keys.rightArrow; + } + else if(selectedIsSelectable) { + module.verbose('Selecting item from keyboard shortcut', $selectedItem); + module.event.item.click.call($selectedItem, event); + if(module.is.searchSelection()) { + module.remove.searchTerm(); + } + } + event.preventDefault(); + } + + // sub-menu actions + if(hasSelectedItem) { + + if(pressedKey == keys.leftArrow) { + + isSubMenuItem = ($parentMenu[0] !== $menu[0]); + + if(isSubMenuItem) { + module.verbose('Left key pressed, closing sub-menu'); + module.animate.hide(false, $parentMenu); + $selectedItem + .removeClass(className.selected) + ; + $parentMenu + .closest(selector.item) + .addClass(className.selected) + ; + event.preventDefault(); + } + } + + // right arrow (show sub-menu) + if(pressedKey == keys.rightArrow) { + if(hasSubMenu) { + module.verbose('Right key pressed, opening sub-menu'); + module.animate.show(false, $subMenu); + $selectedItem + .removeClass(className.selected) + ; + $subMenu + .find(selector.item).eq(0) + .addClass(className.selected) + ; + event.preventDefault(); + } + } + } + + // up arrow (traverse menu up) + if(pressedKey == keys.upArrow) { + $nextItem = (hasSelectedItem && inVisibleMenu) + ? $selectedItem.prevAll(selector.item + ':not(' + selector.unselectable + ')').eq(0) + : $item.eq(0) + ; + if($visibleItems.index( $nextItem ) < 0) { + module.verbose('Up key pressed but reached top of current menu'); + event.preventDefault(); + return; + } + else { + module.verbose('Up key pressed, changing active item'); + $selectedItem + .removeClass(className.selected) + ; + $nextItem + .addClass(className.selected) + ; + module.set.scrollPosition($nextItem); + if(settings.selectOnKeydown && module.is.single()) { + module.set.selectedItem($nextItem); + } + } + event.preventDefault(); + } + + // down arrow (traverse menu down) + if(pressedKey == keys.downArrow) { + $nextItem = (hasSelectedItem && inVisibleMenu) + ? $nextItem = $selectedItem.nextAll(selector.item + ':not(' + selector.unselectable + ')').eq(0) + : $item.eq(0) + ; + if($nextItem.length === 0) { + module.verbose('Down key pressed but reached bottom of current menu'); + event.preventDefault(); + return; + } + else { + module.verbose('Down key pressed, changing active item'); + $item + .removeClass(className.selected) + ; + $nextItem + .addClass(className.selected) + ; + module.set.scrollPosition($nextItem); + if(settings.selectOnKeydown && module.is.single()) { + module.set.selectedItem($nextItem); + } + } + event.preventDefault(); + } + + // page down (show next page) + if(pressedKey == keys.pageUp) { + module.scrollPage('up'); + event.preventDefault(); + } + if(pressedKey == keys.pageDown) { + module.scrollPage('down'); + event.preventDefault(); + } + + // escape (close menu) + if(pressedKey == keys.escape) { + module.verbose('Escape key pressed, closing dropdown'); + module.hide(); + } + + } + else { + // delimiter key + if(delimiterPressed) { + event.preventDefault(); + } + // down arrow (open menu) + if(pressedKey == keys.downArrow && !module.is.visible()) { + module.verbose('Down key pressed, showing dropdown'); + module.select.firstUnfiltered(); + module.show(); + event.preventDefault(); + } + } + } + else { + if( !module.has.search() ) { + module.set.selectedLetter( String.fromCharCode(pressedKey) ); + } + } + } + }, + + trigger: { + change: function() { + var + events = document.createEvent('HTMLEvents'), + inputElement = $input[0] + ; + if(inputElement) { + module.verbose('Triggering native change event'); + events.initEvent('change', true, false); + inputElement.dispatchEvent(events); + } + } + }, + + determine: { + selectAction: function(text, value) { + module.verbose('Determining action', settings.action); + if( $.isFunction( module.action[settings.action] ) ) { + module.verbose('Triggering preset action', settings.action, text, value); + module.action[ settings.action ].call(element, text, value, this); + } + else if( $.isFunction(settings.action) ) { + module.verbose('Triggering user action', settings.action, text, value); + settings.action.call(element, text, value, this); + } + else { + module.error(error.action, settings.action); + } + }, + eventInModule: function(event, callback) { + var + $target = $(event.target), + inDocument = ($target.closest(document.documentElement).length > 0), + inModule = ($target.closest($module).length > 0) + ; + callback = $.isFunction(callback) + ? callback + : function(){} + ; + if(inDocument && !inModule) { + module.verbose('Triggering event', callback); + callback(); + return true; + } + else { + module.verbose('Event occurred in dropdown, canceling callback'); + return false; + } + }, + eventOnElement: function(event, callback) { + var + $target = $(event.target), + $label = $target.closest(selector.siblingLabel), + inVisibleDOM = document.body.contains(event.target), + notOnLabel = ($module.find($label).length === 0), + notInMenu = ($target.closest($menu).length === 0) + ; + callback = $.isFunction(callback) + ? callback + : function(){} + ; + if(inVisibleDOM && notOnLabel && notInMenu) { + module.verbose('Triggering event', callback); + callback(); + return true; + } + else { + module.verbose('Event occurred in dropdown menu, canceling callback'); + return false; + } + } + }, + + action: { + + nothing: function() {}, + + activate: function(text, value, element) { + value = (value !== undefined) + ? value + : text + ; + if( module.can.activate( $(element) ) ) { + module.set.selected(value, $(element)); + if(module.is.multiple() && !module.is.allFiltered()) { + return; + } + else { + module.hideAndClear(); + } + } + }, + + select: function(text, value, element) { + value = (value !== undefined) + ? value + : text + ; + if( module.can.activate( $(element) ) ) { + module.set.value(value, $(element)); + if(module.is.multiple() && !module.is.allFiltered()) { + return; + } + else { + module.hideAndClear(); + } + } + }, + + combo: function(text, value, element) { + value = (value !== undefined) + ? value + : text + ; + module.set.selected(value, $(element)); + module.hideAndClear(); + }, + + hide: function(text, value, element) { + module.set.value(value, text); + module.hideAndClear(); + } + + }, + + get: { + id: function() { + return id; + }, + defaultText: function() { + return $module.data(metadata.defaultText); + }, + defaultValue: function() { + return $module.data(metadata.defaultValue); + }, + placeholderText: function() { + return $module.data(metadata.placeholderText) || ''; + }, + text: function() { + return $text.text(); + }, + query: function() { + return $.trim($search.val()); + }, + searchWidth: function(value) { + value = (value !== undefined) + ? value + : $search.val() + ; + $sizer.text(value); + // prevent rounding issues + return Math.ceil( $sizer.width() + 1); + }, + selectionCount: function() { + var + values = module.get.values(), + count + ; + count = ( module.is.multiple() ) + ? $.isArray(values) + ? values.length + : 0 + : (module.get.value() !== '') + ? 1 + : 0 + ; + return count; + }, + transition: function($subMenu) { + return (settings.transition == 'auto') + ? module.is.upward($subMenu) + ? 'slide up' + : 'slide down' + : settings.transition + ; + }, + userValues: function() { + var + values = module.get.values() + ; + if(!values) { + return false; + } + values = $.isArray(values) + ? values + : [values] + ; + return $.grep(values, function(value) { + return (module.get.item(value) === false); + }); + }, + uniqueArray: function(array) { + return $.grep(array, function (value, index) { + return $.inArray(value, array) === index; + }); + }, + caretPosition: function() { + var + input = $search.get(0), + range, + rangeLength + ; + if('selectionStart' in input) { + return input.selectionStart; + } + else if (document.selection) { + input.focus(); + range = document.selection.createRange(); + rangeLength = range.text.length; + range.moveStart('character', -input.value.length); + return range.text.length - rangeLength; + } + }, + value: function() { + var + value = ($input.length > 0) + ? $input.val() + : $module.data(metadata.value), + isEmptyMultiselect = ($.isArray(value) && value.length === 1 && value[0] === '') + ; + // prevents placeholder element from being selected when multiple + return (value === undefined || isEmptyMultiselect) + ? '' + : value + ; + }, + values: function() { + var + value = module.get.value() + ; + if(value === '') { + return ''; + } + return ( !module.has.selectInput() && module.is.multiple() ) + ? (typeof value == 'string') // delimited string + ? value.split(settings.delimiter) + : '' + : value + ; + }, + remoteValues: function() { + var + values = module.get.values(), + remoteValues = false + ; + if(values) { + if(typeof values == 'string') { + values = [values]; + } + $.each(values, function(index, value) { + var + name = module.read.remoteData(value) + ; + module.verbose('Restoring value from session data', name, value); + if(name) { + if(!remoteValues) { + remoteValues = {}; + } + remoteValues[value] = name; + } + }); + } + return remoteValues; + }, + choiceText: function($choice, preserveHTML) { + preserveHTML = (preserveHTML !== undefined) + ? preserveHTML + : settings.preserveHTML + ; + if($choice) { + if($choice.find(selector.menu).length > 0) { + module.verbose('Retrieving text of element with sub-menu'); + $choice = $choice.clone(); + $choice.find(selector.menu).remove(); + $choice.find(selector.menuIcon).remove(); + } + return ($choice.data(metadata.text) !== undefined) + ? $choice.data(metadata.text) + : (preserveHTML) + ? $.trim($choice.html()) + : $.trim($choice.text()) + ; + } + }, + choiceValue: function($choice, choiceText) { + choiceText = choiceText || module.get.choiceText($choice); + if(!$choice) { + return false; + } + return ($choice.data(metadata.value) !== undefined) + ? String( $choice.data(metadata.value) ) + : (typeof choiceText === 'string') + ? $.trim(choiceText.toLowerCase()) + : String(choiceText) + ; + }, + inputEvent: function() { + var + input = $search[0] + ; + if(input) { + return (input.oninput !== undefined) + ? 'input' + : (input.onpropertychange !== undefined) + ? 'propertychange' + : 'keyup' + ; + } + return false; + }, + selectValues: function() { + var + select = {} + ; + select.values = []; + $module + .find('option') + .each(function() { + var + $option = $(this), + name = $option.html(), + disabled = $option.attr('disabled'), + value = ( $option.attr('value') !== undefined ) + ? $option.attr('value') + : name + ; + if(settings.placeholder === 'auto' && value === '') { + select.placeholder = name; + } + else { + select.values.push({ + name : name, + value : value, + disabled : disabled + }); + } + }) + ; + if(settings.placeholder && settings.placeholder !== 'auto') { + module.debug('Setting placeholder value to', settings.placeholder); + select.placeholder = settings.placeholder; + } + if(settings.sortSelect) { + select.values.sort(function(a, b) { + return (a.name > b.name) + ? 1 + : -1 + ; + }); + module.debug('Retrieved and sorted values from select', select); + } + else { + module.debug('Retrieved values from select', select); + } + return select; + }, + activeItem: function() { + return $item.filter('.' + className.active); + }, + selectedItem: function() { + var + $selectedItem = $item.not(selector.unselectable).filter('.' + className.selected) + ; + return ($selectedItem.length > 0) + ? $selectedItem + : $item.eq(0) + ; + }, + itemWithAdditions: function(value) { + var + $items = module.get.item(value), + $userItems = module.create.userChoice(value), + hasUserItems = ($userItems && $userItems.length > 0) + ; + if(hasUserItems) { + $items = ($items.length > 0) + ? $items.add($userItems) + : $userItems + ; + } + return $items; + }, + item: function(value, strict) { + var + $selectedItem = false, + shouldSearch, + isMultiple + ; + value = (value !== undefined) + ? value + : ( module.get.values() !== undefined) + ? module.get.values() + : module.get.text() + ; + shouldSearch = (isMultiple) + ? (value.length > 0) + : (value !== undefined && value !== null) + ; + isMultiple = (module.is.multiple() && $.isArray(value)); + strict = (value === '' || value === 0) + ? true + : strict || false + ; + if(shouldSearch) { + $item + .each(function() { + var + $choice = $(this), + optionText = module.get.choiceText($choice), + optionValue = module.get.choiceValue($choice, optionText) + ; + // safe early exit + if(optionValue === null || optionValue === undefined) { + return; + } + if(isMultiple) { + if($.inArray( String(optionValue), value) !== -1 || $.inArray(optionText, value) !== -1) { + $selectedItem = ($selectedItem) + ? $selectedItem.add($choice) + : $choice + ; + } + } + else if(strict) { + module.verbose('Ambiguous dropdown value using strict type check', $choice, value); + if( optionValue === value || optionText === value) { + $selectedItem = $choice; + return true; + } + } + else { + if( String(optionValue) == String(value) || optionText == value) { + module.verbose('Found select item by value', optionValue, value); + $selectedItem = $choice; + return true; + } + } + }) + ; + } + return $selectedItem; + } + }, + + check: { + maxSelections: function(selectionCount) { + if(settings.maxSelections) { + selectionCount = (selectionCount !== undefined) + ? selectionCount + : module.get.selectionCount() + ; + if(selectionCount >= settings.maxSelections) { + module.debug('Maximum selection count reached'); + if(settings.useLabels) { + $item.addClass(className.filtered); + module.add.message(message.maxSelections); + } + return true; + } + else { + module.verbose('No longer at maximum selection count'); + module.remove.message(); + module.remove.filteredItem(); + if(module.is.searchSelection()) { + module.filterItems(); + } + return false; + } + } + return true; + } + }, + + restore: { + defaults: function() { + module.clear(); + module.restore.defaultText(); + module.restore.defaultValue(); + }, + defaultText: function() { + var + defaultText = module.get.defaultText(), + placeholderText = module.get.placeholderText + ; + if(defaultText === placeholderText) { + module.debug('Restoring default placeholder text', defaultText); + module.set.placeholderText(defaultText); + } + else { + module.debug('Restoring default text', defaultText); + module.set.text(defaultText); + } + }, + placeholderText: function() { + module.set.placeholderText(); + }, + defaultValue: function() { + var + defaultValue = module.get.defaultValue() + ; + if(defaultValue !== undefined) { + module.debug('Restoring default value', defaultValue); + if(defaultValue !== '') { + module.set.value(defaultValue); + module.set.selected(); + } + else { + module.remove.activeItem(); + module.remove.selectedItem(); + } + } + }, + labels: function() { + if(settings.allowAdditions) { + if(!settings.useLabels) { + module.error(error.labels); + settings.useLabels = true; + } + module.debug('Restoring selected values'); + module.create.userLabels(); + } + module.check.maxSelections(); + }, + selected: function() { + module.restore.values(); + if(module.is.multiple()) { + module.debug('Restoring previously selected values and labels'); + module.restore.labels(); + } + else { + module.debug('Restoring previously selected values'); + } + }, + values: function() { + // prevents callbacks from occurring on initial load + module.set.initialLoad(); + if(settings.apiSettings && settings.saveRemoteData && module.get.remoteValues()) { + module.restore.remoteValues(); + } + else { + module.set.selected(); + } + module.remove.initialLoad(); + }, + remoteValues: function() { + var + values = module.get.remoteValues() + ; + module.debug('Recreating selected from session data', values); + if(values) { + if( module.is.single() ) { + $.each(values, function(value, name) { + module.set.text(name); + }); + } + else { + $.each(values, function(value, name) { + module.add.label(value, name); + }); + } + } + } + }, + + read: { + remoteData: function(value) { + var + name + ; + if(window.Storage === undefined) { + module.error(error.noStorage); + return; + } + name = sessionStorage.getItem(value); + return (name !== undefined) + ? name + : false + ; + } + }, + + save: { + defaults: function() { + module.save.defaultText(); + module.save.placeholderText(); + module.save.defaultValue(); + }, + defaultValue: function() { + var + value = module.get.value() + ; + module.verbose('Saving default value as', value); + $module.data(metadata.defaultValue, value); + }, + defaultText: function() { + var + text = module.get.text() + ; + module.verbose('Saving default text as', text); + $module.data(metadata.defaultText, text); + }, + placeholderText: function() { + var + text + ; + if(settings.placeholder !== false && $text.hasClass(className.placeholder)) { + text = module.get.text(); + module.verbose('Saving placeholder text as', text); + $module.data(metadata.placeholderText, text); + } + }, + remoteData: function(name, value) { + if(window.Storage === undefined) { + module.error(error.noStorage); + return; + } + module.verbose('Saving remote data to session storage', value, name); + sessionStorage.setItem(value, name); + } + }, + + clear: function() { + if(module.is.multiple() && settings.useLabels) { + module.remove.labels(); + } + else { + module.remove.activeItem(); + module.remove.selectedItem(); + } + module.set.placeholderText(); + module.clearValue(); + }, + + clearValue: function() { + module.set.value(''); + }, + + scrollPage: function(direction, $selectedItem) { + var + $currentItem = $selectedItem || module.get.selectedItem(), + $menu = $currentItem.closest(selector.menu), + menuHeight = $menu.outerHeight(), + currentScroll = $menu.scrollTop(), + itemHeight = $item.eq(0).outerHeight(), + itemsPerPage = Math.floor(menuHeight / itemHeight), + maxScroll = $menu.prop('scrollHeight'), + newScroll = (direction == 'up') + ? currentScroll - (itemHeight * itemsPerPage) + : currentScroll + (itemHeight * itemsPerPage), + $selectableItem = $item.not(selector.unselectable), + isWithinRange, + $nextSelectedItem, + elementIndex + ; + elementIndex = (direction == 'up') + ? $selectableItem.index($currentItem) - itemsPerPage + : $selectableItem.index($currentItem) + itemsPerPage + ; + isWithinRange = (direction == 'up') + ? (elementIndex >= 0) + : (elementIndex < $selectableItem.length) + ; + $nextSelectedItem = (isWithinRange) + ? $selectableItem.eq(elementIndex) + : (direction == 'up') + ? $selectableItem.first() + : $selectableItem.last() + ; + if($nextSelectedItem.length > 0) { + module.debug('Scrolling page', direction, $nextSelectedItem); + $currentItem + .removeClass(className.selected) + ; + $nextSelectedItem + .addClass(className.selected) + ; + if(settings.selectOnKeydown && module.is.single()) { + module.set.selectedItem($nextSelectedItem); + } + $menu + .scrollTop(newScroll) + ; + } + }, + + set: { + filtered: function() { + var + isMultiple = module.is.multiple(), + isSearch = module.is.searchSelection(), + isSearchMultiple = (isMultiple && isSearch), + searchValue = (isSearch) + ? module.get.query() + : '', + hasSearchValue = (typeof searchValue === 'string' && searchValue.length > 0), + searchWidth = module.get.searchWidth(), + valueIsSet = searchValue !== '' + ; + if(isMultiple && hasSearchValue) { + module.verbose('Adjusting input width', searchWidth, settings.glyphWidth); + $search.css('width', searchWidth); + } + if(hasSearchValue || (isSearchMultiple && valueIsSet)) { + module.verbose('Hiding placeholder text'); + $text.addClass(className.filtered); + } + else if(!isMultiple || (isSearchMultiple && !valueIsSet)) { + module.verbose('Showing placeholder text'); + $text.removeClass(className.filtered); + } + }, + empty: function() { + $module.addClass(className.empty); + }, + loading: function() { + $module.addClass(className.loading); + }, + placeholderText: function(text) { + text = text || module.get.placeholderText(); + module.debug('Setting placeholder text', text); + module.set.text(text); + $text.addClass(className.placeholder); + }, + tabbable: function() { + if( module.has.search() ) { + module.debug('Added tabindex to searchable dropdown'); + $search + .val('') + .attr('tabindex', 0) + ; + $menu + .attr('tabindex', -1) + ; + } + else { + module.debug('Added tabindex to dropdown'); + if( $module.attr('tabindex') === undefined) { + $module + .attr('tabindex', 0) + ; + $menu + .attr('tabindex', -1) + ; + } + } + }, + initialLoad: function() { + module.verbose('Setting initial load'); + initialLoad = true; + }, + activeItem: function($item) { + if( settings.allowAdditions && $item.filter(selector.addition).length > 0 ) { + $item.addClass(className.filtered); + } + else { + $item.addClass(className.active); + } + }, + partialSearch: function(text) { + var + length = module.get.query().length + ; + $search.val( text.substr(0 , length)); + }, + scrollPosition: function($item, forceScroll) { + var + edgeTolerance = 5, + $menu, + hasActive, + offset, + itemHeight, + itemOffset, + menuOffset, + menuScroll, + menuHeight, + abovePage, + belowPage + ; + + $item = $item || module.get.selectedItem(); + $menu = $item.closest(selector.menu); + hasActive = ($item && $item.length > 0); + forceScroll = (forceScroll !== undefined) + ? forceScroll + : false + ; + if($item && $menu.length > 0 && hasActive) { + itemOffset = $item.position().top; + + $menu.addClass(className.loading); + menuScroll = $menu.scrollTop(); + menuOffset = $menu.offset().top; + itemOffset = $item.offset().top; + offset = menuScroll - menuOffset + itemOffset; + if(!forceScroll) { + menuHeight = $menu.height(); + belowPage = menuScroll + menuHeight < (offset + edgeTolerance); + abovePage = ((offset - edgeTolerance) < menuScroll); + } + module.debug('Scrolling to active item', offset); + if(forceScroll || abovePage || belowPage) { + $menu.scrollTop(offset); + } + $menu.removeClass(className.loading); + } + }, + text: function(text) { + if(settings.action !== 'select') { + if(settings.action == 'combo') { + module.debug('Changing combo button text', text, $combo); + if(settings.preserveHTML) { + $combo.html(text); + } + else { + $combo.text(text); + } + } + else { + if(text !== module.get.placeholderText()) { + $text.removeClass(className.placeholder); + } + module.debug('Changing text', text, $text); + $text + .removeClass(className.filtered) + ; + if(settings.preserveHTML) { + $text.html(text); + } + else { + $text.text(text); + } + } + } + }, + selectedItem: function($item) { + var + value = module.get.choiceValue($item), + text = module.get.choiceText($item, false) + ; + module.debug('Setting user selection to item', $item); + module.remove.activeItem(); + module.set.partialSearch(text); + module.set.activeItem($item); + module.set.selected(value, $item); + module.set.text(text); + }, + selectedLetter: function(letter) { + var + $selectedItem = $item.filter('.' + className.selected), + alreadySelectedLetter = $selectedItem.length > 0 && module.has.firstLetter($selectedItem, letter), + $nextValue = false, + $nextItem + ; + // check next of same letter + if(alreadySelectedLetter) { + $nextItem = $selectedItem.nextAll($item).eq(0); + if( module.has.firstLetter($nextItem, letter) ) { + $nextValue = $nextItem; + } + } + // check all values + if(!$nextValue) { + $item + .each(function(){ + if(module.has.firstLetter($(this), letter)) { + $nextValue = $(this); + return false; + } + }) + ; + } + // set next value + if($nextValue) { + module.verbose('Scrolling to next value with letter', letter); + module.set.scrollPosition($nextValue); + $selectedItem.removeClass(className.selected); + $nextValue.addClass(className.selected); + if(settings.selectOnKeydown && module.is.single()) { + module.set.selectedItem($nextValue); + } + } + }, + direction: function($menu) { + if(settings.direction == 'auto') { + if(module.is.onScreen($menu)) { + module.remove.upward($menu); + } + else { + module.set.upward($menu); + } + } + else if(settings.direction == 'upward') { + module.set.upward($menu); + } + }, + upward: function($menu) { + var $element = $menu || $module; + $element.addClass(className.upward); + }, + value: function(value, text, $selected) { + var + escapedValue = module.escape.value(value), + hasInput = ($input.length > 0), + isAddition = !module.has.value(value), + currentValue = module.get.values(), + stringValue = (value !== undefined) + ? String(value) + : value, + newValue + ; + if(hasInput) { + if(!settings.allowReselection && stringValue == currentValue) { + module.verbose('Skipping value update already same value', value, currentValue); + if(!module.is.initialLoad()) { + return; + } + } + + if( module.is.single() && module.has.selectInput() && module.can.extendSelect() ) { + module.debug('Adding user option', value); + module.add.optionValue(value); + } + module.debug('Updating input value', escapedValue, currentValue); + internalChange = true; + $input + .val(escapedValue) + ; + if(settings.fireOnInit === false && module.is.initialLoad()) { + module.debug('Input native change event ignored on initial load'); + } + else { + module.trigger.change(); + } + internalChange = false; + } + else { + module.verbose('Storing value in metadata', escapedValue, $input); + if(escapedValue !== currentValue) { + $module.data(metadata.value, stringValue); + } + } + if(settings.fireOnInit === false && module.is.initialLoad()) { + module.verbose('No callback on initial load', settings.onChange); + } + else { + settings.onChange.call(element, value, text, $selected); + } + }, + active: function() { + $module + .addClass(className.active) + ; + }, + multiple: function() { + $module.addClass(className.multiple); + }, + visible: function() { + $module.addClass(className.visible); + }, + exactly: function(value, $selectedItem) { + module.debug('Setting selected to exact values'); + module.clear(); + module.set.selected(value, $selectedItem); + }, + selected: function(value, $selectedItem) { + var + isMultiple = module.is.multiple(), + $userSelectedItem + ; + $selectedItem = (settings.allowAdditions) + ? $selectedItem || module.get.itemWithAdditions(value) + : $selectedItem || module.get.item(value) + ; + if(!$selectedItem) { + return; + } + module.debug('Setting selected menu item to', $selectedItem); + if(module.is.multiple()) { + module.remove.searchWidth(); + } + if(module.is.single()) { + module.remove.activeItem(); + module.remove.selectedItem(); + } + else if(settings.useLabels) { + module.remove.selectedItem(); + } + // select each item + $selectedItem + .each(function() { + var + $selected = $(this), + selectedText = module.get.choiceText($selected), + selectedValue = module.get.choiceValue($selected, selectedText), + + isFiltered = $selected.hasClass(className.filtered), + isActive = $selected.hasClass(className.active), + isUserValue = $selected.hasClass(className.addition), + shouldAnimate = (isMultiple && $selectedItem.length == 1) + ; + if(isMultiple) { + if(!isActive || isUserValue) { + if(settings.apiSettings && settings.saveRemoteData) { + module.save.remoteData(selectedText, selectedValue); + } + if(settings.useLabels) { + module.add.value(selectedValue, selectedText, $selected); + module.add.label(selectedValue, selectedText, shouldAnimate); + module.set.activeItem($selected); + module.filterActive(); + module.select.nextAvailable($selectedItem); + } + else { + module.add.value(selectedValue, selectedText, $selected); + module.set.text(module.add.variables(message.count)); + module.set.activeItem($selected); + } + } + else if(!isFiltered) { + module.debug('Selected active value, removing label'); + module.remove.selected(selectedValue); + } + } + else { + if(settings.apiSettings && settings.saveRemoteData) { + module.save.remoteData(selectedText, selectedValue); + } + module.set.text(selectedText); + module.set.value(selectedValue, selectedText, $selected); + $selected + .addClass(className.active) + .addClass(className.selected) + ; + } + }) + ; + } + }, + + add: { + label: function(value, text, shouldAnimate) { + var + $next = module.is.searchSelection() + ? $search + : $text, + escapedValue = module.escape.value(value), + $label + ; + $label = $('<a />') + .addClass(className.label) + .attr('data-value', escapedValue) + .html(templates.label(escapedValue, text)) + ; + $label = settings.onLabelCreate.call($label, escapedValue, text); + + if(module.has.label(value)) { + module.debug('Label already exists, skipping', escapedValue); + return; + } + if(settings.label.variation) { + $label.addClass(settings.label.variation); + } + if(shouldAnimate === true) { + module.debug('Animating in label', $label); + $label + .addClass(className.hidden) + .insertBefore($next) + .transition(settings.label.transition, settings.label.duration) + ; + } + else { + module.debug('Adding selection label', $label); + $label + .insertBefore($next) + ; + } + }, + message: function(message) { + var + $message = $menu.children(selector.message), + html = settings.templates.message(module.add.variables(message)) + ; + if($message.length > 0) { + $message + .html(html) + ; + } + else { + $message = $('<div/>') + .html(html) + .addClass(className.message) + .appendTo($menu) + ; + } + }, + optionValue: function(value) { + var + escapedValue = module.escape.value(value), + $option = $input.find('option[value="' + escapedValue + '"]'), + hasOption = ($option.length > 0) + ; + if(hasOption) { + return; + } + // temporarily disconnect observer + module.disconnect.selectObserver(); + if( module.is.single() ) { + module.verbose('Removing previous user addition'); + $input.find('option.' + className.addition).remove(); + } + $('<option/>') + .prop('value', escapedValue) + .addClass(className.addition) + .html(value) + .appendTo($input) + ; + module.verbose('Adding user addition as an <option>', value); + module.observe.select(); + }, + userSuggestion: function(value) { + var + $addition = $menu.children(selector.addition), + $existingItem = module.get.item(value), + alreadyHasValue = $existingItem && $existingItem.not(selector.addition).length, + hasUserSuggestion = $addition.length > 0, + html + ; + if(settings.useLabels && module.has.maxSelections()) { + return; + } + if(value === '' || alreadyHasValue) { + $addition.remove(); + return; + } + if(hasUserSuggestion) { + $addition + .data(metadata.value, value) + .data(metadata.text, value) + .attr('data-' + metadata.value, value) + .attr('data-' + metadata.text, value) + .removeClass(className.filtered) + ; + if(!settings.hideAdditions) { + html = settings.templates.addition( module.add.variables(message.addResult, value) ); + $addition + .html(html) + ; + } + module.verbose('Replacing user suggestion with new value', $addition); + } + else { + $addition = module.create.userChoice(value); + $addition + .prependTo($menu) + ; + module.verbose('Adding item choice to menu corresponding with user choice addition', $addition); + } + if(!settings.hideAdditions || module.is.allFiltered()) { + $addition + .addClass(className.selected) + .siblings() + .removeClass(className.selected) + ; + } + module.refreshItems(); + }, + variables: function(message, term) { + var + hasCount = (message.search('{count}') !== -1), + hasMaxCount = (message.search('{maxCount}') !== -1), + hasTerm = (message.search('{term}') !== -1), + values, + count, + query + ; + module.verbose('Adding templated variables to message', message); + if(hasCount) { + count = module.get.selectionCount(); + message = message.replace('{count}', count); + } + if(hasMaxCount) { + count = module.get.selectionCount(); + message = message.replace('{maxCount}', settings.maxSelections); + } + if(hasTerm) { + query = term || module.get.query(); + message = message.replace('{term}', query); + } + return message; + }, + value: function(addedValue, addedText, $selectedItem) { + var + currentValue = module.get.values(), + newValue + ; + if(addedValue === '') { + module.debug('Cannot select blank values from multiselect'); + return; + } + // extend current array + if($.isArray(currentValue)) { + newValue = currentValue.concat([addedValue]); + newValue = module.get.uniqueArray(newValue); + } + else { + newValue = [addedValue]; + } + // add values + if( module.has.selectInput() ) { + if(module.can.extendSelect()) { + module.debug('Adding value to select', addedValue, newValue, $input); + module.add.optionValue(addedValue); + } + } + else { + newValue = newValue.join(settings.delimiter); + module.debug('Setting hidden input to delimited value', newValue, $input); + } + + if(settings.fireOnInit === false && module.is.initialLoad()) { + module.verbose('Skipping onadd callback on initial load', settings.onAdd); + } + else { + settings.onAdd.call(element, addedValue, addedText, $selectedItem); + } + module.set.value(newValue, addedValue, addedText, $selectedItem); + module.check.maxSelections(); + } + }, + + remove: { + active: function() { + $module.removeClass(className.active); + }, + activeLabel: function() { + $module.find(selector.label).removeClass(className.active); + }, + empty: function() { + $module.removeClass(className.empty); + }, + loading: function() { + $module.removeClass(className.loading); + }, + initialLoad: function() { + initialLoad = false; + }, + upward: function($menu) { + var $element = $menu || $module; + $element.removeClass(className.upward); + }, + visible: function() { + $module.removeClass(className.visible); + }, + activeItem: function() { + $item.removeClass(className.active); + }, + filteredItem: function() { + if(settings.useLabels && module.has.maxSelections() ) { + return; + } + if(settings.useLabels && module.is.multiple()) { + $item.not('.' + className.active).removeClass(className.filtered); + } + else { + $item.removeClass(className.filtered); + } + module.remove.empty(); + }, + optionValue: function(value) { + var + escapedValue = module.escape.value(value), + $option = $input.find('option[value="' + escapedValue + '"]'), + hasOption = ($option.length > 0) + ; + if(!hasOption || !$option.hasClass(className.addition)) { + return; + } + // temporarily disconnect observer + if(selectObserver) { + selectObserver.disconnect(); + module.verbose('Temporarily disconnecting mutation observer'); + } + $option.remove(); + module.verbose('Removing user addition as an <option>', escapedValue); + if(selectObserver) { + selectObserver.observe($input[0], { + childList : true, + subtree : true + }); + } + }, + message: function() { + $menu.children(selector.message).remove(); + }, + searchWidth: function() { + $search.css('width', ''); + }, + searchTerm: function() { + module.verbose('Cleared search term'); + $search.val(''); + module.set.filtered(); + }, + userAddition: function() { + $item.filter(selector.addition).remove(); + }, + selected: function(value, $selectedItem) { + $selectedItem = (settings.allowAdditions) + ? $selectedItem || module.get.itemWithAdditions(value) + : $selectedItem || module.get.item(value) + ; + + if(!$selectedItem) { + return false; + } + + $selectedItem + .each(function() { + var + $selected = $(this), + selectedText = module.get.choiceText($selected), + selectedValue = module.get.choiceValue($selected, selectedText) + ; + if(module.is.multiple()) { + if(settings.useLabels) { + module.remove.value(selectedValue, selectedText, $selected); + module.remove.label(selectedValue); + } + else { + module.remove.value(selectedValue, selectedText, $selected); + if(module.get.selectionCount() === 0) { + module.set.placeholderText(); + } + else { + module.set.text(module.add.variables(message.count)); + } + } + } + else { + module.remove.value(selectedValue, selectedText, $selected); + } + $selected + .removeClass(className.filtered) + .removeClass(className.active) + ; + if(settings.useLabels) { + $selected.removeClass(className.selected); + } + }) + ; + }, + selectedItem: function() { + $item.removeClass(className.selected); + }, + value: function(removedValue, removedText, $removedItem) { + var + values = module.get.values(), + newValue + ; + if( module.has.selectInput() ) { + module.verbose('Input is <select> removing selected option', removedValue); + newValue = module.remove.arrayValue(removedValue, values); + module.remove.optionValue(removedValue); + } + else { + module.verbose('Removing from delimited values', removedValue); + newValue = module.remove.arrayValue(removedValue, values); + newValue = newValue.join(settings.delimiter); + } + if(settings.fireOnInit === false && module.is.initialLoad()) { + module.verbose('No callback on initial load', settings.onRemove); + } + else { + settings.onRemove.call(element, removedValue, removedText, $removedItem); + } + module.set.value(newValue, removedText, $removedItem); + module.check.maxSelections(); + }, + arrayValue: function(removedValue, values) { + if( !$.isArray(values) ) { + values = [values]; + } + values = $.grep(values, function(value){ + return (removedValue != value); + }); + module.verbose('Removed value from delimited string', removedValue, values); + return values; + }, + label: function(value, shouldAnimate) { + var + $labels = $module.find(selector.label), + $removedLabel = $labels.filter('[data-value="' + value +'"]') + ; + module.verbose('Removing label', $removedLabel); + $removedLabel.remove(); + }, + activeLabels: function($activeLabels) { + $activeLabels = $activeLabels || $module.find(selector.label).filter('.' + className.active); + module.verbose('Removing active label selections', $activeLabels); + module.remove.labels($activeLabels); + }, + labels: function($labels) { + $labels = $labels || $module.find(selector.label); + module.verbose('Removing labels', $labels); + $labels + .each(function(){ + var + $label = $(this), + value = $label.data(metadata.value), + stringValue = (value !== undefined) + ? String(value) + : value, + isUserValue = module.is.userValue(stringValue) + ; + if(settings.onLabelRemove.call($label, value) === false) { + module.debug('Label remove callback cancelled removal'); + return; + } + module.remove.message(); + if(isUserValue) { + module.remove.value(stringValue); + module.remove.label(stringValue); + } + else { + // selected will also remove label + module.remove.selected(stringValue); + } + }) + ; + }, + tabbable: function() { + if( module.has.search() ) { + module.debug('Searchable dropdown initialized'); + $search + .removeAttr('tabindex') + ; + $menu + .removeAttr('tabindex') + ; + } + else { + module.debug('Simple selection dropdown initialized'); + $module + .removeAttr('tabindex') + ; + $menu + .removeAttr('tabindex') + ; + } + } + }, + + has: { + menuSearch: function() { + return (module.has.search() && $search.closest($menu).length > 0); + }, + search: function() { + return ($search.length > 0); + }, + sizer: function() { + return ($sizer.length > 0); + }, + selectInput: function() { + return ( $input.is('select') ); + }, + minCharacters: function(searchTerm) { + if(settings.minCharacters) { + searchTerm = (searchTerm !== undefined) + ? String(searchTerm) + : String(module.get.query()) + ; + return (searchTerm.length >= settings.minCharacters); + } + return true; + }, + firstLetter: function($item, letter) { + var + text, + firstLetter + ; + if(!$item || $item.length === 0 || typeof letter !== 'string') { + return false; + } + text = module.get.choiceText($item, false); + letter = letter.toLowerCase(); + firstLetter = String(text).charAt(0).toLowerCase(); + return (letter == firstLetter); + }, + input: function() { + return ($input.length > 0); + }, + items: function() { + return ($item.length > 0); + }, + menu: function() { + return ($menu.length > 0); + }, + message: function() { + return ($menu.children(selector.message).length !== 0); + }, + label: function(value) { + var + escapedValue = module.escape.value(value), + $labels = $module.find(selector.label) + ; + return ($labels.filter('[data-value="' + escapedValue +'"]').length > 0); + }, + maxSelections: function() { + return (settings.maxSelections && module.get.selectionCount() >= settings.maxSelections); + }, + allResultsFiltered: function() { + var + $normalResults = $item.not(selector.addition) + ; + return ($normalResults.filter(selector.unselectable).length === $normalResults.length); + }, + userSuggestion: function() { + return ($menu.children(selector.addition).length > 0); + }, + query: function() { + return (module.get.query() !== ''); + }, + value: function(value) { + var + values = module.get.values(), + hasValue = $.isArray(values) + ? values && ($.inArray(value, values) !== -1) + : (values == value) + ; + return (hasValue) + ? true + : false + ; + } + }, + + is: { + active: function() { + return $module.hasClass(className.active); + }, + bubbledLabelClick: function(event) { + return $(event.target).is('select, input') && $module.closest('label').length > 0; + }, + bubbledIconClick: function(event) { + return $(event.target).closest($icon).length > 0; + }, + alreadySetup: function() { + return ($module.is('select') && $module.parent(selector.dropdown).length > 0 && $module.prev().length === 0); + }, + animating: function($subMenu) { + return ($subMenu) + ? $subMenu.transition && $subMenu.transition('is animating') + : $menu.transition && $menu.transition('is animating') + ; + }, + disabled: function() { + return $module.hasClass(className.disabled); + }, + focused: function() { + return (document.activeElement === $module[0]); + }, + focusedOnSearch: function() { + return (document.activeElement === $search[0]); + }, + allFiltered: function() { + return( (module.is.multiple() || module.has.search()) && !(settings.hideAdditions == false && module.has.userSuggestion()) && !module.has.message() && module.has.allResultsFiltered() ); + }, + hidden: function($subMenu) { + return !module.is.visible($subMenu); + }, + initialLoad: function() { + return initialLoad; + }, + onScreen: function($subMenu) { + var + $currentMenu = $subMenu || $menu, + canOpenDownward = true, + onScreen = {}, + calculations + ; + $currentMenu.addClass(className.loading); + calculations = { + context: { + scrollTop : $context.scrollTop(), + height : $context.outerHeight() + }, + menu : { + offset: $currentMenu.offset(), + height: $currentMenu.outerHeight() + } + }; + onScreen = { + above : (calculations.context.scrollTop) <= calculations.menu.offset.top - calculations.menu.height, + below : (calculations.context.scrollTop + calculations.context.height) >= calculations.menu.offset.top + calculations.menu.height + }; + if(onScreen.below) { + module.verbose('Dropdown can fit in context downward', onScreen); + canOpenDownward = true; + } + else if(!onScreen.below && !onScreen.above) { + module.verbose('Dropdown cannot fit in either direction, favoring downward', onScreen); + canOpenDownward = true; + } + else { + module.verbose('Dropdown cannot fit below, opening upward', onScreen); + canOpenDownward = false; + } + $currentMenu.removeClass(className.loading); + return canOpenDownward; + }, + inObject: function(needle, object) { + var + found = false + ; + $.each(object, function(index, property) { + if(property == needle) { + found = true; + return true; + } + }); + return found; + }, + multiple: function() { + return $module.hasClass(className.multiple); + }, + single: function() { + return !module.is.multiple(); + }, + selectMutation: function(mutations) { + var + selectChanged = false + ; + $.each(mutations, function(index, mutation) { + if(mutation.target && $(mutation.target).is('select')) { + selectChanged = true; + return true; + } + }); + return selectChanged; + }, + search: function() { + return $module.hasClass(className.search); + }, + searchSelection: function() { + return ( module.has.search() && $search.parent(selector.dropdown).length === 1 ); + }, + selection: function() { + return $module.hasClass(className.selection); + }, + userValue: function(value) { + return ($.inArray(value, module.get.userValues()) !== -1); + }, + upward: function($menu) { + var $element = $menu || $module; + return $element.hasClass(className.upward); + }, + visible: function($subMenu) { + return ($subMenu) + ? $subMenu.hasClass(className.visible) + : $menu.hasClass(className.visible) + ; + } + }, + + can: { + activate: function($item) { + if(settings.useLabels) { + return true; + } + if(!module.has.maxSelections()) { + return true; + } + if(module.has.maxSelections() && $item.hasClass(className.active)) { + return true; + } + return false; + }, + click: function() { + return (hasTouch || settings.on == 'click'); + }, + extendSelect: function() { + return settings.allowAdditions || settings.apiSettings; + }, + show: function() { + return !module.is.disabled() && (module.has.items() || module.has.message()); + }, + useAPI: function() { + return $.fn.api !== undefined; + } + }, + + animate: { + show: function(callback, $subMenu) { + var + $currentMenu = $subMenu || $menu, + start = ($subMenu) + ? function() {} + : function() { + module.hideSubMenus(); + module.hideOthers(); + module.set.active(); + }, + transition + ; + callback = $.isFunction(callback) + ? callback + : function(){} + ; + module.verbose('Doing menu show animation', $currentMenu); + module.set.direction($subMenu); + transition = module.get.transition($subMenu); + if( module.is.selection() ) { + module.set.scrollPosition(module.get.selectedItem(), true); + } + if( module.is.hidden($currentMenu) || module.is.animating($currentMenu) ) { + if(transition == 'none') { + start(); + $currentMenu.transition('show'); + callback.call(element); + } + else if($.fn.transition !== undefined && $module.transition('is supported')) { + $currentMenu + .transition({ + animation : transition + ' in', + debug : settings.debug, + verbose : settings.verbose, + duration : settings.duration, + queue : true, + onStart : start, + onComplete : function() { + callback.call(element); + } + }) + ; + } + else { + module.error(error.noTransition, transition); + } + } + }, + hide: function(callback, $subMenu) { + var + $currentMenu = $subMenu || $menu, + duration = ($subMenu) + ? (settings.duration * 0.9) + : settings.duration, + start = ($subMenu) + ? function() {} + : function() { + if( module.can.click() ) { + module.unbind.intent(); + } + module.remove.active(); + }, + transition = module.get.transition($subMenu) + ; + callback = $.isFunction(callback) + ? callback + : function(){} + ; + if( module.is.visible($currentMenu) || module.is.animating($currentMenu) ) { + module.verbose('Doing menu hide animation', $currentMenu); + + if(transition == 'none') { + start(); + $currentMenu.transition('hide'); + callback.call(element); + } + else if($.fn.transition !== undefined && $module.transition('is supported')) { + $currentMenu + .transition({ + animation : transition + ' out', + duration : settings.duration, + debug : settings.debug, + verbose : settings.verbose, + queue : true, + onStart : start, + onComplete : function() { + if(settings.direction == 'auto') { + module.remove.upward($subMenu); + } + callback.call(element); + } + }) + ; + } + else { + module.error(error.transition); + } + } + } + }, + + hideAndClear: function() { + module.remove.searchTerm(); + if( module.has.maxSelections() ) { + return; + } + if(module.has.search()) { + module.hide(function() { + module.remove.filteredItem(); + }); + } + else { + module.hide(); + } + }, + + delay: { + show: function() { + module.verbose('Delaying show event to ensure user intent'); + clearTimeout(module.timer); + module.timer = setTimeout(module.show, settings.delay.show); + }, + hide: function() { + module.verbose('Delaying hide event to ensure user intent'); + clearTimeout(module.timer); + module.timer = setTimeout(module.hide, settings.delay.hide); + } + }, + + escape: { + value: function(value) { + var + multipleValues = $.isArray(value), + stringValue = (typeof value === 'string'), + isUnparsable = (!stringValue && !multipleValues), + hasQuotes = (stringValue && value.search(regExp.quote) !== -1), + values = [] + ; + if(!module.has.selectInput() || isUnparsable || !hasQuotes) { + return value; + } + module.debug('Encoding quote values for use in select', value); + if(multipleValues) { + $.each(value, function(index, value){ + values.push(value.replace(regExp.quote, '"')); + }); + return values; + } + return value.replace(regExp.quote, '"'); + }, + regExp: function(text) { + text = String(text); + return text.replace(regExp.escape, '\\$&'); + } + }, + + setting: function(name, value) { + module.debug('Changing setting', name, value); + if( $.isPlainObject(name) ) { + $.extend(true, settings, name); + } + else if(value !== undefined) { + if($.isPlainObject(settings[name])) { + $.extend(true, settings[name], value); + } + else { + settings[name] = value; + } + } + else { + return settings[name]; + } + }, + internal: function(name, value) { + if( $.isPlainObject(name) ) { + $.extend(true, module, name); + } + else if(value !== undefined) { + module[name] = value; + } + else { + return module[name]; + } + }, + debug: function() { + if(!settings.silent && settings.debug) { + if(settings.performance) { + module.performance.log(arguments); + } + else { + module.debug = Function.prototype.bind.call(console.info, console, settings.name + ':'); + module.debug.apply(console, arguments); + } + } + }, + verbose: function() { + if(!settings.silent && settings.verbose && settings.debug) { + if(settings.performance) { + module.performance.log(arguments); + } + else { + module.verbose = Function.prototype.bind.call(console.info, console, settings.name + ':'); + module.verbose.apply(console, arguments); + } + } + }, + error: function() { + if(!settings.silent) { + module.error = Function.prototype.bind.call(console.error, console, settings.name + ':'); + module.error.apply(console, arguments); + } + }, + performance: { + log: function(message) { + var + currentTime, + executionTime, + previousTime + ; + if(settings.performance) { + currentTime = new Date().getTime(); + previousTime = time || currentTime; + executionTime = currentTime - previousTime; + time = currentTime; + performance.push({ + 'Name' : message[0], + 'Arguments' : [].slice.call(message, 1) || '', + 'Element' : element, + 'Execution Time' : executionTime + }); + } + clearTimeout(module.performance.timer); + module.performance.timer = setTimeout(module.performance.display, 500); + }, + display: function() { + var + title = settings.name + ':', + totalTime = 0 + ; + time = false; + clearTimeout(module.performance.timer); + $.each(performance, function(index, data) { + totalTime += data['Execution Time']; + }); + title += ' ' + totalTime + 'ms'; + if(moduleSelector) { + title += ' \'' + moduleSelector + '\''; + } + if( (console.group !== undefined || console.table !== undefined) && performance.length > 0) { + console.groupCollapsed(title); + if(console.table) { + console.table(performance); + } + else { + $.each(performance, function(index, data) { + console.log(data['Name'] + ': ' + data['Execution Time']+'ms'); + }); + } + console.groupEnd(); + } + performance = []; + } + }, + invoke: function(query, passedArguments, context) { + var + object = instance, + maxDepth, + found, + response + ; + passedArguments = passedArguments || queryArguments; + context = element || context; + if(typeof query == 'string' && object !== undefined) { + query = query.split(/[\. ]/); + maxDepth = query.length - 1; + $.each(query, function(depth, value) { + var camelCaseValue = (depth != maxDepth) + ? value + query[depth + 1].charAt(0).toUpperCase() + query[depth + 1].slice(1) + : query + ; + if( $.isPlainObject( object[camelCaseValue] ) && (depth != maxDepth) ) { + object = object[camelCaseValue]; + } + else if( object[camelCaseValue] !== undefined ) { + found = object[camelCaseValue]; + return false; + } + else if( $.isPlainObject( object[value] ) && (depth != maxDepth) ) { + object = object[value]; + } + else if( object[value] !== undefined ) { + found = object[value]; + return false; + } + else { + module.error(error.method, query); + return false; + } + }); + } + if ( $.isFunction( found ) ) { + response = found.apply(context, passedArguments); + } + else if(found !== undefined) { + response = found; + } + if($.isArray(returnedValue)) { + returnedValue.push(response); + } + else if(returnedValue !== undefined) { + returnedValue = [returnedValue, response]; + } + else if(response !== undefined) { + returnedValue = response; + } + return found; + } + }; + + if(methodInvoked) { + if(instance === undefined) { + module.initialize(); + } + module.invoke(query); + } + else { + if(instance !== undefined) { + instance.invoke('destroy'); + } + module.initialize(); + } + }) + ; + return (returnedValue !== undefined) + ? returnedValue + : $allModules + ; +}; + +$.fn.dropdown.settings = { + + silent : false, + debug : false, + verbose : false, + performance : true, + + on : 'click', // what event should show menu action on item selection + action : 'activate', // action on item selection (nothing, activate, select, combo, hide, function(){}) + + + apiSettings : false, + selectOnKeydown : true, // Whether selection should occur automatically when keyboard shortcuts used + minCharacters : 0, // Minimum characters required to trigger API call + saveRemoteData : true, // Whether remote name/value pairs should be stored in sessionStorage to allow remote data to be restored on page refresh + throttle : 200, // How long to wait after last user input to search remotely + + context : window, // Context to use when determining if on screen + direction : 'auto', // Whether dropdown should always open in one direction + keepOnScreen : true, // Whether dropdown should check whether it is on screen before showing + + match : 'both', // what to match against with search selection (both, text, or label) + fullTextSearch : false, // search anywhere in value (set to 'exact' to require exact matches) + + placeholder : 'auto', // whether to convert blank <select> values to placeholder text + preserveHTML : true, // preserve html when selecting value + sortSelect : false, // sort selection on init + + forceSelection : true, // force a choice on blur with search selection + + allowAdditions : false, // whether multiple select should allow user added values + hideAdditions : true, // whether or not to hide special message prompting a user they can enter a value + + maxSelections : false, // When set to a number limits the number of selections to this count + useLabels : true, // whether multiple select should filter currently active selections from choices + delimiter : ',', // when multiselect uses normal <input> the values will be delimited with this character + + showOnFocus : true, // show menu on focus + allowReselection : false, // whether current value should trigger callbacks when reselected + allowTab : true, // add tabindex to element + allowCategorySelection : false, // allow elements with sub-menus to be selected + + fireOnInit : false, // Whether callbacks should fire when initializing dropdown values + + transition : 'auto', // auto transition will slide down or up based on direction + duration : 200, // duration of transition + + glyphWidth : 1.037, // widest glyph width in em (W is 1.037 em) used to calculate multiselect input width + + // label settings on multi-select + label: { + transition : 'scale', + duration : 200, + variation : false + }, + + // delay before event + delay : { + hide : 300, + show : 200, + search : 20, + touch : 50 + }, + + /* Callbacks */ + onChange : function(value, text, $selected){}, + onAdd : function(value, text, $selected){}, + onRemove : function(value, text, $selected){}, + + onLabelSelect : function($selectedLabels){}, + onLabelCreate : function(value, text) { return $(this); }, + onLabelRemove : function(value) { return true; }, + onNoResults : function(searchTerm) { return true; }, + onShow : function(){}, + onHide : function(){}, + + /* Component */ + name : 'Dropdown', + namespace : 'dropdown', + + message: { + addResult : 'Add <b>{term}</b>', + count : '{count} selected', + maxSelections : 'Max {maxCount} selections', + noResults : 'No results found.', + serverError : 'There was an error contacting the server' + }, + + error : { + action : 'You called a dropdown action that was not defined', + alreadySetup : 'Once a select has been initialized behaviors must be called on the created ui dropdown', + labels : 'Allowing user additions currently requires the use of labels.', + missingMultiple : '<select> requires multiple property to be set to correctly preserve multiple values', + method : 'The method you called is not defined.', + noAPI : 'The API module is required to load resources remotely', + noStorage : 'Saving remote data requires session storage', + noTransition : 'This module requires ui transitions <https://github.com/Semantic-Org/UI-Transition>' + }, + + regExp : { + escape : /[-[\]{}()*+?.,\\^$|#\s]/g, + quote : /"/g + }, + + metadata : { + defaultText : 'defaultText', + defaultValue : 'defaultValue', + placeholderText : 'placeholder', + text : 'text', + value : 'value' + }, + + // property names for remote query + fields: { + remoteValues : 'results', // grouping for api results + values : 'values', // grouping for all dropdown values + disabled : 'disabled', // whether value should be disabled + name : 'name', // displayed dropdown text + value : 'value', // actual dropdown value + text : 'text' // displayed text when selected + }, + + keys : { + backspace : 8, + delimiter : 188, // comma + deleteKey : 46, + enter : 13, + escape : 27, + pageUp : 33, + pageDown : 34, + leftArrow : 37, + upArrow : 38, + rightArrow : 39, + downArrow : 40 + }, + + selector : { + addition : '.addition', + dropdown : '.ui.dropdown', + hidden : '.hidden', + icon : '> .dropdown.icon', + input : '> input[type="hidden"], > select', + item : '.item', + label : '> .label', + remove : '> .label > .delete.icon', + siblingLabel : '.label', + menu : '.menu', + message : '.message', + menuIcon : '.dropdown.icon', + search : 'input.search, .menu > .search > input, .menu input.search', + sizer : '> input.sizer', + text : '> .text:not(.icon)', + unselectable : '.disabled, .filtered' + }, + + className : { + active : 'active', + addition : 'addition', + animating : 'animating', + disabled : 'disabled', + empty : 'empty', + dropdown : 'ui dropdown', + filtered : 'filtered', + hidden : 'hidden transition', + item : 'item', + label : 'ui label', + loading : 'loading', + menu : 'menu', + message : 'message', + multiple : 'multiple', + placeholder : 'default', + sizer : 'sizer', + search : 'search', + selected : 'selected', + selection : 'selection', + upward : 'upward', + visible : 'visible' + } + +}; + +/* Templates */ +$.fn.dropdown.settings.templates = { + + // generates dropdown from select values + dropdown: function(select) { + var + placeholder = select.placeholder || false, + values = select.values || {}, + html = '' + ; + html += '<i class="dropdown icon"></i>'; + if(select.placeholder) { + html += '<div class="default text">' + placeholder + '</div>'; + } + else { + html += '<div class="text"></div>'; + } + html += '<div class="menu">'; + $.each(select.values, function(index, option) { + html += (option.disabled) + ? '<div class="disabled item" data-value="' + option.value + '">' + option.name + '</div>' + : '<div class="item" data-value="' + option.value + '">' + option.name + '</div>' + ; + }); + html += '</div>'; + return html; + }, + + // generates just menu from select + menu: function(response, fields) { + var + values = response[fields.values] || {}, + html = '' + ; + $.each(values, function(index, option) { + var + maybeText = (option[fields.text]) + ? 'data-text="' + option[fields.text] + '"' + : '', + maybeDisabled = (option[fields.disabled]) + ? 'disabled ' + : '' + ; + html += '<div class="'+ maybeDisabled +'item" data-value="' + option[fields.value] + '"' + maybeText + '>' + html += option[fields.name]; + html += '</div>'; + }); + return html; + }, + + // generates label for multiselect + label: function(value, text) { + return text + '<i class="delete icon"></i>'; + }, + + + // generates messages like "No results" + message: function(message) { + return message; + }, + + // generates user addition to selection menu + addition: function(choice) { + return choice; + } + +}; + +})( jQuery, window, document ); + +/*! + * # Semantic UI 2.2.6 - Embed + * http://github.com/semantic-org/semantic-ui/ + * + * + * Released under the MIT license + * http://opensource.org/licenses/MIT + * + */ + +;(function ($, window, document, undefined) { + +"use strict"; + +window = (typeof window != 'undefined' && window.Math == Math) + ? window + : (typeof self != 'undefined' && self.Math == Math) + ? self + : Function('return this')() +; + +$.fn.embed = function(parameters) { + + var + $allModules = $(this), + + moduleSelector = $allModules.selector || '', + + time = new Date().getTime(), + performance = [], + + query = arguments[0], + methodInvoked = (typeof query == 'string'), + queryArguments = [].slice.call(arguments, 1), + + returnedValue + ; + + $allModules + .each(function() { + var + settings = ( $.isPlainObject(parameters) ) + ? $.extend(true, {}, $.fn.embed.settings, parameters) + : $.extend({}, $.fn.embed.settings), + + selector = settings.selector, + className = settings.className, + sources = settings.sources, + error = settings.error, + metadata = settings.metadata, + namespace = settings.namespace, + templates = settings.templates, + + eventNamespace = '.' + namespace, + moduleNamespace = 'module-' + namespace, + + $window = $(window), + $module = $(this), + $placeholder = $module.find(selector.placeholder), + $icon = $module.find(selector.icon), + $embed = $module.find(selector.embed), + + element = this, + instance = $module.data(moduleNamespace), + module + ; + + module = { + + initialize: function() { + module.debug('Initializing embed'); + module.determine.autoplay(); + module.create(); + module.bind.events(); + module.instantiate(); + }, + + instantiate: function() { + module.verbose('Storing instance of module', module); + instance = module; + $module + .data(moduleNamespace, module) + ; + }, + + destroy: function() { + module.verbose('Destroying previous instance of embed'); + module.reset(); + $module + .removeData(moduleNamespace) + .off(eventNamespace) + ; + }, + + refresh: function() { + module.verbose('Refreshing selector cache'); + $placeholder = $module.find(selector.placeholder); + $icon = $module.find(selector.icon); + $embed = $module.find(selector.embed); + }, + + bind: { + events: function() { + if( module.has.placeholder() ) { + module.debug('Adding placeholder events'); + $module + .on('click' + eventNamespace, selector.placeholder, module.createAndShow) + .on('click' + eventNamespace, selector.icon, module.createAndShow) + ; + } + } + }, + + create: function() { + var + placeholder = module.get.placeholder() + ; + if(placeholder) { + module.createPlaceholder(); + } + else { + module.createAndShow(); + } + }, + + createPlaceholder: function(placeholder) { + var + icon = module.get.icon(), + url = module.get.url(), + embed = module.generate.embed(url) + ; + placeholder = placeholder || module.get.placeholder(); + $module.html( templates.placeholder(placeholder, icon) ); + module.debug('Creating placeholder for embed', placeholder, icon); + }, + + createEmbed: function(url) { + module.refresh(); + url = url || module.get.url(); + $embed = $('<div/>') + .addClass(className.embed) + .html( module.generate.embed(url) ) + .appendTo($module) + ; + settings.onCreate.call(element, url); + module.debug('Creating embed object', $embed); + }, + + changeEmbed: function(url) { + $embed + .html( module.generate.embed(url) ) + ; + }, + + createAndShow: function() { + module.createEmbed(); + module.show(); + }, + + // sets new embed + change: function(source, id, url) { + module.debug('Changing video to ', source, id, url); + $module + .data(metadata.source, source) + .data(metadata.id, id) + ; + if(url) { + $module.data(metadata.url, url); + } + else { + $module.removeData(metadata.url); + } + if(module.has.embed()) { + module.changeEmbed(); + } + else { + module.create(); + } + }, + + // clears embed + reset: function() { + module.debug('Clearing embed and showing placeholder'); + module.remove.active(); + module.remove.embed(); + module.showPlaceholder(); + settings.onReset.call(element); + }, + + // shows current embed + show: function() { + module.debug('Showing embed'); + module.set.active(); + settings.onDisplay.call(element); + }, + + hide: function() { + module.debug('Hiding embed'); + module.showPlaceholder(); + }, + + showPlaceholder: function() { + module.debug('Showing placeholder image'); + module.remove.active(); + settings.onPlaceholderDisplay.call(element); + }, + + get: { + id: function() { + return settings.id || $module.data(metadata.id); + }, + placeholder: function() { + return settings.placeholder || $module.data(metadata.placeholder); + }, + icon: function() { + return (settings.icon) + ? settings.icon + : ($module.data(metadata.icon) !== undefined) + ? $module.data(metadata.icon) + : module.determine.icon() + ; + }, + source: function(url) { + return (settings.source) + ? settings.source + : ($module.data(metadata.source) !== undefined) + ? $module.data(metadata.source) + : module.determine.source() + ; + }, + type: function() { + var source = module.get.source(); + return (sources[source] !== undefined) + ? sources[source].type + : false + ; + }, + url: function() { + return (settings.url) + ? settings.url + : ($module.data(metadata.url) !== undefined) + ? $module.data(metadata.url) + : module.determine.url() + ; + } + }, + + determine: { + autoplay: function() { + if(module.should.autoplay()) { + settings.autoplay = true; + } + }, + source: function(url) { + var + matchedSource = false + ; + url = url || module.get.url(); + if(url) { + $.each(sources, function(name, source) { + if(url.search(source.domain) !== -1) { + matchedSource = name; + return false; + } + }); + } + return matchedSource; + }, + icon: function() { + var + source = module.get.source() + ; + return (sources[source] !== undefined) + ? sources[source].icon + : false + ; + }, + url: function() { + var + id = settings.id || $module.data(metadata.id), + source = settings.source || $module.data(metadata.source), + url + ; + url = (sources[source] !== undefined) + ? sources[source].url.replace('{id}', id) + : false + ; + if(url) { + $module.data(metadata.url, url); + } + return url; + } + }, + + + set: { + active: function() { + $module.addClass(className.active); + } + }, + + remove: { + active: function() { + $module.removeClass(className.active); + }, + embed: function() { + $embed.empty(); + } + }, + + encode: { + parameters: function(parameters) { + var + urlString = [], + index + ; + for (index in parameters) { + urlString.push( encodeURIComponent(index) + '=' + encodeURIComponent( parameters[index] ) ); + } + return urlString.join('&'); + } + }, + + generate: { + embed: function(url) { + module.debug('Generating embed html'); + var + source = module.get.source(), + html, + parameters + ; + url = module.get.url(url); + if(url) { + parameters = module.generate.parameters(source); + html = templates.iframe(url, parameters); + } + else { + module.error(error.noURL, $module); + } + return html; + }, + parameters: function(source, extraParameters) { + var + parameters = (sources[source] && sources[source].parameters !== undefined) + ? sources[source].parameters(settings) + : {} + ; + extraParameters = extraParameters || settings.parameters; + if(extraParameters) { + parameters = $.extend({}, parameters, extraParameters); + } + parameters = settings.onEmbed(parameters); + return module.encode.parameters(parameters); + } + }, + + has: { + embed: function() { + return ($embed.length > 0); + }, + placeholder: function() { + return settings.placeholder || $module.data(metadata.placeholder); + } + }, + + should: { + autoplay: function() { + return (settings.autoplay === 'auto') + ? (settings.placeholder || $module.data(metadata.placeholder) !== undefined) + : settings.autoplay + ; + } + }, + + is: { + video: function() { + return module.get.type() == 'video'; + } + }, + + setting: function(name, value) { + module.debug('Changing setting', name, value); + if( $.isPlainObject(name) ) { + $.extend(true, settings, name); + } + else if(value !== undefined) { + if($.isPlainObject(settings[name])) { + $.extend(true, settings[name], value); + } + else { + settings[name] = value; + } + } + else { + return settings[name]; + } + }, + internal: function(name, value) { + if( $.isPlainObject(name) ) { + $.extend(true, module, name); + } + else if(value !== undefined) { + module[name] = value; + } + else { + return module[name]; + } + }, + debug: function() { + if(!settings.silent && settings.debug) { + if(settings.performance) { + module.performance.log(arguments); + } + else { + module.debug = Function.prototype.bind.call(console.info, console, settings.name + ':'); + module.debug.apply(console, arguments); + } + } + }, + verbose: function() { + if(!settings.silent && settings.verbose && settings.debug) { + if(settings.performance) { + module.performance.log(arguments); + } + else { + module.verbose = Function.prototype.bind.call(console.info, console, settings.name + ':'); + module.verbose.apply(console, arguments); + } + } + }, + error: function() { + if(!settings.silent) { + module.error = Function.prototype.bind.call(console.error, console, settings.name + ':'); + module.error.apply(console, arguments); + } + }, + performance: { + log: function(message) { + var + currentTime, + executionTime, + previousTime + ; + if(settings.performance) { + currentTime = new Date().getTime(); + previousTime = time || currentTime; + executionTime = currentTime - previousTime; + time = currentTime; + performance.push({ + 'Name' : message[0], + 'Arguments' : [].slice.call(message, 1) || '', + 'Element' : element, + 'Execution Time' : executionTime + }); + } + clearTimeout(module.performance.timer); + module.performance.timer = setTimeout(module.performance.display, 500); + }, + display: function() { + var + title = settings.name + ':', + totalTime = 0 + ; + time = false; + clearTimeout(module.performance.timer); + $.each(performance, function(index, data) { + totalTime += data['Execution Time']; + }); + title += ' ' + totalTime + 'ms'; + if(moduleSelector) { + title += ' \'' + moduleSelector + '\''; + } + if($allModules.length > 1) { + title += ' ' + '(' + $allModules.length + ')'; + } + if( (console.group !== undefined || console.table !== undefined) && performance.length > 0) { + console.groupCollapsed(title); + if(console.table) { + console.table(performance); + } + else { + $.each(performance, function(index, data) { + console.log(data['Name'] + ': ' + data['Execution Time']+'ms'); + }); + } + console.groupEnd(); + } + performance = []; + } + }, + invoke: function(query, passedArguments, context) { + var + object = instance, + maxDepth, + found, + response + ; + passedArguments = passedArguments || queryArguments; + context = element || context; + if(typeof query == 'string' && object !== undefined) { + query = query.split(/[\. ]/); + maxDepth = query.length - 1; + $.each(query, function(depth, value) { + var camelCaseValue = (depth != maxDepth) + ? value + query[depth + 1].charAt(0).toUpperCase() + query[depth + 1].slice(1) + : query + ; + if( $.isPlainObject( object[camelCaseValue] ) && (depth != maxDepth) ) { + object = object[camelCaseValue]; + } + else if( object[camelCaseValue] !== undefined ) { + found = object[camelCaseValue]; + return false; + } + else if( $.isPlainObject( object[value] ) && (depth != maxDepth) ) { + object = object[value]; + } + else if( object[value] !== undefined ) { + found = object[value]; + return false; + } + else { + module.error(error.method, query); + return false; + } + }); + } + if ( $.isFunction( found ) ) { + response = found.apply(context, passedArguments); + } + else if(found !== undefined) { + response = found; + } + if($.isArray(returnedValue)) { + returnedValue.push(response); + } + else if(returnedValue !== undefined) { + returnedValue = [returnedValue, response]; + } + else if(response !== undefined) { + returnedValue = response; + } + return found; + } + }; + + if(methodInvoked) { + if(instance === undefined) { + module.initialize(); + } + module.invoke(query); + } + else { + if(instance !== undefined) { + instance.invoke('destroy'); + } + module.initialize(); + } + }) + ; + return (returnedValue !== undefined) + ? returnedValue + : this + ; +}; + +$.fn.embed.settings = { + + name : 'Embed', + namespace : 'embed', + + silent : false, + debug : false, + verbose : false, + performance : true, + + icon : false, + source : false, + url : false, + id : false, + + // standard video settings + autoplay : 'auto', + color : '#444444', + hd : true, + brandedUI : false, + + // additional parameters to include with the embed + parameters: false, + + onDisplay : function() {}, + onPlaceholderDisplay : function() {}, + onReset : function() {}, + onCreate : function(url) {}, + onEmbed : function(parameters) { + return parameters; + }, + + metadata : { + id : 'id', + icon : 'icon', + placeholder : 'placeholder', + source : 'source', + url : 'url' + }, + + error : { + noURL : 'No URL specified', + method : 'The method you called is not defined' + }, + + className : { + active : 'active', + embed : 'embed' + }, + + selector : { + embed : '.embed', + placeholder : '.placeholder', + icon : '.icon' + }, + + sources: { + youtube: { + name : 'youtube', + type : 'video', + icon : 'video play', + domain : 'youtube.com', + url : '//www.youtube.com/embed/{id}', + parameters: function(settings) { + return { + autohide : !settings.brandedUI, + autoplay : settings.autoplay, + color : settings.color || undefined, + hq : settings.hd, + jsapi : settings.api, + modestbranding : !settings.brandedUI + }; + } + }, + vimeo: { + name : 'vimeo', + type : 'video', + icon : 'video play', + domain : 'vimeo.com', + url : '//player.vimeo.com/video/{id}', + parameters: function(settings) { + return { + api : settings.api, + autoplay : settings.autoplay, + byline : settings.brandedUI, + color : settings.color || undefined, + portrait : settings.brandedUI, + title : settings.brandedUI + }; + } + } + }, + + templates: { + iframe : function(url, parameters) { + var src = url; + if (parameters) { + src += '?' + parameters; + } + return '' + + '<iframe src="' + src + '"' + + ' width="100%" height="100%"' + + ' frameborder="0" scrolling="no" webkitAllowFullScreen mozallowfullscreen allowFullScreen></iframe>' + ; + }, + placeholder : function(image, icon) { + var + html = '' + ; + if(icon) { + html += '<i class="' + icon + ' icon"></i>'; + } + if(image) { + html += '<img class="placeholder" src="' + image + '">'; + } + return html; + } + }, + + // NOT YET IMPLEMENTED + api : false, + onPause : function() {}, + onPlay : function() {}, + onStop : function() {} + +}; + + + +})( jQuery, window, document ); + +/*! + * # Semantic UI 2.2.6 - Modal + * http://github.com/semantic-org/semantic-ui/ + * + * + * Released under the MIT license + * http://opensource.org/licenses/MIT + * + */ + +;(function ($, window, document, undefined) { + +"use strict"; + +window = (typeof window != 'undefined' && window.Math == Math) + ? window + : (typeof self != 'undefined' && self.Math == Math) + ? self + : Function('return this')() +; + +$.fn.modal = function(parameters) { + var + $allModules = $(this), + $window = $(window), + $document = $(document), + $body = $('body'), + + moduleSelector = $allModules.selector || '', + + time = new Date().getTime(), + performance = [], + + query = arguments[0], + methodInvoked = (typeof query == 'string'), + queryArguments = [].slice.call(arguments, 1), + + requestAnimationFrame = window.requestAnimationFrame + || window.mozRequestAnimationFrame + || window.webkitRequestAnimationFrame + || window.msRequestAnimationFrame + || function(callback) { setTimeout(callback, 0); }, + + returnedValue + ; + + $allModules + .each(function() { + var + settings = ( $.isPlainObject(parameters) ) + ? $.extend(true, {}, $.fn.modal.settings, parameters) + : $.extend({}, $.fn.modal.settings), + + selector = settings.selector, + className = settings.className, + namespace = settings.namespace, + error = settings.error, + + eventNamespace = '.' + namespace, + moduleNamespace = 'module-' + namespace, + + $module = $(this), + $context = $(settings.context), + $close = $module.find(selector.close), + + $allModals, + $otherModals, + $focusedElement, + $dimmable, + $dimmer, + + element = this, + instance = $module.data(moduleNamespace), + + elementEventNamespace, + id, + observer, + module + ; + module = { + + initialize: function() { + module.verbose('Initializing dimmer', $context); + + module.create.id(); + module.create.dimmer(); + module.refreshModals(); + + module.bind.events(); + if(settings.observeChanges) { + module.observeChanges(); + } + module.instantiate(); + }, + + instantiate: function() { + module.verbose('Storing instance of modal'); + instance = module; + $module + .data(moduleNamespace, instance) + ; + }, + + create: { + dimmer: function() { + var + defaultSettings = { + debug : settings.debug, + dimmerName : 'modals', + duration : { + show : settings.duration, + hide : settings.duration + } + }, + dimmerSettings = $.extend(true, defaultSettings, settings.dimmerSettings) + ; + if(settings.inverted) { + dimmerSettings.variation = (dimmerSettings.variation !== undefined) + ? dimmerSettings.variation + ' inverted' + : 'inverted' + ; + } + if($.fn.dimmer === undefined) { + module.error(error.dimmer); + return; + } + module.debug('Creating dimmer with settings', dimmerSettings); + $dimmable = $context.dimmer(dimmerSettings); + if(settings.detachable) { + module.verbose('Modal is detachable, moving content into dimmer'); + $dimmable.dimmer('add content', $module); + } + else { + module.set.undetached(); + } + if(settings.blurring) { + $dimmable.addClass(className.blurring); + } + $dimmer = $dimmable.dimmer('get dimmer'); + }, + id: function() { + id = (Math.random().toString(16) + '000000000').substr(2,8); + elementEventNamespace = '.' + id; + module.verbose('Creating unique id for element', id); + } + }, + + destroy: function() { + module.verbose('Destroying previous modal'); + $module + .removeData(moduleNamespace) + .off(eventNamespace) + ; + $window.off(elementEventNamespace); + $dimmer.off(elementEventNamespace); + $close.off(eventNamespace); + $context.dimmer('destroy'); + }, + + observeChanges: function() { + if('MutationObserver' in window) { + observer = new MutationObserver(function(mutations) { + module.debug('DOM tree modified, refreshing'); + module.refresh(); + }); + observer.observe(element, { + childList : true, + subtree : true + }); + module.debug('Setting up mutation observer', observer); + } + }, + + refresh: function() { + module.remove.scrolling(); + module.cacheSizes(); + module.set.screenHeight(); + module.set.type(); + module.set.position(); + }, + + refreshModals: function() { + $otherModals = $module.siblings(selector.modal); + $allModals = $otherModals.add($module); + }, + + attachEvents: function(selector, event) { + var + $toggle = $(selector) + ; + event = $.isFunction(module[event]) + ? module[event] + : module.toggle + ; + if($toggle.length > 0) { + module.debug('Attaching modal events to element', selector, event); + $toggle + .off(eventNamespace) + .on('click' + eventNamespace, event) + ; + } + else { + module.error(error.notFound, selector); + } + }, + + bind: { + events: function() { + module.verbose('Attaching events'); + $module + .on('click' + eventNamespace, selector.close, module.event.close) + .on('click' + eventNamespace, selector.approve, module.event.approve) + .on('click' + eventNamespace, selector.deny, module.event.deny) + ; + $window + .on('resize' + elementEventNamespace, module.event.resize) + ; + } + }, + + get: { + id: function() { + return (Math.random().toString(16) + '000000000').substr(2,8); + } + }, + + event: { + approve: function() { + if(settings.onApprove.call(element, $(this)) === false) { + module.verbose('Approve callback returned false cancelling hide'); + return; + } + module.hide(); + }, + deny: function() { + if(settings.onDeny.call(element, $(this)) === false) { + module.verbose('Deny callback returned false cancelling hide'); + return; + } + module.hide(); + }, + close: function() { + module.hide(); + }, + click: function(event) { + var + $target = $(event.target), + isInModal = ($target.closest(selector.modal).length > 0), + isInDOM = $.contains(document.documentElement, event.target) + ; + if(!isInModal && isInDOM) { + module.debug('Dimmer clicked, hiding all modals'); + if( module.is.active() ) { + module.remove.clickaway(); + if(settings.allowMultiple) { + module.hide(); + } + else { + module.hideAll(); + } + } + } + }, + debounce: function(method, delay) { + clearTimeout(module.timer); + module.timer = setTimeout(method, delay); + }, + keyboard: function(event) { + var + keyCode = event.which, + escapeKey = 27 + ; + if(keyCode == escapeKey) { + if(settings.closable) { + module.debug('Escape key pressed hiding modal'); + module.hide(); + } + else { + module.debug('Escape key pressed, but closable is set to false'); + } + event.preventDefault(); + } + }, + resize: function() { + if( $dimmable.dimmer('is active') ) { + requestAnimationFrame(module.refresh); + } + } + }, + + toggle: function() { + if( module.is.active() || module.is.animating() ) { + module.hide(); + } + else { + module.show(); + } + }, + + show: function(callback) { + callback = $.isFunction(callback) + ? callback + : function(){} + ; + module.refreshModals(); + module.showModal(callback); + }, + + hide: function(callback) { + callback = $.isFunction(callback) + ? callback + : function(){} + ; + module.refreshModals(); + module.hideModal(callback); + }, + + showModal: function(callback) { + callback = $.isFunction(callback) + ? callback + : function(){} + ; + if( module.is.animating() || !module.is.active() ) { + + module.showDimmer(); + module.cacheSizes(); + module.set.position(); + module.set.screenHeight(); + module.set.type(); + module.set.clickaway(); + + if( !settings.allowMultiple && module.others.active() ) { + module.hideOthers(module.showModal); + } + else { + settings.onShow.call(element); + if(settings.transition && $.fn.transition !== undefined && $module.transition('is supported')) { + module.debug('Showing modal with css animations'); + $module + .transition({ + debug : settings.debug, + animation : settings.transition + ' in', + queue : settings.queue, + duration : settings.duration, + useFailSafe : true, + onComplete : function() { + settings.onVisible.apply(element); + if(settings.keyboardShortcuts) { + module.add.keyboardShortcuts(); + } + module.save.focus(); + module.set.active(); + if(settings.autofocus) { + module.set.autofocus(); + } + callback(); + } + }) + ; + } + else { + module.error(error.noTransition); + } + } + } + else { + module.debug('Modal is already visible'); + } + }, + + hideModal: function(callback, keepDimmed) { + callback = $.isFunction(callback) + ? callback + : function(){} + ; + module.debug('Hiding modal'); + if(settings.onHide.call(element, $(this)) === false) { + module.verbose('Hide callback returned false cancelling hide'); + return; + } + + if( module.is.animating() || module.is.active() ) { + if(settings.transition && $.fn.transition !== undefined && $module.transition('is supported')) { + module.remove.active(); + $module + .transition({ + debug : settings.debug, + animation : settings.transition + ' out', + queue : settings.queue, + duration : settings.duration, + useFailSafe : true, + onStart : function() { + if(!module.others.active() && !keepDimmed) { + module.hideDimmer(); + } + if(settings.keyboardShortcuts) { + module.remove.keyboardShortcuts(); + } + }, + onComplete : function() { + settings.onHidden.call(element); + module.restore.focus(); + callback(); + } + }) + ; + } + else { + module.error(error.noTransition); + } + } + }, + + showDimmer: function() { + if($dimmable.dimmer('is animating') || !$dimmable.dimmer('is active') ) { + module.debug('Showing dimmer'); + $dimmable.dimmer('show'); + } + else { + module.debug('Dimmer already visible'); + } + }, + + hideDimmer: function() { + if( $dimmable.dimmer('is animating') || ($dimmable.dimmer('is active')) ) { + $dimmable.dimmer('hide', function() { + module.remove.clickaway(); + module.remove.screenHeight(); + }); + } + else { + module.debug('Dimmer is not visible cannot hide'); + return; + } + }, + + hideAll: function(callback) { + var + $visibleModals = $allModals.filter('.' + className.active + ', .' + className.animating) + ; + callback = $.isFunction(callback) + ? callback + : function(){} + ; + if( $visibleModals.length > 0 ) { + module.debug('Hiding all visible modals'); + module.hideDimmer(); + $visibleModals + .modal('hide modal', callback) + ; + } + }, + + hideOthers: function(callback) { + var + $visibleModals = $otherModals.filter('.' + className.active + ', .' + className.animating) + ; + callback = $.isFunction(callback) + ? callback + : function(){} + ; + if( $visibleModals.length > 0 ) { + module.debug('Hiding other modals', $otherModals); + $visibleModals + .modal('hide modal', callback, true) + ; + } + }, + + others: { + active: function() { + return ($otherModals.filter('.' + className.active).length > 0); + }, + animating: function() { + return ($otherModals.filter('.' + className.animating).length > 0); + } + }, + + + add: { + keyboardShortcuts: function() { + module.verbose('Adding keyboard shortcuts'); + $document + .on('keyup' + eventNamespace, module.event.keyboard) + ; + } + }, + + save: { + focus: function() { + $focusedElement = $(document.activeElement).blur(); + } + }, + + restore: { + focus: function() { + if($focusedElement && $focusedElement.length > 0) { + $focusedElement.focus(); + } + } + }, + + remove: { + active: function() { + $module.removeClass(className.active); + }, + clickaway: function() { + if(settings.closable) { + $dimmer + .off('click' + elementEventNamespace) + ; + } + }, + bodyStyle: function() { + if($body.attr('style') === '') { + module.verbose('Removing style attribute'); + $body.removeAttr('style'); + } + }, + screenHeight: function() { + module.debug('Removing page height'); + $body + .css('height', '') + ; + }, + keyboardShortcuts: function() { + module.verbose('Removing keyboard shortcuts'); + $document + .off('keyup' + eventNamespace) + ; + }, + scrolling: function() { + $dimmable.removeClass(className.scrolling); + $module.removeClass(className.scrolling); + } + }, + + cacheSizes: function() { + var + modalHeight = $module.outerHeight() + ; + if(module.cache === undefined || modalHeight !== 0) { + module.cache = { + pageHeight : $(document).outerHeight(), + height : modalHeight + settings.offset, + contextHeight : (settings.context == 'body') + ? $(window).height() + : $dimmable.height() + }; + } + module.debug('Caching modal and container sizes', module.cache); + }, + + can: { + fit: function() { + return ( ( module.cache.height + (settings.padding * 2) ) < module.cache.contextHeight); + } + }, + + is: { + active: function() { + return $module.hasClass(className.active); + }, + animating: function() { + return $module.transition('is supported') + ? $module.transition('is animating') + : $module.is(':visible') + ; + }, + scrolling: function() { + return $dimmable.hasClass(className.scrolling); + }, + modernBrowser: function() { + // appName for IE11 reports 'Netscape' can no longer use + return !(window.ActiveXObject || "ActiveXObject" in window); + } + }, + + set: { + autofocus: function() { + var + $inputs = $module.find('[tabindex], :input').filter(':visible'), + $autofocus = $inputs.filter('[autofocus]'), + $input = ($autofocus.length > 0) + ? $autofocus.first() + : $inputs.first() + ; + if($input.length > 0) { + $input.focus(); + } + }, + clickaway: function() { + if(settings.closable) { + $dimmer + .on('click' + elementEventNamespace, module.event.click) + ; + } + }, + screenHeight: function() { + if( module.can.fit() ) { + $body.css('height', ''); + } + else { + module.debug('Modal is taller than page content, resizing page height'); + $body + .css('height', module.cache.height + (settings.padding * 2) ) + ; + } + }, + active: function() { + $module.addClass(className.active); + }, + scrolling: function() { + $dimmable.addClass(className.scrolling); + $module.addClass(className.scrolling); + }, + type: function() { + if(module.can.fit()) { + module.verbose('Modal fits on screen'); + if(!module.others.active() && !module.others.animating()) { + module.remove.scrolling(); + } + } + else { + module.verbose('Modal cannot fit on screen setting to scrolling'); + module.set.scrolling(); + } + }, + position: function() { + module.verbose('Centering modal on page', module.cache); + if(module.can.fit()) { + $module + .css({ + top: '', + marginTop: -(module.cache.height / 2) + }) + ; + } + else { + $module + .css({ + marginTop : '', + top : $document.scrollTop() + }) + ; + } + }, + undetached: function() { + $dimmable.addClass(className.undetached); + } + }, + + setting: function(name, value) { + module.debug('Changing setting', name, value); + if( $.isPlainObject(name) ) { + $.extend(true, settings, name); + } + else if(value !== undefined) { + if($.isPlainObject(settings[name])) { + $.extend(true, settings[name], value); + } + else { + settings[name] = value; + } + } + else { + return settings[name]; + } + }, + internal: function(name, value) { + if( $.isPlainObject(name) ) { + $.extend(true, module, name); + } + else if(value !== undefined) { + module[name] = value; + } + else { + return module[name]; + } + }, + debug: function() { + if(!settings.silent && settings.debug) { + if(settings.performance) { + module.performance.log(arguments); + } + else { + module.debug = Function.prototype.bind.call(console.info, console, settings.name + ':'); + module.debug.apply(console, arguments); + } + } + }, + verbose: function() { + if(!settings.silent && settings.verbose && settings.debug) { + if(settings.performance) { + module.performance.log(arguments); + } + else { + module.verbose = Function.prototype.bind.call(console.info, console, settings.name + ':'); + module.verbose.apply(console, arguments); + } + } + }, + error: function() { + if(!settings.silent) { + module.error = Function.prototype.bind.call(console.error, console, settings.name + ':'); + module.error.apply(console, arguments); + } + }, + performance: { + log: function(message) { + var + currentTime, + executionTime, + previousTime + ; + if(settings.performance) { + currentTime = new Date().getTime(); + previousTime = time || currentTime; + executionTime = currentTime - previousTime; + time = currentTime; + performance.push({ + 'Name' : message[0], + 'Arguments' : [].slice.call(message, 1) || '', + 'Element' : element, + 'Execution Time' : executionTime + }); + } + clearTimeout(module.performance.timer); + module.performance.timer = setTimeout(module.performance.display, 500); + }, + display: function() { + var + title = settings.name + ':', + totalTime = 0 + ; + time = false; + clearTimeout(module.performance.timer); + $.each(performance, function(index, data) { + totalTime += data['Execution Time']; + }); + title += ' ' + totalTime + 'ms'; + if(moduleSelector) { + title += ' \'' + moduleSelector + '\''; + } + if( (console.group !== undefined || console.table !== undefined) && performance.length > 0) { + console.groupCollapsed(title); + if(console.table) { + console.table(performance); + } + else { + $.each(performance, function(index, data) { + console.log(data['Name'] + ': ' + data['Execution Time']+'ms'); + }); + } + console.groupEnd(); + } + performance = []; + } + }, + invoke: function(query, passedArguments, context) { + var + object = instance, + maxDepth, + found, + response + ; + passedArguments = passedArguments || queryArguments; + context = element || context; + if(typeof query == 'string' && object !== undefined) { + query = query.split(/[\. ]/); + maxDepth = query.length - 1; + $.each(query, function(depth, value) { + var camelCaseValue = (depth != maxDepth) + ? value + query[depth + 1].charAt(0).toUpperCase() + query[depth + 1].slice(1) + : query + ; + if( $.isPlainObject( object[camelCaseValue] ) && (depth != maxDepth) ) { + object = object[camelCaseValue]; + } + else if( object[camelCaseValue] !== undefined ) { + found = object[camelCaseValue]; + return false; + } + else if( $.isPlainObject( object[value] ) && (depth != maxDepth) ) { + object = object[value]; + } + else if( object[value] !== undefined ) { + found = object[value]; + return false; + } + else { + return false; + } + }); + } + if ( $.isFunction( found ) ) { + response = found.apply(context, passedArguments); + } + else if(found !== undefined) { + response = found; + } + if($.isArray(returnedValue)) { + returnedValue.push(response); + } + else if(returnedValue !== undefined) { + returnedValue = [returnedValue, response]; + } + else if(response !== undefined) { + returnedValue = response; + } + return found; + } + }; + + if(methodInvoked) { + if(instance === undefined) { + module.initialize(); + } + module.invoke(query); + } + else { + if(instance !== undefined) { + instance.invoke('destroy'); + } + module.initialize(); + } + }) + ; + + return (returnedValue !== undefined) + ? returnedValue + : this + ; +}; + +$.fn.modal.settings = { + + name : 'Modal', + namespace : 'modal', + + silent : false, + debug : false, + verbose : false, + performance : true, + + observeChanges : false, + + allowMultiple : false, + detachable : true, + closable : true, + autofocus : true, + + inverted : false, + blurring : false, + + dimmerSettings : { + closable : false, + useCSS : true + }, + + // whether to use keyboard shortcuts + keyboardShortcuts: true, + + context : 'body', + + queue : false, + duration : 500, + offset : 0, + transition : 'scale', + + // padding with edge of page + padding : 50, + + // called before show animation + onShow : function(){}, + + // called after show animation + onVisible : function(){}, + + // called before hide animation + onHide : function(){ return true; }, + + // called after hide animation + onHidden : function(){}, + + // called after approve selector match + onApprove : function(){ return true; }, + + // called after deny selector match + onDeny : function(){ return true; }, + + selector : { + close : '> .close', + approve : '.actions .positive, .actions .approve, .actions .ok', + deny : '.actions .negative, .actions .deny, .actions .cancel', + modal : '.ui.modal' + }, + error : { + dimmer : 'UI Dimmer, a required component is not included in this page', + method : 'The method you called is not defined.', + notFound : 'The element you specified could not be found' + }, + className : { + active : 'active', + animating : 'animating', + blurring : 'blurring', + scrolling : 'scrolling', + undetached : 'undetached' + } +}; + + +})( jQuery, window, document ); + +/*! + * # Semantic UI 2.2.6 - Nag + * http://github.com/semantic-org/semantic-ui/ + * + * + * Released under the MIT license + * http://opensource.org/licenses/MIT + * + */ + +;(function ($, window, document, undefined) { + +"use strict"; + +window = (typeof window != 'undefined' && window.Math == Math) + ? window + : (typeof self != 'undefined' && self.Math == Math) + ? self + : Function('return this')() +; + +$.fn.nag = function(parameters) { + var + $allModules = $(this), + moduleSelector = $allModules.selector || '', + + time = new Date().getTime(), + performance = [], + + query = arguments[0], + methodInvoked = (typeof query == 'string'), + queryArguments = [].slice.call(arguments, 1), + returnedValue + ; + $allModules + .each(function() { + var + settings = ( $.isPlainObject(parameters) ) + ? $.extend(true, {}, $.fn.nag.settings, parameters) + : $.extend({}, $.fn.nag.settings), + + className = settings.className, + selector = settings.selector, + error = settings.error, + namespace = settings.namespace, + + eventNamespace = '.' + namespace, + moduleNamespace = namespace + '-module', + + $module = $(this), + + $close = $module.find(selector.close), + $context = (settings.context) + ? $(settings.context) + : $('body'), + + element = this, + instance = $module.data(moduleNamespace), + + moduleOffset, + moduleHeight, + + contextWidth, + contextHeight, + contextOffset, + + yOffset, + yPosition, + + timer, + module, + + requestAnimationFrame = window.requestAnimationFrame + || window.mozRequestAnimationFrame + || window.webkitRequestAnimationFrame + || window.msRequestAnimationFrame + || function(callback) { setTimeout(callback, 0); } + ; + module = { + + initialize: function() { + module.verbose('Initializing element'); + + $module + .on('click' + eventNamespace, selector.close, module.dismiss) + .data(moduleNamespace, module) + ; + + if(settings.detachable && $module.parent()[0] !== $context[0]) { + $module + .detach() + .prependTo($context) + ; + } + + if(settings.displayTime > 0) { + setTimeout(module.hide, settings.displayTime); + } + module.show(); + }, + + destroy: function() { + module.verbose('Destroying instance'); + $module + .removeData(moduleNamespace) + .off(eventNamespace) + ; + }, + + show: function() { + if( module.should.show() && !$module.is(':visible') ) { + module.debug('Showing nag', settings.animation.show); + if(settings.animation.show == 'fade') { + $module + .fadeIn(settings.duration, settings.easing) + ; + } + else { + $module + .slideDown(settings.duration, settings.easing) + ; + } + } + }, + + hide: function() { + module.debug('Showing nag', settings.animation.hide); + if(settings.animation.show == 'fade') { + $module + .fadeIn(settings.duration, settings.easing) + ; + } + else { + $module + .slideUp(settings.duration, settings.easing) + ; + } + }, + + onHide: function() { + module.debug('Removing nag', settings.animation.hide); + $module.remove(); + if (settings.onHide) { + settings.onHide(); + } + }, + + dismiss: function(event) { + if(settings.storageMethod) { + module.storage.set(settings.key, settings.value); + } + module.hide(); + event.stopImmediatePropagation(); + event.preventDefault(); + }, + + should: { + show: function() { + if(settings.persist) { + module.debug('Persistent nag is set, can show nag'); + return true; + } + if( module.storage.get(settings.key) != settings.value.toString() ) { + module.debug('Stored value is not set, can show nag', module.storage.get(settings.key)); + return true; + } + module.debug('Stored value is set, cannot show nag', module.storage.get(settings.key)); + return false; + } + }, + + get: { + storageOptions: function() { + var + options = {} + ; + if(settings.expires) { + options.expires = settings.expires; + } + if(settings.domain) { + options.domain = settings.domain; + } + if(settings.path) { + options.path = settings.path; + } + return options; + } + }, + + clear: function() { + module.storage.remove(settings.key); + }, + + storage: { + set: function(key, value) { + var + options = module.get.storageOptions() + ; + if(settings.storageMethod == 'localstorage' && window.localStorage !== undefined) { + window.localStorage.setItem(key, value); + module.debug('Value stored using local storage', key, value); + } + else if(settings.storageMethod == 'sessionstorage' && window.sessionStorage !== undefined) { + window.sessionStorage.setItem(key, value); + module.debug('Value stored using session storage', key, value); + } + else if($.cookie !== undefined) { + $.cookie(key, value, options); + module.debug('Value stored using cookie', key, value, options); + } + else { + module.error(error.noCookieStorage); + return; + } + }, + get: function(key, value) { + var + storedValue + ; + if(settings.storageMethod == 'localstorage' && window.localStorage !== undefined) { + storedValue = window.localStorage.getItem(key); + } + else if(settings.storageMethod == 'sessionstorage' && window.sessionStorage !== undefined) { + storedValue = window.sessionStorage.getItem(key); + } + // get by cookie + else if($.cookie !== undefined) { + storedValue = $.cookie(key); + } + else { + module.error(error.noCookieStorage); + } + if(storedValue == 'undefined' || storedValue == 'null' || storedValue === undefined || storedValue === null) { + storedValue = undefined; + } + return storedValue; + }, + remove: function(key) { + var + options = module.get.storageOptions() + ; + if(settings.storageMethod == 'localstorage' && window.localStorage !== undefined) { + window.localStorage.removeItem(key); + } + else if(settings.storageMethod == 'sessionstorage' && window.sessionStorage !== undefined) { + window.sessionStorage.removeItem(key); + } + // store by cookie + else if($.cookie !== undefined) { + $.removeCookie(key, options); + } + else { + module.error(error.noStorage); + } + } + }, + + setting: function(name, value) { + module.debug('Changing setting', name, value); + if( $.isPlainObject(name) ) { + $.extend(true, settings, name); + } + else if(value !== undefined) { + if($.isPlainObject(settings[name])) { + $.extend(true, settings[name], value); + } + else { + settings[name] = value; + } + } + else { + return settings[name]; + } + }, + internal: function(name, value) { + if( $.isPlainObject(name) ) { + $.extend(true, module, name); + } + else if(value !== undefined) { + module[name] = value; + } + else { + return module[name]; + } + }, + debug: function() { + if(!settings.silent && settings.debug) { + if(settings.performance) { + module.performance.log(arguments); + } + else { + module.debug = Function.prototype.bind.call(console.info, console, settings.name + ':'); + module.debug.apply(console, arguments); + } + } + }, + verbose: function() { + if(!settings.silent && settings.verbose && settings.debug) { + if(settings.performance) { + module.performance.log(arguments); + } + else { + module.verbose = Function.prototype.bind.call(console.info, console, settings.name + ':'); + module.verbose.apply(console, arguments); + } + } + }, + error: function() { + if(!settings.silent) { + module.error = Function.prototype.bind.call(console.error, console, settings.name + ':'); + module.error.apply(console, arguments); + } + }, + performance: { + log: function(message) { + var + currentTime, + executionTime, + previousTime + ; + if(settings.performance) { + currentTime = new Date().getTime(); + previousTime = time || currentTime; + executionTime = currentTime - previousTime; + time = currentTime; + performance.push({ + 'Name' : message[0], + 'Arguments' : [].slice.call(message, 1) || '', + 'Element' : element, + 'Execution Time' : executionTime + }); + } + clearTimeout(module.performance.timer); + module.performance.timer = setTimeout(module.performance.display, 500); + }, + display: function() { + var + title = settings.name + ':', + totalTime = 0 + ; + time = false; + clearTimeout(module.performance.timer); + $.each(performance, function(index, data) { + totalTime += data['Execution Time']; + }); + title += ' ' + totalTime + 'ms'; + if(moduleSelector) { + title += ' \'' + moduleSelector + '\''; + } + if( (console.group !== undefined || console.table !== undefined) && performance.length > 0) { + console.groupCollapsed(title); + if(console.table) { + console.table(performance); + } + else { + $.each(performance, function(index, data) { + console.log(data['Name'] + ': ' + data['Execution Time']+'ms'); + }); + } + console.groupEnd(); + } + performance = []; + } + }, + invoke: function(query, passedArguments, context) { + var + object = instance, + maxDepth, + found, + response + ; + passedArguments = passedArguments || queryArguments; + context = element || context; + if(typeof query == 'string' && object !== undefined) { + query = query.split(/[\. ]/); + maxDepth = query.length - 1; + $.each(query, function(depth, value) { + var camelCaseValue = (depth != maxDepth) + ? value + query[depth + 1].charAt(0).toUpperCase() + query[depth + 1].slice(1) + : query + ; + if( $.isPlainObject( object[camelCaseValue] ) && (depth != maxDepth) ) { + object = object[camelCaseValue]; + } + else if( object[camelCaseValue] !== undefined ) { + found = object[camelCaseValue]; + return false; + } + else if( $.isPlainObject( object[value] ) && (depth != maxDepth) ) { + object = object[value]; + } + else if( object[value] !== undefined ) { + found = object[value]; + return false; + } + else { + module.error(error.method, query); + return false; + } + }); + } + if ( $.isFunction( found ) ) { + response = found.apply(context, passedArguments); + } + else if(found !== undefined) { + response = found; + } + if($.isArray(returnedValue)) { + returnedValue.push(response); + } + else if(returnedValue !== undefined) { + returnedValue = [returnedValue, response]; + } + else if(response !== undefined) { + returnedValue = response; + } + return found; + } + }; + + if(methodInvoked) { + if(instance === undefined) { + module.initialize(); + } + module.invoke(query); + } + else { + if(instance !== undefined) { + instance.invoke('destroy'); + } + module.initialize(); + } + }) + ; + + return (returnedValue !== undefined) + ? returnedValue + : this + ; +}; + +$.fn.nag.settings = { + + name : 'Nag', + + silent : false, + debug : false, + verbose : false, + performance : true, + + namespace : 'Nag', + + // allows cookie to be overridden + persist : false, + + // set to zero to require manually dismissal, otherwise hides on its own + displayTime : 0, + + animation : { + show : 'slide', + hide : 'slide' + }, + + context : false, + detachable : false, + + expires : 30, + domain : false, + path : '/', + + // type of storage to use + storageMethod : 'cookie', + + // value to store in dismissed localstorage/cookie + key : 'nag', + value : 'dismiss', + + error: { + noCookieStorage : '$.cookie is not included. A storage solution is required.', + noStorage : 'Neither $.cookie or store is defined. A storage solution is required for storing state', + method : 'The method you called is not defined.' + }, + + className : { + bottom : 'bottom', + fixed : 'fixed' + }, + + selector : { + close : '.close.icon' + }, + + speed : 500, + easing : 'easeOutQuad', + + onHide: function() {} + +}; + +// Adds easing +$.extend( $.easing, { + easeOutQuad: function (x, t, b, c, d) { + return -c *(t/=d)*(t-2) + b; + } +}); + +})( jQuery, window, document ); + +/*! + * # Semantic UI 2.2.6 - Popup + * http://github.com/semantic-org/semantic-ui/ + * + * + * Released under the MIT license + * http://opensource.org/licenses/MIT + * + */ + +;(function ($, window, document, undefined) { + +"use strict"; + +window = (typeof window != 'undefined' && window.Math == Math) + ? window + : (typeof self != 'undefined' && self.Math == Math) + ? self + : Function('return this')() +; + +$.fn.popup = function(parameters) { + var + $allModules = $(this), + $document = $(document), + $window = $(window), + $body = $('body'), + + moduleSelector = $allModules.selector || '', + + hasTouch = (true), + time = new Date().getTime(), + performance = [], + + query = arguments[0], + methodInvoked = (typeof query == 'string'), + queryArguments = [].slice.call(arguments, 1), + + returnedValue + ; + $allModules + .each(function() { + var + settings = ( $.isPlainObject(parameters) ) + ? $.extend(true, {}, $.fn.popup.settings, parameters) + : $.extend({}, $.fn.popup.settings), + + selector = settings.selector, + className = settings.className, + error = settings.error, + metadata = settings.metadata, + namespace = settings.namespace, + + eventNamespace = '.' + settings.namespace, + moduleNamespace = 'module-' + namespace, + + $module = $(this), + $context = $(settings.context), + $scrollContext = $(settings.scrollContext), + $boundary = $(settings.boundary), + $target = (settings.target) + ? $(settings.target) + : $module, + + $popup, + $offsetParent, + + searchDepth = 0, + triedPositions = false, + openedWithTouch = false, + + element = this, + instance = $module.data(moduleNamespace), + + documentObserver, + elementNamespace, + id, + module + ; + + module = { + + // binds events + initialize: function() { + module.debug('Initializing', $module); + module.createID(); + module.bind.events(); + if(!module.exists() && settings.preserve) { + module.create(); + } + if(settings.observeChanges) { + module.observeChanges(); + } + module.instantiate(); + }, + + instantiate: function() { + module.verbose('Storing instance', module); + instance = module; + $module + .data(moduleNamespace, instance) + ; + }, + + observeChanges: function() { + if('MutationObserver' in window) { + documentObserver = new MutationObserver(module.event.documentChanged); + documentObserver.observe(document, { + childList : true, + subtree : true + }); + module.debug('Setting up mutation observer', documentObserver); + } + }, + + refresh: function() { + if(settings.popup) { + $popup = $(settings.popup).eq(0); + } + else { + if(settings.inline) { + $popup = $target.nextAll(selector.popup).eq(0); + settings.popup = $popup; + } + } + if(settings.popup) { + $popup.addClass(className.loading); + $offsetParent = module.get.offsetParent(); + $popup.removeClass(className.loading); + if(settings.movePopup && module.has.popup() && module.get.offsetParent($popup)[0] !== $offsetParent[0]) { + module.debug('Moving popup to the same offset parent as activating element'); + $popup + .detach() + .appendTo($offsetParent) + ; + } + } + else { + $offsetParent = (settings.inline) + ? module.get.offsetParent($target) + : module.has.popup() + ? module.get.offsetParent($popup) + : $body + ; + } + if( $offsetParent.is('html') && $offsetParent[0] !== $body[0] ) { + module.debug('Setting page as offset parent'); + $offsetParent = $body; + } + if( module.get.variation() ) { + module.set.variation(); + } + }, + + reposition: function() { + module.refresh(); + module.set.position(); + }, + + destroy: function() { + module.debug('Destroying previous module'); + if(documentObserver) { + documentObserver.disconnect(); + } + // remove element only if was created dynamically + if($popup && !settings.preserve) { + module.removePopup(); + } + // clear all timeouts + clearTimeout(module.hideTimer); + clearTimeout(module.showTimer); + // remove events + module.unbind.close(); + module.unbind.events(); + $module + .removeData(moduleNamespace) + ; + }, + + event: { + start: function(event) { + var + delay = ($.isPlainObject(settings.delay)) + ? settings.delay.show + : settings.delay + ; + clearTimeout(module.hideTimer); + if(!openedWithTouch) { + module.showTimer = setTimeout(module.show, delay); + } + }, + end: function() { + var + delay = ($.isPlainObject(settings.delay)) + ? settings.delay.hide + : settings.delay + ; + clearTimeout(module.showTimer); + module.hideTimer = setTimeout(module.hide, delay); + }, + touchstart: function(event) { + openedWithTouch = true; + module.show(); + }, + resize: function() { + if( module.is.visible() ) { + module.set.position(); + } + }, + documentChanged: function(mutations) { + [].forEach.call(mutations, function(mutation) { + if(mutation.removedNodes) { + [].forEach.call(mutation.removedNodes, function(node) { + if(node == element || $(node).find(element).length > 0) { + module.debug('Element removed from DOM, tearing down events'); + module.destroy(); + } + }); + } + }); + }, + hideGracefully: function(event) { + var + $target = $(event.target), + isInDOM = $.contains(document.documentElement, event.target), + inPopup = ($target.closest(selector.popup).length > 0) + ; + // don't close on clicks inside popup + if(event && !inPopup && isInDOM) { + module.debug('Click occurred outside popup hiding popup'); + module.hide(); + } + else { + module.debug('Click was inside popup, keeping popup open'); + } + } + }, + + // generates popup html from metadata + create: function() { + var + html = module.get.html(), + title = module.get.title(), + content = module.get.content() + ; + + if(html || content || title) { + module.debug('Creating pop-up html'); + if(!html) { + html = settings.templates.popup({ + title : title, + content : content + }); + } + $popup = $('<div/>') + .addClass(className.popup) + .data(metadata.activator, $module) + .html(html) + ; + if(settings.inline) { + module.verbose('Inserting popup element inline', $popup); + $popup + .insertAfter($module) + ; + } + else { + module.verbose('Appending popup element to body', $popup); + $popup + .appendTo( $context ) + ; + } + module.refresh(); + module.set.variation(); + + if(settings.hoverable) { + module.bind.popup(); + } + settings.onCreate.call($popup, element); + } + else if($target.next(selector.popup).length !== 0) { + module.verbose('Pre-existing popup found'); + settings.inline = true; + settings.popup = $target.next(selector.popup).data(metadata.activator, $module); + module.refresh(); + if(settings.hoverable) { + module.bind.popup(); + } + } + else if(settings.popup) { + $(settings.popup).data(metadata.activator, $module); + module.verbose('Used popup specified in settings'); + module.refresh(); + if(settings.hoverable) { + module.bind.popup(); + } + } + else { + module.debug('No content specified skipping display', element); + } + }, + + createID: function() { + id = (Math.random().toString(16) + '000000000').substr(2, 8); + elementNamespace = '.' + id; + module.verbose('Creating unique id for element', id); + }, + + // determines popup state + toggle: function() { + module.debug('Toggling pop-up'); + if( module.is.hidden() ) { + module.debug('Popup is hidden, showing pop-up'); + module.unbind.close(); + module.show(); + } + else { + module.debug('Popup is visible, hiding pop-up'); + module.hide(); + } + }, + + show: function(callback) { + callback = callback || function(){}; + module.debug('Showing pop-up', settings.transition); + if(module.is.hidden() && !( module.is.active() && module.is.dropdown()) ) { + if( !module.exists() ) { + module.create(); + } + if(settings.onShow.call($popup, element) === false) { + module.debug('onShow callback returned false, cancelling popup animation'); + return; + } + else if(!settings.preserve && !settings.popup) { + module.refresh(); + } + if( $popup && module.set.position() ) { + module.save.conditions(); + if(settings.exclusive) { + module.hideAll(); + } + module.animate.show(callback); + } + } + }, + + + hide: function(callback) { + callback = callback || function(){}; + if( module.is.visible() || module.is.animating() ) { + if(settings.onHide.call($popup, element) === false) { + module.debug('onHide callback returned false, cancelling popup animation'); + return; + } + module.remove.visible(); + module.unbind.close(); + module.restore.conditions(); + module.animate.hide(callback); + } + }, + + hideAll: function() { + $(selector.popup) + .filter('.' + className.visible) + .each(function() { + $(this) + .data(metadata.activator) + .popup('hide') + ; + }) + ; + }, + exists: function() { + if(!$popup) { + return false; + } + if(settings.inline || settings.popup) { + return ( module.has.popup() ); + } + else { + return ( $popup.closest($context).length >= 1 ) + ? true + : false + ; + } + }, + + removePopup: function() { + if( module.has.popup() && !settings.popup) { + module.debug('Removing popup', $popup); + $popup.remove(); + $popup = undefined; + settings.onRemove.call($popup, element); + } + }, + + save: { + conditions: function() { + module.cache = { + title: $module.attr('title') + }; + if (module.cache.title) { + $module.removeAttr('title'); + } + module.verbose('Saving original attributes', module.cache.title); + } + }, + restore: { + conditions: function() { + if(module.cache && module.cache.title) { + $module.attr('title', module.cache.title); + module.verbose('Restoring original attributes', module.cache.title); + } + return true; + } + }, + supports: { + svg: function() { + return (typeof SVGGraphicsElement === undefined); + } + }, + animate: { + show: function(callback) { + callback = $.isFunction(callback) ? callback : function(){}; + if(settings.transition && $.fn.transition !== undefined && $module.transition('is supported')) { + module.set.visible(); + $popup + .transition({ + animation : settings.transition + ' in', + queue : false, + debug : settings.debug, + verbose : settings.verbose, + duration : settings.duration, + onComplete : function() { + module.bind.close(); + callback.call($popup, element); + settings.onVisible.call($popup, element); + } + }) + ; + } + else { + module.error(error.noTransition); + } + }, + hide: function(callback) { + callback = $.isFunction(callback) ? callback : function(){}; + module.debug('Hiding pop-up'); + if(settings.onHide.call($popup, element) === false) { + module.debug('onHide callback returned false, cancelling popup animation'); + return; + } + if(settings.transition && $.fn.transition !== undefined && $module.transition('is supported')) { + $popup + .transition({ + animation : settings.transition + ' out', + queue : false, + duration : settings.duration, + debug : settings.debug, + verbose : settings.verbose, + onComplete : function() { + module.reset(); + callback.call($popup, element); + settings.onHidden.call($popup, element); + } + }) + ; + } + else { + module.error(error.noTransition); + } + } + }, + + change: { + content: function(html) { + $popup.html(html); + } + }, + + get: { + html: function() { + $module.removeData(metadata.html); + return $module.data(metadata.html) || settings.html; + }, + title: function() { + $module.removeData(metadata.title); + return $module.data(metadata.title) || settings.title; + }, + content: function() { + $module.removeData(metadata.content); + return $module.data(metadata.content) || $module.attr('title') || settings.content; + }, + variation: function() { + $module.removeData(metadata.variation); + return $module.data(metadata.variation) || settings.variation; + }, + popup: function() { + return $popup; + }, + popupOffset: function() { + return $popup.offset(); + }, + calculations: function() { + var + targetElement = $target[0], + isWindow = ($boundary[0] == window), + targetPosition = (settings.inline || (settings.popup && settings.movePopup)) + ? $target.position() + : $target.offset(), + screenPosition = (isWindow) + ? { top: 0, left: 0 } + : $boundary.offset(), + calculations = {}, + scroll = (isWindow) + ? { top: $window.scrollTop(), left: $window.scrollLeft() } + : { top: 0, left: 0}, + screen + ; + calculations = { + // element which is launching popup + target : { + element : $target[0], + width : $target.outerWidth(), + height : $target.outerHeight(), + top : targetPosition.top, + left : targetPosition.left, + margin : {} + }, + // popup itself + popup : { + width : $popup.outerWidth(), + height : $popup.outerHeight() + }, + // offset container (or 3d context) + parent : { + width : $offsetParent.outerWidth(), + height : $offsetParent.outerHeight() + }, + // screen boundaries + screen : { + top : screenPosition.top, + left : screenPosition.left, + scroll: { + top : scroll.top, + left : scroll.left + }, + width : $boundary.width(), + height : $boundary.height() + } + }; + + // add in container calcs if fluid + if( settings.setFluidWidth && module.is.fluid() ) { + calculations.container = { + width: $popup.parent().outerWidth() + }; + calculations.popup.width = calculations.container.width; + } + + // add in margins if inline + calculations.target.margin.top = (settings.inline) + ? parseInt( window.getComputedStyle(targetElement).getPropertyValue('margin-top'), 10) + : 0 + ; + calculations.target.margin.left = (settings.inline) + ? module.is.rtl() + ? parseInt( window.getComputedStyle(targetElement).getPropertyValue('margin-right'), 10) + : parseInt( window.getComputedStyle(targetElement).getPropertyValue('margin-left'), 10) + : 0 + ; + // calculate screen boundaries + screen = calculations.screen; + calculations.boundary = { + top : screen.top + screen.scroll.top, + bottom : screen.top + screen.scroll.top + screen.height, + left : screen.left + screen.scroll.left, + right : screen.left + screen.scroll.left + screen.width + }; + return calculations; + }, + id: function() { + return id; + }, + startEvent: function() { + if(settings.on == 'hover') { + return 'mouseenter'; + } + else if(settings.on == 'focus') { + return 'focus'; + } + return false; + }, + scrollEvent: function() { + return 'scroll'; + }, + endEvent: function() { + if(settings.on == 'hover') { + return 'mouseleave'; + } + else if(settings.on == 'focus') { + return 'blur'; + } + return false; + }, + distanceFromBoundary: function(offset, calculations) { + var + distanceFromBoundary = {}, + popup, + boundary + ; + calculations = calculations || module.get.calculations(); + + // shorthand + popup = calculations.popup; + boundary = calculations.boundary; + + if(offset) { + distanceFromBoundary = { + top : (offset.top - boundary.top), + left : (offset.left - boundary.left), + right : (boundary.right - (offset.left + popup.width) ), + bottom : (boundary.bottom - (offset.top + popup.height) ) + }; + module.verbose('Distance from boundaries determined', offset, distanceFromBoundary); + } + return distanceFromBoundary; + }, + offsetParent: function($target) { + var + element = ($target !== undefined) + ? $target[0] + : $module[0], + parentNode = element.parentNode, + $node = $(parentNode) + ; + if(parentNode) { + var + is2D = ($node.css('transform') === 'none'), + isStatic = ($node.css('position') === 'static'), + isHTML = $node.is('html') + ; + while(parentNode && !isHTML && isStatic && is2D) { + parentNode = parentNode.parentNode; + $node = $(parentNode); + is2D = ($node.css('transform') === 'none'); + isStatic = ($node.css('position') === 'static'); + isHTML = $node.is('html'); + } + } + return ($node && $node.length > 0) + ? $node + : $() + ; + }, + positions: function() { + return { + 'top left' : false, + 'top center' : false, + 'top right' : false, + 'bottom left' : false, + 'bottom center' : false, + 'bottom right' : false, + 'left center' : false, + 'right center' : false + }; + }, + nextPosition: function(position) { + var + positions = position.split(' '), + verticalPosition = positions[0], + horizontalPosition = positions[1], + opposite = { + top : 'bottom', + bottom : 'top', + left : 'right', + right : 'left' + }, + adjacent = { + left : 'center', + center : 'right', + right : 'left' + }, + backup = { + 'top left' : 'top center', + 'top center' : 'top right', + 'top right' : 'right center', + 'right center' : 'bottom right', + 'bottom right' : 'bottom center', + 'bottom center' : 'bottom left', + 'bottom left' : 'left center', + 'left center' : 'top left' + }, + adjacentsAvailable = (verticalPosition == 'top' || verticalPosition == 'bottom'), + oppositeTried = false, + adjacentTried = false, + nextPosition = false + ; + if(!triedPositions) { + module.verbose('All available positions available'); + triedPositions = module.get.positions(); + } + + module.debug('Recording last position tried', position); + triedPositions[position] = true; + + if(settings.prefer === 'opposite') { + nextPosition = [opposite[verticalPosition], horizontalPosition]; + nextPosition = nextPosition.join(' '); + oppositeTried = (triedPositions[nextPosition] === true); + module.debug('Trying opposite strategy', nextPosition); + } + if((settings.prefer === 'adjacent') && adjacentsAvailable ) { + nextPosition = [verticalPosition, adjacent[horizontalPosition]]; + nextPosition = nextPosition.join(' '); + adjacentTried = (triedPositions[nextPosition] === true); + module.debug('Trying adjacent strategy', nextPosition); + } + if(adjacentTried || oppositeTried) { + module.debug('Using backup position', nextPosition); + nextPosition = backup[position]; + } + return nextPosition; + } + }, + + set: { + position: function(position, calculations) { + + // exit conditions + if($target.length === 0 || $popup.length === 0) { + module.error(error.notFound); + return; + } + var + offset, + distanceAway, + target, + popup, + parent, + positioning, + popupOffset, + distanceFromBoundary + ; + + calculations = calculations || module.get.calculations(); + position = position || $module.data(metadata.position) || settings.position; + + offset = $module.data(metadata.offset) || settings.offset; + distanceAway = settings.distanceAway; + + // shorthand + target = calculations.target; + popup = calculations.popup; + parent = calculations.parent; + + if(target.width === 0 && target.height === 0 && !module.is.svg(target.element)) { + module.debug('Popup target is hidden, no action taken'); + return false; + } + + if(settings.inline) { + module.debug('Adding margin to calculation', target.margin); + if(position == 'left center' || position == 'right center') { + offset += target.margin.top; + distanceAway += -target.margin.left; + } + else if (position == 'top left' || position == 'top center' || position == 'top right') { + offset += target.margin.left; + distanceAway -= target.margin.top; + } + else { + offset += target.margin.left; + distanceAway += target.margin.top; + } + } + + module.debug('Determining popup position from calculations', position, calculations); + + if (module.is.rtl()) { + position = position.replace(/left|right/g, function (match) { + return (match == 'left') + ? 'right' + : 'left' + ; + }); + module.debug('RTL: Popup position updated', position); + } + + // if last attempt use specified last resort position + if(searchDepth == settings.maxSearchDepth && typeof settings.lastResort === 'string') { + position = settings.lastResort; + } + + switch (position) { + case 'top left': + positioning = { + top : 'auto', + bottom : parent.height - target.top + distanceAway, + left : target.left + offset, + right : 'auto' + }; + break; + case 'top center': + positioning = { + bottom : parent.height - target.top + distanceAway, + left : target.left + (target.width / 2) - (popup.width / 2) + offset, + top : 'auto', + right : 'auto' + }; + break; + case 'top right': + positioning = { + bottom : parent.height - target.top + distanceAway, + right : parent.width - target.left - target.width - offset, + top : 'auto', + left : 'auto' + }; + break; + case 'left center': + positioning = { + top : target.top + (target.height / 2) - (popup.height / 2) + offset, + right : parent.width - target.left + distanceAway, + left : 'auto', + bottom : 'auto' + }; + break; + case 'right center': + positioning = { + top : target.top + (target.height / 2) - (popup.height / 2) + offset, + left : target.left + target.width + distanceAway, + bottom : 'auto', + right : 'auto' + }; + break; + case 'bottom left': + positioning = { + top : target.top + target.height + distanceAway, + left : target.left + offset, + bottom : 'auto', + right : 'auto' + }; + break; + case 'bottom center': + positioning = { + top : target.top + target.height + distanceAway, + left : target.left + (target.width / 2) - (popup.width / 2) + offset, + bottom : 'auto', + right : 'auto' + }; + break; + case 'bottom right': + positioning = { + top : target.top + target.height + distanceAway, + right : parent.width - target.left - target.width - offset, + left : 'auto', + bottom : 'auto' + }; + break; + } + if(positioning === undefined) { + module.error(error.invalidPosition, position); + } + + module.debug('Calculated popup positioning values', positioning); + + // tentatively place on stage + $popup + .css(positioning) + .removeClass(className.position) + .addClass(position) + .addClass(className.loading) + ; + + popupOffset = module.get.popupOffset(); + + // see if any boundaries are surpassed with this tentative position + distanceFromBoundary = module.get.distanceFromBoundary(popupOffset, calculations); + + if( module.is.offstage(distanceFromBoundary, position) ) { + module.debug('Position is outside viewport', position); + if(searchDepth < settings.maxSearchDepth) { + searchDepth++; + position = module.get.nextPosition(position); + module.debug('Trying new position', position); + return ($popup) + ? module.set.position(position, calculations) + : false + ; + } + else { + if(settings.lastResort) { + module.debug('No position found, showing with last position'); + } + else { + module.debug('Popup could not find a position to display', $popup); + module.error(error.cannotPlace, element); + module.remove.attempts(); + module.remove.loading(); + module.reset(); + settings.onUnplaceable.call($popup, element); + return false; + } + } + } + module.debug('Position is on stage', position); + module.remove.attempts(); + module.remove.loading(); + if( settings.setFluidWidth && module.is.fluid() ) { + module.set.fluidWidth(calculations); + } + return true; + }, + + fluidWidth: function(calculations) { + calculations = calculations || module.get.calculations(); + module.debug('Automatically setting element width to parent width', calculations.parent.width); + $popup.css('width', calculations.container.width); + }, + + variation: function(variation) { + variation = variation || module.get.variation(); + if(variation && module.has.popup() ) { + module.verbose('Adding variation to popup', variation); + $popup.addClass(variation); + } + }, + + visible: function() { + $module.addClass(className.visible); + } + }, + + remove: { + loading: function() { + $popup.removeClass(className.loading); + }, + variation: function(variation) { + variation = variation || module.get.variation(); + if(variation) { + module.verbose('Removing variation', variation); + $popup.removeClass(variation); + } + }, + visible: function() { + $module.removeClass(className.visible); + }, + attempts: function() { + module.verbose('Resetting all searched positions'); + searchDepth = 0; + triedPositions = false; + } + }, + + bind: { + events: function() { + module.debug('Binding popup events to module'); + if(settings.on == 'click') { + $module + .on('click' + eventNamespace, module.toggle) + ; + } + if(settings.on == 'hover' && hasTouch) { + $module + .on('touchstart' + eventNamespace, module.event.touchstart) + ; + } + if( module.get.startEvent() ) { + $module + .on(module.get.startEvent() + eventNamespace, module.event.start) + .on(module.get.endEvent() + eventNamespace, module.event.end) + ; + } + if(settings.target) { + module.debug('Target set to element', $target); + } + $window.on('resize' + elementNamespace, module.event.resize); + }, + popup: function() { + module.verbose('Allowing hover events on popup to prevent closing'); + if( $popup && module.has.popup() ) { + $popup + .on('mouseenter' + eventNamespace, module.event.start) + .on('mouseleave' + eventNamespace, module.event.end) + ; + } + }, + close: function() { + if(settings.hideOnScroll === true || (settings.hideOnScroll == 'auto' && settings.on != 'click')) { + $scrollContext + .one(module.get.scrollEvent() + elementNamespace, module.event.hideGracefully) + ; + } + if(settings.on == 'hover' && openedWithTouch) { + module.verbose('Binding popup close event to document'); + $document + .on('touchstart' + elementNamespace, function(event) { + module.verbose('Touched away from popup'); + module.event.hideGracefully.call(element, event); + }) + ; + } + if(settings.on == 'click' && settings.closable) { + module.verbose('Binding popup close event to document'); + $document + .on('click' + elementNamespace, function(event) { + module.verbose('Clicked away from popup'); + module.event.hideGracefully.call(element, event); + }) + ; + } + } + }, + + unbind: { + events: function() { + $window + .off(elementNamespace) + ; + $module + .off(eventNamespace) + ; + }, + close: function() { + $document + .off(elementNamespace) + ; + $scrollContext + .off(elementNamespace) + ; + }, + }, + + has: { + popup: function() { + return ($popup && $popup.length > 0); + } + }, + + is: { + offstage: function(distanceFromBoundary, position) { + var + offstage = [] + ; + // return boundaries that have been surpassed + $.each(distanceFromBoundary, function(direction, distance) { + if(distance < -settings.jitter) { + module.debug('Position exceeds allowable distance from edge', direction, distance, position); + offstage.push(direction); + } + }); + if(offstage.length > 0) { + return true; + } + else { + return false; + } + }, + svg: function(element) { + return module.supports.svg() && (element instanceof SVGGraphicsElement); + }, + active: function() { + return $module.hasClass(className.active); + }, + animating: function() { + return ($popup !== undefined && $popup.hasClass(className.animating) ); + }, + fluid: function() { + return ($popup !== undefined && $popup.hasClass(className.fluid)); + }, + visible: function() { + return ($popup !== undefined && $popup.hasClass(className.visible)); + }, + dropdown: function() { + return $module.hasClass(className.dropdown); + }, + hidden: function() { + return !module.is.visible(); + }, + rtl: function () { + return $module.css('direction') == 'rtl'; + } + }, + + reset: function() { + module.remove.visible(); + if(settings.preserve) { + if($.fn.transition !== undefined) { + $popup + .transition('remove transition') + ; + } + } + else { + module.removePopup(); + } + }, + + setting: function(name, value) { + if( $.isPlainObject(name) ) { + $.extend(true, settings, name); + } + else if(value !== undefined) { + settings[name] = value; + } + else { + return settings[name]; + } + }, + internal: function(name, value) { + if( $.isPlainObject(name) ) { + $.extend(true, module, name); + } + else if(value !== undefined) { + module[name] = value; + } + else { + return module[name]; + } + }, + debug: function() { + if(!settings.silent && settings.debug) { + if(settings.performance) { + module.performance.log(arguments); + } + else { + module.debug = Function.prototype.bind.call(console.info, console, settings.name + ':'); + module.debug.apply(console, arguments); + } + } + }, + verbose: function() { + if(!settings.silent && settings.verbose && settings.debug) { + if(settings.performance) { + module.performance.log(arguments); + } + else { + module.verbose = Function.prototype.bind.call(console.info, console, settings.name + ':'); + module.verbose.apply(console, arguments); + } + } + }, + error: function() { + if(!settings.silent) { + module.error = Function.prototype.bind.call(console.error, console, settings.name + ':'); + module.error.apply(console, arguments); + } + }, + performance: { + log: function(message) { + var + currentTime, + executionTime, + previousTime + ; + if(settings.performance) { + currentTime = new Date().getTime(); + previousTime = time || currentTime; + executionTime = currentTime - previousTime; + time = currentTime; + performance.push({ + 'Name' : message[0], + 'Arguments' : [].slice.call(message, 1) || '', + 'Element' : element, + 'Execution Time' : executionTime + }); + } + clearTimeout(module.performance.timer); + module.performance.timer = setTimeout(module.performance.display, 500); + }, + display: function() { + var + title = settings.name + ':', + totalTime = 0 + ; + time = false; + clearTimeout(module.performance.timer); + $.each(performance, function(index, data) { + totalTime += data['Execution Time']; + }); + title += ' ' + totalTime + 'ms'; + if(moduleSelector) { + title += ' \'' + moduleSelector + '\''; + } + if( (console.group !== undefined || console.table !== undefined) && performance.length > 0) { + console.groupCollapsed(title); + if(console.table) { + console.table(performance); + } + else { + $.each(performance, function(index, data) { + console.log(data['Name'] + ': ' + data['Execution Time']+'ms'); + }); + } + console.groupEnd(); + } + performance = []; + } + }, + invoke: function(query, passedArguments, context) { + var + object = instance, + maxDepth, + found, + response + ; + passedArguments = passedArguments || queryArguments; + context = element || context; + if(typeof query == 'string' && object !== undefined) { + query = query.split(/[\. ]/); + maxDepth = query.length - 1; + $.each(query, function(depth, value) { + var camelCaseValue = (depth != maxDepth) + ? value + query[depth + 1].charAt(0).toUpperCase() + query[depth + 1].slice(1) + : query + ; + if( $.isPlainObject( object[camelCaseValue] ) && (depth != maxDepth) ) { + object = object[camelCaseValue]; + } + else if( object[camelCaseValue] !== undefined ) { + found = object[camelCaseValue]; + return false; + } + else if( $.isPlainObject( object[value] ) && (depth != maxDepth) ) { + object = object[value]; + } + else if( object[value] !== undefined ) { + found = object[value]; + return false; + } + else { + return false; + } + }); + } + if ( $.isFunction( found ) ) { + response = found.apply(context, passedArguments); + } + else if(found !== undefined) { + response = found; + } + if($.isArray(returnedValue)) { + returnedValue.push(response); + } + else if(returnedValue !== undefined) { + returnedValue = [returnedValue, response]; + } + else if(response !== undefined) { + returnedValue = response; + } + return found; + } + }; + + if(methodInvoked) { + if(instance === undefined) { + module.initialize(); + } + module.invoke(query); + } + else { + if(instance !== undefined) { + instance.invoke('destroy'); + } + module.initialize(); + } + }) + ; + + return (returnedValue !== undefined) + ? returnedValue + : this + ; +}; + +$.fn.popup.settings = { + + name : 'Popup', + + // module settings + silent : false, + debug : false, + verbose : false, + performance : true, + namespace : 'popup', + + // whether it should use dom mutation observers + observeChanges : true, + + // callback only when element added to dom + onCreate : function(){}, + + // callback before element removed from dom + onRemove : function(){}, + + // callback before show animation + onShow : function(){}, + + // callback after show animation + onVisible : function(){}, + + // callback before hide animation + onHide : function(){}, + + // callback when popup cannot be positioned in visible screen + onUnplaceable : function(){}, + + // callback after hide animation + onHidden : function(){}, + + // when to show popup + on : 'hover', + + // element to use to determine if popup is out of boundary + boundary : window, + + // whether to add touchstart events when using hover + addTouchEvents : true, + + // default position relative to element + position : 'top left', + + // name of variation to use + variation : '', + + // whether popup should be moved to context + movePopup : true, + + // element which popup should be relative to + target : false, + + // jq selector or element that should be used as popup + popup : false, + + // popup should remain inline next to activator + inline : false, + + // popup should be removed from page on hide + preserve : false, + + // popup should not close when being hovered on + hoverable : false, + + // explicitly set content + content : false, + + // explicitly set html + html : false, + + // explicitly set title + title : false, + + // whether automatically close on clickaway when on click + closable : true, + + // automatically hide on scroll + hideOnScroll : 'auto', + + // hide other popups on show + exclusive : false, + + // context to attach popups + context : 'body', + + // context for binding scroll events + scrollContext : window, + + // position to prefer when calculating new position + prefer : 'opposite', + + // specify position to appear even if it doesn't fit + lastResort : false, + + // delay used to prevent accidental refiring of animations due to user error + delay : { + show : 50, + hide : 70 + }, + + // whether fluid variation should assign width explicitly + setFluidWidth : true, + + // transition settings + duration : 200, + transition : 'scale', + + // distance away from activating element in px + distanceAway : 0, + + // number of pixels an element is allowed to be "offstage" for a position to be chosen (allows for rounding) + jitter : 2, + + // offset on aligning axis from calculated position + offset : 0, + + // maximum times to look for a position before failing (9 positions total) + maxSearchDepth : 15, + + error: { + invalidPosition : 'The position you specified is not a valid position', + cannotPlace : 'Popup does not fit within the boundaries of the viewport', + method : 'The method you called is not defined.', + noTransition : 'This module requires ui transitions <https://github.com/Semantic-Org/UI-Transition>', + notFound : 'The target or popup you specified does not exist on the page' + }, + + metadata: { + activator : 'activator', + content : 'content', + html : 'html', + offset : 'offset', + position : 'position', + title : 'title', + variation : 'variation' + }, + + className : { + active : 'active', + animating : 'animating', + dropdown : 'dropdown', + fluid : 'fluid', + loading : 'loading', + popup : 'ui popup', + position : 'top left center bottom right', + visible : 'visible' + }, + + selector : { + popup : '.ui.popup' + }, + + templates: { + escape: function(string) { + var + badChars = /[&<>"'`]/g, + shouldEscape = /[&<>"'`]/, + escape = { + "&": "&", + "<": "<", + ">": ">", + '"': """, + "'": "'", + "`": "`" + }, + escapedChar = function(chr) { + return escape[chr]; + } + ; + if(shouldEscape.test(string)) { + return string.replace(badChars, escapedChar); + } + return string; + }, + popup: function(text) { + var + html = '', + escape = $.fn.popup.settings.templates.escape + ; + if(typeof text !== undefined) { + if(typeof text.title !== undefined && text.title) { + text.title = escape(text.title); + html += '<div class="header">' + text.title + '</div>'; + } + if(typeof text.content !== undefined && text.content) { + text.content = escape(text.content); + html += '<div class="content">' + text.content + '</div>'; + } + } + return html; + } + } + +}; + + +})( jQuery, window, document ); + +/*! + * # Semantic UI 2.2.6 - Progress + * http://github.com/semantic-org/semantic-ui/ + * + * + * Released under the MIT license + * http://opensource.org/licenses/MIT + * + */ + +;(function ($, window, document, undefined) { + +"use strict"; + +window = (typeof window != 'undefined' && window.Math == Math) + ? window + : (typeof self != 'undefined' && self.Math == Math) + ? self + : Function('return this')() +; + +var + global = (typeof window != 'undefined' && window.Math == Math) + ? window + : (typeof self != 'undefined' && self.Math == Math) + ? self + : Function('return this')() +; + +$.fn.progress = function(parameters) { + var + $allModules = $(this), + + moduleSelector = $allModules.selector || '', + + time = new Date().getTime(), + performance = [], + + query = arguments[0], + methodInvoked = (typeof query == 'string'), + queryArguments = [].slice.call(arguments, 1), + + returnedValue + ; + + $allModules + .each(function() { + var + settings = ( $.isPlainObject(parameters) ) + ? $.extend(true, {}, $.fn.progress.settings, parameters) + : $.extend({}, $.fn.progress.settings), + + className = settings.className, + metadata = settings.metadata, + namespace = settings.namespace, + selector = settings.selector, + error = settings.error, + + eventNamespace = '.' + namespace, + moduleNamespace = 'module-' + namespace, + + $module = $(this), + $bar = $(this).find(selector.bar), + $progress = $(this).find(selector.progress), + $label = $(this).find(selector.label), + + element = this, + instance = $module.data(moduleNamespace), + + animating = false, + transitionEnd, + module + ; + + module = { + + initialize: function() { + module.debug('Initializing progress bar', settings); + + module.set.duration(); + module.set.transitionEvent(); + + module.read.metadata(); + module.read.settings(); + + module.instantiate(); + }, + + instantiate: function() { + module.verbose('Storing instance of progress', module); + instance = module; + $module + .data(moduleNamespace, module) + ; + }, + destroy: function() { + module.verbose('Destroying previous progress for', $module); + clearInterval(instance.interval); + module.remove.state(); + $module.removeData(moduleNamespace); + instance = undefined; + }, + + reset: function() { + module.remove.nextValue(); + module.update.progress(0); + }, + + complete: function() { + if(module.percent === undefined || module.percent < 100) { + module.remove.progressPoll(); + module.set.percent(100); + } + }, + + read: { + metadata: function() { + var + data = { + percent : $module.data(metadata.percent), + total : $module.data(metadata.total), + value : $module.data(metadata.value) + } + ; + if(data.percent) { + module.debug('Current percent value set from metadata', data.percent); + module.set.percent(data.percent); + } + if(data.total) { + module.debug('Total value set from metadata', data.total); + module.set.total(data.total); + } + if(data.value) { + module.debug('Current value set from metadata', data.value); + module.set.value(data.value); + module.set.progress(data.value); + } + }, + settings: function() { + if(settings.total !== false) { + module.debug('Current total set in settings', settings.total); + module.set.total(settings.total); + } + if(settings.value !== false) { + module.debug('Current value set in settings', settings.value); + module.set.value(settings.value); + module.set.progress(module.value); + } + if(settings.percent !== false) { + module.debug('Current percent set in settings', settings.percent); + module.set.percent(settings.percent); + } + } + }, + + bind: { + transitionEnd: function(callback) { + var + transitionEnd = module.get.transitionEnd() + ; + $bar + .one(transitionEnd + eventNamespace, function(event) { + clearTimeout(module.failSafeTimer); + callback.call(this, event); + }) + ; + module.failSafeTimer = setTimeout(function() { + $bar.triggerHandler(transitionEnd); + }, settings.duration + settings.failSafeDelay); + module.verbose('Adding fail safe timer', module.timer); + } + }, + + increment: function(incrementValue) { + var + maxValue, + startValue, + newValue + ; + if( module.has.total() ) { + startValue = module.get.value(); + incrementValue = incrementValue || 1; + newValue = startValue + incrementValue; + } + else { + startValue = module.get.percent(); + incrementValue = incrementValue || module.get.randomValue(); + + newValue = startValue + incrementValue; + maxValue = 100; + module.debug('Incrementing percentage by', startValue, newValue); + } + newValue = module.get.normalizedValue(newValue); + module.set.progress(newValue); + }, + decrement: function(decrementValue) { + var + total = module.get.total(), + startValue, + newValue + ; + if(total) { + startValue = module.get.value(); + decrementValue = decrementValue || 1; + newValue = startValue - decrementValue; + module.debug('Decrementing value by', decrementValue, startValue); + } + else { + startValue = module.get.percent(); + decrementValue = decrementValue || module.get.randomValue(); + newValue = startValue - decrementValue; + module.debug('Decrementing percentage by', decrementValue, startValue); + } + newValue = module.get.normalizedValue(newValue); + module.set.progress(newValue); + }, + + has: { + progressPoll: function() { + return module.progressPoll; + }, + total: function() { + return (module.get.total() !== false); + } + }, + + get: { + text: function(templateText) { + var + value = module.value || 0, + total = module.total || 0, + percent = (animating) + ? module.get.displayPercent() + : module.percent || 0, + left = (module.total > 0) + ? (total - value) + : (100 - percent) + ; + templateText = templateText || ''; + templateText = templateText + .replace('{value}', value) + .replace('{total}', total) + .replace('{left}', left) + .replace('{percent}', percent) + ; + module.verbose('Adding variables to progress bar text', templateText); + return templateText; + }, + + normalizedValue: function(value) { + if(value < 0) { + module.debug('Value cannot decrement below 0'); + return 0; + } + if(module.has.total()) { + if(value > module.total) { + module.debug('Value cannot increment above total', module.total); + return module.total; + } + } + else if(value > 100 ) { + module.debug('Value cannot increment above 100 percent'); + return 100; + } + return value; + }, + + updateInterval: function() { + if(settings.updateInterval == 'auto') { + return settings.duration; + } + return settings.updateInterval; + }, + + randomValue: function() { + module.debug('Generating random increment percentage'); + return Math.floor((Math.random() * settings.random.max) + settings.random.min); + }, + + numericValue: function(value) { + return (typeof value === 'string') + ? (value.replace(/[^\d.]/g, '') !== '') + ? +(value.replace(/[^\d.]/g, '')) + : false + : value + ; + }, + + transitionEnd: function() { + var + element = document.createElement('element'), + transitions = { + 'transition' :'transitionend', + 'OTransition' :'oTransitionEnd', + 'MozTransition' :'transitionend', + 'WebkitTransition' :'webkitTransitionEnd' + }, + transition + ; + for(transition in transitions){ + if( element.style[transition] !== undefined ){ + return transitions[transition]; + } + } + }, + + // gets current displayed percentage (if animating values this is the intermediary value) + displayPercent: function() { + var + barWidth = $bar.width(), + totalWidth = $module.width(), + minDisplay = parseInt($bar.css('min-width'), 10), + displayPercent = (barWidth > minDisplay) + ? (barWidth / totalWidth * 100) + : module.percent + ; + return (settings.precision > 0) + ? Math.round(displayPercent * (10 * settings.precision)) / (10 * settings.precision) + : Math.round(displayPercent) + ; + }, + + percent: function() { + return module.percent || 0; + }, + value: function() { + return module.nextValue || module.value || 0; + }, + total: function() { + return module.total || false; + } + }, + + create: { + progressPoll: function() { + module.progressPoll = setTimeout(function() { + module.update.toNextValue(); + module.remove.progressPoll(); + }, module.get.updateInterval()); + }, + }, + + is: { + complete: function() { + return module.is.success() || module.is.warning() || module.is.error(); + }, + success: function() { + return $module.hasClass(className.success); + }, + warning: function() { + return $module.hasClass(className.warning); + }, + error: function() { + return $module.hasClass(className.error); + }, + active: function() { + return $module.hasClass(className.active); + }, + visible: function() { + return $module.is(':visible'); + } + }, + + remove: { + progressPoll: function() { + module.verbose('Removing progress poll timer'); + if(module.progressPoll) { + clearTimeout(module.progressPoll); + delete module.progressPoll; + } + }, + nextValue: function() { + module.verbose('Removing progress value stored for next update'); + delete module.nextValue; + }, + state: function() { + module.verbose('Removing stored state'); + delete module.total; + delete module.percent; + delete module.value; + }, + active: function() { + module.verbose('Removing active state'); + $module.removeClass(className.active); + }, + success: function() { + module.verbose('Removing success state'); + $module.removeClass(className.success); + }, + warning: function() { + module.verbose('Removing warning state'); + $module.removeClass(className.warning); + }, + error: function() { + module.verbose('Removing error state'); + $module.removeClass(className.error); + } + }, + + set: { + barWidth: function(value) { + if(value > 100) { + module.error(error.tooHigh, value); + } + else if (value < 0) { + module.error(error.tooLow, value); + } + else { + $bar + .css('width', value + '%') + ; + $module + .attr('data-percent', parseInt(value, 10)) + ; + } + }, + duration: function(duration) { + duration = duration || settings.duration; + duration = (typeof duration == 'number') + ? duration + 'ms' + : duration + ; + module.verbose('Setting progress bar transition duration', duration); + $bar + .css({ + 'transition-duration': duration + }) + ; + }, + percent: function(percent) { + percent = (typeof percent == 'string') + ? +(percent.replace('%', '')) + : percent + ; + // round display percentage + percent = (settings.precision > 0) + ? Math.round(percent * (10 * settings.precision)) / (10 * settings.precision) + : Math.round(percent) + ; + module.percent = percent; + if( !module.has.total() ) { + module.value = (settings.precision > 0) + ? Math.round( (percent / 100) * module.total * (10 * settings.precision)) / (10 * settings.precision) + : Math.round( (percent / 100) * module.total * 10) / 10 + ; + if(settings.limitValues) { + module.value = (module.value > 100) + ? 100 + : (module.value < 0) + ? 0 + : module.value + ; + } + } + module.set.barWidth(percent); + module.set.labelInterval(); + module.set.labels(); + settings.onChange.call(element, percent, module.value, module.total); + }, + labelInterval: function() { + var + animationCallback = function() { + module.verbose('Bar finished animating, removing continuous label updates'); + clearInterval(module.interval); + animating = false; + module.set.labels(); + } + ; + clearInterval(module.interval); + module.bind.transitionEnd(animationCallback); + animating = true; + module.interval = setInterval(function() { + var + isInDOM = $.contains(document.documentElement, element) + ; + if(!isInDOM) { + clearInterval(module.interval); + animating = false; + } + module.set.labels(); + }, settings.framerate); + }, + labels: function() { + module.verbose('Setting both bar progress and outer label text'); + module.set.barLabel(); + module.set.state(); + }, + label: function(text) { + text = text || ''; + if(text) { + text = module.get.text(text); + module.verbose('Setting label to text', text); + $label.text(text); + } + }, + state: function(percent) { + percent = (percent !== undefined) + ? percent + : module.percent + ; + if(percent === 100) { + if(settings.autoSuccess && !(module.is.warning() || module.is.error() || module.is.success())) { + module.set.success(); + module.debug('Automatically triggering success at 100%'); + } + else { + module.verbose('Reached 100% removing active state'); + module.remove.active(); + module.remove.progressPoll(); + } + } + else if(percent > 0) { + module.verbose('Adjusting active progress bar label', percent); + module.set.active(); + } + else { + module.remove.active(); + module.set.label(settings.text.active); + } + }, + barLabel: function(text) { + if(text !== undefined) { + $progress.text( module.get.text(text) ); + } + else if(settings.label == 'ratio' && module.total) { + module.verbose('Adding ratio to bar label'); + $progress.text( module.get.text(settings.text.ratio) ); + } + else if(settings.label == 'percent') { + module.verbose('Adding percentage to bar label'); + $progress.text( module.get.text(settings.text.percent) ); + } + }, + active: function(text) { + text = text || settings.text.active; + module.debug('Setting active state'); + if(settings.showActivity && !module.is.active() ) { + $module.addClass(className.active); + } + module.remove.warning(); + module.remove.error(); + module.remove.success(); + text = settings.onLabelUpdate('active', text, module.value, module.total); + if(text) { + module.set.label(text); + } + module.bind.transitionEnd(function() { + settings.onActive.call(element, module.value, module.total); + }); + }, + success : function(text) { + text = text || settings.text.success || settings.text.active; + module.debug('Setting success state'); + $module.addClass(className.success); + module.remove.active(); + module.remove.warning(); + module.remove.error(); + module.complete(); + if(settings.text.success) { + text = settings.onLabelUpdate('success', text, module.value, module.total); + module.set.label(text); + } + else { + text = settings.onLabelUpdate('active', text, module.value, module.total); + module.set.label(text); + } + module.bind.transitionEnd(function() { + settings.onSuccess.call(element, module.total); + }); + }, + warning : function(text) { + text = text || settings.text.warning; + module.debug('Setting warning state'); + $module.addClass(className.warning); + module.remove.active(); + module.remove.success(); + module.remove.error(); + module.complete(); + text = settings.onLabelUpdate('warning', text, module.value, module.total); + if(text) { + module.set.label(text); + } + module.bind.transitionEnd(function() { + settings.onWarning.call(element, module.value, module.total); + }); + }, + error : function(text) { + text = text || settings.text.error; + module.debug('Setting error state'); + $module.addClass(className.error); + module.remove.active(); + module.remove.success(); + module.remove.warning(); + module.complete(); + text = settings.onLabelUpdate('error', text, module.value, module.total); + if(text) { + module.set.label(text); + } + module.bind.transitionEnd(function() { + settings.onError.call(element, module.value, module.total); + }); + }, + transitionEvent: function() { + transitionEnd = module.get.transitionEnd(); + }, + total: function(totalValue) { + module.total = totalValue; + }, + value: function(value) { + module.value = value; + }, + progress: function(value) { + if(!module.has.progressPoll()) { + module.debug('First update in progress update interval, immediately updating', value); + module.update.progress(value); + module.create.progressPoll(); + } + else { + module.debug('Updated within interval, setting next update to use new value', value); + module.set.nextValue(value); + } + }, + nextValue: function(value) { + module.nextValue = value; + } + }, + + update: { + toNextValue: function() { + var + nextValue = module.nextValue + ; + if(nextValue) { + module.debug('Update interval complete using last updated value', nextValue); + module.update.progress(nextValue); + module.remove.nextValue(); + } + }, + progress: function(value) { + var + percentComplete + ; + value = module.get.numericValue(value); + if(value === false) { + module.error(error.nonNumeric, value); + } + value = module.get.normalizedValue(value); + if( module.has.total() ) { + module.set.value(value); + percentComplete = (value / module.total) * 100; + module.debug('Calculating percent complete from total', percentComplete); + module.set.percent( percentComplete ); + } + else { + percentComplete = value; + module.debug('Setting value to exact percentage value', percentComplete); + module.set.percent( percentComplete ); + } + } + }, + + setting: function(name, value) { + module.debug('Changing setting', name, value); + if( $.isPlainObject(name) ) { + $.extend(true, settings, name); + } + else if(value !== undefined) { + if($.isPlainObject(settings[name])) { + $.extend(true, settings[name], value); + } + else { + settings[name] = value; + } + } + else { + return settings[name]; + } + }, + internal: function(name, value) { + if( $.isPlainObject(name) ) { + $.extend(true, module, name); + } + else if(value !== undefined) { + module[name] = value; + } + else { + return module[name]; + } + }, + debug: function() { + if(!settings.silent && settings.debug) { + if(settings.performance) { + module.performance.log(arguments); + } + else { + module.debug = Function.prototype.bind.call(console.info, console, settings.name + ':'); + module.debug.apply(console, arguments); + } + } + }, + verbose: function() { + if(!settings.silent && settings.verbose && settings.debug) { + if(settings.performance) { + module.performance.log(arguments); + } + else { + module.verbose = Function.prototype.bind.call(console.info, console, settings.name + ':'); + module.verbose.apply(console, arguments); + } + } + }, + error: function() { + if(!settings.silent) { + module.error = Function.prototype.bind.call(console.error, console, settings.name + ':'); + module.error.apply(console, arguments); + } + }, + performance: { + log: function(message) { + var + currentTime, + executionTime, + previousTime + ; + if(settings.performance) { + currentTime = new Date().getTime(); + previousTime = time || currentTime; + executionTime = currentTime - previousTime; + time = currentTime; + performance.push({ + 'Name' : message[0], + 'Arguments' : [].slice.call(message, 1) || '', + 'Element' : element, + 'Execution Time' : executionTime + }); + } + clearTimeout(module.performance.timer); + module.performance.timer = setTimeout(module.performance.display, 500); + }, + display: function() { + var + title = settings.name + ':', + totalTime = 0 + ; + time = false; + clearTimeout(module.performance.timer); + $.each(performance, function(index, data) { + totalTime += data['Execution Time']; + }); + title += ' ' + totalTime + 'ms'; + if(moduleSelector) { + title += ' \'' + moduleSelector + '\''; + } + if( (console.group !== undefined || console.table !== undefined) && performance.length > 0) { + console.groupCollapsed(title); + if(console.table) { + console.table(performance); + } + else { + $.each(performance, function(index, data) { + console.log(data['Name'] + ': ' + data['Execution Time']+'ms'); + }); + } + console.groupEnd(); + } + performance = []; + } + }, + invoke: function(query, passedArguments, context) { + var + object = instance, + maxDepth, + found, + response + ; + passedArguments = passedArguments || queryArguments; + context = element || context; + if(typeof query == 'string' && object !== undefined) { + query = query.split(/[\. ]/); + maxDepth = query.length - 1; + $.each(query, function(depth, value) { + var camelCaseValue = (depth != maxDepth) + ? value + query[depth + 1].charAt(0).toUpperCase() + query[depth + 1].slice(1) + : query + ; + if( $.isPlainObject( object[camelCaseValue] ) && (depth != maxDepth) ) { + object = object[camelCaseValue]; + } + else if( object[camelCaseValue] !== undefined ) { + found = object[camelCaseValue]; + return false; + } + else if( $.isPlainObject( object[value] ) && (depth != maxDepth) ) { + object = object[value]; + } + else if( object[value] !== undefined ) { + found = object[value]; + return false; + } + else { + module.error(error.method, query); + return false; + } + }); + } + if ( $.isFunction( found ) ) { + response = found.apply(context, passedArguments); + } + else if(found !== undefined) { + response = found; + } + if($.isArray(returnedValue)) { + returnedValue.push(response); + } + else if(returnedValue !== undefined) { + returnedValue = [returnedValue, response]; + } + else if(response !== undefined) { + returnedValue = response; + } + return found; + } + }; + + if(methodInvoked) { + if(instance === undefined) { + module.initialize(); + } + module.invoke(query); + } + else { + if(instance !== undefined) { + instance.invoke('destroy'); + } + module.initialize(); + } + }) + ; + + return (returnedValue !== undefined) + ? returnedValue + : this + ; +}; + +$.fn.progress.settings = { + + name : 'Progress', + namespace : 'progress', + + silent : false, + debug : false, + verbose : false, + performance : true, + + random : { + min : 2, + max : 5 + }, + + duration : 300, + + updateInterval : 'auto', + + autoSuccess : true, + showActivity : true, + limitValues : true, + + label : 'percent', + precision : 0, + framerate : (1000 / 30), /// 30 fps + + percent : false, + total : false, + value : false, + + // delay in ms for fail safe animation callback + failSafeDelay : 100, + + onLabelUpdate : function(state, text, value, total){ + return text; + }, + onChange : function(percent, value, total){}, + onSuccess : function(total){}, + onActive : function(value, total){}, + onError : function(value, total){}, + onWarning : function(value, total){}, + + error : { + method : 'The method you called is not defined.', + nonNumeric : 'Progress value is non numeric', + tooHigh : 'Value specified is above 100%', + tooLow : 'Value specified is below 0%' + }, + + regExp: { + variable: /\{\$*[A-z0-9]+\}/g + }, + + metadata: { + percent : 'percent', + total : 'total', + value : 'value' + }, + + selector : { + bar : '> .bar', + label : '> .label', + progress : '.bar > .progress' + }, + + text : { + active : false, + error : false, + success : false, + warning : false, + percent : '{percent}%', + ratio : '{value} of {total}' + }, + + className : { + active : 'active', + error : 'error', + success : 'success', + warning : 'warning' + } + +}; + + +})( jQuery, window, document ); + +/*! + * # Semantic UI 2.2.6 - Rating + * http://github.com/semantic-org/semantic-ui/ + * + * + * Released under the MIT license + * http://opensource.org/licenses/MIT + * + */ + +;(function ($, window, document, undefined) { + +"use strict"; + +window = (typeof window != 'undefined' && window.Math == Math) + ? window + : (typeof self != 'undefined' && self.Math == Math) + ? self + : Function('return this')() +; + +$.fn.rating = function(parameters) { + var + $allModules = $(this), + moduleSelector = $allModules.selector || '', + + time = new Date().getTime(), + performance = [], + + query = arguments[0], + methodInvoked = (typeof query == 'string'), + queryArguments = [].slice.call(arguments, 1), + returnedValue + ; + $allModules + .each(function() { + var + settings = ( $.isPlainObject(parameters) ) + ? $.extend(true, {}, $.fn.rating.settings, parameters) + : $.extend({}, $.fn.rating.settings), + + namespace = settings.namespace, + className = settings.className, + metadata = settings.metadata, + selector = settings.selector, + error = settings.error, + + eventNamespace = '.' + namespace, + moduleNamespace = 'module-' + namespace, + + element = this, + instance = $(this).data(moduleNamespace), + + $module = $(this), + $icon = $module.find(selector.icon), + + initialLoad, + module + ; + + module = { + + initialize: function() { + module.verbose('Initializing rating module', settings); + + if($icon.length === 0) { + module.setup.layout(); + } + + if(settings.interactive) { + module.enable(); + } + else { + module.disable(); + } + module.set.initialLoad(); + module.set.rating( module.get.initialRating() ); + module.remove.initialLoad(); + module.instantiate(); + }, + + instantiate: function() { + module.verbose('Instantiating module', settings); + instance = module; + $module + .data(moduleNamespace, module) + ; + }, + + destroy: function() { + module.verbose('Destroying previous instance', instance); + module.remove.events(); + $module + .removeData(moduleNamespace) + ; + }, + + refresh: function() { + $icon = $module.find(selector.icon); + }, + + setup: { + layout: function() { + var + maxRating = module.get.maxRating(), + html = $.fn.rating.settings.templates.icon(maxRating) + ; + module.debug('Generating icon html dynamically'); + $module + .html(html) + ; + module.refresh(); + } + }, + + event: { + mouseenter: function() { + var + $activeIcon = $(this) + ; + $activeIcon + .nextAll() + .removeClass(className.selected) + ; + $module + .addClass(className.selected) + ; + $activeIcon + .addClass(className.selected) + .prevAll() + .addClass(className.selected) + ; + }, + mouseleave: function() { + $module + .removeClass(className.selected) + ; + $icon + .removeClass(className.selected) + ; + }, + click: function() { + var + $activeIcon = $(this), + currentRating = module.get.rating(), + rating = $icon.index($activeIcon) + 1, + canClear = (settings.clearable == 'auto') + ? ($icon.length === 1) + : settings.clearable + ; + if(canClear && currentRating == rating) { + module.clearRating(); + } + else { + module.set.rating( rating ); + } + } + }, + + clearRating: function() { + module.debug('Clearing current rating'); + module.set.rating(0); + }, + + bind: { + events: function() { + module.verbose('Binding events'); + $module + .on('mouseenter' + eventNamespace, selector.icon, module.event.mouseenter) + .on('mouseleave' + eventNamespace, selector.icon, module.event.mouseleave) + .on('click' + eventNamespace, selector.icon, module.event.click) + ; + } + }, + + remove: { + events: function() { + module.verbose('Removing events'); + $module + .off(eventNamespace) + ; + }, + initialLoad: function() { + initialLoad = false; + } + }, + + enable: function() { + module.debug('Setting rating to interactive mode'); + module.bind.events(); + $module + .removeClass(className.disabled) + ; + }, + + disable: function() { + module.debug('Setting rating to read-only mode'); + module.remove.events(); + $module + .addClass(className.disabled) + ; + }, + + is: { + initialLoad: function() { + return initialLoad; + } + }, + + get: { + initialRating: function() { + if($module.data(metadata.rating) !== undefined) { + $module.removeData(metadata.rating); + return $module.data(metadata.rating); + } + return settings.initialRating; + }, + maxRating: function() { + if($module.data(metadata.maxRating) !== undefined) { + $module.removeData(metadata.maxRating); + return $module.data(metadata.maxRating); + } + return settings.maxRating; + }, + rating: function() { + var + currentRating = $icon.filter('.' + className.active).length + ; + module.verbose('Current rating retrieved', currentRating); + return currentRating; + } + }, + + set: { + rating: function(rating) { + var + ratingIndex = (rating - 1 >= 0) + ? (rating - 1) + : 0, + $activeIcon = $icon.eq(ratingIndex) + ; + $module + .removeClass(className.selected) + ; + $icon + .removeClass(className.selected) + .removeClass(className.active) + ; + if(rating > 0) { + module.verbose('Setting current rating to', rating); + $activeIcon + .prevAll() + .addBack() + .addClass(className.active) + ; + } + if(!module.is.initialLoad()) { + settings.onRate.call(element, rating); + } + }, + initialLoad: function() { + initialLoad = true; + } + }, + + setting: function(name, value) { + module.debug('Changing setting', name, value); + if( $.isPlainObject(name) ) { + $.extend(true, settings, name); + } + else if(value !== undefined) { + if($.isPlainObject(settings[name])) { + $.extend(true, settings[name], value); + } + else { + settings[name] = value; + } + } + else { + return settings[name]; + } + }, + internal: function(name, value) { + if( $.isPlainObject(name) ) { + $.extend(true, module, name); + } + else if(value !== undefined) { + module[name] = value; + } + else { + return module[name]; + } + }, + debug: function() { + if(!settings.silent && settings.debug) { + if(settings.performance) { + module.performance.log(arguments); + } + else { + module.debug = Function.prototype.bind.call(console.info, console, settings.name + ':'); + module.debug.apply(console, arguments); + } + } + }, + verbose: function() { + if(!settings.silent && settings.verbose && settings.debug) { + if(settings.performance) { + module.performance.log(arguments); + } + else { + module.verbose = Function.prototype.bind.call(console.info, console, settings.name + ':'); + module.verbose.apply(console, arguments); + } + } + }, + error: function() { + if(!settings.silent) { + module.error = Function.prototype.bind.call(console.error, console, settings.name + ':'); + module.error.apply(console, arguments); + } + }, + performance: { + log: function(message) { + var + currentTime, + executionTime, + previousTime + ; + if(settings.performance) { + currentTime = new Date().getTime(); + previousTime = time || currentTime; + executionTime = currentTime - previousTime; + time = currentTime; + performance.push({ + 'Name' : message[0], + 'Arguments' : [].slice.call(message, 1) || '', + 'Element' : element, + 'Execution Time' : executionTime + }); + } + clearTimeout(module.performance.timer); + module.performance.timer = setTimeout(module.performance.display, 500); + }, + display: function() { + var + title = settings.name + ':', + totalTime = 0 + ; + time = false; + clearTimeout(module.performance.timer); + $.each(performance, function(index, data) { + totalTime += data['Execution Time']; + }); + title += ' ' + totalTime + 'ms'; + if(moduleSelector) { + title += ' \'' + moduleSelector + '\''; + } + if($allModules.length > 1) { + title += ' ' + '(' + $allModules.length + ')'; + } + if( (console.group !== undefined || console.table !== undefined) && performance.length > 0) { + console.groupCollapsed(title); + if(console.table) { + console.table(performance); + } + else { + $.each(performance, function(index, data) { + console.log(data['Name'] + ': ' + data['Execution Time']+'ms'); + }); + } + console.groupEnd(); + } + performance = []; + } + }, + invoke: function(query, passedArguments, context) { + var + object = instance, + maxDepth, + found, + response + ; + passedArguments = passedArguments || queryArguments; + context = element || context; + if(typeof query == 'string' && object !== undefined) { + query = query.split(/[\. ]/); + maxDepth = query.length - 1; + $.each(query, function(depth, value) { + var camelCaseValue = (depth != maxDepth) + ? value + query[depth + 1].charAt(0).toUpperCase() + query[depth + 1].slice(1) + : query + ; + if( $.isPlainObject( object[camelCaseValue] ) && (depth != maxDepth) ) { + object = object[camelCaseValue]; + } + else if( object[camelCaseValue] !== undefined ) { + found = object[camelCaseValue]; + return false; + } + else if( $.isPlainObject( object[value] ) && (depth != maxDepth) ) { + object = object[value]; + } + else if( object[value] !== undefined ) { + found = object[value]; + return false; + } + else { + return false; + } + }); + } + if ( $.isFunction( found ) ) { + response = found.apply(context, passedArguments); + } + else if(found !== undefined) { + response = found; + } + if($.isArray(returnedValue)) { + returnedValue.push(response); + } + else if(returnedValue !== undefined) { + returnedValue = [returnedValue, response]; + } + else if(response !== undefined) { + returnedValue = response; + } + return found; + } + }; + if(methodInvoked) { + if(instance === undefined) { + module.initialize(); + } + module.invoke(query); + } + else { + if(instance !== undefined) { + instance.invoke('destroy'); + } + module.initialize(); + } + }) + ; + + return (returnedValue !== undefined) + ? returnedValue + : this + ; +}; + +$.fn.rating.settings = { + + name : 'Rating', + namespace : 'rating', + + slent : false, + debug : false, + verbose : false, + performance : true, + + initialRating : 0, + interactive : true, + maxRating : 4, + clearable : 'auto', + + fireOnInit : false, + + onRate : function(rating){}, + + error : { + method : 'The method you called is not defined', + noMaximum : 'No maximum rating specified. Cannot generate HTML automatically' + }, + + + metadata: { + rating : 'rating', + maxRating : 'maxRating' + }, + + className : { + active : 'active', + disabled : 'disabled', + selected : 'selected', + loading : 'loading' + }, + + selector : { + icon : '.icon' + }, + + templates: { + icon: function(maxRating) { + var + icon = 1, + html = '' + ; + while(icon <= maxRating) { + html += '<i class="icon"></i>'; + icon++; + } + return html; + } + } + +}; + +})( jQuery, window, document ); + +/*! + * # Semantic UI 2.2.6 - Search + * http://github.com/semantic-org/semantic-ui/ + * + * + * Released under the MIT license + * http://opensource.org/licenses/MIT + * + */ + +;(function ($, window, document, undefined) { + +"use strict"; + +window = (typeof window != 'undefined' && window.Math == Math) + ? window + : (typeof self != 'undefined' && self.Math == Math) + ? self + : Function('return this')() +; + +$.fn.search = function(parameters) { + var + $allModules = $(this), + moduleSelector = $allModules.selector || '', + + time = new Date().getTime(), + performance = [], + + query = arguments[0], + methodInvoked = (typeof query == 'string'), + queryArguments = [].slice.call(arguments, 1), + returnedValue + ; + $(this) + .each(function() { + var + settings = ( $.isPlainObject(parameters) ) + ? $.extend(true, {}, $.fn.search.settings, parameters) + : $.extend({}, $.fn.search.settings), + + className = settings.className, + metadata = settings.metadata, + regExp = settings.regExp, + fields = settings.fields, + selector = settings.selector, + error = settings.error, + namespace = settings.namespace, + + eventNamespace = '.' + namespace, + moduleNamespace = namespace + '-module', + + $module = $(this), + $prompt = $module.find(selector.prompt), + $searchButton = $module.find(selector.searchButton), + $results = $module.find(selector.results), + $result = $module.find(selector.result), + $category = $module.find(selector.category), + + element = this, + instance = $module.data(moduleNamespace), + + disabledBubbled = false, + + module + ; + + module = { + + initialize: function() { + module.verbose('Initializing module'); + module.determine.searchFields(); + module.bind.events(); + module.set.type(); + module.create.results(); + module.instantiate(); + }, + instantiate: function() { + module.verbose('Storing instance of module', module); + instance = module; + $module + .data(moduleNamespace, module) + ; + }, + destroy: function() { + module.verbose('Destroying instance'); + $module + .off(eventNamespace) + .removeData(moduleNamespace) + ; + }, + + refresh: function() { + module.debug('Refreshing selector cache'); + $prompt = $module.find(selector.prompt); + $searchButton = $module.find(selector.searchButton); + $category = $module.find(selector.category); + $results = $module.find(selector.results); + $result = $module.find(selector.result); + }, + + refreshResults: function() { + $results = $module.find(selector.results); + $result = $module.find(selector.result); + }, + + bind: { + events: function() { + module.verbose('Binding events to search'); + if(settings.automatic) { + $module + .on(module.get.inputEvent() + eventNamespace, selector.prompt, module.event.input) + ; + $prompt + .attr('autocomplete', 'off') + ; + } + $module + // prompt + .on('focus' + eventNamespace, selector.prompt, module.event.focus) + .on('blur' + eventNamespace, selector.prompt, module.event.blur) + .on('keydown' + eventNamespace, selector.prompt, module.handleKeyboard) + // search button + .on('click' + eventNamespace, selector.searchButton, module.query) + // results + .on('mousedown' + eventNamespace, selector.results, module.event.result.mousedown) + .on('mouseup' + eventNamespace, selector.results, module.event.result.mouseup) + .on('click' + eventNamespace, selector.result, module.event.result.click) + ; + } + }, + + determine: { + searchFields: function() { + // this makes sure $.extend does not add specified search fields to default fields + // this is the only setting which should not extend defaults + if(parameters && parameters.searchFields !== undefined) { + settings.searchFields = parameters.searchFields; + } + } + }, + + event: { + input: function() { + clearTimeout(module.timer); + module.timer = setTimeout(module.query, settings.searchDelay); + }, + focus: function() { + module.set.focus(); + if( module.has.minimumCharacters() ) { + module.query(); + if( module.can.show() ) { + module.showResults(); + } + } + }, + blur: function(event) { + var + pageLostFocus = (document.activeElement === this), + callback = function() { + module.cancel.query(); + module.remove.focus(); + module.timer = setTimeout(module.hideResults, settings.hideDelay); + } + ; + if(pageLostFocus) { + return; + } + if(module.resultsClicked) { + module.debug('Determining if user action caused search to close'); + $module + .one('click.close' + eventNamespace, selector.results, function(event) { + if(module.is.inMessage(event) || disabledBubbled) { + $prompt.focus(); + return; + } + disabledBubbled = false; + if( !module.is.animating() && !module.is.hidden()) { + callback(); + } + }) + ; + } + else { + module.debug('Input blurred without user action, closing results'); + callback(); + } + }, + result: { + mousedown: function() { + module.resultsClicked = true; + }, + mouseup: function() { + module.resultsClicked = false; + }, + click: function(event) { + module.debug('Search result selected'); + var + $result = $(this), + $title = $result.find(selector.title).eq(0), + $link = $result.is('a[href]') + ? $result + : $result.find('a[href]').eq(0), + href = $link.attr('href') || false, + target = $link.attr('target') || false, + title = $title.html(), + // title is used for result lookup + value = ($title.length > 0) + ? $title.text() + : false, + results = module.get.results(), + result = $result.data(metadata.result) || module.get.result(value, results), + returnedValue + ; + if( $.isFunction(settings.onSelect) ) { + if(settings.onSelect.call(element, result, results) === false) { + module.debug('Custom onSelect callback cancelled default select action'); + disabledBubbled = true; + return; + } + } + module.hideResults(); + if(value) { + module.set.value(value); + } + if(href) { + module.verbose('Opening search link found in result', $link); + if(target == '_blank' || event.ctrlKey) { + window.open(href); + } + else { + window.location.href = (href); + } + } + } + } + }, + handleKeyboard: function(event) { + var + // force selector refresh + $result = $module.find(selector.result), + $category = $module.find(selector.category), + $activeResult = $result.filter('.' + className.active), + currentIndex = $result.index( $activeResult ), + resultSize = $result.length, + hasActiveResult = $activeResult.length > 0, + + keyCode = event.which, + keys = { + backspace : 8, + enter : 13, + escape : 27, + upArrow : 38, + downArrow : 40 + }, + newIndex + ; + // search shortcuts + if(keyCode == keys.escape) { + module.verbose('Escape key pressed, blurring search field'); + module.trigger.blur(); + } + if( module.is.visible() ) { + if(keyCode == keys.enter) { + module.verbose('Enter key pressed, selecting active result'); + if( $result.filter('.' + className.active).length > 0 ) { + module.event.result.click.call($result.filter('.' + className.active), event); + event.preventDefault(); + return false; + } + } + else if(keyCode == keys.upArrow && hasActiveResult) { + module.verbose('Up key pressed, changing active result'); + newIndex = (currentIndex - 1 < 0) + ? currentIndex + : currentIndex - 1 + ; + $category + .removeClass(className.active) + ; + $result + .removeClass(className.active) + .eq(newIndex) + .addClass(className.active) + .closest($category) + .addClass(className.active) + ; + event.preventDefault(); + } + else if(keyCode == keys.downArrow) { + module.verbose('Down key pressed, changing active result'); + newIndex = (currentIndex + 1 >= resultSize) + ? currentIndex + : currentIndex + 1 + ; + $category + .removeClass(className.active) + ; + $result + .removeClass(className.active) + .eq(newIndex) + .addClass(className.active) + .closest($category) + .addClass(className.active) + ; + event.preventDefault(); + } + } + else { + // query shortcuts + if(keyCode == keys.enter) { + module.verbose('Enter key pressed, executing query'); + module.query(); + module.set.buttonPressed(); + $prompt.one('keyup', module.remove.buttonFocus); + } + } + }, + + setup: { + api: function(searchTerm) { + var + apiSettings = { + debug : settings.debug, + on : false, + cache : true, + action : 'search', + urlData : { + query : searchTerm + }, + onSuccess : function(response) { + module.parse.response.call(element, response, searchTerm); + }, + onAbort : function(response) { + }, + onFailure : function() { + module.displayMessage(error.serverError); + }, + onError : module.error + }, + searchHTML + ; + $.extend(true, apiSettings, settings.apiSettings); + module.verbose('Setting up API request', apiSettings); + $module.api(apiSettings); + } + }, + + can: { + useAPI: function() { + return $.fn.api !== undefined; + }, + show: function() { + return module.is.focused() && !module.is.visible() && !module.is.empty(); + }, + transition: function() { + return settings.transition && $.fn.transition !== undefined && $module.transition('is supported'); + } + }, + + is: { + animating: function() { + return $results.hasClass(className.animating); + }, + hidden: function() { + return $results.hasClass(className.hidden); + }, + inMessage: function(event) { + if(!event.target) { + return; + } + var + $target = $(event.target), + isInDOM = $.contains(document.documentElement, event.target) + ; + return (isInDOM && $target.closest(selector.message).length > 0); + }, + empty: function() { + return ($results.html() === ''); + }, + visible: function() { + return ($results.filter(':visible').length > 0); + }, + focused: function() { + return ($prompt.filter(':focus').length > 0); + } + }, + + trigger: { + blur: function() { + var + events = document.createEvent('HTMLEvents'), + promptElement = $prompt[0] + ; + if(promptElement) { + module.verbose('Triggering native blur event'); + events.initEvent('blur', false, false); + promptElement.dispatchEvent(events); + } + } + }, + + get: { + inputEvent: function() { + var + prompt = $prompt[0], + inputEvent = (prompt !== undefined && prompt.oninput !== undefined) + ? 'input' + : (prompt !== undefined && prompt.onpropertychange !== undefined) + ? 'propertychange' + : 'keyup' + ; + return inputEvent; + }, + value: function() { + return $prompt.val(); + }, + results: function() { + var + results = $module.data(metadata.results) + ; + return results; + }, + result: function(value, results) { + var + lookupFields = ['title', 'id'], + result = false + ; + value = (value !== undefined) + ? value + : module.get.value() + ; + results = (results !== undefined) + ? results + : module.get.results() + ; + if(settings.type === 'category') { + module.debug('Finding result that matches', value); + $.each(results, function(index, category) { + if($.isArray(category.results)) { + result = module.search.object(value, category.results, lookupFields)[0]; + // don't continue searching if a result is found + if(result) { + return false; + } + } + }); + } + else { + module.debug('Finding result in results object', value); + result = module.search.object(value, results, lookupFields)[0]; + } + return result || false; + }, + }, + + select: { + firstResult: function() { + module.verbose('Selecting first result'); + $result.first().addClass(className.active); + } + }, + + set: { + focus: function() { + $module.addClass(className.focus); + }, + loading: function() { + $module.addClass(className.loading); + }, + value: function(value) { + module.verbose('Setting search input value', value); + $prompt + .val(value) + ; + }, + type: function(type) { + type = type || settings.type; + if(settings.type == 'category') { + $module.addClass(settings.type); + } + }, + buttonPressed: function() { + $searchButton.addClass(className.pressed); + } + }, + + remove: { + loading: function() { + $module.removeClass(className.loading); + }, + focus: function() { + $module.removeClass(className.focus); + }, + buttonPressed: function() { + $searchButton.removeClass(className.pressed); + } + }, + + query: function() { + var + searchTerm = module.get.value(), + cache = module.read.cache(searchTerm) + ; + if( module.has.minimumCharacters() ) { + if(cache) { + module.debug('Reading result from cache', searchTerm); + module.save.results(cache.results); + module.addResults(cache.html); + module.inject.id(cache.results); + } + else { + module.debug('Querying for', searchTerm); + if($.isPlainObject(settings.source) || $.isArray(settings.source)) { + module.search.local(searchTerm); + } + else if( module.can.useAPI() ) { + module.search.remote(searchTerm); + } + else { + module.error(error.source); + } + } + settings.onSearchQuery.call(element, searchTerm); + } + else { + module.hideResults(); + } + }, + + search: { + local: function(searchTerm) { + var + results = module.search.object(searchTerm, settings.content), + searchHTML + ; + module.set.loading(); + module.save.results(results); + module.debug('Returned local search results', results); + + searchHTML = module.generateResults({ + results: results + }); + module.remove.loading(); + module.addResults(searchHTML); + module.inject.id(results); + module.write.cache(searchTerm, { + html : searchHTML, + results : results + }); + }, + remote: function(searchTerm) { + if($module.api('is loading')) { + $module.api('abort'); + } + module.setup.api(searchTerm); + $module + .api('query') + ; + }, + object: function(searchTerm, source, searchFields) { + var + results = [], + fuzzyResults = [], + searchExp = searchTerm.toString().replace(regExp.escape, '\\$&'), + matchRegExp = new RegExp(regExp.beginsWith + searchExp, 'i'), + + // avoid duplicates when pushing results + addResult = function(array, result) { + var + notResult = ($.inArray(result, results) == -1), + notFuzzyResult = ($.inArray(result, fuzzyResults) == -1) + ; + if(notResult && notFuzzyResult) { + array.push(result); + } + } + ; + source = source || settings.source; + searchFields = (searchFields !== undefined) + ? searchFields + : settings.searchFields + ; + + // search fields should be array to loop correctly + if(!$.isArray(searchFields)) { + searchFields = [searchFields]; + } + + // exit conditions if no source + if(source === undefined || source === false) { + module.error(error.source); + return []; + } + + // iterate through search fields looking for matches + $.each(searchFields, function(index, field) { + $.each(source, function(label, content) { + var + fieldExists = (typeof content[field] == 'string') + ; + if(fieldExists) { + if( content[field].search(matchRegExp) !== -1) { + // content starts with value (first in results) + addResult(results, content); + } + else if(settings.searchFullText && module.fuzzySearch(searchTerm, content[field]) ) { + // content fuzzy matches (last in results) + addResult(fuzzyResults, content); + } + } + }); + }); + return $.merge(results, fuzzyResults); + } + }, + + fuzzySearch: function(query, term) { + var + termLength = term.length, + queryLength = query.length + ; + if(typeof query !== 'string') { + return false; + } + query = query.toLowerCase(); + term = term.toLowerCase(); + if(queryLength > termLength) { + return false; + } + if(queryLength === termLength) { + return (query === term); + } + search: for (var characterIndex = 0, nextCharacterIndex = 0; characterIndex < queryLength; characterIndex++) { + var + queryCharacter = query.charCodeAt(characterIndex) + ; + while(nextCharacterIndex < termLength) { + if(term.charCodeAt(nextCharacterIndex++) === queryCharacter) { + continue search; + } + } + return false; + } + return true; + }, + + parse: { + response: function(response, searchTerm) { + var + searchHTML = module.generateResults(response) + ; + module.verbose('Parsing server response', response); + if(response !== undefined) { + if(searchTerm !== undefined && response[fields.results] !== undefined) { + module.addResults(searchHTML); + module.inject.id(response[fields.results]); + module.write.cache(searchTerm, { + html : searchHTML, + results : response[fields.results] + }); + module.save.results(response[fields.results]); + } + } + } + }, + + cancel: { + query: function() { + if( module.can.useAPI() ) { + $module.api('abort'); + } + } + }, + + has: { + minimumCharacters: function() { + var + searchTerm = module.get.value(), + numCharacters = searchTerm.length + ; + return (numCharacters >= settings.minCharacters); + } + }, + + clear: { + cache: function(value) { + var + cache = $module.data(metadata.cache) + ; + if(!value) { + module.debug('Clearing cache', value); + $module.removeData(metadata.cache); + } + else if(value && cache && cache[value]) { + module.debug('Removing value from cache', value); + delete cache[value]; + $module.data(metadata.cache, cache); + } + } + }, + + read: { + cache: function(name) { + var + cache = $module.data(metadata.cache) + ; + if(settings.cache) { + module.verbose('Checking cache for generated html for query', name); + return (typeof cache == 'object') && (cache[name] !== undefined) + ? cache[name] + : false + ; + } + return false; + } + }, + + create: { + id: function(resultIndex, categoryIndex) { + var + resultID = (resultIndex + 1), // not zero indexed + categoryID = (categoryIndex + 1), + firstCharCode, + letterID, + id + ; + if(categoryIndex !== undefined) { + // start char code for "A" + letterID = String.fromCharCode(97 + categoryIndex); + id = letterID + resultID; + module.verbose('Creating category result id', id); + } + else { + id = resultID; + module.verbose('Creating result id', id); + } + return id; + }, + results: function() { + if($results.length === 0) { + $results = $('<div />') + .addClass(className.results) + .appendTo($module) + ; + } + } + }, + + inject: { + result: function(result, resultIndex, categoryIndex) { + module.verbose('Injecting result into results'); + var + $selectedResult = (categoryIndex !== undefined) + ? $results + .children().eq(categoryIndex) + .children(selector.result).eq(resultIndex) + : $results + .children(selector.result).eq(resultIndex) + ; + module.verbose('Injecting results metadata', $selectedResult); + $selectedResult + .data(metadata.result, result) + ; + }, + id: function(results) { + module.debug('Injecting unique ids into results'); + var + // since results may be object, we must use counters + categoryIndex = 0, + resultIndex = 0 + ; + if(settings.type === 'category') { + // iterate through each category result + $.each(results, function(index, category) { + resultIndex = 0; + $.each(category.results, function(index, value) { + var + result = category.results[index] + ; + if(result.id === undefined) { + result.id = module.create.id(resultIndex, categoryIndex); + } + module.inject.result(result, resultIndex, categoryIndex); + resultIndex++; + }); + categoryIndex++; + }); + } + else { + // top level + $.each(results, function(index, value) { + var + result = results[index] + ; + if(result.id === undefined) { + result.id = module.create.id(resultIndex); + } + module.inject.result(result, resultIndex); + resultIndex++; + }); + } + return results; + } + }, + + save: { + results: function(results) { + module.verbose('Saving current search results to metadata', results); + $module.data(metadata.results, results); + } + }, + + write: { + cache: function(name, value) { + var + cache = ($module.data(metadata.cache) !== undefined) + ? $module.data(metadata.cache) + : {} + ; + if(settings.cache) { + module.verbose('Writing generated html to cache', name, value); + cache[name] = value; + $module + .data(metadata.cache, cache) + ; + } + } + }, + + addResults: function(html) { + if( $.isFunction(settings.onResultsAdd) ) { + if( settings.onResultsAdd.call($results, html) === false ) { + module.debug('onResultsAdd callback cancelled default action'); + return false; + } + } + if(html) { + $results + .html(html) + ; + module.refreshResults(); + if(settings.selectFirstResult) { + module.select.firstResult(); + } + module.showResults(); + } + else { + module.hideResults(); + } + }, + + showResults: function() { + if(!module.is.visible()) { + if( module.can.transition() ) { + module.debug('Showing results with css animations'); + $results + .transition({ + animation : settings.transition + ' in', + debug : settings.debug, + verbose : settings.verbose, + duration : settings.duration, + queue : true + }) + ; + } + else { + module.debug('Showing results with javascript'); + $results + .stop() + .fadeIn(settings.duration, settings.easing) + ; + } + settings.onResultsOpen.call($results); + } + }, + hideResults: function() { + if( module.is.visible() ) { + if( module.can.transition() ) { + module.debug('Hiding results with css animations'); + $results + .transition({ + animation : settings.transition + ' out', + debug : settings.debug, + verbose : settings.verbose, + duration : settings.duration, + queue : true + }) + ; + } + else { + module.debug('Hiding results with javascript'); + $results + .stop() + .fadeOut(settings.duration, settings.easing) + ; + } + settings.onResultsClose.call($results); + } + }, + + generateResults: function(response) { + module.debug('Generating html from response', response); + var + template = settings.templates[settings.type], + isProperObject = ($.isPlainObject(response[fields.results]) && !$.isEmptyObject(response[fields.results])), + isProperArray = ($.isArray(response[fields.results]) && response[fields.results].length > 0), + html = '' + ; + if(isProperObject || isProperArray ) { + if(settings.maxResults > 0) { + if(isProperObject) { + if(settings.type == 'standard') { + module.error(error.maxResults); + } + } + else { + response[fields.results] = response[fields.results].slice(0, settings.maxResults); + } + } + if($.isFunction(template)) { + html = template(response, fields); + } + else { + module.error(error.noTemplate, false); + } + } + else if(settings.showNoResults) { + html = module.displayMessage(error.noResults, 'empty'); + } + settings.onResults.call(element, response); + return html; + }, + + displayMessage: function(text, type) { + type = type || 'standard'; + module.debug('Displaying message', text, type); + module.addResults( settings.templates.message(text, type) ); + return settings.templates.message(text, type); + }, + + setting: function(name, value) { + if( $.isPlainObject(name) ) { + $.extend(true, settings, name); + } + else if(value !== undefined) { + settings[name] = value; + } + else { + return settings[name]; + } + }, + internal: function(name, value) { + if( $.isPlainObject(name) ) { + $.extend(true, module, name); + } + else if(value !== undefined) { + module[name] = value; + } + else { + return module[name]; + } + }, + debug: function() { + if(!settings.silent && settings.debug) { + if(settings.performance) { + module.performance.log(arguments); + } + else { + module.debug = Function.prototype.bind.call(console.info, console, settings.name + ':'); + module.debug.apply(console, arguments); + } + } + }, + verbose: function() { + if(!settings.silent && settings.verbose && settings.debug) { + if(settings.performance) { + module.performance.log(arguments); + } + else { + module.verbose = Function.prototype.bind.call(console.info, console, settings.name + ':'); + module.verbose.apply(console, arguments); + } + } + }, + error: function() { + if(!settings.silent) { + module.error = Function.prototype.bind.call(console.error, console, settings.name + ':'); + module.error.apply(console, arguments); + } + }, + performance: { + log: function(message) { + var + currentTime, + executionTime, + previousTime + ; + if(settings.performance) { + currentTime = new Date().getTime(); + previousTime = time || currentTime; + executionTime = currentTime - previousTime; + time = currentTime; + performance.push({ + 'Name' : message[0], + 'Arguments' : [].slice.call(message, 1) || '', + 'Element' : element, + 'Execution Time' : executionTime + }); + } + clearTimeout(module.performance.timer); + module.performance.timer = setTimeout(module.performance.display, 500); + }, + display: function() { + var + title = settings.name + ':', + totalTime = 0 + ; + time = false; + clearTimeout(module.performance.timer); + $.each(performance, function(index, data) { + totalTime += data['Execution Time']; + }); + title += ' ' + totalTime + 'ms'; + if(moduleSelector) { + title += ' \'' + moduleSelector + '\''; + } + if($allModules.length > 1) { + title += ' ' + '(' + $allModules.length + ')'; + } + if( (console.group !== undefined || console.table !== undefined) && performance.length > 0) { + console.groupCollapsed(title); + if(console.table) { + console.table(performance); + } + else { + $.each(performance, function(index, data) { + console.log(data['Name'] + ': ' + data['Execution Time']+'ms'); + }); + } + console.groupEnd(); + } + performance = []; + } + }, + invoke: function(query, passedArguments, context) { + var + object = instance, + maxDepth, + found, + response + ; + passedArguments = passedArguments || queryArguments; + context = element || context; + if(typeof query == 'string' && object !== undefined) { + query = query.split(/[\. ]/); + maxDepth = query.length - 1; + $.each(query, function(depth, value) { + var camelCaseValue = (depth != maxDepth) + ? value + query[depth + 1].charAt(0).toUpperCase() + query[depth + 1].slice(1) + : query + ; + if( $.isPlainObject( object[camelCaseValue] ) && (depth != maxDepth) ) { + object = object[camelCaseValue]; + } + else if( object[camelCaseValue] !== undefined ) { + found = object[camelCaseValue]; + return false; + } + else if( $.isPlainObject( object[value] ) && (depth != maxDepth) ) { + object = object[value]; + } + else if( object[value] !== undefined ) { + found = object[value]; + return false; + } + else { + return false; + } + }); + } + if( $.isFunction( found ) ) { + response = found.apply(context, passedArguments); + } + else if(found !== undefined) { + response = found; + } + if($.isArray(returnedValue)) { + returnedValue.push(response); + } + else if(returnedValue !== undefined) { + returnedValue = [returnedValue, response]; + } + else if(response !== undefined) { + returnedValue = response; + } + return found; + } + }; + if(methodInvoked) { + if(instance === undefined) { + module.initialize(); + } + module.invoke(query); + } + else { + if(instance !== undefined) { + instance.invoke('destroy'); + } + module.initialize(); + } + + }) + ; + + return (returnedValue !== undefined) + ? returnedValue + : this + ; +}; + +$.fn.search.settings = { + + name : 'Search', + namespace : 'search', + + silent : false, + debug : false, + verbose : false, + performance : true, + + // template to use (specified in settings.templates) + type : 'standard', + + // minimum characters required to search + minCharacters : 1, + + // whether to select first result after searching automatically + selectFirstResult : false, + + // API config + apiSettings : false, + + // object to search + source : false, + + // fields to search + searchFields : [ + 'title', + 'description' + ], + + // field to display in standard results template + displayField : '', + + // whether to include fuzzy results in local search + searchFullText : true, + + // whether to add events to prompt automatically + automatic : true, + + // delay before hiding menu after blur + hideDelay : 0, + + // delay before searching + searchDelay : 200, + + // maximum results returned from local + maxResults : 7, + + // whether to store lookups in local cache + cache : true, + + // whether no results errors should be shown + showNoResults : true, + + // transition settings + transition : 'scale', + duration : 200, + easing : 'easeOutExpo', + + // callbacks + onSelect : false, + onResultsAdd : false, + + onSearchQuery : function(query){}, + onResults : function(response){}, + + onResultsOpen : function(){}, + onResultsClose : function(){}, + + className: { + animating : 'animating', + active : 'active', + empty : 'empty', + focus : 'focus', + hidden : 'hidden', + loading : 'loading', + results : 'results', + pressed : 'down' + }, + + error : { + source : 'Cannot search. No source used, and Semantic API module was not included', + noResults : 'Your search returned no results', + logging : 'Error in debug logging, exiting.', + noEndpoint : 'No search endpoint was specified', + noTemplate : 'A valid template name was not specified.', + serverError : 'There was an issue querying the server.', + maxResults : 'Results must be an array to use maxResults setting', + method : 'The method you called is not defined.' + }, + + metadata: { + cache : 'cache', + results : 'results', + result : 'result' + }, + + regExp: { + escape : /[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, + beginsWith : '(?:\s|^)' + }, + + // maps api response attributes to internal representation + fields: { + categories : 'results', // array of categories (category view) + categoryName : 'name', // name of category (category view) + categoryResults : 'results', // array of results (category view) + description : 'description', // result description + image : 'image', // result image + price : 'price', // result price + results : 'results', // array of results (standard) + title : 'title', // result title + url : 'url', // result url + action : 'action', // "view more" object name + actionText : 'text', // "view more" text + actionURL : 'url' // "view more" url + }, + + selector : { + prompt : '.prompt', + searchButton : '.search.button', + results : '.results', + message : '.results > .message', + category : '.category', + result : '.result', + title : '.title, .name' + }, + + templates: { + escape: function(string) { + var + badChars = /[&<>"'`]/g, + shouldEscape = /[&<>"'`]/, + escape = { + "&": "&", + "<": "<", + ">": ">", + '"': """, + "'": "'", + "`": "`" + }, + escapedChar = function(chr) { + return escape[chr]; + } + ; + if(shouldEscape.test(string)) { + return string.replace(badChars, escapedChar); + } + return string; + }, + message: function(message, type) { + var + html = '' + ; + if(message !== undefined && type !== undefined) { + html += '' + + '<div class="message ' + type + '">' + ; + // message type + if(type == 'empty') { + html += '' + + '<div class="header">No Results</div class="header">' + + '<div class="description">' + message + '</div class="description">' + ; + } + else { + html += ' <div class="description">' + message + '</div>'; + } + html += '</div>'; + } + return html; + }, + category: function(response, fields) { + var + html = '', + escape = $.fn.search.settings.templates.escape + ; + if(response[fields.categoryResults] !== undefined) { + + // each category + $.each(response[fields.categoryResults], function(index, category) { + if(category[fields.results] !== undefined && category.results.length > 0) { + + html += '<div class="category">'; + + if(category[fields.categoryName] !== undefined) { + html += '<div class="name">' + category[fields.categoryName] + '</div>'; + } + + // each item inside category + $.each(category.results, function(index, result) { + if(result[fields.url]) { + html += '<a class="result" href="' + result[fields.url] + '">'; + } + else { + html += '<a class="result">'; + } + if(result[fields.image] !== undefined) { + html += '' + + '<div class="image">' + + ' <img src="' + result[fields.image] + '">' + + '</div>' + ; + } + html += '<div class="content">'; + if(result[fields.price] !== undefined) { + html += '<div class="price">' + result[fields.price] + '</div>'; + } + if(result[fields.title] !== undefined) { + html += '<div class="title">' + result[fields.title] + '</div>'; + } + if(result[fields.description] !== undefined) { + html += '<div class="description">' + result[fields.description] + '</div>'; + } + html += '' + + '</div>' + ; + html += '</a>'; + }); + html += '' + + '</div>' + ; + } + }); + if(response[fields.action]) { + html += '' + + '<a href="' + response[fields.action][fields.actionURL] + '" class="action">' + + response[fields.action][fields.actionText] + + '</a>'; + } + return html; + } + return false; + }, + standard: function(response, fields) { + var + html = '' + ; + if(response[fields.results] !== undefined) { + + // each result + $.each(response[fields.results], function(index, result) { + if(result[fields.url]) { + html += '<a class="result" href="' + result[fields.url] + '">'; + } + else { + html += '<a class="result">'; + } + if(result[fields.image] !== undefined) { + html += '' + + '<div class="image">' + + ' <img src="' + result[fields.image] + '">' + + '</div>' + ; + } + html += '<div class="content">'; + if(result[fields.price] !== undefined) { + html += '<div class="price">' + result[fields.price] + '</div>'; + } + if(result[fields.title] !== undefined) { + html += '<div class="title">' + result[fields.title] + '</div>'; + } + if(result[fields.description] !== undefined) { + html += '<div class="description">' + result[fields.description] + '</div>'; + } + html += '' + + '</div>' + ; + html += '</a>'; + }); + + if(response[fields.action]) { + html += '' + + '<a href="' + response[fields.action][fields.actionURL] + '" class="action">' + + response[fields.action][fields.actionText] + + '</a>'; + } + return html; + } + return false; + } + } +}; + +})( jQuery, window, document ); + +/*! + * # Semantic UI 2.2.6 - Shape + * http://github.com/semantic-org/semantic-ui/ + * + * + * Released under the MIT license + * http://opensource.org/licenses/MIT + * + */ + +;(function ($, window, document, undefined) { + +"use strict"; + +window = (typeof window != 'undefined' && window.Math == Math) + ? window + : (typeof self != 'undefined' && self.Math == Math) + ? self + : Function('return this')() +; + +$.fn.shape = function(parameters) { + var + $allModules = $(this), + $body = $('body'), + + time = new Date().getTime(), + performance = [], + + query = arguments[0], + methodInvoked = (typeof query == 'string'), + queryArguments = [].slice.call(arguments, 1), + + requestAnimationFrame = window.requestAnimationFrame + || window.mozRequestAnimationFrame + || window.webkitRequestAnimationFrame + || window.msRequestAnimationFrame + || function(callback) { setTimeout(callback, 0); }, + + returnedValue + ; + + $allModules + .each(function() { + var + moduleSelector = $allModules.selector || '', + settings = ( $.isPlainObject(parameters) ) + ? $.extend(true, {}, $.fn.shape.settings, parameters) + : $.extend({}, $.fn.shape.settings), + + // internal aliases + namespace = settings.namespace, + selector = settings.selector, + error = settings.error, + className = settings.className, + + // define namespaces for modules + eventNamespace = '.' + namespace, + moduleNamespace = 'module-' + namespace, + + // selector cache + $module = $(this), + $sides = $module.find(selector.sides), + $side = $module.find(selector.side), + + // private variables + nextIndex = false, + $activeSide, + $nextSide, + + // standard module + element = this, + instance = $module.data(moduleNamespace), + module + ; + + module = { + + initialize: function() { + module.verbose('Initializing module for', element); + module.set.defaultSide(); + module.instantiate(); + }, + + instantiate: function() { + module.verbose('Storing instance of module', module); + instance = module; + $module + .data(moduleNamespace, instance) + ; + }, + + destroy: function() { + module.verbose('Destroying previous module for', element); + $module + .removeData(moduleNamespace) + .off(eventNamespace) + ; + }, + + refresh: function() { + module.verbose('Refreshing selector cache for', element); + $module = $(element); + $sides = $(this).find(selector.shape); + $side = $(this).find(selector.side); + }, + + repaint: function() { + module.verbose('Forcing repaint event'); + var + shape = $sides[0] || document.createElement('div'), + fakeAssignment = shape.offsetWidth + ; + }, + + animate: function(propertyObject, callback) { + module.verbose('Animating box with properties', propertyObject); + callback = callback || function(event) { + module.verbose('Executing animation callback'); + if(event !== undefined) { + event.stopPropagation(); + } + module.reset(); + module.set.active(); + }; + settings.beforeChange.call($nextSide[0]); + if(module.get.transitionEvent()) { + module.verbose('Starting CSS animation'); + $module + .addClass(className.animating) + ; + $sides + .css(propertyObject) + .one(module.get.transitionEvent(), callback) + ; + module.set.duration(settings.duration); + requestAnimationFrame(function() { + $module + .addClass(className.animating) + ; + $activeSide + .addClass(className.hidden) + ; + }); + } + else { + callback(); + } + }, + + queue: function(method) { + module.debug('Queueing animation of', method); + $sides + .one(module.get.transitionEvent(), function() { + module.debug('Executing queued animation'); + setTimeout(function(){ + $module.shape(method); + }, 0); + }) + ; + }, + + reset: function() { + module.verbose('Animating states reset'); + $module + .removeClass(className.animating) + .attr('style', '') + .removeAttr('style') + ; + // removeAttr style does not consistently work in safari + $sides + .attr('style', '') + .removeAttr('style') + ; + $side + .attr('style', '') + .removeAttr('style') + .removeClass(className.hidden) + ; + $nextSide + .removeClass(className.animating) + .attr('style', '') + .removeAttr('style') + ; + }, + + is: { + complete: function() { + return ($side.filter('.' + className.active)[0] == $nextSide[0]); + }, + animating: function() { + return $module.hasClass(className.animating); + } + }, + + set: { + + defaultSide: function() { + $activeSide = $module.find('.' + settings.className.active); + $nextSide = ( $activeSide.next(selector.side).length > 0 ) + ? $activeSide.next(selector.side) + : $module.find(selector.side).first() + ; + nextIndex = false; + module.verbose('Active side set to', $activeSide); + module.verbose('Next side set to', $nextSide); + }, + + duration: function(duration) { + duration = duration || settings.duration; + duration = (typeof duration == 'number') + ? duration + 'ms' + : duration + ; + module.verbose('Setting animation duration', duration); + if(settings.duration || settings.duration === 0) { + $sides.add($side) + .css({ + '-webkit-transition-duration': duration, + '-moz-transition-duration': duration, + '-ms-transition-duration': duration, + '-o-transition-duration': duration, + 'transition-duration': duration + }) + ; + } + }, + + currentStageSize: function() { + var + $activeSide = $module.find('.' + settings.className.active), + width = $activeSide.outerWidth(true), + height = $activeSide.outerHeight(true) + ; + $module + .css({ + width: width, + height: height + }) + ; + }, + + stageSize: function() { + var + $clone = $module.clone().addClass(className.loading), + $activeSide = $clone.find('.' + settings.className.active), + $nextSide = (nextIndex) + ? $clone.find(selector.side).eq(nextIndex) + : ( $activeSide.next(selector.side).length > 0 ) + ? $activeSide.next(selector.side) + : $clone.find(selector.side).first(), + newWidth = (settings.width == 'next') + ? $nextSide.outerWidth(true) + : (settings.width == 'initial') + ? $module.width() + : settings.width, + newHeight = (settings.height == 'next') + ? $nextSide.outerHeight(true) + : (settings.height == 'initial') + ? $module.height() + : settings.height + ; + $activeSide.removeClass(className.active); + $nextSide.addClass(className.active); + $clone.insertAfter($module); + $clone.remove(); + if(settings.width != 'auto') { + $module.css('width', newWidth + settings.jitter); + module.verbose('Specifying width during animation', newWidth); + } + if(settings.height != 'auto') { + $module.css('height', newHeight + settings.jitter); + module.verbose('Specifying height during animation', newHeight); + } + }, + + nextSide: function(selector) { + nextIndex = selector; + $nextSide = $side.filter(selector); + nextIndex = $side.index($nextSide); + if($nextSide.length === 0) { + module.set.defaultSide(); + module.error(error.side); + } + module.verbose('Next side manually set to', $nextSide); + }, + + active: function() { + module.verbose('Setting new side to active', $nextSide); + $side + .removeClass(className.active) + ; + $nextSide + .addClass(className.active) + ; + settings.onChange.call($nextSide[0]); + module.set.defaultSide(); + } + }, + + flip: { + + up: function() { + if(module.is.complete() && !module.is.animating() && !settings.allowRepeats) { + module.debug('Side already visible', $nextSide); + return; + } + if( !module.is.animating()) { + module.debug('Flipping up', $nextSide); + var + transform = module.get.transform.up() + ; + module.set.stageSize(); + module.stage.above(); + module.animate(transform); + } + else { + module.queue('flip up'); + } + }, + + down: function() { + if(module.is.complete() && !module.is.animating() && !settings.allowRepeats) { + module.debug('Side already visible', $nextSide); + return; + } + if( !module.is.animating()) { + module.debug('Flipping down', $nextSide); + var + transform = module.get.transform.down() + ; + module.set.stageSize(); + module.stage.below(); + module.animate(transform); + } + else { + module.queue('flip down'); + } + }, + + left: function() { + if(module.is.complete() && !module.is.animating() && !settings.allowRepeats) { + module.debug('Side already visible', $nextSide); + return; + } + if( !module.is.animating()) { + module.debug('Flipping left', $nextSide); + var + transform = module.get.transform.left() + ; + module.set.stageSize(); + module.stage.left(); + module.animate(transform); + } + else { + module.queue('flip left'); + } + }, + + right: function() { + if(module.is.complete() && !module.is.animating() && !settings.allowRepeats) { + module.debug('Side already visible', $nextSide); + return; + } + if( !module.is.animating()) { + module.debug('Flipping right', $nextSide); + var + transform = module.get.transform.right() + ; + module.set.stageSize(); + module.stage.right(); + module.animate(transform); + } + else { + module.queue('flip right'); + } + }, + + over: function() { + if(module.is.complete() && !module.is.animating() && !settings.allowRepeats) { + module.debug('Side already visible', $nextSide); + return; + } + if( !module.is.animating()) { + module.debug('Flipping over', $nextSide); + module.set.stageSize(); + module.stage.behind(); + module.animate(module.get.transform.over() ); + } + else { + module.queue('flip over'); + } + }, + + back: function() { + if(module.is.complete() && !module.is.animating() && !settings.allowRepeats) { + module.debug('Side already visible', $nextSide); + return; + } + if( !module.is.animating()) { + module.debug('Flipping back', $nextSide); + module.set.stageSize(); + module.stage.behind(); + module.animate(module.get.transform.back() ); + } + else { + module.queue('flip back'); + } + } + + }, + + get: { + + transform: { + up: function() { + var + translate = { + y: -(($activeSide.outerHeight(true) - $nextSide.outerHeight(true)) / 2), + z: -($activeSide.outerHeight(true) / 2) + } + ; + return { + transform: 'translateY(' + translate.y + 'px) translateZ('+ translate.z + 'px) rotateX(-90deg)' + }; + }, + + down: function() { + var + translate = { + y: -(($activeSide.outerHeight(true) - $nextSide.outerHeight(true)) / 2), + z: -($activeSide.outerHeight(true) / 2) + } + ; + return { + transform: 'translateY(' + translate.y + 'px) translateZ('+ translate.z + 'px) rotateX(90deg)' + }; + }, + + left: function() { + var + translate = { + x : -(($activeSide.outerWidth(true) - $nextSide.outerWidth(true)) / 2), + z : -($activeSide.outerWidth(true) / 2) + } + ; + return { + transform: 'translateX(' + translate.x + 'px) translateZ(' + translate.z + 'px) rotateY(90deg)' + }; + }, + + right: function() { + var + translate = { + x : -(($activeSide.outerWidth(true) - $nextSide.outerWidth(true)) / 2), + z : -($activeSide.outerWidth(true) / 2) + } + ; + return { + transform: 'translateX(' + translate.x + 'px) translateZ(' + translate.z + 'px) rotateY(-90deg)' + }; + }, + + over: function() { + var + translate = { + x : -(($activeSide.outerWidth(true) - $nextSide.outerWidth(true)) / 2) + } + ; + return { + transform: 'translateX(' + translate.x + 'px) rotateY(180deg)' + }; + }, + + back: function() { + var + translate = { + x : -(($activeSide.outerWidth(true) - $nextSide.outerWidth(true)) / 2) + } + ; + return { + transform: 'translateX(' + translate.x + 'px) rotateY(-180deg)' + }; + } + }, + + transitionEvent: function() { + var + element = document.createElement('element'), + transitions = { + 'transition' :'transitionend', + 'OTransition' :'oTransitionEnd', + 'MozTransition' :'transitionend', + 'WebkitTransition' :'webkitTransitionEnd' + }, + transition + ; + for(transition in transitions){ + if( element.style[transition] !== undefined ){ + return transitions[transition]; + } + } + }, + + nextSide: function() { + return ( $activeSide.next(selector.side).length > 0 ) + ? $activeSide.next(selector.side) + : $module.find(selector.side).first() + ; + } + + }, + + stage: { + + above: function() { + var + box = { + origin : (($activeSide.outerHeight(true) - $nextSide.outerHeight(true)) / 2), + depth : { + active : ($nextSide.outerHeight(true) / 2), + next : ($activeSide.outerHeight(true) / 2) + } + } + ; + module.verbose('Setting the initial animation position as above', $nextSide, box); + $sides + .css({ + 'transform' : 'translateZ(-' + box.depth.active + 'px)' + }) + ; + $activeSide + .css({ + 'transform' : 'rotateY(0deg) translateZ(' + box.depth.active + 'px)' + }) + ; + $nextSide + .addClass(className.animating) + .css({ + 'top' : box.origin + 'px', + 'transform' : 'rotateX(90deg) translateZ(' + box.depth.next + 'px)' + }) + ; + }, + + below: function() { + var + box = { + origin : (($activeSide.outerHeight(true) - $nextSide.outerHeight(true)) / 2), + depth : { + active : ($nextSide.outerHeight(true) / 2), + next : ($activeSide.outerHeight(true) / 2) + } + } + ; + module.verbose('Setting the initial animation position as below', $nextSide, box); + $sides + .css({ + 'transform' : 'translateZ(-' + box.depth.active + 'px)' + }) + ; + $activeSide + .css({ + 'transform' : 'rotateY(0deg) translateZ(' + box.depth.active + 'px)' + }) + ; + $nextSide + .addClass(className.animating) + .css({ + 'top' : box.origin + 'px', + 'transform' : 'rotateX(-90deg) translateZ(' + box.depth.next + 'px)' + }) + ; + }, + + left: function() { + var + height = { + active : $activeSide.outerWidth(true), + next : $nextSide.outerWidth(true) + }, + box = { + origin : ( ( height.active - height.next ) / 2), + depth : { + active : (height.next / 2), + next : (height.active / 2) + } + } + ; + module.verbose('Setting the initial animation position as left', $nextSide, box); + $sides + .css({ + 'transform' : 'translateZ(-' + box.depth.active + 'px)' + }) + ; + $activeSide + .css({ + 'transform' : 'rotateY(0deg) translateZ(' + box.depth.active + 'px)' + }) + ; + $nextSide + .addClass(className.animating) + .css({ + 'left' : box.origin + 'px', + 'transform' : 'rotateY(-90deg) translateZ(' + box.depth.next + 'px)' + }) + ; + }, + + right: function() { + var + height = { + active : $activeSide.outerWidth(true), + next : $nextSide.outerWidth(true) + }, + box = { + origin : ( ( height.active - height.next ) / 2), + depth : { + active : (height.next / 2), + next : (height.active / 2) + } + } + ; + module.verbose('Setting the initial animation position as left', $nextSide, box); + $sides + .css({ + 'transform' : 'translateZ(-' + box.depth.active + 'px)' + }) + ; + $activeSide + .css({ + 'transform' : 'rotateY(0deg) translateZ(' + box.depth.active + 'px)' + }) + ; + $nextSide + .addClass(className.animating) + .css({ + 'left' : box.origin + 'px', + 'transform' : 'rotateY(90deg) translateZ(' + box.depth.next + 'px)' + }) + ; + }, + + behind: function() { + var + height = { + active : $activeSide.outerWidth(true), + next : $nextSide.outerWidth(true) + }, + box = { + origin : ( ( height.active - height.next ) / 2), + depth : { + active : (height.next / 2), + next : (height.active / 2) + } + } + ; + module.verbose('Setting the initial animation position as behind', $nextSide, box); + $activeSide + .css({ + 'transform' : 'rotateY(0deg)' + }) + ; + $nextSide + .addClass(className.animating) + .css({ + 'left' : box.origin + 'px', + 'transform' : 'rotateY(-180deg)' + }) + ; + } + }, + setting: function(name, value) { + module.debug('Changing setting', name, value); + if( $.isPlainObject(name) ) { + $.extend(true, settings, name); + } + else if(value !== undefined) { + if($.isPlainObject(settings[name])) { + $.extend(true, settings[name], value); + } + else { + settings[name] = value; + } + } + else { + return settings[name]; + } + }, + internal: function(name, value) { + if( $.isPlainObject(name) ) { + $.extend(true, module, name); + } + else if(value !== undefined) { + module[name] = value; + } + else { + return module[name]; + } + }, + debug: function() { + if(!settings.silent && settings.debug) { + if(settings.performance) { + module.performance.log(arguments); + } + else { + module.debug = Function.prototype.bind.call(console.info, console, settings.name + ':'); + module.debug.apply(console, arguments); + } + } + }, + verbose: function() { + if(!settings.silent && settings.verbose && settings.debug) { + if(settings.performance) { + module.performance.log(arguments); + } + else { + module.verbose = Function.prototype.bind.call(console.info, console, settings.name + ':'); + module.verbose.apply(console, arguments); + } + } + }, + error: function() { + if(!settings.silent) { + module.error = Function.prototype.bind.call(console.error, console, settings.name + ':'); + module.error.apply(console, arguments); + } + }, + performance: { + log: function(message) { + var + currentTime, + executionTime, + previousTime + ; + if(settings.performance) { + currentTime = new Date().getTime(); + previousTime = time || currentTime; + executionTime = currentTime - previousTime; + time = currentTime; + performance.push({ + 'Name' : message[0], + 'Arguments' : [].slice.call(message, 1) || '', + 'Element' : element, + 'Execution Time' : executionTime + }); + } + clearTimeout(module.performance.timer); + module.performance.timer = setTimeout(module.performance.display, 500); + }, + display: function() { + var + title = settings.name + ':', + totalTime = 0 + ; + time = false; + clearTimeout(module.performance.timer); + $.each(performance, function(index, data) { + totalTime += data['Execution Time']; + }); + title += ' ' + totalTime + 'ms'; + if(moduleSelector) { + title += ' \'' + moduleSelector + '\''; + } + if($allModules.length > 1) { + title += ' ' + '(' + $allModules.length + ')'; + } + if( (console.group !== undefined || console.table !== undefined) && performance.length > 0) { + console.groupCollapsed(title); + if(console.table) { + console.table(performance); + } + else { + $.each(performance, function(index, data) { + console.log(data['Name'] + ': ' + data['Execution Time']+'ms'); + }); + } + console.groupEnd(); + } + performance = []; + } + }, + invoke: function(query, passedArguments, context) { + var + object = instance, + maxDepth, + found, + response + ; + passedArguments = passedArguments || queryArguments; + context = element || context; + if(typeof query == 'string' && object !== undefined) { + query = query.split(/[\. ]/); + maxDepth = query.length - 1; + $.each(query, function(depth, value) { + var camelCaseValue = (depth != maxDepth) + ? value + query[depth + 1].charAt(0).toUpperCase() + query[depth + 1].slice(1) + : query + ; + if( $.isPlainObject( object[camelCaseValue] ) && (depth != maxDepth) ) { + object = object[camelCaseValue]; + } + else if( object[camelCaseValue] !== undefined ) { + found = object[camelCaseValue]; + return false; + } + else if( $.isPlainObject( object[value] ) && (depth != maxDepth) ) { + object = object[value]; + } + else if( object[value] !== undefined ) { + found = object[value]; + return false; + } + else { + return false; + } + }); + } + if ( $.isFunction( found ) ) { + response = found.apply(context, passedArguments); + } + else if(found !== undefined) { + response = found; + } + if($.isArray(returnedValue)) { + returnedValue.push(response); + } + else if(returnedValue !== undefined) { + returnedValue = [returnedValue, response]; + } + else if(response !== undefined) { + returnedValue = response; + } + return found; + } + }; + + if(methodInvoked) { + if(instance === undefined) { + module.initialize(); + } + module.invoke(query); + } + else { + if(instance !== undefined) { + instance.invoke('destroy'); + } + module.initialize(); + } + }) + ; + + return (returnedValue !== undefined) + ? returnedValue + : this + ; +}; + +$.fn.shape.settings = { + + // module info + name : 'Shape', + + // hide all debug content + silent : false, + + // debug content outputted to console + debug : false, + + // verbose debug output + verbose : false, + + // fudge factor in pixels when swapping from 2d to 3d (can be useful to correct rounding errors) + jitter : 0, + + // performance data output + performance: true, + + // event namespace + namespace : 'shape', + + // width during animation, can be set to 'auto', initial', 'next' or pixel amount + width: 'initial', + + // height during animation, can be set to 'auto', 'initial', 'next' or pixel amount + height: 'initial', + + // callback occurs on side change + beforeChange : function() {}, + onChange : function() {}, + + // allow animation to same side + allowRepeats: false, + + // animation duration + duration : false, + + // possible errors + error: { + side : 'You tried to switch to a side that does not exist.', + method : 'The method you called is not defined' + }, + + // classnames used + className : { + animating : 'animating', + hidden : 'hidden', + loading : 'loading', + active : 'active' + }, + + // selectors used + selector : { + sides : '.sides', + side : '.side' + } + +}; + + +})( jQuery, window, document ); + +/*! + * # Semantic UI 2.2.6 - Sidebar + * http://github.com/semantic-org/semantic-ui/ + * + * + * Released under the MIT license + * http://opensource.org/licenses/MIT + * + */ + +;(function ($, window, document, undefined) { + +"use strict"; + +window = (typeof window != 'undefined' && window.Math == Math) + ? window + : (typeof self != 'undefined' && self.Math == Math) + ? self + : Function('return this')() +; + +$.fn.sidebar = function(parameters) { + var + $allModules = $(this), + $window = $(window), + $document = $(document), + $html = $('html'), + $head = $('head'), + + moduleSelector = $allModules.selector || '', + + time = new Date().getTime(), + performance = [], + + query = arguments[0], + methodInvoked = (typeof query == 'string'), + queryArguments = [].slice.call(arguments, 1), + + requestAnimationFrame = window.requestAnimationFrame + || window.mozRequestAnimationFrame + || window.webkitRequestAnimationFrame + || window.msRequestAnimationFrame + || function(callback) { setTimeout(callback, 0); }, + + returnedValue + ; + + $allModules + .each(function() { + var + settings = ( $.isPlainObject(parameters) ) + ? $.extend(true, {}, $.fn.sidebar.settings, parameters) + : $.extend({}, $.fn.sidebar.settings), + + selector = settings.selector, + className = settings.className, + namespace = settings.namespace, + regExp = settings.regExp, + error = settings.error, + + eventNamespace = '.' + namespace, + moduleNamespace = 'module-' + namespace, + + $module = $(this), + $context = $(settings.context), + + $sidebars = $module.children(selector.sidebar), + $fixed = $context.children(selector.fixed), + $pusher = $context.children(selector.pusher), + $style, + + element = this, + instance = $module.data(moduleNamespace), + + elementNamespace, + id, + currentScroll, + transitionEvent, + + module + ; + + module = { + + initialize: function() { + module.debug('Initializing sidebar', parameters); + + module.create.id(); + + transitionEvent = module.get.transitionEvent(); + + if(module.is.ios()) { + module.set.ios(); + } + + // avoids locking rendering if initialized in onReady + if(settings.delaySetup) { + requestAnimationFrame(module.setup.layout); + } + else { + module.setup.layout(); + } + + requestAnimationFrame(function() { + module.setup.cache(); + }); + + module.instantiate(); + }, + + instantiate: function() { + module.verbose('Storing instance of module', module); + instance = module; + $module + .data(moduleNamespace, module) + ; + }, + + create: { + id: function() { + id = (Math.random().toString(16) + '000000000').substr(2,8); + elementNamespace = '.' + id; + module.verbose('Creating unique id for element', id); + } + }, + + destroy: function() { + module.verbose('Destroying previous module for', $module); + $module + .off(eventNamespace) + .removeData(moduleNamespace) + ; + if(module.is.ios()) { + module.remove.ios(); + } + // bound by uuid + $context.off(elementNamespace); + $window.off(elementNamespace); + $document.off(elementNamespace); + }, + + event: { + clickaway: function(event) { + var + clickedInPusher = ($pusher.find(event.target).length > 0 || $pusher.is(event.target)), + clickedContext = ($context.is(event.target)) + ; + if(clickedInPusher) { + module.verbose('User clicked on dimmed page'); + module.hide(); + } + if(clickedContext) { + module.verbose('User clicked on dimmable context (scaled out page)'); + module.hide(); + } + }, + touch: function(event) { + //event.stopPropagation(); + }, + containScroll: function(event) { + if(element.scrollTop <= 0) { + element.scrollTop = 1; + } + if((element.scrollTop + element.offsetHeight) >= element.scrollHeight) { + element.scrollTop = element.scrollHeight - element.offsetHeight - 1; + } + }, + scroll: function(event) { + if( $(event.target).closest(selector.sidebar).length === 0 ) { + event.preventDefault(); + } + } + }, + + bind: { + clickaway: function() { + module.verbose('Adding clickaway events to context', $context); + if(settings.closable) { + $context + .on('click' + elementNamespace, module.event.clickaway) + .on('touchend' + elementNamespace, module.event.clickaway) + ; + } + }, + scrollLock: function() { + if(settings.scrollLock) { + module.debug('Disabling page scroll'); + $window + .on('DOMMouseScroll' + elementNamespace, module.event.scroll) + ; + } + module.verbose('Adding events to contain sidebar scroll'); + $document + .on('touchmove' + elementNamespace, module.event.touch) + ; + $module + .on('scroll' + eventNamespace, module.event.containScroll) + ; + } + }, + unbind: { + clickaway: function() { + module.verbose('Removing clickaway events from context', $context); + $context.off(elementNamespace); + }, + scrollLock: function() { + module.verbose('Removing scroll lock from page'); + $document.off(elementNamespace); + $window.off(elementNamespace); + $module.off('scroll' + eventNamespace); + } + }, + + add: { + inlineCSS: function() { + var + width = module.cache.width || $module.outerWidth(), + height = module.cache.height || $module.outerHeight(), + isRTL = module.is.rtl(), + direction = module.get.direction(), + distance = { + left : width, + right : -width, + top : height, + bottom : -height + }, + style + ; + + if(isRTL){ + module.verbose('RTL detected, flipping widths'); + distance.left = -width; + distance.right = width; + } + + style = '<style>'; + + if(direction === 'left' || direction === 'right') { + module.debug('Adding CSS rules for animation distance', width); + style += '' + + ' .ui.visible.' + direction + '.sidebar ~ .fixed,' + + ' .ui.visible.' + direction + '.sidebar ~ .pusher {' + + ' -webkit-transform: translate3d('+ distance[direction] + 'px, 0, 0);' + + ' transform: translate3d('+ distance[direction] + 'px, 0, 0);' + + ' }' + ; + } + else if(direction === 'top' || direction == 'bottom') { + style += '' + + ' .ui.visible.' + direction + '.sidebar ~ .fixed,' + + ' .ui.visible.' + direction + '.sidebar ~ .pusher {' + + ' -webkit-transform: translate3d(0, ' + distance[direction] + 'px, 0);' + + ' transform: translate3d(0, ' + distance[direction] + 'px, 0);' + + ' }' + ; + } + + /* IE is only browser not to create context with transforms */ + /* https://www.w3.org/Bugs/Public/show_bug.cgi?id=16328 */ + if( module.is.ie() ) { + if(direction === 'left' || direction === 'right') { + module.debug('Adding CSS rules for animation distance', width); + style += '' + + ' body.pushable > .ui.visible.' + direction + '.sidebar ~ .pusher:after {' + + ' -webkit-transform: translate3d('+ distance[direction] + 'px, 0, 0);' + + ' transform: translate3d('+ distance[direction] + 'px, 0, 0);' + + ' }' + ; + } + else if(direction === 'top' || direction == 'bottom') { + style += '' + + ' body.pushable > .ui.visible.' + direction + '.sidebar ~ .pusher:after {' + + ' -webkit-transform: translate3d(0, ' + distance[direction] + 'px, 0);' + + ' transform: translate3d(0, ' + distance[direction] + 'px, 0);' + + ' }' + ; + } + /* opposite sides visible forces content overlay */ + style += '' + + ' body.pushable > .ui.visible.left.sidebar ~ .ui.visible.right.sidebar ~ .pusher:after,' + + ' body.pushable > .ui.visible.right.sidebar ~ .ui.visible.left.sidebar ~ .pusher:after {' + + ' -webkit-transform: translate3d(0px, 0, 0);' + + ' transform: translate3d(0px, 0, 0);' + + ' }' + ; + } + style += '</style>'; + $style = $(style) + .appendTo($head) + ; + module.debug('Adding sizing css to head', $style); + } + }, + + refresh: function() { + module.verbose('Refreshing selector cache'); + $context = $(settings.context); + $sidebars = $context.children(selector.sidebar); + $pusher = $context.children(selector.pusher); + $fixed = $context.children(selector.fixed); + module.clear.cache(); + }, + + refreshSidebars: function() { + module.verbose('Refreshing other sidebars'); + $sidebars = $context.children(selector.sidebar); + }, + + repaint: function() { + module.verbose('Forcing repaint event'); + element.style.display = 'none'; + var ignored = element.offsetHeight; + element.scrollTop = element.scrollTop; + element.style.display = ''; + }, + + setup: { + cache: function() { + module.cache = { + width : $module.outerWidth(), + height : $module.outerHeight(), + rtl : ($module.css('direction') == 'rtl') + }; + }, + layout: function() { + if( $context.children(selector.pusher).length === 0 ) { + module.debug('Adding wrapper element for sidebar'); + module.error(error.pusher); + $pusher = $('<div class="pusher" />'); + $context + .children() + .not(selector.omitted) + .not($sidebars) + .wrapAll($pusher) + ; + module.refresh(); + } + if($module.nextAll(selector.pusher).length === 0 || $module.nextAll(selector.pusher)[0] !== $pusher[0]) { + module.debug('Moved sidebar to correct parent element'); + module.error(error.movedSidebar, element); + $module.detach().prependTo($context); + module.refresh(); + } + module.clear.cache(); + module.set.pushable(); + module.set.direction(); + } + }, + + attachEvents: function(selector, event) { + var + $toggle = $(selector) + ; + event = $.isFunction(module[event]) + ? module[event] + : module.toggle + ; + if($toggle.length > 0) { + module.debug('Attaching sidebar events to element', selector, event); + $toggle + .on('click' + eventNamespace, event) + ; + } + else { + module.error(error.notFound, selector); + } + }, + + show: function(callback) { + callback = $.isFunction(callback) + ? callback + : function(){} + ; + if(module.is.hidden()) { + module.refreshSidebars(); + if(settings.overlay) { + module.error(error.overlay); + settings.transition = 'overlay'; + } + module.refresh(); + if(module.othersActive()) { + module.debug('Other sidebars currently visible'); + if(settings.exclusive) { + // if not overlay queue animation after hide + if(settings.transition != 'overlay') { + module.hideOthers(module.show); + return; + } + else { + module.hideOthers(); + } + } + else { + settings.transition = 'overlay'; + } + } + module.pushPage(function() { + callback.call(element); + settings.onShow.call(element); + }); + settings.onChange.call(element); + settings.onVisible.call(element); + } + else { + module.debug('Sidebar is already visible'); + } + }, + + hide: function(callback) { + callback = $.isFunction(callback) + ? callback + : function(){} + ; + if(module.is.visible() || module.is.animating()) { + module.debug('Hiding sidebar', callback); + module.refreshSidebars(); + module.pullPage(function() { + callback.call(element); + settings.onHidden.call(element); + }); + settings.onChange.call(element); + settings.onHide.call(element); + } + }, + + othersAnimating: function() { + return ($sidebars.not($module).filter('.' + className.animating).length > 0); + }, + othersVisible: function() { + return ($sidebars.not($module).filter('.' + className.visible).length > 0); + }, + othersActive: function() { + return(module.othersVisible() || module.othersAnimating()); + }, + + hideOthers: function(callback) { + var + $otherSidebars = $sidebars.not($module).filter('.' + className.visible), + sidebarCount = $otherSidebars.length, + callbackCount = 0 + ; + callback = callback || function(){}; + $otherSidebars + .sidebar('hide', function() { + callbackCount++; + if(callbackCount == sidebarCount) { + callback(); + } + }) + ; + }, + + toggle: function() { + module.verbose('Determining toggled direction'); + if(module.is.hidden()) { + module.show(); + } + else { + module.hide(); + } + }, + + pushPage: function(callback) { + var + transition = module.get.transition(), + $transition = (transition === 'overlay' || module.othersActive()) + ? $module + : $pusher, + animate, + dim, + transitionEnd + ; + callback = $.isFunction(callback) + ? callback + : function(){} + ; + if(settings.transition == 'scale down') { + module.scrollToTop(); + } + module.set.transition(transition); + module.repaint(); + animate = function() { + module.bind.clickaway(); + module.add.inlineCSS(); + module.set.animating(); + module.set.visible(); + }; + dim = function() { + module.set.dimmed(); + }; + transitionEnd = function(event) { + if( event.target == $transition[0] ) { + $transition.off(transitionEvent + elementNamespace, transitionEnd); + module.remove.animating(); + module.bind.scrollLock(); + callback.call(element); + } + }; + $transition.off(transitionEvent + elementNamespace); + $transition.on(transitionEvent + elementNamespace, transitionEnd); + requestAnimationFrame(animate); + if(settings.dimPage && !module.othersVisible()) { + requestAnimationFrame(dim); + } + }, + + pullPage: function(callback) { + var + transition = module.get.transition(), + $transition = (transition == 'overlay' || module.othersActive()) + ? $module + : $pusher, + animate, + transitionEnd + ; + callback = $.isFunction(callback) + ? callback + : function(){} + ; + module.verbose('Removing context push state', module.get.direction()); + + module.unbind.clickaway(); + module.unbind.scrollLock(); + + animate = function() { + module.set.transition(transition); + module.set.animating(); + module.remove.visible(); + if(settings.dimPage && !module.othersVisible()) { + $pusher.removeClass(className.dimmed); + } + }; + transitionEnd = function(event) { + if( event.target == $transition[0] ) { + $transition.off(transitionEvent + elementNamespace, transitionEnd); + module.remove.animating(); + module.remove.transition(); + module.remove.inlineCSS(); + if(transition == 'scale down' || (settings.returnScroll && module.is.mobile()) ) { + module.scrollBack(); + } + callback.call(element); + } + }; + $transition.off(transitionEvent + elementNamespace); + $transition.on(transitionEvent + elementNamespace, transitionEnd); + requestAnimationFrame(animate); + }, + + scrollToTop: function() { + module.verbose('Scrolling to top of page to avoid animation issues'); + currentScroll = $(window).scrollTop(); + $module.scrollTop(0); + window.scrollTo(0, 0); + }, + + scrollBack: function() { + module.verbose('Scrolling back to original page position'); + window.scrollTo(0, currentScroll); + }, + + clear: { + cache: function() { + module.verbose('Clearing cached dimensions'); + module.cache = {}; + } + }, + + set: { + + // ios only (scroll on html not document). This prevent auto-resize canvas/scroll in ios + ios: function() { + $html.addClass(className.ios); + }, + + // container + pushed: function() { + $context.addClass(className.pushed); + }, + pushable: function() { + $context.addClass(className.pushable); + }, + + // pusher + dimmed: function() { + $pusher.addClass(className.dimmed); + }, + + // sidebar + active: function() { + $module.addClass(className.active); + }, + animating: function() { + $module.addClass(className.animating); + }, + transition: function(transition) { + transition = transition || module.get.transition(); + $module.addClass(transition); + }, + direction: function(direction) { + direction = direction || module.get.direction(); + $module.addClass(className[direction]); + }, + visible: function() { + $module.addClass(className.visible); + }, + overlay: function() { + $module.addClass(className.overlay); + } + }, + remove: { + + inlineCSS: function() { + module.debug('Removing inline css styles', $style); + if($style && $style.length > 0) { + $style.remove(); + } + }, + + // ios scroll on html not document + ios: function() { + $html.removeClass(className.ios); + }, + + // context + pushed: function() { + $context.removeClass(className.pushed); + }, + pushable: function() { + $context.removeClass(className.pushable); + }, + + // sidebar + active: function() { + $module.removeClass(className.active); + }, + animating: function() { + $module.removeClass(className.animating); + }, + transition: function(transition) { + transition = transition || module.get.transition(); + $module.removeClass(transition); + }, + direction: function(direction) { + direction = direction || module.get.direction(); + $module.removeClass(className[direction]); + }, + visible: function() { + $module.removeClass(className.visible); + }, + overlay: function() { + $module.removeClass(className.overlay); + } + }, + + get: { + direction: function() { + if($module.hasClass(className.top)) { + return className.top; + } + else if($module.hasClass(className.right)) { + return className.right; + } + else if($module.hasClass(className.bottom)) { + return className.bottom; + } + return className.left; + }, + transition: function() { + var + direction = module.get.direction(), + transition + ; + transition = ( module.is.mobile() ) + ? (settings.mobileTransition == 'auto') + ? settings.defaultTransition.mobile[direction] + : settings.mobileTransition + : (settings.transition == 'auto') + ? settings.defaultTransition.computer[direction] + : settings.transition + ; + module.verbose('Determined transition', transition); + return transition; + }, + transitionEvent: function() { + var + element = document.createElement('element'), + transitions = { + 'transition' :'transitionend', + 'OTransition' :'oTransitionEnd', + 'MozTransition' :'transitionend', + 'WebkitTransition' :'webkitTransitionEnd' + }, + transition + ; + for(transition in transitions){ + if( element.style[transition] !== undefined ){ + return transitions[transition]; + } + } + } + }, + + is: { + + ie: function() { + var + isIE11 = (!(window.ActiveXObject) && 'ActiveXObject' in window), + isIE = ('ActiveXObject' in window) + ; + return (isIE11 || isIE); + }, + + ios: function() { + var + userAgent = navigator.userAgent, + isIOS = userAgent.match(regExp.ios), + isMobileChrome = userAgent.match(regExp.mobileChrome) + ; + if(isIOS && !isMobileChrome) { + module.verbose('Browser was found to be iOS', userAgent); + return true; + } + else { + return false; + } + }, + mobile: function() { + var + userAgent = navigator.userAgent, + isMobile = userAgent.match(regExp.mobile) + ; + if(isMobile) { + module.verbose('Browser was found to be mobile', userAgent); + return true; + } + else { + module.verbose('Browser is not mobile, using regular transition', userAgent); + return false; + } + }, + hidden: function() { + return !module.is.visible(); + }, + visible: function() { + return $module.hasClass(className.visible); + }, + // alias + open: function() { + return module.is.visible(); + }, + closed: function() { + return module.is.hidden(); + }, + vertical: function() { + return $module.hasClass(className.top); + }, + animating: function() { + return $context.hasClass(className.animating); + }, + rtl: function () { + if(module.cache.rtl === undefined) { + module.cache.rtl = ($module.css('direction') == 'rtl'); + } + return module.cache.rtl; + } + }, + + setting: function(name, value) { + module.debug('Changing setting', name, value); + if( $.isPlainObject(name) ) { + $.extend(true, settings, name); + } + else if(value !== undefined) { + if($.isPlainObject(settings[name])) { + $.extend(true, settings[name], value); + } + else { + settings[name] = value; + } + } + else { + return settings[name]; + } + }, + internal: function(name, value) { + if( $.isPlainObject(name) ) { + $.extend(true, module, name); + } + else if(value !== undefined) { + module[name] = value; + } + else { + return module[name]; + } + }, + debug: function() { + if(!settings.silent && settings.debug) { + if(settings.performance) { + module.performance.log(arguments); + } + else { + module.debug = Function.prototype.bind.call(console.info, console, settings.name + ':'); + module.debug.apply(console, arguments); + } + } + }, + verbose: function() { + if(!settings.silent && settings.verbose && settings.debug) { + if(settings.performance) { + module.performance.log(arguments); + } + else { + module.verbose = Function.prototype.bind.call(console.info, console, settings.name + ':'); + module.verbose.apply(console, arguments); + } + } + }, + error: function() { + if(!settings.silent) { + module.error = Function.prototype.bind.call(console.error, console, settings.name + ':'); + module.error.apply(console, arguments); + } + }, + performance: { + log: function(message) { + var + currentTime, + executionTime, + previousTime + ; + if(settings.performance) { + currentTime = new Date().getTime(); + previousTime = time || currentTime; + executionTime = currentTime - previousTime; + time = currentTime; + performance.push({ + 'Name' : message[0], + 'Arguments' : [].slice.call(message, 1) || '', + 'Element' : element, + 'Execution Time' : executionTime + }); + } + clearTimeout(module.performance.timer); + module.performance.timer = setTimeout(module.performance.display, 500); + }, + display: function() { + var + title = settings.name + ':', + totalTime = 0 + ; + time = false; + clearTimeout(module.performance.timer); + $.each(performance, function(index, data) { + totalTime += data['Execution Time']; + }); + title += ' ' + totalTime + 'ms'; + if(moduleSelector) { + title += ' \'' + moduleSelector + '\''; + } + if( (console.group !== undefined || console.table !== undefined) && performance.length > 0) { + console.groupCollapsed(title); + if(console.table) { + console.table(performance); + } + else { + $.each(performance, function(index, data) { + console.log(data['Name'] + ': ' + data['Execution Time']+'ms'); + }); + } + console.groupEnd(); + } + performance = []; + } + }, + invoke: function(query, passedArguments, context) { + var + object = instance, + maxDepth, + found, + response + ; + passedArguments = passedArguments || queryArguments; + context = element || context; + if(typeof query == 'string' && object !== undefined) { + query = query.split(/[\. ]/); + maxDepth = query.length - 1; + $.each(query, function(depth, value) { + var camelCaseValue = (depth != maxDepth) + ? value + query[depth + 1].charAt(0).toUpperCase() + query[depth + 1].slice(1) + : query + ; + if( $.isPlainObject( object[camelCaseValue] ) && (depth != maxDepth) ) { + object = object[camelCaseValue]; + } + else if( object[camelCaseValue] !== undefined ) { + found = object[camelCaseValue]; + return false; + } + else if( $.isPlainObject( object[value] ) && (depth != maxDepth) ) { + object = object[value]; + } + else if( object[value] !== undefined ) { + found = object[value]; + return false; + } + else { + module.error(error.method, query); + return false; + } + }); + } + if ( $.isFunction( found ) ) { + response = found.apply(context, passedArguments); + } + else if(found !== undefined) { + response = found; + } + if($.isArray(returnedValue)) { + returnedValue.push(response); + } + else if(returnedValue !== undefined) { + returnedValue = [returnedValue, response]; + } + else if(response !== undefined) { + returnedValue = response; + } + return found; + } + } + ; + + if(methodInvoked) { + if(instance === undefined) { + module.initialize(); + } + module.invoke(query); + } + else { + if(instance !== undefined) { + module.invoke('destroy'); + } + module.initialize(); + } + }); + + return (returnedValue !== undefined) + ? returnedValue + : this + ; +}; + +$.fn.sidebar.settings = { + + name : 'Sidebar', + namespace : 'sidebar', + + silent : false, + debug : false, + verbose : false, + performance : true, + + transition : 'auto', + mobileTransition : 'auto', + + defaultTransition : { + computer: { + left : 'uncover', + right : 'uncover', + top : 'overlay', + bottom : 'overlay' + }, + mobile: { + left : 'uncover', + right : 'uncover', + top : 'overlay', + bottom : 'overlay' + } + }, + + context : 'body', + exclusive : false, + closable : true, + dimPage : true, + scrollLock : false, + returnScroll : false, + delaySetup : false, + + duration : 500, + + onChange : function(){}, + onShow : function(){}, + onHide : function(){}, + + onHidden : function(){}, + onVisible : function(){}, + + className : { + active : 'active', + animating : 'animating', + dimmed : 'dimmed', + ios : 'ios', + pushable : 'pushable', + pushed : 'pushed', + right : 'right', + top : 'top', + left : 'left', + bottom : 'bottom', + visible : 'visible' + }, + + selector: { + fixed : '.fixed', + omitted : 'script, link, style, .ui.modal, .ui.dimmer, .ui.nag, .ui.fixed', + pusher : '.pusher', + sidebar : '.ui.sidebar' + }, + + regExp: { + ios : /(iPad|iPhone|iPod)/g, + mobileChrome : /(CriOS)/g, + mobile : /Mobile|iP(hone|od|ad)|Android|BlackBerry|IEMobile|Kindle|NetFront|Silk-Accelerated|(hpw|web)OS|Fennec|Minimo|Opera M(obi|ini)|Blazer|Dolfin|Dolphin|Skyfire|Zune/g + }, + + error : { + method : 'The method you called is not defined.', + pusher : 'Had to add pusher element. For optimal performance make sure body content is inside a pusher element', + movedSidebar : 'Had to move sidebar. For optimal performance make sure sidebar and pusher are direct children of your body tag', + overlay : 'The overlay setting is no longer supported, use animation: overlay', + notFound : 'There were no elements that matched the specified selector' + } + +}; + + +})( jQuery, window, document ); + +/*! + * # Semantic UI 2.2.6 - Sticky + * http://github.com/semantic-org/semantic-ui/ + * + * + * Released under the MIT license + * http://opensource.org/licenses/MIT + * + */ + +;(function ($, window, document, undefined) { + +"use strict"; + +window = (typeof window != 'undefined' && window.Math == Math) + ? window + : (typeof self != 'undefined' && self.Math == Math) + ? self + : Function('return this')() +; + +$.fn.sticky = function(parameters) { + var + $allModules = $(this), + moduleSelector = $allModules.selector || '', + + time = new Date().getTime(), + performance = [], + + query = arguments[0], + methodInvoked = (typeof query == 'string'), + queryArguments = [].slice.call(arguments, 1), + returnedValue + ; + + $allModules + .each(function() { + var + settings = ( $.isPlainObject(parameters) ) + ? $.extend(true, {}, $.fn.sticky.settings, parameters) + : $.extend({}, $.fn.sticky.settings), + + className = settings.className, + namespace = settings.namespace, + error = settings.error, + + eventNamespace = '.' + namespace, + moduleNamespace = 'module-' + namespace, + + $module = $(this), + $window = $(window), + $scroll = $(settings.scrollContext), + $container, + $context, + + selector = $module.selector || '', + instance = $module.data(moduleNamespace), + + requestAnimationFrame = window.requestAnimationFrame + || window.mozRequestAnimationFrame + || window.webkitRequestAnimationFrame + || window.msRequestAnimationFrame + || function(callback) { setTimeout(callback, 0); }, + + element = this, + + documentObserver, + observer, + module + ; + + module = { + + initialize: function() { + + module.determineContainer(); + module.determineContext(); + module.verbose('Initializing sticky', settings, $container); + + module.save.positions(); + module.checkErrors(); + module.bind.events(); + + if(settings.observeChanges) { + module.observeChanges(); + } + module.instantiate(); + }, + + instantiate: function() { + module.verbose('Storing instance of module', module); + instance = module; + $module + .data(moduleNamespace, module) + ; + }, + + destroy: function() { + module.verbose('Destroying previous instance'); + module.reset(); + if(documentObserver) { + documentObserver.disconnect(); + } + if(observer) { + observer.disconnect(); + } + $window + .off('load' + eventNamespace, module.event.load) + .off('resize' + eventNamespace, module.event.resize) + ; + $scroll + .off('scrollchange' + eventNamespace, module.event.scrollchange) + ; + $module.removeData(moduleNamespace); + }, + + observeChanges: function() { + if('MutationObserver' in window) { + documentObserver = new MutationObserver(module.event.documentChanged); + observer = new MutationObserver(module.event.changed); + documentObserver.observe(document, { + childList : true, + subtree : true + }); + observer.observe(element, { + childList : true, + subtree : true + }); + observer.observe($context[0], { + childList : true, + subtree : true + }); + module.debug('Setting up mutation observer', observer); + } + }, + + determineContainer: function() { + if(settings.container) { + $container = $(settings.container); + } + else { + $container = $module.offsetParent(); + } + }, + + determineContext: function() { + if(settings.context) { + $context = $(settings.context); + } + else { + $context = $container; + } + if($context.length === 0) { + module.error(error.invalidContext, settings.context, $module); + return; + } + }, + + checkErrors: function() { + if( module.is.hidden() ) { + module.error(error.visible, $module); + } + if(module.cache.element.height > module.cache.context.height) { + module.reset(); + module.error(error.elementSize, $module); + return; + } + }, + + bind: { + events: function() { + $window + .on('load' + eventNamespace, module.event.load) + .on('resize' + eventNamespace, module.event.resize) + ; + // pub/sub pattern + $scroll + .off('scroll' + eventNamespace) + .on('scroll' + eventNamespace, module.event.scroll) + .on('scrollchange' + eventNamespace, module.event.scrollchange) + ; + } + }, + + event: { + changed: function(mutations) { + clearTimeout(module.timer); + module.timer = setTimeout(function() { + module.verbose('DOM tree modified, updating sticky menu', mutations); + module.refresh(); + }, 100); + }, + documentChanged: function(mutations) { + [].forEach.call(mutations, function(mutation) { + if(mutation.removedNodes) { + [].forEach.call(mutation.removedNodes, function(node) { + if(node == element || $(node).find(element).length > 0) { + module.debug('Element removed from DOM, tearing down events'); + module.destroy(); + } + }); + } + }); + }, + load: function() { + module.verbose('Page contents finished loading'); + requestAnimationFrame(module.refresh); + }, + resize: function() { + module.verbose('Window resized'); + requestAnimationFrame(module.refresh); + }, + scroll: function() { + requestAnimationFrame(function() { + $scroll.triggerHandler('scrollchange' + eventNamespace, $scroll.scrollTop() ); + }); + }, + scrollchange: function(event, scrollPosition) { + module.stick(scrollPosition); + settings.onScroll.call(element); + } + }, + + refresh: function(hardRefresh) { + module.reset(); + if(!settings.context) { + module.determineContext(); + } + if(hardRefresh) { + module.determineContainer(); + } + module.save.positions(); + module.stick(); + settings.onReposition.call(element); + }, + + supports: { + sticky: function() { + var + $element = $('<div/>'), + element = $element[0] + ; + $element.addClass(className.supported); + return($element.css('position').match('sticky')); + } + }, + + save: { + lastScroll: function(scroll) { + module.lastScroll = scroll; + }, + elementScroll: function(scroll) { + module.elementScroll = scroll; + }, + positions: function() { + var + scrollContext = { + height : $scroll.height() + }, + element = { + margin: { + top : parseInt($module.css('margin-top'), 10), + bottom : parseInt($module.css('margin-bottom'), 10), + }, + offset : $module.offset(), + width : $module.outerWidth(), + height : $module.outerHeight() + }, + context = { + offset : $context.offset(), + height : $context.outerHeight() + }, + container = { + height: $container.outerHeight() + } + ; + if( !module.is.standardScroll() ) { + module.debug('Non-standard scroll. Removing scroll offset from element offset'); + + scrollContext.top = $scroll.scrollTop(); + scrollContext.left = $scroll.scrollLeft(); + + element.offset.top += scrollContext.top; + context.offset.top += scrollContext.top; + element.offset.left += scrollContext.left; + context.offset.left += scrollContext.left; + } + module.cache = { + fits : ( element.height < scrollContext.height ), + scrollContext : { + height : scrollContext.height + }, + element: { + margin : element.margin, + top : element.offset.top - element.margin.top, + left : element.offset.left, + width : element.width, + height : element.height, + bottom : element.offset.top + element.height + }, + context: { + top : context.offset.top, + height : context.height, + bottom : context.offset.top + context.height + } + }; + module.set.containerSize(); + module.set.size(); + module.stick(); + module.debug('Caching element positions', module.cache); + } + }, + + get: { + direction: function(scroll) { + var + direction = 'down' + ; + scroll = scroll || $scroll.scrollTop(); + if(module.lastScroll !== undefined) { + if(module.lastScroll < scroll) { + direction = 'down'; + } + else if(module.lastScroll > scroll) { + direction = 'up'; + } + } + return direction; + }, + scrollChange: function(scroll) { + scroll = scroll || $scroll.scrollTop(); + return (module.lastScroll) + ? (scroll - module.lastScroll) + : 0 + ; + }, + currentElementScroll: function() { + if(module.elementScroll) { + return module.elementScroll; + } + return ( module.is.top() ) + ? Math.abs(parseInt($module.css('top'), 10)) || 0 + : Math.abs(parseInt($module.css('bottom'), 10)) || 0 + ; + }, + + elementScroll: function(scroll) { + scroll = scroll || $scroll.scrollTop(); + var + element = module.cache.element, + scrollContext = module.cache.scrollContext, + delta = module.get.scrollChange(scroll), + maxScroll = (element.height - scrollContext.height + settings.offset), + elementScroll = module.get.currentElementScroll(), + possibleScroll = (elementScroll + delta) + ; + if(module.cache.fits || possibleScroll < 0) { + elementScroll = 0; + } + else if(possibleScroll > maxScroll ) { + elementScroll = maxScroll; + } + else { + elementScroll = possibleScroll; + } + return elementScroll; + } + }, + + remove: { + lastScroll: function() { + delete module.lastScroll; + }, + elementScroll: function(scroll) { + delete module.elementScroll; + }, + offset: function() { + $module.css('margin-top', ''); + } + }, + + set: { + offset: function() { + module.verbose('Setting offset on element', settings.offset); + $module + .css('margin-top', settings.offset) + ; + }, + containerSize: function() { + var + tagName = $container.get(0).tagName + ; + if(tagName === 'HTML' || tagName == 'body') { + // this can trigger for too many reasons + //module.error(error.container, tagName, $module); + module.determineContainer(); + } + else { + if( Math.abs($container.outerHeight() - module.cache.context.height) > settings.jitter) { + module.debug('Context has padding, specifying exact height for container', module.cache.context.height); + $container.css({ + height: module.cache.context.height + }); + } + } + }, + minimumSize: function() { + var + element = module.cache.element + ; + $container + .css('min-height', element.height) + ; + }, + scroll: function(scroll) { + module.debug('Setting scroll on element', scroll); + if(module.elementScroll == scroll) { + return; + } + if( module.is.top() ) { + $module + .css('bottom', '') + .css('top', -scroll) + ; + } + if( module.is.bottom() ) { + $module + .css('top', '') + .css('bottom', scroll) + ; + } + }, + size: function() { + if(module.cache.element.height !== 0 && module.cache.element.width !== 0) { + element.style.setProperty('width', module.cache.element.width + 'px', 'important'); + element.style.setProperty('height', module.cache.element.height + 'px', 'important'); + } + } + }, + + is: { + standardScroll: function() { + return ($scroll[0] == window); + }, + top: function() { + return $module.hasClass(className.top); + }, + bottom: function() { + return $module.hasClass(className.bottom); + }, + initialPosition: function() { + return (!module.is.fixed() && !module.is.bound()); + }, + hidden: function() { + return (!$module.is(':visible')); + }, + bound: function() { + return $module.hasClass(className.bound); + }, + fixed: function() { + return $module.hasClass(className.fixed); + } + }, + + stick: function(scroll) { + var + cachedPosition = scroll || $scroll.scrollTop(), + cache = module.cache, + fits = cache.fits, + element = cache.element, + scrollContext = cache.scrollContext, + context = cache.context, + offset = (module.is.bottom() && settings.pushing) + ? settings.bottomOffset + : settings.offset, + scroll = { + top : cachedPosition + offset, + bottom : cachedPosition + offset + scrollContext.height + }, + direction = module.get.direction(scroll.top), + elementScroll = (fits) + ? 0 + : module.get.elementScroll(scroll.top), + + // shorthand + doesntFit = !fits, + elementVisible = (element.height !== 0) + ; + + if(elementVisible) { + + if( module.is.initialPosition() ) { + if(scroll.top >= context.bottom) { + module.debug('Initial element position is bottom of container'); + module.bindBottom(); + } + else if(scroll.top > element.top) { + if( (element.height + scroll.top - elementScroll) >= context.bottom ) { + module.debug('Initial element position is bottom of container'); + module.bindBottom(); + } + else { + module.debug('Initial element position is fixed'); + module.fixTop(); + } + } + + } + else if( module.is.fixed() ) { + + // currently fixed top + if( module.is.top() ) { + if( scroll.top <= element.top ) { + module.debug('Fixed element reached top of container'); + module.setInitialPosition(); + } + else if( (element.height + scroll.top - elementScroll) >= context.bottom ) { + module.debug('Fixed element reached bottom of container'); + module.bindBottom(); + } + // scroll element if larger than screen + else if(doesntFit) { + module.set.scroll(elementScroll); + module.save.lastScroll(scroll.top); + module.save.elementScroll(elementScroll); + } + } + + // currently fixed bottom + else if(module.is.bottom() ) { + + // top edge + if( (scroll.bottom - element.height) <= element.top) { + module.debug('Bottom fixed rail has reached top of container'); + module.setInitialPosition(); + } + // bottom edge + else if(scroll.bottom >= context.bottom) { + module.debug('Bottom fixed rail has reached bottom of container'); + module.bindBottom(); + } + // scroll element if larger than screen + else if(doesntFit) { + module.set.scroll(elementScroll); + module.save.lastScroll(scroll.top); + module.save.elementScroll(elementScroll); + } + + } + } + else if( module.is.bottom() ) { + if( scroll.top <= element.top ) { + module.debug('Jumped from bottom fixed to top fixed, most likely used home/end button'); + module.setInitialPosition(); + } + else { + if(settings.pushing) { + if(module.is.bound() && scroll.bottom <= context.bottom ) { + module.debug('Fixing bottom attached element to bottom of browser.'); + module.fixBottom(); + } + } + else { + if(module.is.bound() && (scroll.top <= context.bottom - element.height) ) { + module.debug('Fixing bottom attached element to top of browser.'); + module.fixTop(); + } + } + } + } + } + }, + + bindTop: function() { + module.debug('Binding element to top of parent container'); + module.remove.offset(); + $module + .css({ + left : '', + top : '', + marginBottom : '' + }) + .removeClass(className.fixed) + .removeClass(className.bottom) + .addClass(className.bound) + .addClass(className.top) + ; + settings.onTop.call(element); + settings.onUnstick.call(element); + }, + bindBottom: function() { + module.debug('Binding element to bottom of parent container'); + module.remove.offset(); + $module + .css({ + left : '', + top : '' + }) + .removeClass(className.fixed) + .removeClass(className.top) + .addClass(className.bound) + .addClass(className.bottom) + ; + settings.onBottom.call(element); + settings.onUnstick.call(element); + }, + + setInitialPosition: function() { + module.debug('Returning to initial position'); + module.unfix(); + module.unbind(); + }, + + + fixTop: function() { + module.debug('Fixing element to top of page'); + module.set.minimumSize(); + module.set.offset(); + $module + .css({ + left : module.cache.element.left, + bottom : '', + marginBottom : '' + }) + .removeClass(className.bound) + .removeClass(className.bottom) + .addClass(className.fixed) + .addClass(className.top) + ; + settings.onStick.call(element); + }, + + fixBottom: function() { + module.debug('Sticking element to bottom of page'); + module.set.minimumSize(); + module.set.offset(); + $module + .css({ + left : module.cache.element.left, + bottom : '', + marginBottom : '' + }) + .removeClass(className.bound) + .removeClass(className.top) + .addClass(className.fixed) + .addClass(className.bottom) + ; + settings.onStick.call(element); + }, + + unbind: function() { + if( module.is.bound() ) { + module.debug('Removing container bound position on element'); + module.remove.offset(); + $module + .removeClass(className.bound) + .removeClass(className.top) + .removeClass(className.bottom) + ; + } + }, + + unfix: function() { + if( module.is.fixed() ) { + module.debug('Removing fixed position on element'); + module.remove.offset(); + $module + .removeClass(className.fixed) + .removeClass(className.top) + .removeClass(className.bottom) + ; + settings.onUnstick.call(element); + } + }, + + reset: function() { + module.debug('Resetting elements position'); + module.unbind(); + module.unfix(); + module.resetCSS(); + module.remove.offset(); + module.remove.lastScroll(); + }, + + resetCSS: function() { + $module + .css({ + width : '', + height : '' + }) + ; + $container + .css({ + height: '' + }) + ; + }, + + setting: function(name, value) { + if( $.isPlainObject(name) ) { + $.extend(true, settings, name); + } + else if(value !== undefined) { + settings[name] = value; + } + else { + return settings[name]; + } + }, + internal: function(name, value) { + if( $.isPlainObject(name) ) { + $.extend(true, module, name); + } + else if(value !== undefined) { + module[name] = value; + } + else { + return module[name]; + } + }, + debug: function() { + if(!settings.silent && settings.debug) { + if(settings.performance) { + module.performance.log(arguments); + } + else { + module.debug = Function.prototype.bind.call(console.info, console, settings.name + ':'); + module.debug.apply(console, arguments); + } + } + }, + verbose: function() { + if(!settings.silent && settings.verbose && settings.debug) { + if(settings.performance) { + module.performance.log(arguments); + } + else { + module.verbose = Function.prototype.bind.call(console.info, console, settings.name + ':'); + module.verbose.apply(console, arguments); + } + } + }, + error: function() { + if(!settings.silent) { + module.error = Function.prototype.bind.call(console.error, console, settings.name + ':'); + module.error.apply(console, arguments); + } + }, + performance: { + log: function(message) { + var + currentTime, + executionTime, + previousTime + ; + if(settings.performance) { + currentTime = new Date().getTime(); + previousTime = time || currentTime; + executionTime = currentTime - previousTime; + time = currentTime; + performance.push({ + 'Name' : message[0], + 'Arguments' : [].slice.call(message, 1) || '', + 'Element' : element, + 'Execution Time' : executionTime + }); + } + clearTimeout(module.performance.timer); + module.performance.timer = setTimeout(module.performance.display, 0); + }, + display: function() { + var + title = settings.name + ':', + totalTime = 0 + ; + time = false; + clearTimeout(module.performance.timer); + $.each(performance, function(index, data) { + totalTime += data['Execution Time']; + }); + title += ' ' + totalTime + 'ms'; + if(moduleSelector) { + title += ' \'' + moduleSelector + '\''; + } + if( (console.group !== undefined || console.table !== undefined) && performance.length > 0) { + console.groupCollapsed(title); + if(console.table) { + console.table(performance); + } + else { + $.each(performance, function(index, data) { + console.log(data['Name'] + ': ' + data['Execution Time']+'ms'); + }); + } + console.groupEnd(); + } + performance = []; + } + }, + invoke: function(query, passedArguments, context) { + var + object = instance, + maxDepth, + found, + response + ; + passedArguments = passedArguments || queryArguments; + context = element || context; + if(typeof query == 'string' && object !== undefined) { + query = query.split(/[\. ]/); + maxDepth = query.length - 1; + $.each(query, function(depth, value) { + var camelCaseValue = (depth != maxDepth) + ? value + query[depth + 1].charAt(0).toUpperCase() + query[depth + 1].slice(1) + : query + ; + if( $.isPlainObject( object[camelCaseValue] ) && (depth != maxDepth) ) { + object = object[camelCaseValue]; + } + else if( object[camelCaseValue] !== undefined ) { + found = object[camelCaseValue]; + return false; + } + else if( $.isPlainObject( object[value] ) && (depth != maxDepth) ) { + object = object[value]; + } + else if( object[value] !== undefined ) { + found = object[value]; + return false; + } + else { + return false; + } + }); + } + if ( $.isFunction( found ) ) { + response = found.apply(context, passedArguments); + } + else if(found !== undefined) { + response = found; + } + if($.isArray(returnedValue)) { + returnedValue.push(response); + } + else if(returnedValue !== undefined) { + returnedValue = [returnedValue, response]; + } + else if(response !== undefined) { + returnedValue = response; + } + return found; + } + }; + + if(methodInvoked) { + if(instance === undefined) { + module.initialize(); + } + module.invoke(query); + } + else { + if(instance !== undefined) { + instance.invoke('destroy'); + } + module.initialize(); + } + }) + ; + + return (returnedValue !== undefined) + ? returnedValue + : this + ; +}; + +$.fn.sticky.settings = { + + name : 'Sticky', + namespace : 'sticky', + + silent : false, + debug : false, + verbose : true, + performance : true, + + // whether to stick in the opposite direction on scroll up + pushing : false, + + context : false, + container : false, + + // Context to watch scroll events + scrollContext : window, + + // Offset to adjust scroll + offset : 0, + + // Offset to adjust scroll when attached to bottom of screen + bottomOffset : 0, + + jitter : 5, // will only set container height if difference between context and container is larger than this number + + // Whether to automatically observe changes with Mutation Observers + observeChanges : false, + + // Called when position is recalculated + onReposition : function(){}, + + // Called on each scroll + onScroll : function(){}, + + // Called when element is stuck to viewport + onStick : function(){}, + + // Called when element is unstuck from viewport + onUnstick : function(){}, + + // Called when element reaches top of context + onTop : function(){}, + + // Called when element reaches bottom of context + onBottom : function(){}, + + error : { + container : 'Sticky element must be inside a relative container', + visible : 'Element is hidden, you must call refresh after element becomes visible. Use silent setting to surpress this warning in production.', + method : 'The method you called is not defined.', + invalidContext : 'Context specified does not exist', + elementSize : 'Sticky element is larger than its container, cannot create sticky.' + }, + + className : { + bound : 'bound', + fixed : 'fixed', + supported : 'native', + top : 'top', + bottom : 'bottom' + } + +}; + +})( jQuery, window, document ); + +/*! + * # Semantic UI 2.2.6 - Tab + * http://github.com/semantic-org/semantic-ui/ + * + * + * Released under the MIT license + * http://opensource.org/licenses/MIT + * + */ + +;(function ($, window, document, undefined) { + +"use strict"; + +window = (typeof window != 'undefined' && window.Math == Math) + ? window + : (typeof self != 'undefined' && self.Math == Math) + ? self + : Function('return this')() +; + +$.fn.tab = function(parameters) { + + var + // use window context if none specified + $allModules = $.isFunction(this) + ? $(window) + : $(this), + + moduleSelector = $allModules.selector || '', + time = new Date().getTime(), + performance = [], + + query = arguments[0], + methodInvoked = (typeof query == 'string'), + queryArguments = [].slice.call(arguments, 1), + + initializedHistory = false, + returnedValue + ; + + $allModules + .each(function() { + var + + settings = ( $.isPlainObject(parameters) ) + ? $.extend(true, {}, $.fn.tab.settings, parameters) + : $.extend({}, $.fn.tab.settings), + + className = settings.className, + metadata = settings.metadata, + selector = settings.selector, + error = settings.error, + + eventNamespace = '.' + settings.namespace, + moduleNamespace = 'module-' + settings.namespace, + + $module = $(this), + $context, + $tabs, + + cache = {}, + firstLoad = true, + recursionDepth = 0, + element = this, + instance = $module.data(moduleNamespace), + + activeTabPath, + parameterArray, + module, + + historyEvent + + ; + + module = { + + initialize: function() { + module.debug('Initializing tab menu item', $module); + module.fix.callbacks(); + module.determineTabs(); + + module.debug('Determining tabs', settings.context, $tabs); + // set up automatic routing + if(settings.auto) { + module.set.auto(); + } + module.bind.events(); + + if(settings.history && !initializedHistory) { + module.initializeHistory(); + initializedHistory = true; + } + + module.instantiate(); + }, + + instantiate: function () { + module.verbose('Storing instance of module', module); + instance = module; + $module + .data(moduleNamespace, module) + ; + }, + + destroy: function() { + module.debug('Destroying tabs', $module); + $module + .removeData(moduleNamespace) + .off(eventNamespace) + ; + }, + + bind: { + events: function() { + // if using $.tab don't add events + if( !$.isWindow( element ) ) { + module.debug('Attaching tab activation events to element', $module); + $module + .on('click' + eventNamespace, module.event.click) + ; + } + } + }, + + determineTabs: function() { + var + $reference + ; + + // determine tab context + if(settings.context === 'parent') { + if($module.closest(selector.ui).length > 0) { + $reference = $module.closest(selector.ui); + module.verbose('Using closest UI element as parent', $reference); + } + else { + $reference = $module; + } + $context = $reference.parent(); + module.verbose('Determined parent element for creating context', $context); + } + else if(settings.context) { + $context = $(settings.context); + module.verbose('Using selector for tab context', settings.context, $context); + } + else { + $context = $('body'); + } + // find tabs + if(settings.childrenOnly) { + $tabs = $context.children(selector.tabs); + module.debug('Searching tab context children for tabs', $context, $tabs); + } + else { + $tabs = $context.find(selector.tabs); + module.debug('Searching tab context for tabs', $context, $tabs); + } + }, + + fix: { + callbacks: function() { + if( $.isPlainObject(parameters) && (parameters.onTabLoad || parameters.onTabInit) ) { + if(parameters.onTabLoad) { + parameters.onLoad = parameters.onTabLoad; + delete parameters.onTabLoad; + module.error(error.legacyLoad, parameters.onLoad); + } + if(parameters.onTabInit) { + parameters.onFirstLoad = parameters.onTabInit; + delete parameters.onTabInit; + module.error(error.legacyInit, parameters.onFirstLoad); + } + settings = $.extend(true, {}, $.fn.tab.settings, parameters); + } + } + }, + + initializeHistory: function() { + module.debug('Initializing page state'); + if( $.address === undefined ) { + module.error(error.state); + return false; + } + else { + if(settings.historyType == 'state') { + module.debug('Using HTML5 to manage state'); + if(settings.path !== false) { + $.address + .history(true) + .state(settings.path) + ; + } + else { + module.error(error.path); + return false; + } + } + $.address + .bind('change', module.event.history.change) + ; + } + }, + + event: { + click: function(event) { + var + tabPath = $(this).data(metadata.tab) + ; + if(tabPath !== undefined) { + if(settings.history) { + module.verbose('Updating page state', event); + $.address.value(tabPath); + } + else { + module.verbose('Changing tab', event); + module.changeTab(tabPath); + } + event.preventDefault(); + } + else { + module.debug('No tab specified'); + } + }, + history: { + change: function(event) { + var + tabPath = event.pathNames.join('/') || module.get.initialPath(), + pageTitle = settings.templates.determineTitle(tabPath) || false + ; + module.performance.display(); + module.debug('History change event', tabPath, event); + historyEvent = event; + if(tabPath !== undefined) { + module.changeTab(tabPath); + } + if(pageTitle) { + $.address.title(pageTitle); + } + } + } + }, + + refresh: function() { + if(activeTabPath) { + module.debug('Refreshing tab', activeTabPath); + module.changeTab(activeTabPath); + } + }, + + cache: { + + read: function(cacheKey) { + return (cacheKey !== undefined) + ? cache[cacheKey] + : false + ; + }, + add: function(cacheKey, content) { + cacheKey = cacheKey || activeTabPath; + module.debug('Adding cached content for', cacheKey); + cache[cacheKey] = content; + }, + remove: function(cacheKey) { + cacheKey = cacheKey || activeTabPath; + module.debug('Removing cached content for', cacheKey); + delete cache[cacheKey]; + } + }, + + set: { + auto: function() { + var + url = (typeof settings.path == 'string') + ? settings.path.replace(/\/$/, '') + '/{$tab}' + : '/{$tab}' + ; + module.verbose('Setting up automatic tab retrieval from server', url); + if($.isPlainObject(settings.apiSettings)) { + settings.apiSettings.url = url; + } + else { + settings.apiSettings = { + url: url + }; + } + }, + loading: function(tabPath) { + var + $tab = module.get.tabElement(tabPath), + isLoading = $tab.hasClass(className.loading) + ; + if(!isLoading) { + module.verbose('Setting loading state for', $tab); + $tab + .addClass(className.loading) + .siblings($tabs) + .removeClass(className.active + ' ' + className.loading) + ; + if($tab.length > 0) { + settings.onRequest.call($tab[0], tabPath); + } + } + }, + state: function(state) { + $.address.value(state); + } + }, + + changeTab: function(tabPath) { + var + pushStateAvailable = (window.history && window.history.pushState), + shouldIgnoreLoad = (pushStateAvailable && settings.ignoreFirstLoad && firstLoad), + remoteContent = (settings.auto || $.isPlainObject(settings.apiSettings) ), + // only add default path if not remote content + pathArray = (remoteContent && !shouldIgnoreLoad) + ? module.utilities.pathToArray(tabPath) + : module.get.defaultPathArray(tabPath) + ; + tabPath = module.utilities.arrayToPath(pathArray); + $.each(pathArray, function(index, tab) { + var + currentPathArray = pathArray.slice(0, index + 1), + currentPath = module.utilities.arrayToPath(currentPathArray), + + isTab = module.is.tab(currentPath), + isLastIndex = (index + 1 == pathArray.length), + + $tab = module.get.tabElement(currentPath), + $anchor, + nextPathArray, + nextPath, + isLastTab + ; + module.verbose('Looking for tab', tab); + if(isTab) { + module.verbose('Tab was found', tab); + // scope up + activeTabPath = currentPath; + parameterArray = module.utilities.filterArray(pathArray, currentPathArray); + + if(isLastIndex) { + isLastTab = true; + } + else { + nextPathArray = pathArray.slice(0, index + 2); + nextPath = module.utilities.arrayToPath(nextPathArray); + isLastTab = ( !module.is.tab(nextPath) ); + if(isLastTab) { + module.verbose('Tab parameters found', nextPathArray); + } + } + if(isLastTab && remoteContent) { + if(!shouldIgnoreLoad) { + module.activate.navigation(currentPath); + module.fetch.content(currentPath, tabPath); + } + else { + module.debug('Ignoring remote content on first tab load', currentPath); + firstLoad = false; + module.cache.add(tabPath, $tab.html()); + module.activate.all(currentPath); + settings.onFirstLoad.call($tab[0], currentPath, parameterArray, historyEvent); + settings.onLoad.call($tab[0], currentPath, parameterArray, historyEvent); + } + return false; + } + else { + module.debug('Opened local tab', currentPath); + module.activate.all(currentPath); + if( !module.cache.read(currentPath) ) { + module.cache.add(currentPath, true); + module.debug('First time tab loaded calling tab init'); + settings.onFirstLoad.call($tab[0], currentPath, parameterArray, historyEvent); + } + settings.onLoad.call($tab[0], currentPath, parameterArray, historyEvent); + } + + } + else if(tabPath.search('/') == -1 && tabPath !== '') { + // look for in page anchor + $anchor = $('#' + tabPath + ', a[name="' + tabPath + '"]'); + currentPath = $anchor.closest('[data-tab]').data(metadata.tab); + $tab = module.get.tabElement(currentPath); + // if anchor exists use parent tab + if($anchor && $anchor.length > 0 && currentPath) { + module.debug('Anchor link used, opening parent tab', $tab, $anchor); + if( !$tab.hasClass(className.active) ) { + setTimeout(function() { + module.scrollTo($anchor); + }, 0); + } + module.activate.all(currentPath); + if( !module.cache.read(currentPath) ) { + module.cache.add(currentPath, true); + module.debug('First time tab loaded calling tab init'); + settings.onFirstLoad.call($tab[0], currentPath, parameterArray, historyEvent); + } + settings.onLoad.call($tab[0], currentPath, parameterArray, historyEvent); + return false; + } + } + else { + module.error(error.missingTab, $module, $context, currentPath); + return false; + } + }); + }, + + scrollTo: function($element) { + var + scrollOffset = ($element && $element.length > 0) + ? $element.offset().top + : false + ; + if(scrollOffset !== false) { + module.debug('Forcing scroll to an in-page link in a hidden tab', scrollOffset, $element); + $(document).scrollTop(scrollOffset); + } + }, + + update: { + content: function(tabPath, html, evaluateScripts) { + var + $tab = module.get.tabElement(tabPath), + tab = $tab[0] + ; + evaluateScripts = (evaluateScripts !== undefined) + ? evaluateScripts + : settings.evaluateScripts + ; + if(typeof settings.cacheType == 'string' && settings.cacheType.toLowerCase() == 'dom' && typeof html !== 'string') { + $tab + .empty() + .append($(html).clone(true)) + ; + } + else { + if(evaluateScripts) { + module.debug('Updating HTML and evaluating inline scripts', tabPath, html); + $tab.html(html); + } + else { + module.debug('Updating HTML', tabPath, html); + tab.innerHTML = html; + } + } + } + }, + + fetch: { + + content: function(tabPath, fullTabPath) { + var + $tab = module.get.tabElement(tabPath), + apiSettings = { + dataType : 'html', + encodeParameters : false, + on : 'now', + cache : settings.alwaysRefresh, + headers : { + 'X-Remote': true + }, + onSuccess : function(response) { + if(settings.cacheType == 'response') { + module.cache.add(fullTabPath, response); + } + module.update.content(tabPath, response); + if(tabPath == activeTabPath) { + module.debug('Content loaded', tabPath); + module.activate.tab(tabPath); + } + else { + module.debug('Content loaded in background', tabPath); + } + settings.onFirstLoad.call($tab[0], tabPath, parameterArray, historyEvent); + settings.onLoad.call($tab[0], tabPath, parameterArray, historyEvent); + + if(typeof settings.cacheType == 'string' && settings.cacheType.toLowerCase() == 'dom' && $tab.children().length > 0) { + setTimeout(function() { + var + $clone = $tab.children().clone(true) + ; + $clone = $clone.not('script'); + module.cache.add(fullTabPath, $clone); + }, 0); + } + else { + module.cache.add(fullTabPath, $tab.html()); + } + }, + urlData: { + tab: fullTabPath + } + }, + request = $tab.api('get request') || false, + existingRequest = ( request && request.state() === 'pending' ), + requestSettings, + cachedContent + ; + + fullTabPath = fullTabPath || tabPath; + cachedContent = module.cache.read(fullTabPath); + + + if(settings.cache && cachedContent) { + module.activate.tab(tabPath); + module.debug('Adding cached content', fullTabPath); + if(settings.evaluateScripts == 'once') { + module.update.content(tabPath, cachedContent, false); + } + else { + module.update.content(tabPath, cachedContent); + } + settings.onLoad.call($tab[0], tabPath, parameterArray, historyEvent); + } + else if(existingRequest) { + module.set.loading(tabPath); + module.debug('Content is already loading', fullTabPath); + } + else if($.api !== undefined) { + requestSettings = $.extend(true, {}, settings.apiSettings, apiSettings); + module.debug('Retrieving remote content', fullTabPath, requestSettings); + module.set.loading(tabPath); + $tab.api(requestSettings); + } + else { + module.error(error.api); + } + } + }, + + activate: { + all: function(tabPath) { + module.activate.tab(tabPath); + module.activate.navigation(tabPath); + }, + tab: function(tabPath) { + var + $tab = module.get.tabElement(tabPath), + $deactiveTabs = (settings.deactivate == 'siblings') + ? $tab.siblings($tabs) + : $tabs.not($tab), + isActive = $tab.hasClass(className.active) + ; + module.verbose('Showing tab content for', $tab); + if(!isActive) { + $tab + .addClass(className.active) + ; + $deactiveTabs + .removeClass(className.active + ' ' + className.loading) + ; + if($tab.length > 0) { + settings.onVisible.call($tab[0], tabPath); + } + } + }, + navigation: function(tabPath) { + var + $navigation = module.get.navElement(tabPath), + $deactiveNavigation = (settings.deactivate == 'siblings') + ? $navigation.siblings($allModules) + : $allModules.not($navigation), + isActive = $navigation.hasClass(className.active) + ; + module.verbose('Activating tab navigation for', $navigation, tabPath); + if(!isActive) { + $navigation + .addClass(className.active) + ; + $deactiveNavigation + .removeClass(className.active + ' ' + className.loading) + ; + } + } + }, + + deactivate: { + all: function() { + module.deactivate.navigation(); + module.deactivate.tabs(); + }, + navigation: function() { + $allModules + .removeClass(className.active) + ; + }, + tabs: function() { + $tabs + .removeClass(className.active + ' ' + className.loading) + ; + } + }, + + is: { + tab: function(tabName) { + return (tabName !== undefined) + ? ( module.get.tabElement(tabName).length > 0 ) + : false + ; + } + }, + + get: { + initialPath: function() { + return $allModules.eq(0).data(metadata.tab) || $tabs.eq(0).data(metadata.tab); + }, + path: function() { + return $.address.value(); + }, + // adds default tabs to tab path + defaultPathArray: function(tabPath) { + return module.utilities.pathToArray( module.get.defaultPath(tabPath) ); + }, + defaultPath: function(tabPath) { + var + $defaultNav = $allModules.filter('[data-' + metadata.tab + '^="' + tabPath + '/"]').eq(0), + defaultTab = $defaultNav.data(metadata.tab) || false + ; + if( defaultTab ) { + module.debug('Found default tab', defaultTab); + if(recursionDepth < settings.maxDepth) { + recursionDepth++; + return module.get.defaultPath(defaultTab); + } + module.error(error.recursion); + } + else { + module.debug('No default tabs found for', tabPath, $tabs); + } + recursionDepth = 0; + return tabPath; + }, + navElement: function(tabPath) { + tabPath = tabPath || activeTabPath; + return $allModules.filter('[data-' + metadata.tab + '="' + tabPath + '"]'); + }, + tabElement: function(tabPath) { + var + $fullPathTab, + $simplePathTab, + tabPathArray, + lastTab + ; + tabPath = tabPath || activeTabPath; + tabPathArray = module.utilities.pathToArray(tabPath); + lastTab = module.utilities.last(tabPathArray); + $fullPathTab = $tabs.filter('[data-' + metadata.tab + '="' + tabPath + '"]'); + $simplePathTab = $tabs.filter('[data-' + metadata.tab + '="' + lastTab + '"]'); + return ($fullPathTab.length > 0) + ? $fullPathTab + : $simplePathTab + ; + }, + tab: function() { + return activeTabPath; + } + }, + + utilities: { + filterArray: function(keepArray, removeArray) { + return $.grep(keepArray, function(keepValue) { + return ( $.inArray(keepValue, removeArray) == -1); + }); + }, + last: function(array) { + return $.isArray(array) + ? array[ array.length - 1] + : false + ; + }, + pathToArray: function(pathName) { + if(pathName === undefined) { + pathName = activeTabPath; + } + return typeof pathName == 'string' + ? pathName.split('/') + : [pathName] + ; + }, + arrayToPath: function(pathArray) { + return $.isArray(pathArray) + ? pathArray.join('/') + : false + ; + } + }, + + setting: function(name, value) { + module.debug('Changing setting', name, value); + if( $.isPlainObject(name) ) { + $.extend(true, settings, name); + } + else if(value !== undefined) { + if($.isPlainObject(settings[name])) { + $.extend(true, settings[name], value); + } + else { + settings[name] = value; + } + } + else { + return settings[name]; + } + }, + internal: function(name, value) { + if( $.isPlainObject(name) ) { + $.extend(true, module, name); + } + else if(value !== undefined) { + module[name] = value; + } + else { + return module[name]; + } + }, + debug: function() { + if(!settings.silent && settings.debug) { + if(settings.performance) { + module.performance.log(arguments); + } + else { + module.debug = Function.prototype.bind.call(console.info, console, settings.name + ':'); + module.debug.apply(console, arguments); + } + } + }, + verbose: function() { + if(!settings.silent && settings.verbose && settings.debug) { + if(settings.performance) { + module.performance.log(arguments); + } + else { + module.verbose = Function.prototype.bind.call(console.info, console, settings.name + ':'); + module.verbose.apply(console, arguments); + } + } + }, + error: function() { + if(!settings.silent) { + module.error = Function.prototype.bind.call(console.error, console, settings.name + ':'); + module.error.apply(console, arguments); + } + }, + performance: { + log: function(message) { + var + currentTime, + executionTime, + previousTime + ; + if(settings.performance) { + currentTime = new Date().getTime(); + previousTime = time || currentTime; + executionTime = currentTime - previousTime; + time = currentTime; + performance.push({ + 'Name' : message[0], + 'Arguments' : [].slice.call(message, 1) || '', + 'Element' : element, + 'Execution Time' : executionTime + }); + } + clearTimeout(module.performance.timer); + module.performance.timer = setTimeout(module.performance.display, 500); + }, + display: function() { + var + title = settings.name + ':', + totalTime = 0 + ; + time = false; + clearTimeout(module.performance.timer); + $.each(performance, function(index, data) { + totalTime += data['Execution Time']; + }); + title += ' ' + totalTime + 'ms'; + if(moduleSelector) { + title += ' \'' + moduleSelector + '\''; + } + if( (console.group !== undefined || console.table !== undefined) && performance.length > 0) { + console.groupCollapsed(title); + if(console.table) { + console.table(performance); + } + else { + $.each(performance, function(index, data) { + console.log(data['Name'] + ': ' + data['Execution Time']+'ms'); + }); + } + console.groupEnd(); + } + performance = []; + } + }, + invoke: function(query, passedArguments, context) { + var + object = instance, + maxDepth, + found, + response + ; + passedArguments = passedArguments || queryArguments; + context = element || context; + if(typeof query == 'string' && object !== undefined) { + query = query.split(/[\. ]/); + maxDepth = query.length - 1; + $.each(query, function(depth, value) { + var camelCaseValue = (depth != maxDepth) + ? value + query[depth + 1].charAt(0).toUpperCase() + query[depth + 1].slice(1) + : query + ; + if( $.isPlainObject( object[camelCaseValue] ) && (depth != maxDepth) ) { + object = object[camelCaseValue]; + } + else if( object[camelCaseValue] !== undefined ) { + found = object[camelCaseValue]; + return false; + } + else if( $.isPlainObject( object[value] ) && (depth != maxDepth) ) { + object = object[value]; + } + else if( object[value] !== undefined ) { + found = object[value]; + return false; + } + else { + module.error(error.method, query); + return false; + } + }); + } + if ( $.isFunction( found ) ) { + response = found.apply(context, passedArguments); + } + else if(found !== undefined) { + response = found; + } + if($.isArray(returnedValue)) { + returnedValue.push(response); + } + else if(returnedValue !== undefined) { + returnedValue = [returnedValue, response]; + } + else if(response !== undefined) { + returnedValue = response; + } + return found; + } + }; + if(methodInvoked) { + if(instance === undefined) { + module.initialize(); + } + module.invoke(query); + } + else { + if(instance !== undefined) { + instance.invoke('destroy'); + } + module.initialize(); + } + }) + ; + return (returnedValue !== undefined) + ? returnedValue + : this + ; + +}; + +// shortcut for tabbed content with no defined navigation +$.tab = function() { + $(window).tab.apply(this, arguments); +}; + +$.fn.tab.settings = { + + name : 'Tab', + namespace : 'tab', + + silent : false, + debug : false, + verbose : false, + performance : true, + + auto : false, // uses pjax style endpoints fetching content from same url with remote-content headers + history : false, // use browser history + historyType : 'hash', // #/ or html5 state + path : false, // base path of url + + context : false, // specify a context that tabs must appear inside + childrenOnly : false, // use only tabs that are children of context + maxDepth : 25, // max depth a tab can be nested + + deactivate : 'siblings', // whether tabs should deactivate sibling menu elements or all elements initialized together + + alwaysRefresh : false, // load tab content new every tab click + cache : true, // cache the content requests to pull locally + cacheType : 'response', // Whether to cache exact response, or to html cache contents after scripts execute + ignoreFirstLoad : false, // don't load remote content on first load + + apiSettings : false, // settings for api call + evaluateScripts : 'once', // whether inline scripts should be parsed (true/false/once). Once will not re-evaluate on cached content + + onFirstLoad : function(tabPath, parameterArray, historyEvent) {}, // called first time loaded + onLoad : function(tabPath, parameterArray, historyEvent) {}, // called on every load + onVisible : function(tabPath, parameterArray, historyEvent) {}, // called every time tab visible + onRequest : function(tabPath, parameterArray, historyEvent) {}, // called ever time a tab beings loading remote content + + templates : { + determineTitle: function(tabArray) {} // returns page title for path + }, + + error: { + api : 'You attempted to load content without API module', + method : 'The method you called is not defined', + missingTab : 'Activated tab cannot be found. Tabs are case-sensitive.', + noContent : 'The tab you specified is missing a content url.', + path : 'History enabled, but no path was specified', + recursion : 'Max recursive depth reached', + legacyInit : 'onTabInit has been renamed to onFirstLoad in 2.0, please adjust your code.', + legacyLoad : 'onTabLoad has been renamed to onLoad in 2.0. Please adjust your code', + state : 'History requires Asual\'s Address library <https://github.com/asual/jquery-address>' + }, + + metadata : { + tab : 'tab', + loaded : 'loaded', + promise: 'promise' + }, + + className : { + loading : 'loading', + active : 'active' + }, + + selector : { + tabs : '.ui.tab', + ui : '.ui' + } + +}; + +})( jQuery, window, document ); + +/*! + * # Semantic UI 2.2.6 - Transition + * http://github.com/semantic-org/semantic-ui/ + * + * + * Released under the MIT license + * http://opensource.org/licenses/MIT + * + */ + +;(function ($, window, document, undefined) { + +"use strict"; + +window = (typeof window != 'undefined' && window.Math == Math) + ? window + : (typeof self != 'undefined' && self.Math == Math) + ? self + : Function('return this')() +; + +$.fn.transition = function() { + var + $allModules = $(this), + moduleSelector = $allModules.selector || '', + + time = new Date().getTime(), + performance = [], + + moduleArguments = arguments, + query = moduleArguments[0], + queryArguments = [].slice.call(arguments, 1), + methodInvoked = (typeof query === 'string'), + + requestAnimationFrame = window.requestAnimationFrame + || window.mozRequestAnimationFrame + || window.webkitRequestAnimationFrame + || window.msRequestAnimationFrame + || function(callback) { setTimeout(callback, 0); }, + + returnedValue + ; + $allModules + .each(function(index) { + var + $module = $(this), + element = this, + + // set at run time + settings, + instance, + + error, + className, + metadata, + animationEnd, + animationName, + + namespace, + moduleNamespace, + eventNamespace, + module + ; + + module = { + + initialize: function() { + + // get full settings + settings = module.get.settings.apply(element, moduleArguments); + + // shorthand + className = settings.className; + error = settings.error; + metadata = settings.metadata; + + // define namespace + eventNamespace = '.' + settings.namespace; + moduleNamespace = 'module-' + settings.namespace; + instance = $module.data(moduleNamespace) || module; + + // get vendor specific events + animationEnd = module.get.animationEndEvent(); + + if(methodInvoked) { + methodInvoked = module.invoke(query); + } + + // method not invoked, lets run an animation + if(methodInvoked === false) { + module.verbose('Converted arguments into settings object', settings); + if(settings.interval) { + module.delay(settings.animate); + } + else { + module.animate(); + } + module.instantiate(); + } + }, + + instantiate: function() { + module.verbose('Storing instance of module', module); + instance = module; + $module + .data(moduleNamespace, instance) + ; + }, + + destroy: function() { + module.verbose('Destroying previous module for', element); + $module + .removeData(moduleNamespace) + ; + }, + + refresh: function() { + module.verbose('Refreshing display type on next animation'); + delete module.displayType; + }, + + forceRepaint: function() { + module.verbose('Forcing element repaint'); + var + $parentElement = $module.parent(), + $nextElement = $module.next() + ; + if($nextElement.length === 0) { + $module.detach().appendTo($parentElement); + } + else { + $module.detach().insertBefore($nextElement); + } + }, + + repaint: function() { + module.verbose('Repainting element'); + var + fakeAssignment = element.offsetWidth + ; + }, + + delay: function(interval) { + var + direction = module.get.animationDirection(), + shouldReverse, + delay + ; + if(!direction) { + direction = module.can.transition() + ? module.get.direction() + : 'static' + ; + } + interval = (interval !== undefined) + ? interval + : settings.interval + ; + shouldReverse = (settings.reverse == 'auto' && direction == className.outward); + delay = (shouldReverse || settings.reverse == true) + ? ($allModules.length - index) * settings.interval + : index * settings.interval + ; + module.debug('Delaying animation by', delay); + setTimeout(module.animate, delay); + }, + + animate: function(overrideSettings) { + settings = overrideSettings || settings; + if(!module.is.supported()) { + module.error(error.support); + return false; + } + module.debug('Preparing animation', settings.animation); + if(module.is.animating()) { + if(settings.queue) { + if(!settings.allowRepeats && module.has.direction() && module.is.occurring() && module.queuing !== true) { + module.debug('Animation is currently occurring, preventing queueing same animation', settings.animation); + } + else { + module.queue(settings.animation); + } + return false; + } + else if(!settings.allowRepeats && module.is.occurring()) { + module.debug('Animation is already occurring, will not execute repeated animation', settings.animation); + return false; + } + else { + module.debug('New animation started, completing previous early', settings.animation); + instance.complete(); + } + } + if( module.can.animate() ) { + module.set.animating(settings.animation); + } + else { + module.error(error.noAnimation, settings.animation, element); + } + }, + + reset: function() { + module.debug('Resetting animation to beginning conditions'); + module.remove.animationCallbacks(); + module.restore.conditions(); + module.remove.animating(); + }, + + queue: function(animation) { + module.debug('Queueing animation of', animation); + module.queuing = true; + $module + .one(animationEnd + '.queue' + eventNamespace, function() { + module.queuing = false; + module.repaint(); + module.animate.apply(this, settings); + }) + ; + }, + + complete: function (event) { + module.debug('Animation complete', settings.animation); + module.remove.completeCallback(); + module.remove.failSafe(); + if(!module.is.looping()) { + if( module.is.outward() ) { + module.verbose('Animation is outward, hiding element'); + module.restore.conditions(); + module.hide(); + } + else if( module.is.inward() ) { + module.verbose('Animation is outward, showing element'); + module.restore.conditions(); + module.show(); + } + else { + module.verbose('Static animation completed'); + module.restore.conditions(); + settings.onComplete.call(element); + } + } + }, + + force: { + visible: function() { + var + style = $module.attr('style'), + userStyle = module.get.userStyle(), + displayType = module.get.displayType(), + overrideStyle = userStyle + 'display: ' + displayType + ' !important;', + currentDisplay = $module.css('display'), + emptyStyle = (style === undefined || style === '') + ; + if(currentDisplay !== displayType) { + module.verbose('Overriding default display to show element', displayType); + $module + .attr('style', overrideStyle) + ; + } + else if(emptyStyle) { + $module.removeAttr('style'); + } + }, + hidden: function() { + var + style = $module.attr('style'), + currentDisplay = $module.css('display'), + emptyStyle = (style === undefined || style === '') + ; + if(currentDisplay !== 'none' && !module.is.hidden()) { + module.verbose('Overriding default display to hide element'); + $module + .css('display', 'none') + ; + } + else if(emptyStyle) { + $module + .removeAttr('style') + ; + } + } + }, + + has: { + direction: function(animation) { + var + hasDirection = false + ; + animation = animation || settings.animation; + if(typeof animation === 'string') { + animation = animation.split(' '); + $.each(animation, function(index, word){ + if(word === className.inward || word === className.outward) { + hasDirection = true; + } + }); + } + return hasDirection; + }, + inlineDisplay: function() { + var + style = $module.attr('style') || '' + ; + return $.isArray(style.match(/display.*?;/, '')); + } + }, + + set: { + animating: function(animation) { + var + animationClass, + direction + ; + // remove previous callbacks + module.remove.completeCallback(); + + // determine exact animation + animation = animation || settings.animation; + animationClass = module.get.animationClass(animation); + + // save animation class in cache to restore class names + module.save.animation(animationClass); + + // override display if necessary so animation appears visibly + module.force.visible(); + + module.remove.hidden(); + module.remove.direction(); + + module.start.animation(animationClass); + + }, + duration: function(animationName, duration) { + duration = duration || settings.duration; + duration = (typeof duration == 'number') + ? duration + 'ms' + : duration + ; + if(duration || duration === 0) { + module.verbose('Setting animation duration', duration); + $module + .css({ + 'animation-duration': duration + }) + ; + } + }, + direction: function(direction) { + direction = direction || module.get.direction(); + if(direction == className.inward) { + module.set.inward(); + } + else { + module.set.outward(); + } + }, + looping: function() { + module.debug('Transition set to loop'); + $module + .addClass(className.looping) + ; + }, + hidden: function() { + $module + .addClass(className.transition) + .addClass(className.hidden) + ; + }, + inward: function() { + module.debug('Setting direction to inward'); + $module + .removeClass(className.outward) + .addClass(className.inward) + ; + }, + outward: function() { + module.debug('Setting direction to outward'); + $module + .removeClass(className.inward) + .addClass(className.outward) + ; + }, + visible: function() { + $module + .addClass(className.transition) + .addClass(className.visible) + ; + } + }, + + start: { + animation: function(animationClass) { + animationClass = animationClass || module.get.animationClass(); + module.debug('Starting tween', animationClass); + $module + .addClass(animationClass) + .one(animationEnd + '.complete' + eventNamespace, module.complete) + ; + if(settings.useFailSafe) { + module.add.failSafe(); + } + module.set.duration(settings.duration); + settings.onStart.call(element); + } + }, + + save: { + animation: function(animation) { + if(!module.cache) { + module.cache = {}; + } + module.cache.animation = animation; + }, + displayType: function(displayType) { + if(displayType !== 'none') { + $module.data(metadata.displayType, displayType); + } + }, + transitionExists: function(animation, exists) { + $.fn.transition.exists[animation] = exists; + module.verbose('Saving existence of transition', animation, exists); + } + }, + + restore: { + conditions: function() { + var + animation = module.get.currentAnimation() + ; + if(animation) { + $module + .removeClass(animation) + ; + module.verbose('Removing animation class', module.cache); + } + module.remove.duration(); + } + }, + + add: { + failSafe: function() { + var + duration = module.get.duration() + ; + module.timer = setTimeout(function() { + $module.triggerHandler(animationEnd); + }, duration + settings.failSafeDelay); + module.verbose('Adding fail safe timer', module.timer); + } + }, + + remove: { + animating: function() { + $module.removeClass(className.animating); + }, + animationCallbacks: function() { + module.remove.queueCallback(); + module.remove.completeCallback(); + }, + queueCallback: function() { + $module.off('.queue' + eventNamespace); + }, + completeCallback: function() { + $module.off('.complete' + eventNamespace); + }, + display: function() { + $module.css('display', ''); + }, + direction: function() { + $module + .removeClass(className.inward) + .removeClass(className.outward) + ; + }, + duration: function() { + $module + .css('animation-duration', '') + ; + }, + failSafe: function() { + module.verbose('Removing fail safe timer', module.timer); + if(module.timer) { + clearTimeout(module.timer); + } + }, + hidden: function() { + $module.removeClass(className.hidden); + }, + visible: function() { + $module.removeClass(className.visible); + }, + looping: function() { + module.debug('Transitions are no longer looping'); + if( module.is.looping() ) { + module.reset(); + $module + .removeClass(className.looping) + ; + } + }, + transition: function() { + $module + .removeClass(className.visible) + .removeClass(className.hidden) + ; + } + }, + get: { + settings: function(animation, duration, onComplete) { + // single settings object + if(typeof animation == 'object') { + return $.extend(true, {}, $.fn.transition.settings, animation); + } + // all arguments provided + else if(typeof onComplete == 'function') { + return $.extend({}, $.fn.transition.settings, { + animation : animation, + onComplete : onComplete, + duration : duration + }); + } + // only duration provided + else if(typeof duration == 'string' || typeof duration == 'number') { + return $.extend({}, $.fn.transition.settings, { + animation : animation, + duration : duration + }); + } + // duration is actually settings object + else if(typeof duration == 'object') { + return $.extend({}, $.fn.transition.settings, duration, { + animation : animation + }); + } + // duration is actually callback + else if(typeof duration == 'function') { + return $.extend({}, $.fn.transition.settings, { + animation : animation, + onComplete : duration + }); + } + // only animation provided + else { + return $.extend({}, $.fn.transition.settings, { + animation : animation + }); + } + }, + animationClass: function(animation) { + var + animationClass = animation || settings.animation, + directionClass = (module.can.transition() && !module.has.direction()) + ? module.get.direction() + ' ' + : '' + ; + return className.animating + ' ' + + className.transition + ' ' + + directionClass + + animationClass + ; + }, + currentAnimation: function() { + return (module.cache && module.cache.animation !== undefined) + ? module.cache.animation + : false + ; + }, + currentDirection: function() { + return module.is.inward() + ? className.inward + : className.outward + ; + }, + direction: function() { + return module.is.hidden() || !module.is.visible() + ? className.inward + : className.outward + ; + }, + animationDirection: function(animation) { + var + direction + ; + animation = animation || settings.animation; + if(typeof animation === 'string') { + animation = animation.split(' '); + // search animation name for out/in class + $.each(animation, function(index, word){ + if(word === className.inward) { + direction = className.inward; + } + else if(word === className.outward) { + direction = className.outward; + } + }); + } + // return found direction + if(direction) { + return direction; + } + return false; + }, + duration: function(duration) { + duration = duration || settings.duration; + if(duration === false) { + duration = $module.css('animation-duration') || 0; + } + return (typeof duration === 'string') + ? (duration.indexOf('ms') > -1) + ? parseFloat(duration) + : parseFloat(duration) * 1000 + : duration + ; + }, + displayType: function(shouldDetermine) { + shouldDetermine = (shouldDetermine !== undefined) + ? shouldDetermine + : true + ; + if(settings.displayType) { + return settings.displayType; + } + if(shouldDetermine && $module.data(metadata.displayType) === undefined) { + // create fake element to determine display state + module.can.transition(true); + } + return $module.data(metadata.displayType); + }, + userStyle: function(style) { + style = style || $module.attr('style') || ''; + return style.replace(/display.*?;/, ''); + }, + transitionExists: function(animation) { + return $.fn.transition.exists[animation]; + }, + animationStartEvent: function() { + var + element = document.createElement('div'), + animations = { + 'animation' :'animationstart', + 'OAnimation' :'oAnimationStart', + 'MozAnimation' :'mozAnimationStart', + 'WebkitAnimation' :'webkitAnimationStart' + }, + animation + ; + for(animation in animations){ + if( element.style[animation] !== undefined ){ + return animations[animation]; + } + } + return false; + }, + animationEndEvent: function() { + var + element = document.createElement('div'), + animations = { + 'animation' :'animationend', + 'OAnimation' :'oAnimationEnd', + 'MozAnimation' :'mozAnimationEnd', + 'WebkitAnimation' :'webkitAnimationEnd' + }, + animation + ; + for(animation in animations){ + if( element.style[animation] !== undefined ){ + return animations[animation]; + } + } + return false; + } + + }, + + can: { + transition: function(forced) { + var + animation = settings.animation, + transitionExists = module.get.transitionExists(animation), + displayType = module.get.displayType(false), + elementClass, + tagName, + $clone, + currentAnimation, + inAnimation, + directionExists + ; + if( transitionExists === undefined || forced) { + module.verbose('Determining whether animation exists'); + elementClass = $module.attr('class'); + tagName = $module.prop('tagName'); + + $clone = $('<' + tagName + ' />').addClass( elementClass ).insertAfter($module); + currentAnimation = $clone + .addClass(animation) + .removeClass(className.inward) + .removeClass(className.outward) + .addClass(className.animating) + .addClass(className.transition) + .css('animationName') + ; + inAnimation = $clone + .addClass(className.inward) + .css('animationName') + ; + if(!displayType) { + displayType = $clone + .attr('class', elementClass) + .removeAttr('style') + .removeClass(className.hidden) + .removeClass(className.visible) + .show() + .css('display') + ; + module.verbose('Determining final display state', displayType); + module.save.displayType(displayType); + } + + $clone.remove(); + if(currentAnimation != inAnimation) { + module.debug('Direction exists for animation', animation); + directionExists = true; + } + else if(currentAnimation == 'none' || !currentAnimation) { + module.debug('No animation defined in css', animation); + return; + } + else { + module.debug('Static animation found', animation, displayType); + directionExists = false; + } + module.save.transitionExists(animation, directionExists); + } + return (transitionExists !== undefined) + ? transitionExists + : directionExists + ; + }, + animate: function() { + // can transition does not return a value if animation does not exist + return (module.can.transition() !== undefined); + } + }, + + is: { + animating: function() { + return $module.hasClass(className.animating); + }, + inward: function() { + return $module.hasClass(className.inward); + }, + outward: function() { + return $module.hasClass(className.outward); + }, + looping: function() { + return $module.hasClass(className.looping); + }, + occurring: function(animation) { + animation = animation || settings.animation; + animation = '.' + animation.replace(' ', '.'); + return ( $module.filter(animation).length > 0 ); + }, + visible: function() { + return $module.is(':visible'); + }, + hidden: function() { + return $module.css('visibility') === 'hidden'; + }, + supported: function() { + return(animationEnd !== false); + } + }, + + hide: function() { + module.verbose('Hiding element'); + if( module.is.animating() ) { + module.reset(); + } + element.blur(); // IE will trigger focus change if element is not blurred before hiding + module.remove.display(); + module.remove.visible(); + module.set.hidden(); + module.force.hidden(); + settings.onHide.call(element); + settings.onComplete.call(element); + // module.repaint(); + }, + + show: function(display) { + module.verbose('Showing element', display); + module.remove.hidden(); + module.set.visible(); + module.force.visible(); + settings.onShow.call(element); + settings.onComplete.call(element); + // module.repaint(); + }, + + toggle: function() { + if( module.is.visible() ) { + module.hide(); + } + else { + module.show(); + } + }, + + stop: function() { + module.debug('Stopping current animation'); + $module.triggerHandler(animationEnd); + }, + + stopAll: function() { + module.debug('Stopping all animation'); + module.remove.queueCallback(); + $module.triggerHandler(animationEnd); + }, + + clear: { + queue: function() { + module.debug('Clearing animation queue'); + module.remove.queueCallback(); + } + }, + + enable: function() { + module.verbose('Starting animation'); + $module.removeClass(className.disabled); + }, + + disable: function() { + module.debug('Stopping animation'); + $module.addClass(className.disabled); + }, + + setting: function(name, value) { + module.debug('Changing setting', name, value); + if( $.isPlainObject(name) ) { + $.extend(true, settings, name); + } + else if(value !== undefined) { + if($.isPlainObject(settings[name])) { + $.extend(true, settings[name], value); + } + else { + settings[name] = value; + } + } + else { + return settings[name]; + } + }, + internal: function(name, value) { + if( $.isPlainObject(name) ) { + $.extend(true, module, name); + } + else if(value !== undefined) { + module[name] = value; + } + else { + return module[name]; + } + }, + debug: function() { + if(!settings.silent && settings.debug) { + if(settings.performance) { + module.performance.log(arguments); + } + else { + module.debug = Function.prototype.bind.call(console.info, console, settings.name + ':'); + module.debug.apply(console, arguments); + } + } + }, + verbose: function() { + if(!settings.silent && settings.verbose && settings.debug) { + if(settings.performance) { + module.performance.log(arguments); + } + else { + module.verbose = Function.prototype.bind.call(console.info, console, settings.name + ':'); + module.verbose.apply(console, arguments); + } + } + }, + error: function() { + if(!settings.silent) { + module.error = Function.prototype.bind.call(console.error, console, settings.name + ':'); + module.error.apply(console, arguments); + } + }, + performance: { + log: function(message) { + var + currentTime, + executionTime, + previousTime + ; + if(settings.performance) { + currentTime = new Date().getTime(); + previousTime = time || currentTime; + executionTime = currentTime - previousTime; + time = currentTime; + performance.push({ + 'Name' : message[0], + 'Arguments' : [].slice.call(message, 1) || '', + 'Element' : element, + 'Execution Time' : executionTime + }); + } + clearTimeout(module.performance.timer); + module.performance.timer = setTimeout(module.performance.display, 500); + }, + display: function() { + var + title = settings.name + ':', + totalTime = 0 + ; + time = false; + clearTimeout(module.performance.timer); + $.each(performance, function(index, data) { + totalTime += data['Execution Time']; + }); + title += ' ' + totalTime + 'ms'; + if(moduleSelector) { + title += ' \'' + moduleSelector + '\''; + } + if($allModules.length > 1) { + title += ' ' + '(' + $allModules.length + ')'; + } + if( (console.group !== undefined || console.table !== undefined) && performance.length > 0) { + console.groupCollapsed(title); + if(console.table) { + console.table(performance); + } + else { + $.each(performance, function(index, data) { + console.log(data['Name'] + ': ' + data['Execution Time']+'ms'); + }); + } + console.groupEnd(); + } + performance = []; + } + }, + // modified for transition to return invoke success + invoke: function(query, passedArguments, context) { + var + object = instance, + maxDepth, + found, + response + ; + passedArguments = passedArguments || queryArguments; + context = element || context; + if(typeof query == 'string' && object !== undefined) { + query = query.split(/[\. ]/); + maxDepth = query.length - 1; + $.each(query, function(depth, value) { + var camelCaseValue = (depth != maxDepth) + ? value + query[depth + 1].charAt(0).toUpperCase() + query[depth + 1].slice(1) + : query + ; + if( $.isPlainObject( object[camelCaseValue] ) && (depth != maxDepth) ) { + object = object[camelCaseValue]; + } + else if( object[camelCaseValue] !== undefined ) { + found = object[camelCaseValue]; + return false; + } + else if( $.isPlainObject( object[value] ) && (depth != maxDepth) ) { + object = object[value]; + } + else if( object[value] !== undefined ) { + found = object[value]; + return false; + } + else { + return false; + } + }); + } + if ( $.isFunction( found ) ) { + response = found.apply(context, passedArguments); + } + else if(found !== undefined) { + response = found; + } + + if($.isArray(returnedValue)) { + returnedValue.push(response); + } + else if(returnedValue !== undefined) { + returnedValue = [returnedValue, response]; + } + else if(response !== undefined) { + returnedValue = response; + } + return (found !== undefined) + ? found + : false + ; + } + }; + module.initialize(); + }) + ; + return (returnedValue !== undefined) + ? returnedValue + : this + ; +}; + +// Records if CSS transition is available +$.fn.transition.exists = {}; + +$.fn.transition.settings = { + + // module info + name : 'Transition', + + // hide all output from this component regardless of other settings + silent : false, + + // debug content outputted to console + debug : false, + + // verbose debug output + verbose : false, + + // performance data output + performance : true, + + // event namespace + namespace : 'transition', + + // delay between animations in group + interval : 0, + + // whether group animations should be reversed + reverse : 'auto', + + // animation callback event + onStart : function() {}, + onComplete : function() {}, + onShow : function() {}, + onHide : function() {}, + + // whether timeout should be used to ensure callback fires in cases animationend does not + useFailSafe : true, + + // delay in ms for fail safe + failSafeDelay : 100, + + // whether EXACT animation can occur twice in a row + allowRepeats : false, + + // Override final display type on visible + displayType : false, + + // animation duration + animation : 'fade', + duration : false, + + // new animations will occur after previous ones + queue : true, + + metadata : { + displayType: 'display' + }, + + className : { + animating : 'animating', + disabled : 'disabled', + hidden : 'hidden', + inward : 'in', + loading : 'loading', + looping : 'looping', + outward : 'out', + transition : 'transition', + visible : 'visible' + }, + + // possible errors + error: { + noAnimation : 'Element is no longer attached to DOM. Unable to animate. Use silent setting to surpress this warning in production.', + repeated : 'That animation is already occurring, cancelling repeated animation', + method : 'The method you called is not defined', + support : 'This browser does not support CSS animations' + } + +}; + + +})( jQuery, window, document ); + +/*! + * # Semantic UI 2.2.6 - API + * http://github.com/semantic-org/semantic-ui/ + * + * + * Released under the MIT license + * http://opensource.org/licenses/MIT + * + */ + +;(function ($, window, document, undefined) { + +"use strict"; + +var + window = (typeof window != 'undefined' && window.Math == Math) + ? window + : (typeof self != 'undefined' && self.Math == Math) + ? self + : Function('return this')() +; + +$.api = $.fn.api = function(parameters) { + + var + // use window context if none specified + $allModules = $.isFunction(this) + ? $(window) + : $(this), + moduleSelector = $allModules.selector || '', + time = new Date().getTime(), + performance = [], + + query = arguments[0], + methodInvoked = (typeof query == 'string'), + queryArguments = [].slice.call(arguments, 1), + + returnedValue + ; + + $allModules + .each(function() { + var + settings = ( $.isPlainObject(parameters) ) + ? $.extend(true, {}, $.fn.api.settings, parameters) + : $.extend({}, $.fn.api.settings), + + // internal aliases + namespace = settings.namespace, + metadata = settings.metadata, + selector = settings.selector, + error = settings.error, + className = settings.className, + + // define namespaces for modules + eventNamespace = '.' + namespace, + moduleNamespace = 'module-' + namespace, + + // element that creates request + $module = $(this), + $form = $module.closest(selector.form), + + // context used for state + $context = (settings.stateContext) + ? $(settings.stateContext) + : $module, + + // request details + ajaxSettings, + requestSettings, + url, + data, + requestStartTime, + + // standard module + element = this, + context = $context[0], + instance = $module.data(moduleNamespace), + module + ; + + module = { + + initialize: function() { + if(!methodInvoked) { + module.bind.events(); + } + module.instantiate(); + }, + + instantiate: function() { + module.verbose('Storing instance of module', module); + instance = module; + $module + .data(moduleNamespace, instance) + ; + }, + + destroy: function() { + module.verbose('Destroying previous module for', element); + $module + .removeData(moduleNamespace) + .off(eventNamespace) + ; + }, + + bind: { + events: function() { + var + triggerEvent = module.get.event() + ; + if( triggerEvent ) { + module.verbose('Attaching API events to element', triggerEvent); + $module + .on(triggerEvent + eventNamespace, module.event.trigger) + ; + } + else if(settings.on == 'now') { + module.debug('Querying API endpoint immediately'); + module.query(); + } + } + }, + + decode: { + json: function(response) { + if(response !== undefined && typeof response == 'string') { + try { + response = JSON.parse(response); + } + catch(e) { + // isnt json string + } + } + return response; + } + }, + + read: { + cachedResponse: function(url) { + var + response + ; + if(window.Storage === undefined) { + module.error(error.noStorage); + return; + } + response = sessionStorage.getItem(url); + module.debug('Using cached response', url, response); + response = module.decode.json(response); + return response; + } + }, + write: { + cachedResponse: function(url, response) { + if(response && response === '') { + module.debug('Response empty, not caching', response); + return; + } + if(window.Storage === undefined) { + module.error(error.noStorage); + return; + } + if( $.isPlainObject(response) ) { + response = JSON.stringify(response); + } + sessionStorage.setItem(url, response); + module.verbose('Storing cached response for url', url, response); + } + }, + + query: function() { + + if(module.is.disabled()) { + module.debug('Element is disabled API request aborted'); + return; + } + + if(module.is.loading()) { + if(settings.interruptRequests) { + module.debug('Interrupting previous request'); + module.abort(); + } + else { + module.debug('Cancelling request, previous request is still pending'); + return; + } + } + + // pass element metadata to url (value, text) + if(settings.defaultData) { + $.extend(true, settings.urlData, module.get.defaultData()); + } + + // Add form content + if(settings.serializeForm) { + settings.data = module.add.formData(settings.data); + } + + // call beforesend and get any settings changes + requestSettings = module.get.settings(); + + // check if before send cancelled request + if(requestSettings === false) { + module.cancelled = true; + module.error(error.beforeSend); + return; + } + else { + module.cancelled = false; + } + + // get url + url = module.get.templatedURL(); + + if(!url && !module.is.mocked()) { + module.error(error.missingURL); + return; + } + + // replace variables + url = module.add.urlData( url ); + // missing url parameters + if( !url && !module.is.mocked()) { + return; + } + + requestSettings.url = settings.base + url; + + // look for jQuery ajax parameters in settings + ajaxSettings = $.extend(true, {}, settings, { + type : settings.method || settings.type, + data : data, + url : settings.base + url, + beforeSend : settings.beforeXHR, + success : function() {}, + failure : function() {}, + complete : function() {} + }); + + module.debug('Querying URL', ajaxSettings.url); + module.verbose('Using AJAX settings', ajaxSettings); + if(settings.cache === 'local' && module.read.cachedResponse(url)) { + module.debug('Response returned from local cache'); + module.request = module.create.request(); + module.request.resolveWith(context, [ module.read.cachedResponse(url) ]); + return; + } + + if( !settings.throttle ) { + module.debug('Sending request', data, ajaxSettings.method); + module.send.request(); + } + else { + if(!settings.throttleFirstRequest && !module.timer) { + module.debug('Sending request', data, ajaxSettings.method); + module.send.request(); + module.timer = setTimeout(function(){}, settings.throttle); + } + else { + module.debug('Throttling request', settings.throttle); + clearTimeout(module.timer); + module.timer = setTimeout(function() { + if(module.timer) { + delete module.timer; + } + module.debug('Sending throttled request', data, ajaxSettings.method); + module.send.request(); + }, settings.throttle); + } + } + + }, + + should: { + removeError: function() { + return ( settings.hideError === true || (settings.hideError === 'auto' && !module.is.form()) ); + } + }, + + is: { + disabled: function() { + return ($module.filter(selector.disabled).length > 0); + }, + expectingJSON: function() { + return settings.dataType === 'json' || settings.dataType === 'jsonp'; + }, + form: function() { + return $module.is('form') || $context.is('form'); + }, + mocked: function() { + return (settings.mockResponse || settings.mockResponseAsync || settings.response || settings.responseAsync); + }, + input: function() { + return $module.is('input'); + }, + loading: function() { + return (module.request) + ? (module.request.state() == 'pending') + : false + ; + }, + abortedRequest: function(xhr) { + if(xhr && xhr.readyState !== undefined && xhr.readyState === 0) { + module.verbose('XHR request determined to be aborted'); + return true; + } + else { + module.verbose('XHR request was not aborted'); + return false; + } + }, + validResponse: function(response) { + if( (!module.is.expectingJSON()) || !$.isFunction(settings.successTest) ) { + module.verbose('Response is not JSON, skipping validation', settings.successTest, response); + return true; + } + module.debug('Checking JSON returned success', settings.successTest, response); + if( settings.successTest(response) ) { + module.debug('Response passed success test', response); + return true; + } + else { + module.debug('Response failed success test', response); + return false; + } + } + }, + + was: { + cancelled: function() { + return (module.cancelled || false); + }, + succesful: function() { + return (module.request && module.request.state() == 'resolved'); + }, + failure: function() { + return (module.request && module.request.state() == 'rejected'); + }, + complete: function() { + return (module.request && (module.request.state() == 'resolved' || module.request.state() == 'rejected') ); + } + }, + + add: { + urlData: function(url, urlData) { + var + requiredVariables, + optionalVariables + ; + if(url) { + requiredVariables = url.match(settings.regExp.required); + optionalVariables = url.match(settings.regExp.optional); + urlData = urlData || settings.urlData; + if(requiredVariables) { + module.debug('Looking for required URL variables', requiredVariables); + $.each(requiredVariables, function(index, templatedString) { + var + // allow legacy {$var} style + variable = (templatedString.indexOf('$') !== -1) + ? templatedString.substr(2, templatedString.length - 3) + : templatedString.substr(1, templatedString.length - 2), + value = ($.isPlainObject(urlData) && urlData[variable] !== undefined) + ? urlData[variable] + : ($module.data(variable) !== undefined) + ? $module.data(variable) + : ($context.data(variable) !== undefined) + ? $context.data(variable) + : urlData[variable] + ; + // remove value + if(value === undefined) { + module.error(error.requiredParameter, variable, url); + url = false; + return false; + } + else { + module.verbose('Found required variable', variable, value); + value = (settings.encodeParameters) + ? module.get.urlEncodedValue(value) + : value + ; + url = url.replace(templatedString, value); + } + }); + } + if(optionalVariables) { + module.debug('Looking for optional URL variables', requiredVariables); + $.each(optionalVariables, function(index, templatedString) { + var + // allow legacy {/$var} style + variable = (templatedString.indexOf('$') !== -1) + ? templatedString.substr(3, templatedString.length - 4) + : templatedString.substr(2, templatedString.length - 3), + value = ($.isPlainObject(urlData) && urlData[variable] !== undefined) + ? urlData[variable] + : ($module.data(variable) !== undefined) + ? $module.data(variable) + : ($context.data(variable) !== undefined) + ? $context.data(variable) + : urlData[variable] + ; + // optional replacement + if(value !== undefined) { + module.verbose('Optional variable Found', variable, value); + url = url.replace(templatedString, value); + } + else { + module.verbose('Optional variable not found', variable); + // remove preceding slash if set + if(url.indexOf('/' + templatedString) !== -1) { + url = url.replace('/' + templatedString, ''); + } + else { + url = url.replace(templatedString, ''); + } + } + }); + } + } + return url; + }, + formData: function(data) { + var + canSerialize = ($.fn.serializeObject !== undefined), + formData = (canSerialize) + ? $form.serializeObject() + : $form.serialize(), + hasOtherData + ; + data = data || settings.data; + hasOtherData = $.isPlainObject(data); + + if(hasOtherData) { + if(canSerialize) { + module.debug('Extending existing data with form data', data, formData); + data = $.extend(true, {}, data, formData); + } + else { + module.error(error.missingSerialize); + module.debug('Cant extend data. Replacing data with form data', data, formData); + data = formData; + } + } + else { + module.debug('Adding form data', formData); + data = formData; + } + return data; + } + }, + + send: { + request: function() { + module.set.loading(); + module.request = module.create.request(); + if( module.is.mocked() ) { + module.mockedXHR = module.create.mockedXHR(); + } + else { + module.xhr = module.create.xhr(); + } + settings.onRequest.call(context, module.request, module.xhr); + } + }, + + event: { + trigger: function(event) { + module.query(); + if(event.type == 'submit' || event.type == 'click') { + event.preventDefault(); + } + }, + xhr: { + always: function() { + // nothing special + }, + done: function(response, textStatus, xhr) { + var + context = this, + elapsedTime = (new Date().getTime() - requestStartTime), + timeLeft = (settings.loadingDuration - elapsedTime), + translatedResponse = ( $.isFunction(settings.onResponse) ) + ? module.is.expectingJSON() + ? settings.onResponse.call(context, $.extend(true, {}, response)) + : settings.onResponse.call(context, response) + : false + ; + timeLeft = (timeLeft > 0) + ? timeLeft + : 0 + ; + if(translatedResponse) { + module.debug('Modified API response in onResponse callback', settings.onResponse, translatedResponse, response); + response = translatedResponse; + } + if(timeLeft > 0) { + module.debug('Response completed early delaying state change by', timeLeft); + } + setTimeout(function() { + if( module.is.validResponse(response) ) { + module.request.resolveWith(context, [response, xhr]); + } + else { + module.request.rejectWith(context, [xhr, 'invalid']); + } + }, timeLeft); + }, + fail: function(xhr, status, httpMessage) { + var + context = this, + elapsedTime = (new Date().getTime() - requestStartTime), + timeLeft = (settings.loadingDuration - elapsedTime) + ; + timeLeft = (timeLeft > 0) + ? timeLeft + : 0 + ; + if(timeLeft > 0) { + module.debug('Response completed early delaying state change by', timeLeft); + } + setTimeout(function() { + if( module.is.abortedRequest(xhr) ) { + module.request.rejectWith(context, [xhr, 'aborted', httpMessage]); + } + else { + module.request.rejectWith(context, [xhr, 'error', status, httpMessage]); + } + }, timeLeft); + } + }, + request: { + done: function(response, xhr) { + module.debug('Successful API Response', response); + if(settings.cache === 'local' && url) { + module.write.cachedResponse(url, response); + module.debug('Saving server response locally', module.cache); + } + settings.onSuccess.call(context, response, $module, xhr); + }, + complete: function(firstParameter, secondParameter) { + var + xhr, + response + ; + // have to guess callback parameters based on request success + if( module.was.succesful() ) { + response = firstParameter; + xhr = secondParameter; + } + else { + xhr = firstParameter; + response = module.get.responseFromXHR(xhr); + } + module.remove.loading(); + settings.onComplete.call(context, response, $module, xhr); + }, + fail: function(xhr, status, httpMessage) { + var + // pull response from xhr if available + response = module.get.responseFromXHR(xhr), + errorMessage = module.get.errorFromRequest(response, status, httpMessage) + ; + if(status == 'aborted') { + module.debug('XHR Aborted (Most likely caused by page navigation or CORS Policy)', status, httpMessage); + settings.onAbort.call(context, status, $module, xhr); + return true; + } + else if(status == 'invalid') { + module.debug('JSON did not pass success test. A server-side error has most likely occurred', response); + } + else if(status == 'error') { + if(xhr !== undefined) { + module.debug('XHR produced a server error', status, httpMessage); + // make sure we have an error to display to console + if( xhr.status != 200 && httpMessage !== undefined && httpMessage !== '') { + module.error(error.statusMessage + httpMessage, ajaxSettings.url); + } + settings.onError.call(context, errorMessage, $module, xhr); + } + } + + if(settings.errorDuration && status !== 'aborted') { + module.debug('Adding error state'); + module.set.error(); + if( module.should.removeError() ) { + setTimeout(module.remove.error, settings.errorDuration); + } + } + module.debug('API Request failed', errorMessage, xhr); + settings.onFailure.call(context, response, $module, xhr); + } + } + }, + + create: { + + request: function() { + // api request promise + return $.Deferred() + .always(module.event.request.complete) + .done(module.event.request.done) + .fail(module.event.request.fail) + ; + }, + + mockedXHR: function () { + var + // xhr does not simulate these properties of xhr but must return them + textStatus = false, + status = false, + httpMessage = false, + responder = settings.mockResponse || settings.response, + asyncResponder = settings.mockResponseAsync || settings.responseAsync, + asyncCallback, + response, + mockedXHR + ; + + mockedXHR = $.Deferred() + .always(module.event.xhr.complete) + .done(module.event.xhr.done) + .fail(module.event.xhr.fail) + ; + + if(responder) { + if( $.isFunction(responder) ) { + module.debug('Using specified synchronous callback', responder); + response = responder.call(context, requestSettings); + } + else { + module.debug('Using settings specified response', responder); + response = responder; + } + // simulating response + mockedXHR.resolveWith(context, [ response, textStatus, { responseText: response }]); + } + else if( $.isFunction(asyncResponder) ) { + asyncCallback = function(response) { + module.debug('Async callback returned response', response); + + if(response) { + mockedXHR.resolveWith(context, [ response, textStatus, { responseText: response }]); + } + else { + mockedXHR.rejectWith(context, [{ responseText: response }, status, httpMessage]); + } + }; + module.debug('Using specified async response callback', asyncResponder); + asyncResponder.call(context, requestSettings, asyncCallback); + } + return mockedXHR; + }, + + xhr: function() { + var + xhr + ; + // ajax request promise + xhr = $.ajax(ajaxSettings) + .always(module.event.xhr.always) + .done(module.event.xhr.done) + .fail(module.event.xhr.fail) + ; + module.verbose('Created server request', xhr, ajaxSettings); + return xhr; + } + }, + + set: { + error: function() { + module.verbose('Adding error state to element', $context); + $context.addClass(className.error); + }, + loading: function() { + module.verbose('Adding loading state to element', $context); + $context.addClass(className.loading); + requestStartTime = new Date().getTime(); + } + }, + + remove: { + error: function() { + module.verbose('Removing error state from element', $context); + $context.removeClass(className.error); + }, + loading: function() { + module.verbose('Removing loading state from element', $context); + $context.removeClass(className.loading); + } + }, + + get: { + responseFromXHR: function(xhr) { + return $.isPlainObject(xhr) + ? (module.is.expectingJSON()) + ? module.decode.json(xhr.responseText) + : xhr.responseText + : false + ; + }, + errorFromRequest: function(response, status, httpMessage) { + return ($.isPlainObject(response) && response.error !== undefined) + ? response.error // use json error message + : (settings.error[status] !== undefined) // use server error message + ? settings.error[status] + : httpMessage + ; + }, + request: function() { + return module.request || false; + }, + xhr: function() { + return module.xhr || false; + }, + settings: function() { + var + runSettings + ; + runSettings = settings.beforeSend.call(context, settings); + if(runSettings) { + if(runSettings.success !== undefined) { + module.debug('Legacy success callback detected', runSettings); + module.error(error.legacyParameters, runSettings.success); + runSettings.onSuccess = runSettings.success; + } + if(runSettings.failure !== undefined) { + module.debug('Legacy failure callback detected', runSettings); + module.error(error.legacyParameters, runSettings.failure); + runSettings.onFailure = runSettings.failure; + } + if(runSettings.complete !== undefined) { + module.debug('Legacy complete callback detected', runSettings); + module.error(error.legacyParameters, runSettings.complete); + runSettings.onComplete = runSettings.complete; + } + } + if(runSettings === undefined) { + module.error(error.noReturnedValue); + } + if(runSettings === false) { + return runSettings; + } + return (runSettings !== undefined) + ? $.extend(true, {}, runSettings) + : $.extend(true, {}, settings) + ; + }, + urlEncodedValue: function(value) { + var + decodedValue = window.decodeURIComponent(value), + encodedValue = window.encodeURIComponent(value), + alreadyEncoded = (decodedValue !== value) + ; + if(alreadyEncoded) { + module.debug('URL value is already encoded, avoiding double encoding', value); + return value; + } + module.verbose('Encoding value using encodeURIComponent', value, encodedValue); + return encodedValue; + }, + defaultData: function() { + var + data = {} + ; + if( !$.isWindow(element) ) { + if( module.is.input() ) { + data.value = $module.val(); + } + else if( module.is.form() ) { + + } + else { + data.text = $module.text(); + } + } + return data; + }, + event: function() { + if( $.isWindow(element) || settings.on == 'now' ) { + module.debug('API called without element, no events attached'); + return false; + } + else if(settings.on == 'auto') { + if( $module.is('input') ) { + return (element.oninput !== undefined) + ? 'input' + : (element.onpropertychange !== undefined) + ? 'propertychange' + : 'keyup' + ; + } + else if( $module.is('form') ) { + return 'submit'; + } + else { + return 'click'; + } + } + else { + return settings.on; + } + }, + templatedURL: function(action) { + action = action || $module.data(metadata.action) || settings.action || false; + url = $module.data(metadata.url) || settings.url || false; + if(url) { + module.debug('Using specified url', url); + return url; + } + if(action) { + module.debug('Looking up url for action', action, settings.api); + if(settings.api[action] === undefined && !module.is.mocked()) { + module.error(error.missingAction, settings.action, settings.api); + return; + } + url = settings.api[action]; + } + else if( module.is.form() ) { + url = $module.attr('action') || $context.attr('action') || false; + module.debug('No url or action specified, defaulting to form action', url); + } + return url; + } + }, + + abort: function() { + var + xhr = module.get.xhr() + ; + if( xhr && xhr.state() !== 'resolved') { + module.debug('Cancelling API request'); + xhr.abort(); + } + }, + + // reset state + reset: function() { + module.remove.error(); + module.remove.loading(); + }, + + setting: function(name, value) { + module.debug('Changing setting', name, value); + if( $.isPlainObject(name) ) { + $.extend(true, settings, name); + } + else if(value !== undefined) { + if($.isPlainObject(settings[name])) { + $.extend(true, settings[name], value); + } + else { + settings[name] = value; + } + } + else { + return settings[name]; + } + }, + internal: function(name, value) { + if( $.isPlainObject(name) ) { + $.extend(true, module, name); + } + else if(value !== undefined) { + module[name] = value; + } + else { + return module[name]; + } + }, + debug: function() { + if(!settings.silent && settings.debug) { + if(settings.performance) { + module.performance.log(arguments); + } + else { + module.debug = Function.prototype.bind.call(console.info, console, settings.name + ':'); + module.debug.apply(console, arguments); + } + } + }, + verbose: function() { + if(!settings.silent && settings.verbose && settings.debug) { + if(settings.performance) { + module.performance.log(arguments); + } + else { + module.verbose = Function.prototype.bind.call(console.info, console, settings.name + ':'); + module.verbose.apply(console, arguments); + } + } + }, + error: function() { + if(!settings.silent) { + module.error = Function.prototype.bind.call(console.error, console, settings.name + ':'); + module.error.apply(console, arguments); + } + }, + performance: { + log: function(message) { + var + currentTime, + executionTime, + previousTime + ; + if(settings.performance) { + currentTime = new Date().getTime(); + previousTime = time || currentTime; + executionTime = currentTime - previousTime; + time = currentTime; + performance.push({ + 'Name' : message[0], + 'Arguments' : [].slice.call(message, 1) || '', + //'Element' : element, + 'Execution Time' : executionTime + }); + } + clearTimeout(module.performance.timer); + module.performance.timer = setTimeout(module.performance.display, 500); + }, + display: function() { + var + title = settings.name + ':', + totalTime = 0 + ; + time = false; + clearTimeout(module.performance.timer); + $.each(performance, function(index, data) { + totalTime += data['Execution Time']; + }); + title += ' ' + totalTime + 'ms'; + if(moduleSelector) { + title += ' \'' + moduleSelector + '\''; + } + if( (console.group !== undefined || console.table !== undefined) && performance.length > 0) { + console.groupCollapsed(title); + if(console.table) { + console.table(performance); + } + else { + $.each(performance, function(index, data) { + console.log(data['Name'] + ': ' + data['Execution Time']+'ms'); + }); + } + console.groupEnd(); + } + performance = []; + } + }, + invoke: function(query, passedArguments, context) { + var + object = instance, + maxDepth, + found, + response + ; + passedArguments = passedArguments || queryArguments; + context = element || context; + if(typeof query == 'string' && object !== undefined) { + query = query.split(/[\. ]/); + maxDepth = query.length - 1; + $.each(query, function(depth, value) { + var camelCaseValue = (depth != maxDepth) + ? value + query[depth + 1].charAt(0).toUpperCase() + query[depth + 1].slice(1) + : query + ; + if( $.isPlainObject( object[camelCaseValue] ) && (depth != maxDepth) ) { + object = object[camelCaseValue]; + } + else if( object[camelCaseValue] !== undefined ) { + found = object[camelCaseValue]; + return false; + } + else if( $.isPlainObject( object[value] ) && (depth != maxDepth) ) { + object = object[value]; + } + else if( object[value] !== undefined ) { + found = object[value]; + return false; + } + else { + module.error(error.method, query); + return false; + } + }); + } + if ( $.isFunction( found ) ) { + response = found.apply(context, passedArguments); + } + else if(found !== undefined) { + response = found; + } + if($.isArray(returnedValue)) { + returnedValue.push(response); + } + else if(returnedValue !== undefined) { + returnedValue = [returnedValue, response]; + } + else if(response !== undefined) { + returnedValue = response; + } + return found; + } + }; + + if(methodInvoked) { + if(instance === undefined) { + module.initialize(); + } + module.invoke(query); + } + else { + if(instance !== undefined) { + instance.invoke('destroy'); + } + module.initialize(); + } + }) + ; + + return (returnedValue !== undefined) + ? returnedValue + : this + ; +}; + +$.api.settings = { + + name : 'API', + namespace : 'api', + + debug : false, + verbose : false, + performance : true, + + // object containing all templates endpoints + api : {}, + + // whether to cache responses + cache : true, + + // whether new requests should abort previous requests + interruptRequests : true, + + // event binding + on : 'auto', + + // context for applying state classes + stateContext : false, + + // duration for loading state + loadingDuration : 0, + + // whether to hide errors after a period of time + hideError : 'auto', + + // duration for error state + errorDuration : 2000, + + // whether parameters should be encoded with encodeURIComponent + encodeParameters : true, + + // API action to use + action : false, + + // templated URL to use + url : false, + + // base URL to apply to all endpoints + base : '', + + // data that will + urlData : {}, + + // whether to add default data to url data + defaultData : true, + + // whether to serialize closest form + serializeForm : false, + + // how long to wait before request should occur + throttle : 0, + + // whether to throttle first request or only repeated + throttleFirstRequest : true, + + // standard ajax settings + method : 'get', + data : {}, + dataType : 'json', + + // mock response + mockResponse : false, + mockResponseAsync : false, + + // aliases for mock + response : false, + responseAsync : false, + + // callbacks before request + beforeSend : function(settings) { return settings; }, + beforeXHR : function(xhr) {}, + onRequest : function(promise, xhr) {}, + + // after request + onResponse : false, // function(response) { }, + + // response was successful, if JSON passed validation + onSuccess : function(response, $module) {}, + + // request finished without aborting + onComplete : function(response, $module) {}, + + // failed JSON success test + onFailure : function(response, $module) {}, + + // server error + onError : function(errorMessage, $module) {}, + + // request aborted + onAbort : function(errorMessage, $module) {}, + + successTest : false, + + // errors + error : { + beforeSend : 'The before send function has aborted the request', + error : 'There was an error with your request', + exitConditions : 'API Request Aborted. Exit conditions met', + JSONParse : 'JSON could not be parsed during error handling', + legacyParameters : 'You are using legacy API success callback names', + method : 'The method you called is not defined', + missingAction : 'API action used but no url was defined', + missingSerialize : 'jquery-serialize-object is required to add form data to an existing data object', + missingURL : 'No URL specified for api event', + noReturnedValue : 'The beforeSend callback must return a settings object, beforeSend ignored.', + noStorage : 'Caching responses locally requires session storage', + parseError : 'There was an error parsing your request', + requiredParameter : 'Missing a required URL parameter: ', + statusMessage : 'Server gave an error: ', + timeout : 'Your request timed out' + }, + + regExp : { + required : /\{\$*[A-z0-9]+\}/g, + optional : /\{\/\$*[A-z0-9]+\}/g, + }, + + className: { + loading : 'loading', + error : 'error' + }, + + selector: { + disabled : '.disabled', + form : 'form' + }, + + metadata: { + action : 'action', + url : 'url' + } +}; + + + +})( jQuery, window, document ); + +/*! + * # Semantic UI 2.2.6 - State + * http://github.com/semantic-org/semantic-ui/ + * + * + * Released under the MIT license + * http://opensource.org/licenses/MIT + * + */ + +;(function ($, window, document, undefined) { + +"use strict"; + +window = (typeof window != 'undefined' && window.Math == Math) + ? window + : (typeof self != 'undefined' && self.Math == Math) + ? self + : Function('return this')() +; + +$.fn.state = function(parameters) { + var + $allModules = $(this), + + moduleSelector = $allModules.selector || '', + + hasTouch = ('ontouchstart' in document.documentElement), + time = new Date().getTime(), + performance = [], + + query = arguments[0], + methodInvoked = (typeof query == 'string'), + queryArguments = [].slice.call(arguments, 1), + + returnedValue + ; + $allModules + .each(function() { + var + settings = ( $.isPlainObject(parameters) ) + ? $.extend(true, {}, $.fn.state.settings, parameters) + : $.extend({}, $.fn.state.settings), + + error = settings.error, + metadata = settings.metadata, + className = settings.className, + namespace = settings.namespace, + states = settings.states, + text = settings.text, + + eventNamespace = '.' + namespace, + moduleNamespace = namespace + '-module', + + $module = $(this), + + element = this, + instance = $module.data(moduleNamespace), + + module + ; + module = { + + initialize: function() { + module.verbose('Initializing module'); + + // allow module to guess desired state based on element + if(settings.automatic) { + module.add.defaults(); + } + + // bind events with delegated events + if(settings.context && moduleSelector !== '') { + $(settings.context) + .on(moduleSelector, 'mouseenter' + eventNamespace, module.change.text) + .on(moduleSelector, 'mouseleave' + eventNamespace, module.reset.text) + .on(moduleSelector, 'click' + eventNamespace, module.toggle.state) + ; + } + else { + $module + .on('mouseenter' + eventNamespace, module.change.text) + .on('mouseleave' + eventNamespace, module.reset.text) + .on('click' + eventNamespace, module.toggle.state) + ; + } + module.instantiate(); + }, + + instantiate: function() { + module.verbose('Storing instance of module', module); + instance = module; + $module + .data(moduleNamespace, module) + ; + }, + + destroy: function() { + module.verbose('Destroying previous module', instance); + $module + .off(eventNamespace) + .removeData(moduleNamespace) + ; + }, + + refresh: function() { + module.verbose('Refreshing selector cache'); + $module = $(element); + }, + + add: { + defaults: function() { + var + userStates = parameters && $.isPlainObject(parameters.states) + ? parameters.states + : {} + ; + $.each(settings.defaults, function(type, typeStates) { + if( module.is[type] !== undefined && module.is[type]() ) { + module.verbose('Adding default states', type, element); + $.extend(settings.states, typeStates, userStates); + } + }); + } + }, + + is: { + + active: function() { + return $module.hasClass(className.active); + }, + loading: function() { + return $module.hasClass(className.loading); + }, + inactive: function() { + return !( $module.hasClass(className.active) ); + }, + state: function(state) { + if(className[state] === undefined) { + return false; + } + return $module.hasClass( className[state] ); + }, + + enabled: function() { + return !( $module.is(settings.filter.active) ); + }, + disabled: function() { + return ( $module.is(settings.filter.active) ); + }, + textEnabled: function() { + return !( $module.is(settings.filter.text) ); + }, + + // definitions for automatic type detection + button: function() { + return $module.is('.button:not(a, .submit)'); + }, + input: function() { + return $module.is('input'); + }, + progress: function() { + return $module.is('.ui.progress'); + } + }, + + allow: function(state) { + module.debug('Now allowing state', state); + states[state] = true; + }, + disallow: function(state) { + module.debug('No longer allowing', state); + states[state] = false; + }, + + allows: function(state) { + return states[state] || false; + }, + + enable: function() { + $module.removeClass(className.disabled); + }, + + disable: function() { + $module.addClass(className.disabled); + }, + + setState: function(state) { + if(module.allows(state)) { + $module.addClass( className[state] ); + } + }, + + removeState: function(state) { + if(module.allows(state)) { + $module.removeClass( className[state] ); + } + }, + + toggle: { + state: function() { + var + apiRequest, + requestCancelled + ; + if( module.allows('active') && module.is.enabled() ) { + module.refresh(); + if($.fn.api !== undefined) { + apiRequest = $module.api('get request'); + requestCancelled = $module.api('was cancelled'); + if( requestCancelled ) { + module.debug('API Request cancelled by beforesend'); + settings.activateTest = function(){ return false; }; + settings.deactivateTest = function(){ return false; }; + } + else if(apiRequest) { + module.listenTo(apiRequest); + return; + } + } + module.change.state(); + } + } + }, + + listenTo: function(apiRequest) { + module.debug('API request detected, waiting for state signal', apiRequest); + if(apiRequest) { + if(text.loading) { + module.update.text(text.loading); + } + $.when(apiRequest) + .then(function() { + if(apiRequest.state() == 'resolved') { + module.debug('API request succeeded'); + settings.activateTest = function(){ return true; }; + settings.deactivateTest = function(){ return true; }; + } + else { + module.debug('API request failed'); + settings.activateTest = function(){ return false; }; + settings.deactivateTest = function(){ return false; }; + } + module.change.state(); + }) + ; + } + }, + + // checks whether active/inactive state can be given + change: { + + state: function() { + module.debug('Determining state change direction'); + // inactive to active change + if( module.is.inactive() ) { + module.activate(); + } + else { + module.deactivate(); + } + if(settings.sync) { + module.sync(); + } + settings.onChange.call(element); + }, + + text: function() { + if( module.is.textEnabled() ) { + if(module.is.disabled() ) { + module.verbose('Changing text to disabled text', text.hover); + module.update.text(text.disabled); + } + else if( module.is.active() ) { + if(text.hover) { + module.verbose('Changing text to hover text', text.hover); + module.update.text(text.hover); + } + else if(text.deactivate) { + module.verbose('Changing text to deactivating text', text.deactivate); + module.update.text(text.deactivate); + } + } + else { + if(text.hover) { + module.verbose('Changing text to hover text', text.hover); + module.update.text(text.hover); + } + else if(text.activate){ + module.verbose('Changing text to activating text', text.activate); + module.update.text(text.activate); + } + } + } + } + + }, + + activate: function() { + if( settings.activateTest.call(element) ) { + module.debug('Setting state to active'); + $module + .addClass(className.active) + ; + module.update.text(text.active); + settings.onActivate.call(element); + } + }, + + deactivate: function() { + if( settings.deactivateTest.call(element) ) { + module.debug('Setting state to inactive'); + $module + .removeClass(className.active) + ; + module.update.text(text.inactive); + settings.onDeactivate.call(element); + } + }, + + sync: function() { + module.verbose('Syncing other buttons to current state'); + if( module.is.active() ) { + $allModules + .not($module) + .state('activate'); + } + else { + $allModules + .not($module) + .state('deactivate') + ; + } + }, + + get: { + text: function() { + return (settings.selector.text) + ? $module.find(settings.selector.text).text() + : $module.html() + ; + }, + textFor: function(state) { + return text[state] || false; + } + }, + + flash: { + text: function(text, duration, callback) { + var + previousText = module.get.text() + ; + module.debug('Flashing text message', text, duration); + text = text || settings.text.flash; + duration = duration || settings.flashDuration; + callback = callback || function() {}; + module.update.text(text); + setTimeout(function(){ + module.update.text(previousText); + callback.call(element); + }, duration); + } + }, + + reset: { + // on mouseout sets text to previous value + text: function() { + var + activeText = text.active || $module.data(metadata.storedText), + inactiveText = text.inactive || $module.data(metadata.storedText) + ; + if( module.is.textEnabled() ) { + if( module.is.active() && activeText) { + module.verbose('Resetting active text', activeText); + module.update.text(activeText); + } + else if(inactiveText) { + module.verbose('Resetting inactive text', activeText); + module.update.text(inactiveText); + } + } + } + }, + + update: { + text: function(text) { + var + currentText = module.get.text() + ; + if(text && text !== currentText) { + module.debug('Updating text', text); + if(settings.selector.text) { + $module + .data(metadata.storedText, text) + .find(settings.selector.text) + .text(text) + ; + } + else { + $module + .data(metadata.storedText, text) + .html(text) + ; + } + } + else { + module.debug('Text is already set, ignoring update', text); + } + } + }, + + setting: function(name, value) { + module.debug('Changing setting', name, value); + if( $.isPlainObject(name) ) { + $.extend(true, settings, name); + } + else if(value !== undefined) { + if($.isPlainObject(settings[name])) { + $.extend(true, settings[name], value); + } + else { + settings[name] = value; + } + } + else { + return settings[name]; + } + }, + internal: function(name, value) { + if( $.isPlainObject(name) ) { + $.extend(true, module, name); + } + else if(value !== undefined) { + module[name] = value; + } + else { + return module[name]; + } + }, + debug: function() { + if(!settings.silent && settings.debug) { + if(settings.performance) { + module.performance.log(arguments); + } + else { + module.debug = Function.prototype.bind.call(console.info, console, settings.name + ':'); + module.debug.apply(console, arguments); + } + } + }, + verbose: function() { + if(!settings.silent && settings.verbose && settings.debug) { + if(settings.performance) { + module.performance.log(arguments); + } + else { + module.verbose = Function.prototype.bind.call(console.info, console, settings.name + ':'); + module.verbose.apply(console, arguments); + } + } + }, + error: function() { + if(!settings.silent) { + module.error = Function.prototype.bind.call(console.error, console, settings.name + ':'); + module.error.apply(console, arguments); + } + }, + performance: { + log: function(message) { + var + currentTime, + executionTime, + previousTime + ; + if(settings.performance) { + currentTime = new Date().getTime(); + previousTime = time || currentTime; + executionTime = currentTime - previousTime; + time = currentTime; + performance.push({ + 'Name' : message[0], + 'Arguments' : [].slice.call(message, 1) || '', + 'Element' : element, + 'Execution Time' : executionTime + }); + } + clearTimeout(module.performance.timer); + module.performance.timer = setTimeout(module.performance.display, 500); + }, + display: function() { + var + title = settings.name + ':', + totalTime = 0 + ; + time = false; + clearTimeout(module.performance.timer); + $.each(performance, function(index, data) { + totalTime += data['Execution Time']; + }); + title += ' ' + totalTime + 'ms'; + if(moduleSelector) { + title += ' \'' + moduleSelector + '\''; + } + if( (console.group !== undefined || console.table !== undefined) && performance.length > 0) { + console.groupCollapsed(title); + if(console.table) { + console.table(performance); + } + else { + $.each(performance, function(index, data) { + console.log(data['Name'] + ': ' + data['Execution Time']+'ms'); + }); + } + console.groupEnd(); + } + performance = []; + } + }, + invoke: function(query, passedArguments, context) { + var + object = instance, + maxDepth, + found, + response + ; + passedArguments = passedArguments || queryArguments; + context = element || context; + if(typeof query == 'string' && object !== undefined) { + query = query.split(/[\. ]/); + maxDepth = query.length - 1; + $.each(query, function(depth, value) { + var camelCaseValue = (depth != maxDepth) + ? value + query[depth + 1].charAt(0).toUpperCase() + query[depth + 1].slice(1) + : query + ; + if( $.isPlainObject( object[camelCaseValue] ) && (depth != maxDepth) ) { + object = object[camelCaseValue]; + } + else if( object[camelCaseValue] !== undefined ) { + found = object[camelCaseValue]; + return false; + } + else if( $.isPlainObject( object[value] ) && (depth != maxDepth) ) { + object = object[value]; + } + else if( object[value] !== undefined ) { + found = object[value]; + return false; + } + else { + module.error(error.method, query); + return false; + } + }); + } + if ( $.isFunction( found ) ) { + response = found.apply(context, passedArguments); + } + else if(found !== undefined) { + response = found; + } + if($.isArray(returnedValue)) { + returnedValue.push(response); + } + else if(returnedValue !== undefined) { + returnedValue = [returnedValue, response]; + } + else if(response !== undefined) { + returnedValue = response; + } + return found; + } + }; + + if(methodInvoked) { + if(instance === undefined) { + module.initialize(); + } + module.invoke(query); + } + else { + if(instance !== undefined) { + instance.invoke('destroy'); + } + module.initialize(); + } + }) + ; + + return (returnedValue !== undefined) + ? returnedValue + : this + ; +}; + +$.fn.state.settings = { + + // module info + name : 'State', + + // debug output + debug : false, + + // verbose debug output + verbose : false, + + // namespace for events + namespace : 'state', + + // debug data includes performance + performance : true, + + // callback occurs on state change + onActivate : function() {}, + onDeactivate : function() {}, + onChange : function() {}, + + // state test functions + activateTest : function() { return true; }, + deactivateTest : function() { return true; }, + + // whether to automatically map default states + automatic : true, + + // activate / deactivate changes all elements instantiated at same time + sync : false, + + // default flash text duration, used for temporarily changing text of an element + flashDuration : 1000, + + // selector filter + filter : { + text : '.loading, .disabled', + active : '.disabled' + }, + + context : false, + + // error + error: { + beforeSend : 'The before send function has cancelled state change', + method : 'The method you called is not defined.' + }, + + // metadata + metadata: { + promise : 'promise', + storedText : 'stored-text' + }, + + // change class on state + className: { + active : 'active', + disabled : 'disabled', + error : 'error', + loading : 'loading', + success : 'success', + warning : 'warning' + }, + + selector: { + // selector for text node + text: false + }, + + defaults : { + input: { + disabled : true, + loading : true, + active : true + }, + button: { + disabled : true, + loading : true, + active : true, + }, + progress: { + active : true, + success : true, + warning : true, + error : true + } + }, + + states : { + active : true, + disabled : true, + error : true, + loading : true, + success : true, + warning : true + }, + + text : { + disabled : false, + flash : false, + hover : false, + active : false, + inactive : false, + activate : false, + deactivate : false + } + +}; + + + +})( jQuery, window, document ); + +/*! + * # Semantic UI 2.2.6 - Visibility + * http://github.com/semantic-org/semantic-ui/ + * + * + * Released under the MIT license + * http://opensource.org/licenses/MIT + * + */ + +;(function ($, window, document, undefined) { + +"use strict"; + +window = (typeof window != 'undefined' && window.Math == Math) + ? window + : (typeof self != 'undefined' && self.Math == Math) + ? self + : Function('return this')() +; + +$.fn.visibility = function(parameters) { + var + $allModules = $(this), + moduleSelector = $allModules.selector || '', + + time = new Date().getTime(), + performance = [], + + query = arguments[0], + methodInvoked = (typeof query == 'string'), + queryArguments = [].slice.call(arguments, 1), + returnedValue, + + moduleCount = $allModules.length, + loadedCount = 0 + ; + + $allModules + .each(function() { + var + settings = ( $.isPlainObject(parameters) ) + ? $.extend(true, {}, $.fn.visibility.settings, parameters) + : $.extend({}, $.fn.visibility.settings), + + className = settings.className, + namespace = settings.namespace, + error = settings.error, + metadata = settings.metadata, + + eventNamespace = '.' + namespace, + moduleNamespace = 'module-' + namespace, + + $window = $(window), + + $module = $(this), + $context = $(settings.context), + + $placeholder, + + selector = $module.selector || '', + instance = $module.data(moduleNamespace), + + requestAnimationFrame = window.requestAnimationFrame + || window.mozRequestAnimationFrame + || window.webkitRequestAnimationFrame + || window.msRequestAnimationFrame + || function(callback) { setTimeout(callback, 0); }, + + element = this, + disabled = false, + + contextObserver, + observer, + module + ; + + module = { + + initialize: function() { + module.debug('Initializing', settings); + + module.setup.cache(); + + if( module.should.trackChanges() ) { + + if(settings.type == 'image') { + module.setup.image(); + } + if(settings.type == 'fixed') { + module.setup.fixed(); + } + + if(settings.observeChanges) { + module.observeChanges(); + } + module.bind.events(); + } + + module.save.position(); + if( !module.is.visible() ) { + module.error(error.visible, $module); + } + + if(settings.initialCheck) { + module.checkVisibility(); + } + module.instantiate(); + }, + + instantiate: function() { + module.debug('Storing instance', module); + $module + .data(moduleNamespace, module) + ; + instance = module; + }, + + destroy: function() { + module.verbose('Destroying previous module'); + if(observer) { + observer.disconnect(); + } + if(contextObserver) { + contextObserver.disconnect(); + } + $window + .off('load' + eventNamespace, module.event.load) + .off('resize' + eventNamespace, module.event.resize) + ; + $context + .off('scroll' + eventNamespace, module.event.scroll) + .off('scrollchange' + eventNamespace, module.event.scrollchange) + ; + if(settings.type == 'fixed') { + module.resetFixed(); + module.remove.placeholder(); + } + $module + .off(eventNamespace) + .removeData(moduleNamespace) + ; + }, + + observeChanges: function() { + if('MutationObserver' in window) { + contextObserver = new MutationObserver(module.event.contextChanged); + observer = new MutationObserver(module.event.changed); + contextObserver.observe(document, { + childList : true, + subtree : true + }); + observer.observe(element, { + childList : true, + subtree : true + }); + module.debug('Setting up mutation observer', observer); + } + }, + + bind: { + events: function() { + module.verbose('Binding visibility events to scroll and resize'); + if(settings.refreshOnLoad) { + $window + .on('load' + eventNamespace, module.event.load) + ; + } + $window + .on('resize' + eventNamespace, module.event.resize) + ; + // pub/sub pattern + $context + .off('scroll' + eventNamespace) + .on('scroll' + eventNamespace, module.event.scroll) + .on('scrollchange' + eventNamespace, module.event.scrollchange) + ; + } + }, + + event: { + changed: function(mutations) { + module.verbose('DOM tree modified, updating visibility calculations'); + module.timer = setTimeout(function() { + module.verbose('DOM tree modified, updating sticky menu'); + module.refresh(); + }, 100); + }, + contextChanged: function(mutations) { + [].forEach.call(mutations, function(mutation) { + if(mutation.removedNodes) { + [].forEach.call(mutation.removedNodes, function(node) { + if(node == element || $(node).find(element).length > 0) { + module.debug('Element removed from DOM, tearing down events'); + module.destroy(); + } + }); + } + }); + }, + resize: function() { + module.debug('Window resized'); + if(settings.refreshOnResize) { + requestAnimationFrame(module.refresh); + } + }, + load: function() { + module.debug('Page finished loading'); + requestAnimationFrame(module.refresh); + }, + // publishes scrollchange event on one scroll + scroll: function() { + if(settings.throttle) { + clearTimeout(module.timer); + module.timer = setTimeout(function() { + $context.triggerHandler('scrollchange' + eventNamespace, [ $context.scrollTop() ]); + }, settings.throttle); + } + else { + requestAnimationFrame(function() { + $context.triggerHandler('scrollchange' + eventNamespace, [ $context.scrollTop() ]); + }); + } + }, + // subscribes to scrollchange + scrollchange: function(event, scrollPosition) { + module.checkVisibility(scrollPosition); + }, + }, + + precache: function(images, callback) { + if (!(images instanceof Array)) { + images = [images]; + } + var + imagesLength = images.length, + loadedCounter = 0, + cache = [], + cacheImage = document.createElement('img'), + handleLoad = function() { + loadedCounter++; + if (loadedCounter >= images.length) { + if ($.isFunction(callback)) { + callback(); + } + } + } + ; + while (imagesLength--) { + cacheImage = document.createElement('img'); + cacheImage.onload = handleLoad; + cacheImage.onerror = handleLoad; + cacheImage.src = images[imagesLength]; + cache.push(cacheImage); + } + }, + + enableCallbacks: function() { + module.debug('Allowing callbacks to occur'); + disabled = false; + }, + + disableCallbacks: function() { + module.debug('Disabling all callbacks temporarily'); + disabled = true; + }, + + should: { + trackChanges: function() { + if(methodInvoked) { + module.debug('One time query, no need to bind events'); + return false; + } + module.debug('Callbacks being attached'); + return true; + } + }, + + setup: { + cache: function() { + module.cache = { + occurred : {}, + screen : {}, + element : {}, + }; + }, + image: function() { + var + src = $module.data(metadata.src) + ; + if(src) { + module.verbose('Lazy loading image', src); + settings.once = true; + settings.observeChanges = false; + + // show when top visible + settings.onOnScreen = function() { + module.debug('Image on screen', element); + module.precache(src, function() { + module.set.image(src, function() { + loadedCount++; + if(loadedCount == moduleCount) { + settings.onAllLoaded.call(this); + } + settings.onLoad.call(this); + }); + }); + }; + } + }, + fixed: function() { + module.debug('Setting up fixed'); + settings.once = false; + settings.observeChanges = false; + settings.initialCheck = true; + settings.refreshOnLoad = true; + if(!parameters.transition) { + settings.transition = false; + } + module.create.placeholder(); + module.debug('Added placeholder', $placeholder); + settings.onTopPassed = function() { + module.debug('Element passed, adding fixed position', $module); + module.show.placeholder(); + module.set.fixed(); + if(settings.transition) { + if($.fn.transition !== undefined) { + $module.transition(settings.transition, settings.duration); + } + } + }; + settings.onTopPassedReverse = function() { + module.debug('Element returned to position, removing fixed', $module); + module.hide.placeholder(); + module.remove.fixed(); + }; + } + }, + + create: { + placeholder: function() { + module.verbose('Creating fixed position placeholder'); + $placeholder = $module + .clone(false) + .css('display', 'none') + .addClass(className.placeholder) + .insertAfter($module) + ; + } + }, + + show: { + placeholder: function() { + module.verbose('Showing placeholder'); + $placeholder + .css('display', 'block') + .css('visibility', 'hidden') + ; + } + }, + hide: { + placeholder: function() { + module.verbose('Hiding placeholder'); + $placeholder + .css('display', 'none') + .css('visibility', '') + ; + } + }, + + set: { + fixed: function() { + module.verbose('Setting element to fixed position'); + $module + .addClass(className.fixed) + .css({ + position : 'fixed', + top : settings.offset + 'px', + left : 'auto', + zIndex : settings.zIndex + }) + ; + settings.onFixed.call(element); + }, + image: function(src, callback) { + $module + .attr('src', src) + ; + if(settings.transition) { + if( $.fn.transition !== undefined ) { + $module.transition(settings.transition, settings.duration, callback); + } + else { + $module.fadeIn(settings.duration, callback); + } + } + else { + $module.show(); + } + } + }, + + is: { + onScreen: function() { + var + calculations = module.get.elementCalculations() + ; + return calculations.onScreen; + }, + offScreen: function() { + var + calculations = module.get.elementCalculations() + ; + return calculations.offScreen; + }, + visible: function() { + if(module.cache && module.cache.element) { + return !(module.cache.element.width === 0 && module.cache.element.offset.top === 0); + } + return false; + } + }, + + refresh: function() { + module.debug('Refreshing constants (width/height)'); + if(settings.type == 'fixed') { + module.resetFixed(); + } + module.reset(); + module.save.position(); + if(settings.checkOnRefresh) { + module.checkVisibility(); + } + settings.onRefresh.call(element); + }, + + resetFixed: function () { + module.remove.fixed(); + module.remove.occurred(); + }, + + reset: function() { + module.verbose('Resetting all cached values'); + if( $.isPlainObject(module.cache) ) { + module.cache.screen = {}; + module.cache.element = {}; + } + }, + + checkVisibility: function(scroll) { + module.verbose('Checking visibility of element', module.cache.element); + + if( !disabled && module.is.visible() ) { + + // save scroll position + module.save.scroll(scroll); + + // update calculations derived from scroll + module.save.calculations(); + + // percentage + module.passed(); + + // reverse (must be first) + module.passingReverse(); + module.topVisibleReverse(); + module.bottomVisibleReverse(); + module.topPassedReverse(); + module.bottomPassedReverse(); + + // one time + module.onScreen(); + module.offScreen(); + module.passing(); + module.topVisible(); + module.bottomVisible(); + module.topPassed(); + module.bottomPassed(); + + // on update callback + if(settings.onUpdate) { + settings.onUpdate.call(element, module.get.elementCalculations()); + } + } + }, + + passed: function(amount, newCallback) { + var + calculations = module.get.elementCalculations(), + amountInPixels + ; + // assign callback + if(amount && newCallback) { + settings.onPassed[amount] = newCallback; + } + else if(amount !== undefined) { + return (module.get.pixelsPassed(amount) > calculations.pixelsPassed); + } + else if(calculations.passing) { + $.each(settings.onPassed, function(amount, callback) { + if(calculations.bottomVisible || calculations.pixelsPassed > module.get.pixelsPassed(amount)) { + module.execute(callback, amount); + } + else if(!settings.once) { + module.remove.occurred(callback); + } + }); + } + }, + + onScreen: function(newCallback) { + var + calculations = module.get.elementCalculations(), + callback = newCallback || settings.onOnScreen, + callbackName = 'onScreen' + ; + if(newCallback) { + module.debug('Adding callback for onScreen', newCallback); + settings.onOnScreen = newCallback; + } + if(calculations.onScreen) { + module.execute(callback, callbackName); + } + else if(!settings.once) { + module.remove.occurred(callbackName); + } + if(newCallback !== undefined) { + return calculations.onOnScreen; + } + }, + + offScreen: function(newCallback) { + var + calculations = module.get.elementCalculations(), + callback = newCallback || settings.onOffScreen, + callbackName = 'offScreen' + ; + if(newCallback) { + module.debug('Adding callback for offScreen', newCallback); + settings.onOffScreen = newCallback; + } + if(calculations.offScreen) { + module.execute(callback, callbackName); + } + else if(!settings.once) { + module.remove.occurred(callbackName); + } + if(newCallback !== undefined) { + return calculations.onOffScreen; + } + }, + + passing: function(newCallback) { + var + calculations = module.get.elementCalculations(), + callback = newCallback || settings.onPassing, + callbackName = 'passing' + ; + if(newCallback) { + module.debug('Adding callback for passing', newCallback); + settings.onPassing = newCallback; + } + if(calculations.passing) { + module.execute(callback, callbackName); + } + else if(!settings.once) { + module.remove.occurred(callbackName); + } + if(newCallback !== undefined) { + return calculations.passing; + } + }, + + + topVisible: function(newCallback) { + var + calculations = module.get.elementCalculations(), + callback = newCallback || settings.onTopVisible, + callbackName = 'topVisible' + ; + if(newCallback) { + module.debug('Adding callback for top visible', newCallback); + settings.onTopVisible = newCallback; + } + if(calculations.topVisible) { + module.execute(callback, callbackName); + } + else if(!settings.once) { + module.remove.occurred(callbackName); + } + if(newCallback === undefined) { + return calculations.topVisible; + } + }, + + bottomVisible: function(newCallback) { + var + calculations = module.get.elementCalculations(), + callback = newCallback || settings.onBottomVisible, + callbackName = 'bottomVisible' + ; + if(newCallback) { + module.debug('Adding callback for bottom visible', newCallback); + settings.onBottomVisible = newCallback; + } + if(calculations.bottomVisible) { + module.execute(callback, callbackName); + } + else if(!settings.once) { + module.remove.occurred(callbackName); + } + if(newCallback === undefined) { + return calculations.bottomVisible; + } + }, + + topPassed: function(newCallback) { + var + calculations = module.get.elementCalculations(), + callback = newCallback || settings.onTopPassed, + callbackName = 'topPassed' + ; + if(newCallback) { + module.debug('Adding callback for top passed', newCallback); + settings.onTopPassed = newCallback; + } + if(calculations.topPassed) { + module.execute(callback, callbackName); + } + else if(!settings.once) { + module.remove.occurred(callbackName); + } + if(newCallback === undefined) { + return calculations.topPassed; + } + }, + + bottomPassed: function(newCallback) { + var + calculations = module.get.elementCalculations(), + callback = newCallback || settings.onBottomPassed, + callbackName = 'bottomPassed' + ; + if(newCallback) { + module.debug('Adding callback for bottom passed', newCallback); + settings.onBottomPassed = newCallback; + } + if(calculations.bottomPassed) { + module.execute(callback, callbackName); + } + else if(!settings.once) { + module.remove.occurred(callbackName); + } + if(newCallback === undefined) { + return calculations.bottomPassed; + } + }, + + passingReverse: function(newCallback) { + var + calculations = module.get.elementCalculations(), + callback = newCallback || settings.onPassingReverse, + callbackName = 'passingReverse' + ; + if(newCallback) { + module.debug('Adding callback for passing reverse', newCallback); + settings.onPassingReverse = newCallback; + } + if(!calculations.passing) { + if(module.get.occurred('passing')) { + module.execute(callback, callbackName); + } + } + else if(!settings.once) { + module.remove.occurred(callbackName); + } + if(newCallback !== undefined) { + return !calculations.passing; + } + }, + + + topVisibleReverse: function(newCallback) { + var + calculations = module.get.elementCalculations(), + callback = newCallback || settings.onTopVisibleReverse, + callbackName = 'topVisibleReverse' + ; + if(newCallback) { + module.debug('Adding callback for top visible reverse', newCallback); + settings.onTopVisibleReverse = newCallback; + } + if(!calculations.topVisible) { + if(module.get.occurred('topVisible')) { + module.execute(callback, callbackName); + } + } + else if(!settings.once) { + module.remove.occurred(callbackName); + } + if(newCallback === undefined) { + return !calculations.topVisible; + } + }, + + bottomVisibleReverse: function(newCallback) { + var + calculations = module.get.elementCalculations(), + callback = newCallback || settings.onBottomVisibleReverse, + callbackName = 'bottomVisibleReverse' + ; + if(newCallback) { + module.debug('Adding callback for bottom visible reverse', newCallback); + settings.onBottomVisibleReverse = newCallback; + } + if(!calculations.bottomVisible) { + if(module.get.occurred('bottomVisible')) { + module.execute(callback, callbackName); + } + } + else if(!settings.once) { + module.remove.occurred(callbackName); + } + if(newCallback === undefined) { + return !calculations.bottomVisible; + } + }, + + topPassedReverse: function(newCallback) { + var + calculations = module.get.elementCalculations(), + callback = newCallback || settings.onTopPassedReverse, + callbackName = 'topPassedReverse' + ; + if(newCallback) { + module.debug('Adding callback for top passed reverse', newCallback); + settings.onTopPassedReverse = newCallback; + } + if(!calculations.topPassed) { + if(module.get.occurred('topPassed')) { + module.execute(callback, callbackName); + } + } + else if(!settings.once) { + module.remove.occurred(callbackName); + } + if(newCallback === undefined) { + return !calculations.onTopPassed; + } + }, + + bottomPassedReverse: function(newCallback) { + var + calculations = module.get.elementCalculations(), + callback = newCallback || settings.onBottomPassedReverse, + callbackName = 'bottomPassedReverse' + ; + if(newCallback) { + module.debug('Adding callback for bottom passed reverse', newCallback); + settings.onBottomPassedReverse = newCallback; + } + if(!calculations.bottomPassed) { + if(module.get.occurred('bottomPassed')) { + module.execute(callback, callbackName); + } + } + else if(!settings.once) { + module.remove.occurred(callbackName); + } + if(newCallback === undefined) { + return !calculations.bottomPassed; + } + }, + + execute: function(callback, callbackName) { + var + calculations = module.get.elementCalculations(), + screen = module.get.screenCalculations() + ; + callback = callback || false; + if(callback) { + if(settings.continuous) { + module.debug('Callback being called continuously', callbackName, calculations); + callback.call(element, calculations, screen); + } + else if(!module.get.occurred(callbackName)) { + module.debug('Conditions met', callbackName, calculations); + callback.call(element, calculations, screen); + } + } + module.save.occurred(callbackName); + }, + + remove: { + fixed: function() { + module.debug('Removing fixed position'); + $module + .removeClass(className.fixed) + .css({ + position : '', + top : '', + left : '', + zIndex : '' + }) + ; + settings.onUnfixed.call(element); + }, + placeholder: function() { + module.debug('Removing placeholder content'); + if($placeholder) { + $placeholder.remove(); + } + }, + occurred: function(callback) { + if(callback) { + var + occurred = module.cache.occurred + ; + if(occurred[callback] !== undefined && occurred[callback] === true) { + module.debug('Callback can now be called again', callback); + module.cache.occurred[callback] = false; + } + } + else { + module.cache.occurred = {}; + } + } + }, + + save: { + calculations: function() { + module.verbose('Saving all calculations necessary to determine positioning'); + module.save.direction(); + module.save.screenCalculations(); + module.save.elementCalculations(); + }, + occurred: function(callback) { + if(callback) { + if(module.cache.occurred[callback] === undefined || (module.cache.occurred[callback] !== true)) { + module.verbose('Saving callback occurred', callback); + module.cache.occurred[callback] = true; + } + } + }, + scroll: function(scrollPosition) { + scrollPosition = scrollPosition + settings.offset || $context.scrollTop() + settings.offset; + module.cache.scroll = scrollPosition; + }, + direction: function() { + var + scroll = module.get.scroll(), + lastScroll = module.get.lastScroll(), + direction + ; + if(scroll > lastScroll && lastScroll) { + direction = 'down'; + } + else if(scroll < lastScroll && lastScroll) { + direction = 'up'; + } + else { + direction = 'static'; + } + module.cache.direction = direction; + return module.cache.direction; + }, + elementPosition: function() { + var + element = module.cache.element, + screen = module.get.screenSize() + ; + module.verbose('Saving element position'); + // (quicker than $.extend) + element.fits = (element.height < screen.height); + element.offset = $module.offset(); + element.width = $module.outerWidth(); + element.height = $module.outerHeight(); + // store + module.cache.element = element; + return element; + }, + elementCalculations: function() { + var + screen = module.get.screenCalculations(), + element = module.get.elementPosition() + ; + // offset + if(settings.includeMargin) { + element.margin = {}; + element.margin.top = parseInt($module.css('margin-top'), 10); + element.margin.bottom = parseInt($module.css('margin-bottom'), 10); + element.top = element.offset.top - element.margin.top; + element.bottom = element.offset.top + element.height + element.margin.bottom; + } + else { + element.top = element.offset.top; + element.bottom = element.offset.top + element.height; + } + + // visibility + element.topVisible = (screen.bottom >= element.top); + element.topPassed = (screen.top >= element.top); + element.bottomVisible = (screen.bottom >= element.bottom); + element.bottomPassed = (screen.top >= element.bottom); + element.pixelsPassed = 0; + element.percentagePassed = 0; + + // meta calculations + element.onScreen = (element.topVisible && !element.bottomPassed); + element.passing = (element.topPassed && !element.bottomPassed); + element.offScreen = (!element.onScreen); + + // passing calculations + if(element.passing) { + element.pixelsPassed = (screen.top - element.top); + element.percentagePassed = (screen.top - element.top) / element.height; + } + module.cache.element = element; + module.verbose('Updated element calculations', element); + return element; + }, + screenCalculations: function() { + var + scroll = module.get.scroll() + ; + module.save.direction(); + module.cache.screen.top = scroll; + module.cache.screen.bottom = scroll + module.cache.screen.height; + return module.cache.screen; + }, + screenSize: function() { + module.verbose('Saving window position'); + module.cache.screen = { + height: $context.height() + }; + }, + position: function() { + module.save.screenSize(); + module.save.elementPosition(); + } + }, + + get: { + pixelsPassed: function(amount) { + var + element = module.get.elementCalculations() + ; + if(amount.search('%') > -1) { + return ( element.height * (parseInt(amount, 10) / 100) ); + } + return parseInt(amount, 10); + }, + occurred: function(callback) { + return (module.cache.occurred !== undefined) + ? module.cache.occurred[callback] || false + : false + ; + }, + direction: function() { + if(module.cache.direction === undefined) { + module.save.direction(); + } + return module.cache.direction; + }, + elementPosition: function() { + if(module.cache.element === undefined) { + module.save.elementPosition(); + } + return module.cache.element; + }, + elementCalculations: function() { + if(module.cache.element === undefined) { + module.save.elementCalculations(); + } + return module.cache.element; + }, + screenCalculations: function() { + if(module.cache.screen === undefined) { + module.save.screenCalculations(); + } + return module.cache.screen; + }, + screenSize: function() { + if(module.cache.screen === undefined) { + module.save.screenSize(); + } + return module.cache.screen; + }, + scroll: function() { + if(module.cache.scroll === undefined) { + module.save.scroll(); + } + return module.cache.scroll; + }, + lastScroll: function() { + if(module.cache.screen === undefined) { + module.debug('First scroll event, no last scroll could be found'); + return false; + } + return module.cache.screen.top; + } + }, + + setting: function(name, value) { + if( $.isPlainObject(name) ) { + $.extend(true, settings, name); + } + else if(value !== undefined) { + settings[name] = value; + } + else { + return settings[name]; + } + }, + internal: function(name, value) { + if( $.isPlainObject(name) ) { + $.extend(true, module, name); + } + else if(value !== undefined) { + module[name] = value; + } + else { + return module[name]; + } + }, + debug: function() { + if(!settings.silent && settings.debug) { + if(settings.performance) { + module.performance.log(arguments); + } + else { + module.debug = Function.prototype.bind.call(console.info, console, settings.name + ':'); + module.debug.apply(console, arguments); + } + } + }, + verbose: function() { + if(!settings.silent && settings.verbose && settings.debug) { + if(settings.performance) { + module.performance.log(arguments); + } + else { + module.verbose = Function.prototype.bind.call(console.info, console, settings.name + ':'); + module.verbose.apply(console, arguments); + } + } + }, + error: function() { + if(!settings.silent) { + module.error = Function.prototype.bind.call(console.error, console, settings.name + ':'); + module.error.apply(console, arguments); + } + }, + performance: { + log: function(message) { + var + currentTime, + executionTime, + previousTime + ; + if(settings.performance) { + currentTime = new Date().getTime(); + previousTime = time || currentTime; + executionTime = currentTime - previousTime; + time = currentTime; + performance.push({ + 'Name' : message[0], + 'Arguments' : [].slice.call(message, 1) || '', + 'Element' : element, + 'Execution Time' : executionTime + }); + } + clearTimeout(module.performance.timer); + module.performance.timer = setTimeout(module.performance.display, 500); + }, + display: function() { + var + title = settings.name + ':', + totalTime = 0 + ; + time = false; + clearTimeout(module.performance.timer); + $.each(performance, function(index, data) { + totalTime += data['Execution Time']; + }); + title += ' ' + totalTime + 'ms'; + if(moduleSelector) { + title += ' \'' + moduleSelector + '\''; + } + if( (console.group !== undefined || console.table !== undefined) && performance.length > 0) { + console.groupCollapsed(title); + if(console.table) { + console.table(performance); + } + else { + $.each(performance, function(index, data) { + console.log(data['Name'] + ': ' + data['Execution Time']+'ms'); + }); + } + console.groupEnd(); + } + performance = []; + } + }, + invoke: function(query, passedArguments, context) { + var + object = instance, + maxDepth, + found, + response + ; + passedArguments = passedArguments || queryArguments; + context = element || context; + if(typeof query == 'string' && object !== undefined) { + query = query.split(/[\. ]/); + maxDepth = query.length - 1; + $.each(query, function(depth, value) { + var camelCaseValue = (depth != maxDepth) + ? value + query[depth + 1].charAt(0).toUpperCase() + query[depth + 1].slice(1) + : query + ; + if( $.isPlainObject( object[camelCaseValue] ) && (depth != maxDepth) ) { + object = object[camelCaseValue]; + } + else if( object[camelCaseValue] !== undefined ) { + found = object[camelCaseValue]; + return false; + } + else if( $.isPlainObject( object[value] ) && (depth != maxDepth) ) { + object = object[value]; + } + else if( object[value] !== undefined ) { + found = object[value]; + return false; + } + else { + module.error(error.method, query); + return false; + } + }); + } + if ( $.isFunction( found ) ) { + response = found.apply(context, passedArguments); + } + else if(found !== undefined) { + response = found; + } + if($.isArray(returnedValue)) { + returnedValue.push(response); + } + else if(returnedValue !== undefined) { + returnedValue = [returnedValue, response]; + } + else if(response !== undefined) { + returnedValue = response; + } + return found; + } + }; + + if(methodInvoked) { + if(instance === undefined) { + module.initialize(); + } + instance.save.scroll(); + instance.save.calculations(); + module.invoke(query); + } + else { + if(instance !== undefined) { + instance.invoke('destroy'); + } + module.initialize(); + } + }) + ; + + return (returnedValue !== undefined) + ? returnedValue + : this + ; +}; + +$.fn.visibility.settings = { + + name : 'Visibility', + namespace : 'visibility', + + debug : false, + verbose : false, + performance : true, + + // whether to use mutation observers to follow changes + observeChanges : true, + + // check position immediately on init + initialCheck : true, + + // whether to refresh calculations after all page images load + refreshOnLoad : true, + + // whether to refresh calculations after page resize event + refreshOnResize : true, + + // should call callbacks on refresh event (resize, etc) + checkOnRefresh : true, + + // callback should only occur one time + once : true, + + // callback should fire continuously whe evaluates to true + continuous : false, + + // offset to use with scroll top + offset : 0, + + // whether to include margin in elements position + includeMargin : false, + + // scroll context for visibility checks + context : window, + + // visibility check delay in ms (defaults to animationFrame) + throttle : false, + + // special visibility type (image, fixed) + type : false, + + // z-index to use with visibility 'fixed' + zIndex : '10', + + // image only animation settings + transition : 'fade in', + duration : 1000, + + // array of callbacks for percentage + onPassed : {}, + + // standard callbacks + onOnScreen : false, + onOffScreen : false, + onPassing : false, + onTopVisible : false, + onBottomVisible : false, + onTopPassed : false, + onBottomPassed : false, + + // reverse callbacks + onPassingReverse : false, + onTopVisibleReverse : false, + onBottomVisibleReverse : false, + onTopPassedReverse : false, + onBottomPassedReverse : false, + + // special callbacks for image + onLoad : function() {}, + onAllLoaded : function() {}, + + // special callbacks for fixed position + onFixed : function() {}, + onUnfixed : function() {}, + + // utility callbacks + onUpdate : false, // disabled by default for performance + onRefresh : function(){}, + + metadata : { + src: 'src' + }, + + className: { + fixed : 'fixed', + placeholder : 'placeholder' + }, + + error : { + method : 'The method you called is not defined.', + visible : 'Element is hidden, you must call refresh after element becomes visible' + } + +}; + +})( jQuery, window, document ); diff --git a/src/semantic.min.js b/src/semantic.min.js new file mode 100644 index 0000000..a7481f7 --- /dev/null +++ b/src/semantic.min.js @@ -0,0 +1,19 @@ + /* + * # Semantic UI - 2.2.6 + * https://github.com/Semantic-Org/Semantic-UI + * http://www.semantic-ui.com/ + * + * Copyright 2014 Contributors + * Released under the MIT license + * http://opensource.org/licenses/MIT + * + */ +!function(e,t,n,i){e.site=e.fn.site=function(o){var a,r,s=(new Date).getTime(),l=[],c=arguments[0],u="string"==typeof c,d=[].slice.call(arguments,1),f=e.isPlainObject(o)?e.extend(!0,{},e.site.settings,o):e.extend({},e.site.settings),m=f.namespace,g=f.error,p="module-"+m,h=e(n),v=h,b=this,y=v.data(p);return a={initialize:function(){a.instantiate()},instantiate:function(){a.verbose("Storing instance of site",a),y=a,v.data(p,a)},normalize:function(){a.fix.console(),a.fix.requestAnimationFrame()},fix:{console:function(){a.debug("Normalizing window.console"),console!==i&&console.log!==i||(a.verbose("Console not available, normalizing events"),a.disable.console()),"undefined"!=typeof console.group&&"undefined"!=typeof console.groupEnd&&"undefined"!=typeof console.groupCollapsed||(a.verbose("Console group not available, normalizing events"),t.console.group=function(){},t.console.groupEnd=function(){},t.console.groupCollapsed=function(){}),"undefined"==typeof console.markTimeline&&(a.verbose("Mark timeline not available, normalizing events"),t.console.markTimeline=function(){})},consoleClear:function(){a.debug("Disabling programmatic console clearing"),t.console.clear=function(){}},requestAnimationFrame:function(){a.debug("Normalizing requestAnimationFrame"),t.requestAnimationFrame===i&&(a.debug("RequestAnimationFrame not available, normalizing event"),t.requestAnimationFrame=t.requestAnimationFrame||t.mozRequestAnimationFrame||t.webkitRequestAnimationFrame||t.msRequestAnimationFrame||function(e){setTimeout(e,0)})}},moduleExists:function(t){return e.fn[t]!==i&&e.fn[t].settings!==i},enabled:{modules:function(t){var n=[];return t=t||f.modules,e.each(t,function(e,t){a.moduleExists(t)&&n.push(t)}),n}},disabled:{modules:function(t){var n=[];return t=t||f.modules,e.each(t,function(e,t){a.moduleExists(t)||n.push(t)}),n}},change:{setting:function(t,n,o,r){o="string"==typeof o?"all"===o?f.modules:[o]:o||f.modules,r=r===i||r,e.each(o,function(i,o){var s,l=!a.moduleExists(o)||(e.fn[o].settings.namespace||!1);a.moduleExists(o)&&(a.verbose("Changing default setting",t,n,o),e.fn[o].settings[t]=n,r&&l&&(s=e(":data(module-"+l+")"),s.length>0&&(a.verbose("Modifying existing settings",s),s[o]("setting",t,n))))})},settings:function(t,n,o){n="string"==typeof n?[n]:n||f.modules,o=o===i||o,e.each(n,function(n,i){var r;a.moduleExists(i)&&(a.verbose("Changing default setting",t,i),e.extend(!0,e.fn[i].settings,t),o&&m&&(r=e(":data(module-"+m+")"),r.length>0&&(a.verbose("Modifying existing settings",r),r[i]("setting",t))))})}},enable:{console:function(){a.console(!0)},debug:function(e,t){e=e||f.modules,a.debug("Enabling debug for modules",e),a.change.setting("debug",!0,e,t)},verbose:function(e,t){e=e||f.modules,a.debug("Enabling verbose debug for modules",e),a.change.setting("verbose",!0,e,t)}},disable:{console:function(){a.console(!1)},debug:function(e,t){e=e||f.modules,a.debug("Disabling debug for modules",e),a.change.setting("debug",!1,e,t)},verbose:function(e,t){e=e||f.modules,a.debug("Disabling verbose debug for modules",e),a.change.setting("verbose",!1,e,t)}},console:function(e){if(e){if(y.cache.console===i)return void a.error(g.console);a.debug("Restoring console function"),t.console=y.cache.console}else a.debug("Disabling console function"),y.cache.console=t.console,t.console={clear:function(){},error:function(){},group:function(){},groupCollapsed:function(){},groupEnd:function(){},info:function(){},log:function(){},markTimeline:function(){},warn:function(){}}},destroy:function(){a.verbose("Destroying previous site for",v),v.removeData(p)},cache:{},setting:function(t,n){if(e.isPlainObject(t))e.extend(!0,f,t);else{if(n===i)return f[t];f[t]=n}},internal:function(t,n){if(e.isPlainObject(t))e.extend(!0,a,t);else{if(n===i)return a[t];a[t]=n}},debug:function(){f.debug&&(f.performance?a.performance.log(arguments):(a.debug=Function.prototype.bind.call(console.info,console,f.name+":"),a.debug.apply(console,arguments)))},verbose:function(){f.verbose&&f.debug&&(f.performance?a.performance.log(arguments):(a.verbose=Function.prototype.bind.call(console.info,console,f.name+":"),a.verbose.apply(console,arguments)))},error:function(){a.error=Function.prototype.bind.call(console.error,console,f.name+":"),a.error.apply(console,arguments)},performance:{log:function(e){var t,n,i;f.performance&&(t=(new Date).getTime(),i=s||t,n=t-i,s=t,l.push({Element:b,Name:e[0],Arguments:[].slice.call(e,1)||"","Execution Time":n})),clearTimeout(a.performance.timer),a.performance.timer=setTimeout(a.performance.display,500)},display:function(){var t=f.name+":",n=0;s=!1,clearTimeout(a.performance.timer),e.each(l,function(e,t){n+=t["Execution Time"]}),t+=" "+n+"ms",(console.group!==i||console.table!==i)&&l.length>0&&(console.groupCollapsed(t),console.table?console.table(l):e.each(l,function(e,t){console.log(t.Name+": "+t["Execution Time"]+"ms")}),console.groupEnd()),l=[]}},invoke:function(t,n,o){var s,l,c,u=y;return n=n||d,o=b||o,"string"==typeof t&&u!==i&&(t=t.split(/[\. ]/),s=t.length-1,e.each(t,function(n,o){var r=n!=s?o+t[n+1].charAt(0).toUpperCase()+t[n+1].slice(1):t;if(e.isPlainObject(u[r])&&n!=s)u=u[r];else{if(u[r]!==i)return l=u[r],!1;if(!e.isPlainObject(u[o])||n==s)return u[o]!==i?(l=u[o],!1):(a.error(g.method,t),!1);u=u[o]}})),e.isFunction(l)?c=l.apply(o,n):l!==i&&(c=l),e.isArray(r)?r.push(c):r!==i?r=[r,c]:c!==i&&(r=c),l}},u?(y===i&&a.initialize(),a.invoke(c)):(y!==i&&a.destroy(),a.initialize()),r!==i?r:this},e.site.settings={name:"Site",namespace:"site",error:{console:"Console cannot be restored, most likely it was overwritten outside of module",method:"The method you called is not defined."},debug:!1,verbose:!1,performance:!0,modules:["accordion","api","checkbox","dimmer","dropdown","embed","form","modal","nag","popup","rating","shape","sidebar","state","sticky","tab","transition","visit","visibility"],siteNamespace:"site",namespaceStub:{cache:{},config:{},sections:{},section:{},utilities:{}}},e.extend(e.expr[":"],{data:e.expr.createPseudo?e.expr.createPseudo(function(t){return function(n){return!!e.data(n,t)}}):function(t,n,i){return!!e.data(t,i[3])}})}(jQuery,window,document),function(e,t,n,i){"use strict";t="undefined"!=typeof t&&t.Math==Math?t:"undefined"!=typeof self&&self.Math==Math?self:Function("return this")(),e.fn.form=function(t){var o,a=e(this),r=a.selector||"",s=(new Date).getTime(),l=[],c=arguments[0],u=arguments[1],d="string"==typeof c,f=[].slice.call(arguments,1);return a.each(function(){var m,g,p,h,v,b,y,x,C,w,k,S,T,A,R,E,P,F,O=e(this),D=this,q=[],j=!1;F={initialize:function(){F.get.settings(),d?(P===i&&F.instantiate(),F.invoke(c)):(P!==i&&P.invoke("destroy"),F.verbose("Initializing form validation",O,x),F.bindEvents(),F.set.defaults(),F.instantiate())},instantiate:function(){F.verbose("Storing instance of module",F),P=F,O.data(R,F)},destroy:function(){F.verbose("Destroying previous module",P),F.removeEvents(),O.removeData(R)},refresh:function(){F.verbose("Refreshing selector cache"),m=O.find(k.field),g=O.find(k.group),p=O.find(k.message),h=O.find(k.prompt),v=O.find(k.submit),b=O.find(k.clear),y=O.find(k.reset)},submit:function(){F.verbose("Submitting form",O),O.submit()},attachEvents:function(t,n){n=n||"submit",e(t).on("click"+E,function(e){F[n](),e.preventDefault()})},bindEvents:function(){F.verbose("Attaching form events"),O.on("submit"+E,F.validate.form).on("blur"+E,k.field,F.event.field.blur).on("click"+E,k.submit,F.submit).on("click"+E,k.reset,F.reset).on("click"+E,k.clear,F.clear),x.keyboardShortcuts&&O.on("keydown"+E,k.field,F.event.field.keydown),m.each(function(){var t=e(this),n=t.prop("type"),i=F.get.changeEvent(n,t);e(this).on(i+E,F.event.field.change)})},clear:function(){m.each(function(){var t=e(this),n=t.parent(),i=t.closest(g),o=i.find(k.prompt),a=t.data(w.defaultValue)||"",r=n.is(k.uiCheckbox),s=n.is(k.uiDropdown),l=i.hasClass(S.error);l&&(F.verbose("Resetting error on field",i),i.removeClass(S.error),o.remove()),s?(F.verbose("Resetting dropdown value",n,a),n.dropdown("clear")):r?t.prop("checked",!1):(F.verbose("Resetting field value",t,a),t.val(""))})},reset:function(){m.each(function(){var t=e(this),n=t.parent(),o=t.closest(g),a=o.find(k.prompt),r=t.data(w.defaultValue),s=n.is(k.uiCheckbox),l=n.is(k.uiDropdown),c=o.hasClass(S.error);r!==i&&(c&&(F.verbose("Resetting error on field",o),o.removeClass(S.error),a.remove()),l?(F.verbose("Resetting dropdown value",n,r),n.dropdown("restore defaults")):s?(F.verbose("Resetting checkbox value",n,r),t.prop("checked",r)):(F.verbose("Resetting field value",t,r),t.val(r)))})},is:{bracketedRule:function(e){return e.type&&e.type.match(x.regExp.bracket)},empty:function(e){return!e||0===e.length||(e.is('input[type="checkbox"]')?!e.is(":checked"):F.is.blank(e))},blank:function(t){return""===e.trim(t.val())},valid:function(){var t=!0;return F.verbose("Checking if form is valid"),e.each(C,function(e,n){F.validate.field(n,e)||(t=!1)}),t}},removeEvents:function(){O.off(E),m.off(E),v.off(E),m.off(E)},event:{field:{keydown:function(t){var n=e(this),i=t.which,o=n.is(k.input),a=n.is(k.checkbox),r=n.closest(k.uiDropdown).length>0,s={enter:13,escape:27};i==s.escape&&(F.verbose("Escape key pressed blurring field"),n.blur()),t.ctrlKey||i!=s.enter||!o||r||a||(j||(n.one("keyup"+E,F.event.field.keyup),F.submit(),F.debug("Enter pressed on input submitting form")),j=!0)},keyup:function(){j=!1},blur:function(t){var n=e(this),i=n.closest(g),o=F.get.validation(n);i.hasClass(S.error)?(F.debug("Revalidating field",n,o),o&&F.validate.field(o)):"blur"!=x.on&&"change"!=x.on||o&&F.validate.field(o)},change:function(t){var n=e(this),i=n.closest(g),o=F.get.validation(n);("change"==x.on||i.hasClass(S.error)&&x.revalidate)&&(clearTimeout(F.timer),F.timer=setTimeout(function(){F.debug("Revalidating field",n,F.get.validation(n)),F.validate.field(o)},x.delay))}}},get:{ancillaryValue:function(e){return!(!e.type||!e.value&&!F.is.bracketedRule(e))&&(e.value!==i?e.value:e.type.match(x.regExp.bracket)[1]+"")},ruleName:function(e){return F.is.bracketedRule(e)?e.type.replace(e.type.match(x.regExp.bracket)[0],""):e.type},changeEvent:function(e,t){return"checkbox"==e||"radio"==e||"hidden"==e||t.is("select")?"change":F.get.inputEvent()},inputEvent:function(){return n.createElement("input").oninput!==i?"input":n.createElement("input").onpropertychange!==i?"propertychange":"keyup"},prompt:function(e,t){var n,i,o,a=F.get.ruleName(e),r=F.get.ancillaryValue(e),s=e.prompt||x.prompt[a]||x.text.unspecifiedRule,l=s.search("{value}")!==-1,c=s.search("{name}")!==-1;return(c||l)&&(i=F.get.field(t.identifier)),l&&(s=s.replace("{value}",i.val())),c&&(n=i.closest(k.group).find("label").eq(0),o=1==n.length?n.text():i.prop("placeholder")||x.text.unspecifiedField,s=s.replace("{name}",o)),s=s.replace("{identifier}",t.identifier),s=s.replace("{ruleValue}",r),e.prompt||F.verbose("Using default validation prompt for type",s,a),s},settings:function(){if(e.isPlainObject(t)){var n,o=Object.keys(t),a=o.length>0&&(t[o[0]].identifier!==i&&t[o[0]].rules!==i);a?(x=e.extend(!0,{},e.fn.form.settings,u),C=e.extend({},e.fn.form.settings.defaults,t),F.error(x.error.oldSyntax,D),F.verbose("Extending settings from legacy parameters",C,x)):(t.fields&&(n=Object.keys(t.fields),("string"==typeof t.fields[n[0]]||e.isArray(t.fields[n[0]]))&&e.each(t.fields,function(n,i){"string"==typeof i&&(i=[i]),t.fields[n]={rules:[]},e.each(i,function(e,i){t.fields[n].rules.push({type:i})})})),x=e.extend(!0,{},e.fn.form.settings,t),C=e.extend({},e.fn.form.settings.defaults,x.fields),F.verbose("Extending settings",C,x))}else x=e.fn.form.settings,C=e.fn.form.settings.defaults,F.verbose("Using default form validation",C,x);A=x.namespace,w=x.metadata,k=x.selector,S=x.className,T=x.error,R="module-"+A,E="."+A,P=O.data(R),F.refresh()},field:function(t){return F.verbose("Finding field with identifier",t),m.filter("#"+t).length>0?m.filter("#"+t):m.filter('[name="'+t+'"]').length>0?m.filter('[name="'+t+'"]'):m.filter('[name="'+t+'[]"]').length>0?m.filter('[name="'+t+'[]"]'):m.filter("[data-"+w.validate+'="'+t+'"]').length>0?m.filter("[data-"+w.validate+'="'+t+'"]'):e("<input/>")},fields:function(t){var n=e();return e.each(t,function(e,t){n=n.add(F.get.field(t))}),n},validation:function(t){var n,i;return!!C&&(e.each(C,function(e,o){i=o.identifier||e,F.get.field(i)[0]==t[0]&&(o.identifier=i,n=o)}),n||!1)},value:function(e){var t,n=[];return n.push(e),t=F.get.values.call(D,n),t[e]},values:function(t){var n=e.isArray(t)?F.get.fields(t):m,i={};return n.each(function(t,n){var o=e(n),a=(o.prop("type"),o.prop("name")),r=o.val(),s=o.is(k.checkbox),l=o.is(k.radio),c=a.indexOf("[]")!==-1,u=!!s&&o.is(":checked");a&&(c?(a=a.replace("[]",""),i[a]||(i[a]=[]),s?u?i[a].push(r||!0):i[a].push(!1):i[a].push(r)):l?u&&(i[a]=r):s?u?i[a]=r||!0:i[a]=!1:i[a]=r)}),i}},has:{field:function(e){return F.verbose("Checking for existence of a field with identifier",e),"string"!=typeof e&&F.error(T.identifier,e),m.filter("#"+e).length>0||(m.filter('[name="'+e+'"]').length>0||m.filter("[data-"+w.validate+'="'+e+'"]').length>0)}},add:{prompt:function(t,n){var o=F.get.field(t),a=o.closest(g),r=a.children(k.prompt),s=0!==r.length;n="string"==typeof n?[n]:n,F.verbose("Adding field error state",t),a.addClass(S.error),x.inline&&(s||(r=x.templates.prompt(n),r.appendTo(a)),r.html(n[0]),s?F.verbose("Inline errors are disabled, no inline error added",t):x.transition&&e.fn.transition!==i&&O.transition("is supported")?(F.verbose("Displaying error with css transition",x.transition),r.transition(x.transition+" in",x.duration)):(F.verbose("Displaying error with fallback javascript animation"),r.fadeIn(x.duration)))},errors:function(e){F.debug("Adding form error messages",e),F.set.error(),p.html(x.templates.error(e))}},remove:{prompt:function(t){var n=F.get.field(t),o=n.closest(g),a=o.children(k.prompt);o.removeClass(S.error),x.inline&&a.is(":visible")&&(F.verbose("Removing prompt for field",t),x.transition&&e.fn.transition!==i&&O.transition("is supported")?a.transition(x.transition+" out",x.duration,function(){a.remove()}):a.fadeOut(x.duration,function(){a.remove()}))}},set:{success:function(){O.removeClass(S.error).addClass(S.success)},defaults:function(){m.each(function(){var t=e(this),n=t.filter(k.checkbox).length>0,i=n?t.is(":checked"):t.val();t.data(w.defaultValue,i)})},error:function(){O.removeClass(S.success).addClass(S.error)},value:function(e,t){var n={};return n[e]=t,F.set.values.call(D,n)},values:function(t){e.isEmptyObject(t)||e.each(t,function(t,n){var i,o=F.get.field(t),a=o.parent(),r=e.isArray(n),s=a.is(k.uiCheckbox),l=a.is(k.uiDropdown),c=o.is(k.radio)&&s,u=o.length>0;u&&(r&&s?(F.verbose("Selecting multiple",n,o),a.checkbox("uncheck"),e.each(n,function(e,t){i=o.filter('[value="'+t+'"]'),a=i.parent(),i.length>0&&a.checkbox("check")})):c?(F.verbose("Selecting radio value",n,o),o.filter('[value="'+n+'"]').parent(k.uiCheckbox).checkbox("check")):s?(F.verbose("Setting checkbox value",n,a),n===!0?a.checkbox("check"):a.checkbox("uncheck")):l?(F.verbose("Setting dropdown value",n,a),a.dropdown("set selected",n)):(F.verbose("Setting field value",n,o),o.val(n)))})}},validate:{form:function(e,t){var n=F.get.values();if(j)return!1;if(q=[],F.is.valid()){if(F.debug("Form has no validation errors, submitting"),F.set.success(),t!==!0)return x.onSuccess.call(D,e,n)}else if(F.debug("Form has errors"),F.set.error(),x.inline||F.add.errors(q),O.data("moduleApi")!==i&&e.stopImmediatePropagation(),t!==!0)return x.onFailure.call(D,q,n)},field:function(t,n){var o=t.identifier||n,a=F.get.field(o),r=!!t.depends&&F.get.field(t.depends),s=!0,l=[];return t.identifier||(F.debug("Using field name as identifier",o),t.identifier=o),a.prop("disabled")?(F.debug("Field is disabled. Skipping",o),s=!0):t.optional&&F.is.blank(a)?(F.debug("Field is optional and blank. Skipping",o),s=!0):t.depends&&F.is.empty(r)?(F.debug("Field depends on another value that is not present or empty. Skipping",r),s=!0):t.rules!==i&&e.each(t.rules,function(e,n){F.has.field(o)&&!F.validate.rule(t,n)&&(F.debug("Field is invalid",o,n.type),l.push(F.get.prompt(n,t)),s=!1)}),s?(F.remove.prompt(o,l),x.onValid.call(a),!0):(q=q.concat(l),F.add.prompt(o,l),x.onInvalid.call(a,l),!1)},rule:function(t,n){var o=F.get.field(t.identifier),a=(n.type,o.val()),r=F.get.ancillaryValue(n),s=F.get.ruleName(n),l=x.rules[s];return e.isFunction(l)?(a=a===i||""===a||null===a?"":e.trim(a+""),l.call(o,a,r)):void F.error(T.noRule,s)}},setting:function(t,n){if(e.isPlainObject(t))e.extend(!0,x,t);else{if(n===i)return x[t];x[t]=n}},internal:function(t,n){if(e.isPlainObject(t))e.extend(!0,F,t);else{if(n===i)return F[t];F[t]=n}},debug:function(){!x.silent&&x.debug&&(x.performance?F.performance.log(arguments):(F.debug=Function.prototype.bind.call(console.info,console,x.name+":"),F.debug.apply(console,arguments)))},verbose:function(){!x.silent&&x.verbose&&x.debug&&(x.performance?F.performance.log(arguments):(F.verbose=Function.prototype.bind.call(console.info,console,x.name+":"),F.verbose.apply(console,arguments)))},error:function(){x.silent||(F.error=Function.prototype.bind.call(console.error,console,x.name+":"),F.error.apply(console,arguments))},performance:{log:function(e){var t,n,i;x.performance&&(t=(new Date).getTime(),i=s||t,n=t-i,s=t,l.push({Name:e[0],Arguments:[].slice.call(e,1)||"",Element:D,"Execution Time":n})),clearTimeout(F.performance.timer),F.performance.timer=setTimeout(F.performance.display,500)},display:function(){var t=x.name+":",n=0;s=!1,clearTimeout(F.performance.timer),e.each(l,function(e,t){n+=t["Execution Time"]}),t+=" "+n+"ms",r&&(t+=" '"+r+"'"),a.length>1&&(t+=" ("+a.length+")"),(console.group!==i||console.table!==i)&&l.length>0&&(console.groupCollapsed(t),console.table?console.table(l):e.each(l,function(e,t){console.log(t.Name+": "+t["Execution Time"]+"ms")}),console.groupEnd()),l=[]}},invoke:function(t,n,a){var r,s,l,c=P;return n=n||f,a=D||a,"string"==typeof t&&c!==i&&(t=t.split(/[\. ]/),r=t.length-1,e.each(t,function(n,o){var a=n!=r?o+t[n+1].charAt(0).toUpperCase()+t[n+1].slice(1):t;if(e.isPlainObject(c[a])&&n!=r)c=c[a];else{if(c[a]!==i)return s=c[a],!1;if(!e.isPlainObject(c[o])||n==r)return c[o]!==i&&(s=c[o],!1);c=c[o]}})),e.isFunction(s)?l=s.apply(a,n):s!==i&&(l=s),e.isArray(o)?o.push(l):o!==i?o=[o,l]:l!==i&&(o=l),s}},F.initialize()}),o!==i?o:this},e.fn.form.settings={name:"Form",namespace:"form",debug:!1,verbose:!1,performance:!0,fields:!1,keyboardShortcuts:!0,on:"submit",inline:!1,delay:200,revalidate:!0,transition:"scale",duration:200,onValid:function(){},onInvalid:function(){},onSuccess:function(){return!0},onFailure:function(){return!1},metadata:{defaultValue:"default",validate:"validate"},regExp:{bracket:/\[(.*)\]/i,decimal:/^\d*(\.)\d+/,email:/^[a-z0-9!#$%&'*+\/=?^_`{|}~.-]+@[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/i,escape:/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g,flags:/^\/(.*)\/(.*)?/,integer:/^\-?\d+$/,number:/^\-?\d*(\.\d+)?$/,url:/(https?:\/\/(?:www\.|(?!www))[^\s\.]+\.[^\s]{2,}|www\.[^\s]+\.[^\s]{2,})/i},text:{unspecifiedRule:"Please enter a valid value",unspecifiedField:"This field"},prompt:{empty:"{name} must have a value",checked:"{name} must be checked",email:"{name} must be a valid e-mail",url:"{name} must be a valid url",regExp:"{name} is not formatted correctly",integer:"{name} must be an integer",decimal:"{name} must be a decimal number",number:"{name} must be set to a number",is:'{name} must be "{ruleValue}"',isExactly:'{name} must be exactly "{ruleValue}"',not:'{name} cannot be set to "{ruleValue}"',notExactly:'{name} cannot be set to exactly "{ruleValue}"',contain:'{name} cannot contain "{ruleValue}"',containExactly:'{name} cannot contain exactly "{ruleValue}"',doesntContain:'{name} must contain "{ruleValue}"',doesntContainExactly:'{name} must contain exactly "{ruleValue}"',minLength:"{name} must be at least {ruleValue} characters",length:"{name} must be at least {ruleValue} characters",exactLength:"{name} must be exactly {ruleValue} characters",maxLength:"{name} cannot be longer than {ruleValue} characters",match:"{name} must match {ruleValue} field",different:"{name} must have a different value than {ruleValue} field",creditCard:"{name} must be a valid credit card number",minCount:"{name} must have at least {ruleValue} choices",exactCount:"{name} must have exactly {ruleValue} choices",maxCount:"{name} must have {ruleValue} or less choices"},selector:{checkbox:'input[type="checkbox"], input[type="radio"]',clear:".clear",field:"input, textarea, select",group:".field",input:"input",message:".error.message",prompt:".prompt.label",radio:'input[type="radio"]',reset:'.reset:not([type="reset"])',submit:'.submit:not([type="submit"])',uiCheckbox:".ui.checkbox",uiDropdown:".ui.dropdown"},className:{error:"error",label:"ui prompt label",pressed:"down",success:"success"},error:{identifier:"You must specify a string identifier for each field",method:"The method you called is not defined.",noRule:"There is no rule matching the one you specified",oldSyntax:"Starting in 2.0 forms now only take a single settings object. Validation settings converted to new syntax automatically."},templates:{error:function(t){var n='<ul class="list">';return e.each(t,function(e,t){n+="<li>"+t+"</li>"}),n+="</ul>",e(n)},prompt:function(t){return e("<div/>").addClass("ui basic red pointing prompt label").html(t[0])}},rules:{empty:function(t){return!(t===i||""===t||e.isArray(t)&&0===t.length)},checked:function(){return e(this).filter(":checked").length>0},email:function(t){return e.fn.form.settings.regExp.email.test(t)},url:function(t){return e.fn.form.settings.regExp.url.test(t)},regExp:function(t,n){if(n instanceof RegExp)return t.match(n);var i,o=n.match(e.fn.form.settings.regExp.flags);return o&&(n=o.length>=2?o[1]:n,i=o.length>=3?o[2]:""),t.match(new RegExp(n,i))},integer:function(t,n){var o,a,r,s=e.fn.form.settings.regExp.integer;return n&&["",".."].indexOf(n)===-1&&(n.indexOf("..")==-1?s.test(n)&&(o=a=n-0):(r=n.split("..",2),s.test(r[0])&&(o=r[0]-0),s.test(r[1])&&(a=r[1]-0))),s.test(t)&&(o===i||t>=o)&&(a===i||t<=a)},decimal:function(t){return e.fn.form.settings.regExp.decimal.test(t)},number:function(t){return e.fn.form.settings.regExp.number.test(t)},is:function(e,t){return t="string"==typeof t?t.toLowerCase():t,e="string"==typeof e?e.toLowerCase():e,e==t},isExactly:function(e,t){return e==t},not:function(e,t){return e="string"==typeof e?e.toLowerCase():e,t="string"==typeof t?t.toLowerCase():t,e!=t},notExactly:function(e,t){return e!=t},contains:function(t,n){return n=n.replace(e.fn.form.settings.regExp.escape,"\\$&"),t.search(new RegExp(n,"i"))!==-1},containsExactly:function(t,n){return n=n.replace(e.fn.form.settings.regExp.escape,"\\$&"),t.search(new RegExp(n))!==-1},doesntContain:function(t,n){return n=n.replace(e.fn.form.settings.regExp.escape,"\\$&"),t.search(new RegExp(n,"i"))===-1},doesntContainExactly:function(t,n){return n=n.replace(e.fn.form.settings.regExp.escape,"\\$&"),t.search(new RegExp(n))===-1},minLength:function(e,t){return e!==i&&e.length>=t},length:function(e,t){return e!==i&&e.length>=t},exactLength:function(e,t){return e!==i&&e.length==t},maxLength:function(e,t){return e!==i&&e.length<=t},match:function(t,n){var o;e(this);return e('[data-validate="'+n+'"]').length>0?o=e('[data-validate="'+n+'"]').val():e("#"+n).length>0?o=e("#"+n).val():e('[name="'+n+'"]').length>0?o=e('[name="'+n+'"]').val():e('[name="'+n+'[]"]').length>0&&(o=e('[name="'+n+'[]"]')),o!==i&&t.toString()==o.toString()},different:function(t,n){var o;e(this);return e('[data-validate="'+n+'"]').length>0?o=e('[data-validate="'+n+'"]').val():e("#"+n).length>0?o=e("#"+n).val():e('[name="'+n+'"]').length>0?o=e('[name="'+n+'"]').val():e('[name="'+n+'[]"]').length>0&&(o=e('[name="'+n+'[]"]')),o!==i&&t.toString()!==o.toString()},creditCard:function(t,n){var i,o,a={visa:{pattern:/^4/,length:[16]},amex:{pattern:/^3[47]/,length:[15]},mastercard:{pattern:/^5[1-5]/,length:[16]},discover:{pattern:/^(6011|622(12[6-9]|1[3-9][0-9]|[2-8][0-9]{2}|9[0-1][0-9]|92[0-5]|64[4-9])|65)/,length:[16]},unionPay:{pattern:/^(62|88)/,length:[16,17,18,19]},jcb:{pattern:/^35(2[89]|[3-8][0-9])/,length:[16]},maestro:{pattern:/^(5018|5020|5038|6304|6759|676[1-3])/,length:[12,13,14,15,16,17,18,19]},dinersClub:{pattern:/^(30[0-5]|^36)/,length:[14]},laser:{pattern:/^(6304|670[69]|6771)/,length:[16,17,18,19]},visaElectron:{pattern:/^(4026|417500|4508|4844|491(3|7))/,length:[16]}},r={},s=!1,l="string"==typeof n&&n.split(",");if("string"==typeof t&&0!==t.length){if(l&&(e.each(l,function(n,i){o=a[i],o&&(r={length:e.inArray(t.length,o.length)!==-1,pattern:t.search(o.pattern)!==-1},r.length&&r.pattern&&(s=!0))}),!s))return!1;if(i={number:e.inArray(t.length,a.unionPay.length)!==-1,pattern:t.search(a.unionPay.pattern)!==-1},i.number&&i.pattern)return!0;for(var c=t.length,u=0,d=[[0,1,2,3,4,5,6,7,8,9],[0,2,4,6,8,1,3,5,7,9]],f=0;c--;)f+=d[u][parseInt(t.charAt(c),10)],u^=1;return f%10===0&&f>0}},minCount:function(e,t){return 0==t||(1==t?""!==e:e.split(",").length>=t)},exactCount:function(e,t){return 0==t?""===e:1==t?""!==e&&e.search(",")===-1:e.split(",").length==t},maxCount:function(e,t){return 0!=t&&(1==t?e.search(",")===-1:e.split(",").length<=t)}}}}(jQuery,window,document),function(e,t,n,i){"use strict";t="undefined"!=typeof t&&t.Math==Math?t:"undefined"!=typeof self&&self.Math==Math?self:Function("return this")(),e.fn.accordion=function(n){var o,a=e(this),r=(new Date).getTime(),s=[],l=arguments[0],c="string"==typeof l,u=[].slice.call(arguments,1);t.requestAnimationFrame||t.mozRequestAnimationFrame||t.webkitRequestAnimationFrame||t.msRequestAnimationFrame||function(e){setTimeout(e,0)};return a.each(function(){var d,f,m=e.isPlainObject(n)?e.extend(!0,{},e.fn.accordion.settings,n):e.extend({},e.fn.accordion.settings),g=m.className,p=m.namespace,h=m.selector,v=m.error,b="."+p,y="module-"+p,x=a.selector||"",C=e(this),w=C.find(h.title),k=C.find(h.content),S=this,T=C.data(y);f={initialize:function(){f.debug("Initializing",C),f.bind.events(),m.observeChanges&&f.observeChanges(),f.instantiate()},instantiate:function(){T=f,C.data(y,f)},destroy:function(){f.debug("Destroying previous instance",C),C.off(b).removeData(y)},refresh:function(){w=C.find(h.title),k=C.find(h.content)},observeChanges:function(){"MutationObserver"in t&&(d=new MutationObserver(function(e){f.debug("DOM tree modified, updating selector cache"),f.refresh()}),d.observe(S,{childList:!0,subtree:!0}),f.debug("Setting up mutation observer",d))},bind:{events:function(){f.debug("Binding delegated events"),C.on(m.on+b,h.trigger,f.event.click)}},event:{click:function(){f.toggle.call(this)}},toggle:function(t){var n=t!==i?"number"==typeof t?w.eq(t):e(t).closest(h.title):e(this).closest(h.title),o=n.next(k),a=o.hasClass(g.animating),r=o.hasClass(g.active),s=r&&!a,l=!r&&a;f.debug("Toggling visibility of content",n),s||l?m.collapsible?f.close.call(n):f.debug("Cannot close accordion content collapsing is disabled"):f.open.call(n)},open:function(t){var n=t!==i?"number"==typeof t?w.eq(t):e(t).closest(h.title):e(this).closest(h.title),o=n.next(k),a=o.hasClass(g.animating),r=o.hasClass(g.active),s=r||a;return s?void f.debug("Accordion already open, skipping",o):(f.debug("Opening accordion content",n),m.onOpening.call(o),m.exclusive&&f.closeOthers.call(n),n.addClass(g.active),o.stop(!0,!0).addClass(g.animating),m.animateChildren&&(e.fn.transition!==i&&C.transition("is supported")?o.children().transition({animation:"fade in",queue:!1,useFailSafe:!0,debug:m.debug,verbose:m.verbose,duration:m.duration}):o.children().stop(!0,!0).animate({opacity:1},m.duration,f.resetOpacity)),void o.slideDown(m.duration,m.easing,function(){o.removeClass(g.animating).addClass(g.active),f.reset.display.call(this),m.onOpen.call(this),m.onChange.call(this)}))},close:function(t){var n=t!==i?"number"==typeof t?w.eq(t):e(t).closest(h.title):e(this).closest(h.title),o=n.next(k),a=o.hasClass(g.animating),r=o.hasClass(g.active),s=!r&&a,l=r&&a;!r&&!s||l||(f.debug("Closing accordion content",o),m.onClosing.call(o),n.removeClass(g.active),o.stop(!0,!0).addClass(g.animating),m.animateChildren&&(e.fn.transition!==i&&C.transition("is supported")?o.children().transition({animation:"fade out",queue:!1,useFailSafe:!0,debug:m.debug,verbose:m.verbose,duration:m.duration}):o.children().stop(!0,!0).animate({opacity:0},m.duration,f.resetOpacity)),o.slideUp(m.duration,m.easing,function(){o.removeClass(g.animating).removeClass(g.active),f.reset.display.call(this),m.onClose.call(this),m.onChange.call(this)}))},closeOthers:function(t){var n,o,a,r=t!==i?w.eq(t):e(this).closest(h.title),s=r.parents(h.content).prev(h.title),l=r.closest(h.accordion),c=h.title+"."+g.active+":visible",u=h.content+"."+g.active+":visible";m.closeNested?(n=l.find(c).not(s),a=n.next(k)):(n=l.find(c).not(s),o=l.find(u).find(c).not(s),n=n.not(o),a=n.next(k)),n.length>0&&(f.debug("Exclusive enabled, closing other content",n),n.removeClass(g.active),a.removeClass(g.animating).stop(!0,!0),m.animateChildren&&(e.fn.transition!==i&&C.transition("is supported")?a.children().transition({animation:"fade out",useFailSafe:!0,debug:m.debug,verbose:m.verbose,duration:m.duration}):a.children().stop(!0,!0).animate({opacity:0},m.duration,f.resetOpacity)),a.slideUp(m.duration,m.easing,function(){e(this).removeClass(g.active),f.reset.display.call(this)}))},reset:{display:function(){f.verbose("Removing inline display from element",this),e(this).css("display",""),""===e(this).attr("style")&&e(this).attr("style","").removeAttr("style")},opacity:function(){f.verbose("Removing inline opacity from element",this),e(this).css("opacity",""),""===e(this).attr("style")&&e(this).attr("style","").removeAttr("style")}},setting:function(t,n){if(f.debug("Changing setting",t,n),e.isPlainObject(t))e.extend(!0,m,t);else{if(n===i)return m[t];e.isPlainObject(m[t])?e.extend(!0,m[t],n):m[t]=n}},internal:function(t,n){return f.debug("Changing internal",t,n),n===i?f[t]:void(e.isPlainObject(t)?e.extend(!0,f,t):f[t]=n)},debug:function(){!m.silent&&m.debug&&(m.performance?f.performance.log(arguments):(f.debug=Function.prototype.bind.call(console.info,console,m.name+":"),f.debug.apply(console,arguments)))},verbose:function(){!m.silent&&m.verbose&&m.debug&&(m.performance?f.performance.log(arguments):(f.verbose=Function.prototype.bind.call(console.info,console,m.name+":"),f.verbose.apply(console,arguments)))},error:function(){m.silent||(f.error=Function.prototype.bind.call(console.error,console,m.name+":"),f.error.apply(console,arguments))},performance:{log:function(e){var t,n,i;m.performance&&(t=(new Date).getTime(),i=r||t,n=t-i,r=t,s.push({Name:e[0],Arguments:[].slice.call(e,1)||"",Element:S,"Execution Time":n})),clearTimeout(f.performance.timer),f.performance.timer=setTimeout(f.performance.display,500)},display:function(){var t=m.name+":",n=0;r=!1,clearTimeout(f.performance.timer),e.each(s,function(e,t){n+=t["Execution Time"]}),t+=" "+n+"ms",x&&(t+=" '"+x+"'"),(console.group!==i||console.table!==i)&&s.length>0&&(console.groupCollapsed(t),console.table?console.table(s):e.each(s,function(e,t){console.log(t.Name+": "+t["Execution Time"]+"ms")}),console.groupEnd()),s=[]}},invoke:function(t,n,a){var r,s,l,c=T;return n=n||u,a=S||a,"string"==typeof t&&c!==i&&(t=t.split(/[\. ]/),r=t.length-1,e.each(t,function(n,o){var a=n!=r?o+t[n+1].charAt(0).toUpperCase()+t[n+1].slice(1):t;if(e.isPlainObject(c[a])&&n!=r)c=c[a];else{if(c[a]!==i)return s=c[a],!1;if(!e.isPlainObject(c[o])||n==r)return c[o]!==i?(s=c[o],!1):(f.error(v.method,t),!1);c=c[o]}})),e.isFunction(s)?l=s.apply(a,n):s!==i&&(l=s),e.isArray(o)?o.push(l):o!==i?o=[o,l]:l!==i&&(o=l),s}},c?(T===i&&f.initialize(),f.invoke(l)):(T!==i&&T.invoke("destroy"),f.initialize())}),o!==i?o:this},e.fn.accordion.settings={name:"Accordion",namespace:"accordion",silent:!1,debug:!1,verbose:!1,performance:!0,on:"click",observeChanges:!0,exclusive:!0,collapsible:!0,closeNested:!1,animateChildren:!0,duration:350,easing:"easeOutQuad",onOpening:function(){},onOpen:function(){},onClosing:function(){}, +onClose:function(){},onChange:function(){},error:{method:"The method you called is not defined"},className:{active:"active",animating:"animating"},selector:{accordion:".accordion",title:".title",trigger:".title",content:".content"}},e.extend(e.easing,{easeOutQuad:function(e,t,n,i,o){return-i*(t/=o)*(t-2)+n}})}(jQuery,window,document),function(e,t,n,i){"use strict";t="undefined"!=typeof t&&t.Math==Math?t:"undefined"!=typeof self&&self.Math==Math?self:Function("return this")(),e.fn.checkbox=function(o){var a,r=e(this),s=r.selector||"",l=(new Date).getTime(),c=[],u=arguments[0],d="string"==typeof u,f=[].slice.call(arguments,1);return r.each(function(){var r,m,g=e.extend(!0,{},e.fn.checkbox.settings,o),p=g.className,h=g.namespace,v=g.selector,b=g.error,y="."+h,x="module-"+h,C=e(this),w=e(this).children(v.label),k=e(this).children(v.input),S=k[0],T=!1,A=!1,R=C.data(x),E=this;m={initialize:function(){m.verbose("Initializing checkbox",g),m.create.label(),m.bind.events(),m.set.tabbable(),m.hide.input(),m.observeChanges(),m.instantiate(),m.setup()},instantiate:function(){m.verbose("Storing instance of module",m),R=m,C.data(x,m)},destroy:function(){m.verbose("Destroying module"),m.unbind.events(),m.show.input(),C.removeData(x)},fix:{reference:function(){C.is(v.input)&&(m.debug("Behavior called on <input> adjusting invoked element"),C=C.closest(v.checkbox),m.refresh())}},setup:function(){m.set.initialLoad(),m.is.indeterminate()?(m.debug("Initial value is indeterminate"),m.indeterminate()):m.is.checked()?(m.debug("Initial value is checked"),m.check()):(m.debug("Initial value is unchecked"),m.uncheck()),m.remove.initialLoad()},refresh:function(){w=C.children(v.label),k=C.children(v.input),S=k[0]},hide:{input:function(){m.verbose("Modifying <input> z-index to be unselectable"),k.addClass(p.hidden)}},show:{input:function(){m.verbose("Modifying <input> z-index to be selectable"),k.removeClass(p.hidden)}},observeChanges:function(){"MutationObserver"in t&&(r=new MutationObserver(function(e){m.debug("DOM tree modified, updating selector cache"),m.refresh()}),r.observe(E,{childList:!0,subtree:!0}),m.debug("Setting up mutation observer",r))},attachEvents:function(t,n){var i=e(t);n=e.isFunction(m[n])?m[n]:m.toggle,i.length>0?(m.debug("Attaching checkbox events to element",t,n),i.on("click"+y,n)):m.error(b.notFound)},event:{click:function(t){var n=e(t.target);return n.is(v.input)?void m.verbose("Using default check action on initialized checkbox"):n.is(v.link)?void m.debug("Clicking link inside checkbox, skipping toggle"):(m.toggle(),k.focus(),void t.preventDefault())},keydown:function(e){var t=e.which,n={enter:13,space:32,escape:27};t==n.escape?(m.verbose("Escape key pressed blurring field"),k.blur(),A=!0):e.ctrlKey||t!=n.space&&t!=n.enter?A=!1:(m.verbose("Enter/space key pressed, toggling checkbox"),m.toggle(),A=!0)},keyup:function(e){A&&e.preventDefault()}},check:function(){m.should.allowCheck()&&(m.debug("Checking checkbox",k),m.set.checked(),m.should.ignoreCallbacks()||(g.onChecked.call(S),g.onChange.call(S)))},uncheck:function(){m.should.allowUncheck()&&(m.debug("Unchecking checkbox"),m.set.unchecked(),m.should.ignoreCallbacks()||(g.onUnchecked.call(S),g.onChange.call(S)))},indeterminate:function(){return m.should.allowIndeterminate()?void m.debug("Checkbox is already indeterminate"):(m.debug("Making checkbox indeterminate"),m.set.indeterminate(),void(m.should.ignoreCallbacks()||(g.onIndeterminate.call(S),g.onChange.call(S))))},determinate:function(){return m.should.allowDeterminate()?void m.debug("Checkbox is already determinate"):(m.debug("Making checkbox determinate"),m.set.determinate(),void(m.should.ignoreCallbacks()||(g.onDeterminate.call(S),g.onChange.call(S))))},enable:function(){return m.is.enabled()?void m.debug("Checkbox is already enabled"):(m.debug("Enabling checkbox"),m.set.enabled(),g.onEnable.call(S),void g.onEnabled.call(S))},disable:function(){return m.is.disabled()?void m.debug("Checkbox is already disabled"):(m.debug("Disabling checkbox"),m.set.disabled(),g.onDisable.call(S),void g.onDisabled.call(S))},get:{radios:function(){var t=m.get.name();return e('input[name="'+t+'"]').closest(v.checkbox)},otherRadios:function(){return m.get.radios().not(C)},name:function(){return k.attr("name")}},is:{initialLoad:function(){return T},radio:function(){return k.hasClass(p.radio)||"radio"==k.attr("type")},indeterminate:function(){return k.prop("indeterminate")!==i&&k.prop("indeterminate")},checked:function(){return k.prop("checked")!==i&&k.prop("checked")},disabled:function(){return k.prop("disabled")!==i&&k.prop("disabled")},enabled:function(){return!m.is.disabled()},determinate:function(){return!m.is.indeterminate()},unchecked:function(){return!m.is.checked()}},should:{allowCheck:function(){return m.is.determinate()&&m.is.checked()&&!m.should.forceCallbacks()?(m.debug("Should not allow check, checkbox is already checked"),!1):g.beforeChecked.apply(S)!==!1||(m.debug("Should not allow check, beforeChecked cancelled"),!1)},allowUncheck:function(){return m.is.determinate()&&m.is.unchecked()&&!m.should.forceCallbacks()?(m.debug("Should not allow uncheck, checkbox is already unchecked"),!1):g.beforeUnchecked.apply(S)!==!1||(m.debug("Should not allow uncheck, beforeUnchecked cancelled"),!1)},allowIndeterminate:function(){return m.is.indeterminate()&&!m.should.forceCallbacks()?(m.debug("Should not allow indeterminate, checkbox is already indeterminate"),!1):g.beforeIndeterminate.apply(S)!==!1||(m.debug("Should not allow indeterminate, beforeIndeterminate cancelled"),!1)},allowDeterminate:function(){return m.is.determinate()&&!m.should.forceCallbacks()?(m.debug("Should not allow determinate, checkbox is already determinate"),!1):g.beforeDeterminate.apply(S)!==!1||(m.debug("Should not allow determinate, beforeDeterminate cancelled"),!1)},forceCallbacks:function(){return m.is.initialLoad()&&g.fireOnInit},ignoreCallbacks:function(){return T&&!g.fireOnInit}},can:{change:function(){return!(C.hasClass(p.disabled)||C.hasClass(p.readOnly)||k.prop("disabled")||k.prop("readonly"))},uncheck:function(){return"boolean"==typeof g.uncheckable?g.uncheckable:!m.is.radio()}},set:{initialLoad:function(){T=!0},checked:function(){return m.verbose("Setting class to checked"),C.removeClass(p.indeterminate).addClass(p.checked),m.is.radio()&&m.uncheckOthers(),!m.is.indeterminate()&&m.is.checked()?void m.debug("Input is already checked, skipping input property change"):(m.verbose("Setting state to checked",S),k.prop("indeterminate",!1).prop("checked",!0),void m.trigger.change())},unchecked:function(){return m.verbose("Removing checked class"),C.removeClass(p.indeterminate).removeClass(p.checked),!m.is.indeterminate()&&m.is.unchecked()?void m.debug("Input is already unchecked"):(m.debug("Setting state to unchecked"),k.prop("indeterminate",!1).prop("checked",!1),void m.trigger.change())},indeterminate:function(){return m.verbose("Setting class to indeterminate"),C.addClass(p.indeterminate),m.is.indeterminate()?void m.debug("Input is already indeterminate, skipping input property change"):(m.debug("Setting state to indeterminate"),k.prop("indeterminate",!0),void m.trigger.change())},determinate:function(){return m.verbose("Removing indeterminate class"),C.removeClass(p.indeterminate),m.is.determinate()?void m.debug("Input is already determinate, skipping input property change"):(m.debug("Setting state to determinate"),void k.prop("indeterminate",!1))},disabled:function(){return m.verbose("Setting class to disabled"),C.addClass(p.disabled),m.is.disabled()?void m.debug("Input is already disabled, skipping input property change"):(m.debug("Setting state to disabled"),k.prop("disabled","disabled"),void m.trigger.change())},enabled:function(){return m.verbose("Removing disabled class"),C.removeClass(p.disabled),m.is.enabled()?void m.debug("Input is already enabled, skipping input property change"):(m.debug("Setting state to enabled"),k.prop("disabled",!1),void m.trigger.change())},tabbable:function(){m.verbose("Adding tabindex to checkbox"),k.attr("tabindex")===i&&k.attr("tabindex",0)}},remove:{initialLoad:function(){T=!1}},trigger:{change:function(){var e=n.createEvent("HTMLEvents"),t=k[0];t&&(m.verbose("Triggering native change event"),e.initEvent("change",!0,!1),t.dispatchEvent(e))}},create:{label:function(){k.prevAll(v.label).length>0?(k.prev(v.label).detach().insertAfter(k),m.debug("Moving existing label",w)):m.has.label()||(w=e("<label>").insertAfter(k),m.debug("Creating label",w))}},has:{label:function(){return w.length>0}},bind:{events:function(){m.verbose("Attaching checkbox events"),C.on("click"+y,m.event.click).on("keydown"+y,v.input,m.event.keydown).on("keyup"+y,v.input,m.event.keyup)}},unbind:{events:function(){m.debug("Removing events"),C.off(y)}},uncheckOthers:function(){var e=m.get.otherRadios();m.debug("Unchecking other radios",e),e.removeClass(p.checked)},toggle:function(){return m.can.change()?void(m.is.indeterminate()||m.is.unchecked()?(m.debug("Currently unchecked"),m.check()):m.is.checked()&&m.can.uncheck()&&(m.debug("Currently checked"),m.uncheck())):void(m.is.radio()||m.debug("Checkbox is read-only or disabled, ignoring toggle"))},setting:function(t,n){if(m.debug("Changing setting",t,n),e.isPlainObject(t))e.extend(!0,g,t);else{if(n===i)return g[t];e.isPlainObject(g[t])?e.extend(!0,g[t],n):g[t]=n}},internal:function(t,n){if(e.isPlainObject(t))e.extend(!0,m,t);else{if(n===i)return m[t];m[t]=n}},debug:function(){!g.silent&&g.debug&&(g.performance?m.performance.log(arguments):(m.debug=Function.prototype.bind.call(console.info,console,g.name+":"),m.debug.apply(console,arguments)))},verbose:function(){!g.silent&&g.verbose&&g.debug&&(g.performance?m.performance.log(arguments):(m.verbose=Function.prototype.bind.call(console.info,console,g.name+":"),m.verbose.apply(console,arguments)))},error:function(){g.silent||(m.error=Function.prototype.bind.call(console.error,console,g.name+":"),m.error.apply(console,arguments))},performance:{log:function(e){var t,n,i;g.performance&&(t=(new Date).getTime(),i=l||t,n=t-i,l=t,c.push({Name:e[0],Arguments:[].slice.call(e,1)||"",Element:E,"Execution Time":n})),clearTimeout(m.performance.timer),m.performance.timer=setTimeout(m.performance.display,500)},display:function(){var t=g.name+":",n=0;l=!1,clearTimeout(m.performance.timer),e.each(c,function(e,t){n+=t["Execution Time"]}),t+=" "+n+"ms",s&&(t+=" '"+s+"'"),(console.group!==i||console.table!==i)&&c.length>0&&(console.groupCollapsed(t),console.table?console.table(c):e.each(c,function(e,t){console.log(t.Name+": "+t["Execution Time"]+"ms")}),console.groupEnd()),c=[]}},invoke:function(t,n,o){var r,s,l,c=R;return n=n||f,o=E||o,"string"==typeof t&&c!==i&&(t=t.split(/[\. ]/),r=t.length-1,e.each(t,function(n,o){var a=n!=r?o+t[n+1].charAt(0).toUpperCase()+t[n+1].slice(1):t;if(e.isPlainObject(c[a])&&n!=r)c=c[a];else{if(c[a]!==i)return s=c[a],!1;if(!e.isPlainObject(c[o])||n==r)return c[o]!==i?(s=c[o],!1):(m.error(b.method,t),!1);c=c[o]}})),e.isFunction(s)?l=s.apply(o,n):s!==i&&(l=s),e.isArray(a)?a.push(l):a!==i?a=[a,l]:l!==i&&(a=l),s}},d?(R===i&&m.initialize(),m.invoke(u)):(R!==i&&R.invoke("destroy"),m.initialize())}),a!==i?a:this},e.fn.checkbox.settings={name:"Checkbox",namespace:"checkbox",silent:!1,debug:!1,verbose:!0,performance:!0,uncheckable:"auto",fireOnInit:!1,onChange:function(){},beforeChecked:function(){},beforeUnchecked:function(){},beforeDeterminate:function(){},beforeIndeterminate:function(){},onChecked:function(){},onUnchecked:function(){},onDeterminate:function(){},onIndeterminate:function(){},onEnable:function(){},onDisable:function(){},onEnabled:function(){},onDisabled:function(){},className:{checked:"checked",indeterminate:"indeterminate",disabled:"disabled",hidden:"hidden",radio:"radio",readOnly:"read-only"},error:{method:"The method you called is not defined"},selector:{checkbox:".ui.checkbox",label:"label, .box",input:'input[type="checkbox"], input[type="radio"]',link:"a[href]"}}}(jQuery,window,document),function(e,t,n,i){"use strict";t="undefined"!=typeof t&&t.Math==Math?t:"undefined"!=typeof self&&self.Math==Math?self:Function("return this")(),e.fn.dimmer=function(t){var o,a=e(this),r=(new Date).getTime(),s=[],l=arguments[0],c="string"==typeof l,u=[].slice.call(arguments,1);return a.each(function(){var d,f,m,g=e.isPlainObject(t)?e.extend(!0,{},e.fn.dimmer.settings,t):e.extend({},e.fn.dimmer.settings),p=g.selector,h=g.namespace,v=g.className,b=g.error,y="."+h,x="module-"+h,C=a.selector||"",w="ontouchstart"in n.documentElement?"touchstart":"click",k=e(this),S=this,T=k.data(x);m={preinitialize:function(){m.is.dimmer()?(f=k.parent(),d=k):(f=k,d=m.has.dimmer()?g.dimmerName?f.find(p.dimmer).filter("."+g.dimmerName):f.find(p.dimmer):m.create(),m.set.variation())},initialize:function(){m.debug("Initializing dimmer",g),m.bind.events(),m.set.dimmable(),m.instantiate()},instantiate:function(){m.verbose("Storing instance of module",m),T=m,k.data(x,T)},destroy:function(){m.verbose("Destroying previous module",d),m.unbind.events(),m.remove.variation(),f.off(y)},bind:{events:function(){"hover"==g.on?f.on("mouseenter"+y,m.show).on("mouseleave"+y,m.hide):"click"==g.on&&f.on(w+y,m.toggle),m.is.page()&&(m.debug("Setting as a page dimmer",f),m.set.pageDimmer()),m.is.closable()&&(m.verbose("Adding dimmer close event",d),f.on(w+y,p.dimmer,m.event.click))}},unbind:{events:function(){k.removeData(x),f.off(y)}},event:{click:function(t){m.verbose("Determining if event occured on dimmer",t),(0===d.find(t.target).length||e(t.target).is(p.content))&&(m.hide(),t.stopImmediatePropagation())}},addContent:function(t){var n=e(t);m.debug("Add content to dimmer",n),n.parent()[0]!==d[0]&&n.detach().appendTo(d)},create:function(){var t=e(g.template.dimmer());return g.dimmerName&&(m.debug("Creating named dimmer",g.dimmerName),t.addClass(g.dimmerName)),t.appendTo(f),t},show:function(t){t=e.isFunction(t)?t:function(){},m.debug("Showing dimmer",d,g),m.is.dimmed()&&!m.is.animating()||!m.is.enabled()?m.debug("Dimmer is already shown or disabled"):(m.animate.show(t),g.onShow.call(S),g.onChange.call(S))},hide:function(t){t=e.isFunction(t)?t:function(){},m.is.dimmed()||m.is.animating()?(m.debug("Hiding dimmer",d),m.animate.hide(t),g.onHide.call(S),g.onChange.call(S)):m.debug("Dimmer is not visible")},toggle:function(){m.verbose("Toggling dimmer visibility",d),m.is.dimmed()?m.hide():m.show()},animate:{show:function(t){t=e.isFunction(t)?t:function(){},g.useCSS&&e.fn.transition!==i&&d.transition("is supported")?("auto"!==g.opacity&&m.set.opacity(),d.transition({animation:g.transition+" in",queue:!1,duration:m.get.duration(),useFailSafe:!0,onStart:function(){m.set.dimmed()},onComplete:function(){m.set.active(),t()}})):(m.verbose("Showing dimmer animation with javascript"),m.set.dimmed(),"auto"==g.opacity&&(g.opacity=.8),d.stop().css({opacity:0,width:"100%",height:"100%"}).fadeTo(m.get.duration(),g.opacity,function(){d.removeAttr("style"),m.set.active(),t()}))},hide:function(t){t=e.isFunction(t)?t:function(){},g.useCSS&&e.fn.transition!==i&&d.transition("is supported")?(m.verbose("Hiding dimmer with css"),d.transition({animation:g.transition+" out",queue:!1,duration:m.get.duration(),useFailSafe:!0,onStart:function(){m.remove.dimmed()},onComplete:function(){m.remove.active(),t()}})):(m.verbose("Hiding dimmer with javascript"),m.remove.dimmed(),d.stop().fadeOut(m.get.duration(),function(){m.remove.active(),d.removeAttr("style"),t()}))}},get:{dimmer:function(){return d},duration:function(){return"object"==typeof g.duration?m.is.active()?g.duration.hide:g.duration.show:g.duration}},has:{dimmer:function(){return g.dimmerName?k.find(p.dimmer).filter("."+g.dimmerName).length>0:k.find(p.dimmer).length>0}},is:{active:function(){return d.hasClass(v.active)},animating:function(){return d.is(":animated")||d.hasClass(v.animating)},closable:function(){return"auto"==g.closable?"hover"!=g.on:g.closable},dimmer:function(){return k.hasClass(v.dimmer)},dimmable:function(){return k.hasClass(v.dimmable)},dimmed:function(){return f.hasClass(v.dimmed)},disabled:function(){return f.hasClass(v.disabled)},enabled:function(){return!m.is.disabled()},page:function(){return f.is("body")},pageDimmer:function(){return d.hasClass(v.pageDimmer)}},can:{show:function(){return!d.hasClass(v.disabled)}},set:{opacity:function(e){var t=d.css("background-color"),n=t.split(","),i=n&&3==n.length,o=n&&4==n.length;e=0===g.opacity?0:g.opacity||e,i||o?(n[3]=e+")",t=n.join(",")):t="rgba(0, 0, 0, "+e+")",m.debug("Setting opacity to",e),d.css("background-color",t)},active:function(){d.addClass(v.active)},dimmable:function(){f.addClass(v.dimmable)},dimmed:function(){f.addClass(v.dimmed)},pageDimmer:function(){d.addClass(v.pageDimmer)},disabled:function(){d.addClass(v.disabled)},variation:function(e){e=e||g.variation,e&&d.addClass(e)}},remove:{active:function(){d.removeClass(v.active)},dimmed:function(){f.removeClass(v.dimmed)},disabled:function(){d.removeClass(v.disabled)},variation:function(e){e=e||g.variation,e&&d.removeClass(e)}},setting:function(t,n){if(m.debug("Changing setting",t,n),e.isPlainObject(t))e.extend(!0,g,t);else{if(n===i)return g[t];e.isPlainObject(g[t])?e.extend(!0,g[t],n):g[t]=n}},internal:function(t,n){if(e.isPlainObject(t))e.extend(!0,m,t);else{if(n===i)return m[t];m[t]=n}},debug:function(){!g.silent&&g.debug&&(g.performance?m.performance.log(arguments):(m.debug=Function.prototype.bind.call(console.info,console,g.name+":"),m.debug.apply(console,arguments)))},verbose:function(){!g.silent&&g.verbose&&g.debug&&(g.performance?m.performance.log(arguments):(m.verbose=Function.prototype.bind.call(console.info,console,g.name+":"),m.verbose.apply(console,arguments)))},error:function(){g.silent||(m.error=Function.prototype.bind.call(console.error,console,g.name+":"),m.error.apply(console,arguments))},performance:{log:function(e){var t,n,i;g.performance&&(t=(new Date).getTime(),i=r||t,n=t-i,r=t,s.push({Name:e[0],Arguments:[].slice.call(e,1)||"",Element:S,"Execution Time":n})),clearTimeout(m.performance.timer),m.performance.timer=setTimeout(m.performance.display,500)},display:function(){var t=g.name+":",n=0;r=!1,clearTimeout(m.performance.timer),e.each(s,function(e,t){n+=t["Execution Time"]}),t+=" "+n+"ms",C&&(t+=" '"+C+"'"),a.length>1&&(t+=" ("+a.length+")"),(console.group!==i||console.table!==i)&&s.length>0&&(console.groupCollapsed(t),console.table?console.table(s):e.each(s,function(e,t){console.log(t.Name+": "+t["Execution Time"]+"ms")}),console.groupEnd()),s=[]}},invoke:function(t,n,a){var r,s,l,c=T;return n=n||u,a=S||a,"string"==typeof t&&c!==i&&(t=t.split(/[\. ]/),r=t.length-1,e.each(t,function(n,o){var a=n!=r?o+t[n+1].charAt(0).toUpperCase()+t[n+1].slice(1):t;if(e.isPlainObject(c[a])&&n!=r)c=c[a];else{if(c[a]!==i)return s=c[a],!1;if(!e.isPlainObject(c[o])||n==r)return c[o]!==i?(s=c[o],!1):(m.error(b.method,t),!1);c=c[o]}})),e.isFunction(s)?l=s.apply(a,n):s!==i&&(l=s),e.isArray(o)?o.push(l):o!==i?o=[o,l]:l!==i&&(o=l),s}},m.preinitialize(),c?(T===i&&m.initialize(),m.invoke(l)):(T!==i&&T.invoke("destroy"),m.initialize())}),o!==i?o:this},e.fn.dimmer.settings={name:"Dimmer",namespace:"dimmer",silent:!1,debug:!1,verbose:!1,performance:!0,dimmerName:!1,variation:!1,closable:"auto",useCSS:!0,transition:"fade",on:!1,opacity:"auto",duration:{show:500,hide:500},onChange:function(){},onShow:function(){},onHide:function(){},error:{method:"The method you called is not defined."},className:{active:"active",animating:"animating",dimmable:"dimmable",dimmed:"dimmed",dimmer:"dimmer",disabled:"disabled",hide:"hide",pageDimmer:"page",show:"show"},selector:{dimmer:"> .ui.dimmer",content:".ui.dimmer > .content, .ui.dimmer > .content > .center"},template:{dimmer:function(){return e("<div />").attr("class","ui dimmer")}}}}(jQuery,window,document),function(e,t,n,i){"use strict";t="undefined"!=typeof t&&t.Math==Math?t:"undefined"!=typeof self&&self.Math==Math?self:Function("return this")(),e.fn.dropdown=function(o){var a,r=e(this),s=e(n),l=r.selector||"",c="ontouchstart"in n.documentElement,u=(new Date).getTime(),d=[],f=arguments[0],m="string"==typeof f,g=[].slice.call(arguments,1);return r.each(function(p){var h,v,b,y,x,C,w,k,S=e.isPlainObject(o)?e.extend(!0,{},e.fn.dropdown.settings,o):e.extend({},e.fn.dropdown.settings),T=S.className,A=S.message,R=S.fields,E=S.keys,P=S.metadata,F=S.namespace,O=S.regExp,D=S.selector,q=S.error,j=S.templates,z="."+F,M="module-"+F,I=e(this),L=e(S.context),N=I.find(D.text),V=I.find(D.search),H=I.find(D.sizer),U=I.find(D.input),W=I.find(D.icon),B=I.prev().find(D.text).length>0?I.prev().find(D.text):I.prev(),Q=I.children(D.menu),X=Q.find(D.item),$=!1,Y=!1,K=!1,Z=this,J=I.data(M);k={initialize:function(){k.debug("Initializing dropdown",S),k.is.alreadySetup()?k.setup.reference():(k.setup.layout(),k.refreshData(),k.save.defaults(),k.restore.selected(),k.create.id(),k.bind.events(),k.observeChanges(),k.instantiate())},instantiate:function(){k.verbose("Storing instance of dropdown",k),J=k,I.data(M,k)},destroy:function(){k.verbose("Destroying previous dropdown",I),k.remove.tabbable(),I.off(z).removeData(M),Q.off(z),s.off(y),k.disconnect.menuObserver(),k.disconnect.selectObserver()},observeChanges:function(){"MutationObserver"in t&&(C=new MutationObserver(k.event.select.mutation),w=new MutationObserver(k.event.menu.mutation),k.debug("Setting up mutation observer",C,w),k.observe.select(),k.observe.menu())},disconnect:{menuObserver:function(){w&&w.disconnect()},selectObserver:function(){C&&C.disconnect()}},observe:{select:function(){k.has.input()&&C.observe(U[0],{childList:!0,subtree:!0})},menu:function(){k.has.menu()&&w.observe(Q[0],{childList:!0,subtree:!0})}},create:{id:function(){x=(Math.random().toString(16)+"000000000").substr(2,8),y="."+x,k.verbose("Creating unique id for element",x)},userChoice:function(t){var n,o,a;return!!(t=t||k.get.userValues())&&(t=e.isArray(t)?t:[t],e.each(t,function(t,r){k.get.item(r)===!1&&(a=S.templates.addition(k.add.variables(A.addResult,r)),o=e("<div />").html(a).attr("data-"+P.value,r).attr("data-"+P.text,r).addClass(T.addition).addClass(T.item),S.hideAdditions&&o.addClass(T.hidden),n=n===i?o:n.add(o),k.verbose("Creating user choices for value",r,o))}),n)},userLabels:function(t){var n=k.get.userValues();n&&(k.debug("Adding user labels",n),e.each(n,function(e,t){k.verbose("Adding custom user value"),k.add.label(t,t)}))},menu:function(){Q=e("<div />").addClass(T.menu).appendTo(I)},sizer:function(){H=e("<span />").addClass(T.sizer).insertAfter(V)}},search:function(e){e=e!==i?e:k.get.query(),k.verbose("Searching for query",e),k.has.minCharacters(e)?k.filter(e):k.hide()},select:{firstUnfiltered:function(){k.verbose("Selecting first non-filtered element"),k.remove.selectedItem(),X.not(D.unselectable).not(D.addition+D.hidden).eq(0).addClass(T.selected)},nextAvailable:function(e){e=e.eq(0);var t=e.nextAll(D.item).not(D.unselectable).eq(0),n=e.prevAll(D.item).not(D.unselectable).eq(0),i=t.length>0;i?(k.verbose("Moving selection to",t),t.addClass(T.selected)):(k.verbose("Moving selection to",n),n.addClass(T.selected))}},setup:{api:function(){var e={debug:S.debug,urlData:{value:k.get.value(),query:k.get.query()},on:!1};k.verbose("First request, initializing API"),I.api(e)},layout:function(){I.is("select")&&(k.setup.select(),k.setup.returnedObject()),k.has.menu()||k.create.menu(),k.is.search()&&!k.has.search()&&(k.verbose("Adding search input"),V=e("<input />").addClass(T.search).prop("autocomplete","off").insertBefore(N)),k.is.multiple()&&k.is.searchSelection()&&!k.has.sizer()&&k.create.sizer(),S.allowTab&&k.set.tabbable()},select:function(){var t=k.get.selectValues();k.debug("Dropdown initialized on a select",t),I.is("select")&&(U=I),U.parent(D.dropdown).length>0?(k.debug("UI dropdown already exists. Creating dropdown menu only"),I=U.closest(D.dropdown),k.has.menu()||k.create.menu(),Q=I.children(D.menu),k.setup.menu(t)):(k.debug("Creating entire dropdown from select"),I=e("<div />").attr("class",U.attr("class")).addClass(T.selection).addClass(T.dropdown).html(j.dropdown(t)).insertBefore(U),U.hasClass(T.multiple)&&U.prop("multiple")===!1&&(k.error(q.missingMultiple),U.prop("multiple",!0)),U.is("[multiple]")&&k.set.multiple(),U.prop("disabled")&&(k.debug("Disabling dropdown"),I.addClass(T.disabled)),U.removeAttr("class").detach().prependTo(I)),k.refresh()},menu:function(e){Q.html(j.menu(e,R)),X=Q.find(D.item)},reference:function(){k.debug("Dropdown behavior was called on select, replacing with closest dropdown"),I=I.parent(D.dropdown),k.refresh(),k.setup.returnedObject(),m&&(J=k,k.invoke(f))},returnedObject:function(){var e=r.slice(0,p),t=r.slice(p+1);r=e.add(I).add(t)}},refresh:function(){k.refreshSelectors(),k.refreshData()},refreshItems:function(){X=Q.find(D.item)},refreshSelectors:function(){k.verbose("Refreshing selector cache"),N=I.find(D.text),V=I.find(D.search),U=I.find(D.input),W=I.find(D.icon),B=I.prev().find(D.text).length>0?I.prev().find(D.text):I.prev(),Q=I.children(D.menu),X=Q.find(D.item)},refreshData:function(){k.verbose("Refreshing cached metadata"),X.removeData(P.text).removeData(P.value)},clearData:function(){k.verbose("Clearing metadata"),X.removeData(P.text).removeData(P.value),I.removeData(P.defaultText).removeData(P.defaultValue).removeData(P.placeholderText)},toggle:function(){k.verbose("Toggling menu visibility"),k.is.active()?k.hide():k.show()},show:function(t){if(t=e.isFunction(t)?t:function(){},k.can.show()&&!k.is.active()){if(k.debug("Showing dropdown"),!k.has.message()||k.has.maxSelections()||k.has.allResultsFiltered()||k.remove.message(),k.is.allFiltered())return!0;S.onShow.call(Z)!==!1&&k.animate.show(function(){k.can.click()&&k.bind.intent(),k.has.menuSearch()&&k.focusSearch(),k.set.visible(),t.call(Z)})}},hide:function(t){t=e.isFunction(t)?t:function(){},k.is.active()&&(k.debug("Hiding dropdown"),S.onHide.call(Z)!==!1&&k.animate.hide(function(){k.remove.visible(),t.call(Z)}))},hideOthers:function(){k.verbose("Finding other dropdowns to hide"),r.not(I).has(D.menu+"."+T.visible).dropdown("hide")},hideMenu:function(){k.verbose("Hiding menu instantaneously"),k.remove.active(),k.remove.visible(),Q.transition("hide")},hideSubMenus:function(){var e=Q.children(D.item).find(D.menu);k.verbose("Hiding sub menus",e),e.transition("hide")},bind:{events:function(){c&&k.bind.touchEvents(),k.bind.keyboardEvents(),k.bind.inputEvents(),k.bind.mouseEvents()},touchEvents:function(){k.debug("Touch device detected binding additional touch events"),k.is.searchSelection()||k.is.single()&&I.on("touchstart"+z,k.event.test.toggle),Q.on("touchstart"+z,D.item,k.event.item.mouseenter)},keyboardEvents:function(){k.verbose("Binding keyboard events"),I.on("keydown"+z,k.event.keydown),k.has.search()&&I.on(k.get.inputEvent()+z,D.search,k.event.input),k.is.multiple()&&s.on("keydown"+y,k.event.document.keydown)},inputEvents:function(){k.verbose("Binding input change events"),I.on("change"+z,D.input,k.event.change)},mouseEvents:function(){k.verbose("Binding mouse events"),k.is.multiple()&&I.on("click"+z,D.label,k.event.label.click).on("click"+z,D.remove,k.event.remove.click),k.is.searchSelection()?(I.on("mousedown"+z,k.event.mousedown).on("mouseup"+z,k.event.mouseup).on("mousedown"+z,D.menu,k.event.menu.mousedown).on("mouseup"+z,D.menu,k.event.menu.mouseup).on("click"+z,D.icon,k.event.icon.click).on("focus"+z,D.search,k.event.search.focus).on("click"+z,D.search,k.event.search.focus).on("blur"+z,D.search,k.event.search.blur).on("click"+z,D.text,k.event.text.focus),k.is.multiple()&&I.on("click"+z,k.event.click)):("click"==S.on?I.on("click"+z,D.icon,k.event.icon.click).on("click"+z,k.event.test.toggle):"hover"==S.on?I.on("mouseenter"+z,k.delay.show).on("mouseleave"+z,k.delay.hide):I.on(S.on+z,k.toggle),I.on("mousedown"+z,k.event.mousedown).on("mouseup"+z,k.event.mouseup).on("focus"+z,k.event.focus).on("blur"+z,k.event.blur)),Q.on("mouseenter"+z,D.item,k.event.item.mouseenter).on("mouseleave"+z,D.item,k.event.item.mouseleave).on("click"+z,D.item,k.event.item.click)},intent:function(){k.verbose("Binding hide intent event to document"),c&&s.on("touchstart"+y,k.event.test.touch).on("touchmove"+y,k.event.test.touch),s.on("click"+y,k.event.test.hide)}},unbind:{intent:function(){k.verbose("Removing hide intent event from document"),c&&s.off("touchstart"+y).off("touchmove"+y),s.off("click"+y)}},filter:function(e){var t=e!==i?e:k.get.query(),n=function(){k.is.multiple()&&k.filterActive(),k.select.firstUnfiltered(),k.has.allResultsFiltered()?S.onNoResults.call(Z,t)?S.allowAdditions?S.hideAdditions&&(k.verbose("User addition with no menu, setting empty style"),k.set.empty(),k.hideMenu()):(k.verbose("All items filtered, showing message",t),k.add.message(A.noResults)):(k.verbose("All items filtered, hiding dropdown",t),k.hideMenu()):(k.remove.empty(),k.remove.message()),S.allowAdditions&&k.add.userSuggestion(e),k.is.searchSelection()&&k.can.show()&&k.is.focusedOnSearch()&&k.show()};S.useLabels&&k.has.maxSelections()||(S.apiSettings?k.can.useAPI()?k.queryRemote(t,function(){n()}):k.error(q.noAPI):(k.filterItems(t),n()))},queryRemote:function(t,n){var i={errorDuration:!1,cache:"local",throttle:S.throttle,urlData:{query:t},onError:function(){k.add.message(A.serverError),n()},onFailure:function(){k.add.message(A.serverError),n()},onSuccess:function(e){k.remove.message(),k.setup.menu({values:e[R.remoteValues]}),n()}};I.api("get request")||k.setup.api(),i=e.extend(!0,{},i,S.apiSettings),I.api("setting",i).api("query")},filterItems:function(t){var n=t!==i?t:k.get.query(),o=null,a=k.escape.regExp(n),r=new RegExp("^"+a,"igm");k.has.query()&&(o=[],k.verbose("Searching for matching values",n),X.each(function(){var t,i,a=e(this);if("both"==S.match||"text"==S.match){if(t=String(k.get.choiceText(a,!1)),t.search(r)!==-1)return o.push(this),!0;if("exact"===S.fullTextSearch&&k.exactSearch(n,t))return o.push(this),!0;if(S.fullTextSearch===!0&&k.fuzzySearch(n,t))return o.push(this),!0}if("both"==S.match||"value"==S.match){if(i=String(k.get.choiceValue(a,t)),i.search(r)!==-1)return o.push(this),!0;if(S.fullTextSearch&&k.fuzzySearch(n,i))return o.push(this),!0}})),k.debug("Showing only matched items",n),k.remove.filteredItem(),o&&X.not(o).addClass(T.filtered)},fuzzySearch:function(e,t){var n=t.length,i=e.length;if(e=e.toLowerCase(),t=t.toLowerCase(),i>n)return!1;if(i===n)return e===t;e:for(var o=0,a=0;o<i;o++){for(var r=e.charCodeAt(o);a<n;)if(t.charCodeAt(a++)===r)continue e;return!1}return!0},exactSearch:function(e,t){return e=e.toLowerCase(),t=t.toLowerCase(),t.indexOf(e)>-1},filterActive:function(){S.useLabels&&X.filter("."+T.active).addClass(T.filtered)},focusSearch:function(e){k.has.search()&&!k.is.focusedOnSearch()&&(e?(I.off("focus"+z,D.search),V.focus(),I.on("focus"+z,D.search,k.event.search.focus)):V.focus())},forceSelection:function(){var e=X.not(T.filtered).filter("."+T.selected).eq(0),t=X.not(T.filtered).filter("."+T.active).eq(0),n=e.length>0?e:t,i=n.length>0;return i?(k.debug("Forcing partial selection to selected item",n),void k.event.item.click.call(n,{},!0)):void(S.allowAdditions?(k.set.selected(k.get.query()),k.remove.searchTerm()):k.remove.searchTerm())},event:{change:function(){K||(k.debug("Input changed, updating selection"),k.set.selected())},focus:function(){S.showOnFocus&&!$&&k.is.hidden()&&!v&&k.show()},blur:function(e){v=n.activeElement===this,$||v||(k.remove.activeLabel(),k.hide())},mousedown:function(){k.is.searchSelection()?b=!0:$=!0},mouseup:function(){k.is.searchSelection()?b=!1:$=!1},click:function(t){var n=e(t.target);n.is(I)&&(k.is.focusedOnSearch()?k.show():k.focusSearch())},search:{focus:function(){$=!0,k.is.multiple()&&k.remove.activeLabel(),S.showOnFocus&&k.search()},blur:function(e){v=n.activeElement===this,b||Y||v||(S.forceSelection&&k.forceSelection(),k.hide()),b=!1}},icon:{click:function(e){k.toggle()}},text:{focus:function(e){$=!0,k.focusSearch()}},input:function(e){(k.is.multiple()||k.is.searchSelection())&&k.set.filtered(),clearTimeout(k.timer),k.timer=setTimeout(k.search,S.delay.search); +},label:{click:function(t){var n=e(this),i=I.find(D.label),o=i.filter("."+T.active),a=n.nextAll("."+T.active),r=n.prevAll("."+T.active),s=a.length>0?n.nextUntil(a).add(o).add(n):n.prevUntil(r).add(o).add(n);t.shiftKey?(o.removeClass(T.active),s.addClass(T.active)):t.ctrlKey?n.toggleClass(T.active):(o.removeClass(T.active),n.addClass(T.active)),S.onLabelSelect.apply(this,i.filter("."+T.active))}},remove:{click:function(){var t=e(this).parent();t.hasClass(T.active)?k.remove.activeLabels():k.remove.activeLabels(t)}},test:{toggle:function(e){var t=k.is.multiple()?k.show:k.toggle;k.is.bubbledLabelClick(e)||k.is.bubbledIconClick(e)||k.determine.eventOnElement(e,t)&&e.preventDefault()},touch:function(e){k.determine.eventOnElement(e,function(){"touchstart"==e.type?k.timer=setTimeout(function(){k.hide()},S.delay.touch):"touchmove"==e.type&&clearTimeout(k.timer)}),e.stopPropagation()},hide:function(e){k.determine.eventInModule(e,k.hide)}},select:{mutation:function(e){k.debug("<select> modified, recreating menu"),k.setup.select()}},menu:{mutation:function(t){var n=t[0],i=e(n.addedNodes?n.addedNodes[0]:!1),o=e(n.removedNodes?n.removedNodes[0]:!1),a=i.add(o),r=a.is(D.addition)||a.closest(D.addition).length>0,s=a.is(D.message)||a.closest(D.message).length>0;r||s?(k.debug("Updating item selector cache"),k.refreshItems()):(k.debug("Menu modified, updating selector cache"),k.refresh())},mousedown:function(){Y=!0},mouseup:function(){Y=!1}},item:{mouseenter:function(t){var n=e(t.target),i=e(this),o=i.children(D.menu),a=i.siblings(D.item).children(D.menu),r=o.length>0,s=o.find(n).length>0;!s&&r&&(clearTimeout(k.itemTimer),k.itemTimer=setTimeout(function(){k.verbose("Showing sub-menu",o),e.each(a,function(){k.animate.hide(!1,e(this))}),k.animate.show(!1,o)},S.delay.show),t.preventDefault())},mouseleave:function(t){var n=e(this).children(D.menu);n.length>0&&(clearTimeout(k.itemTimer),k.itemTimer=setTimeout(function(){k.verbose("Hiding sub-menu",n),k.animate.hide(!1,n)},S.delay.hide))},click:function(t,n){var i=e(this),o=e(t?t.target:""),a=i.find(D.menu),r=k.get.choiceText(i),s=k.get.choiceValue(i,r),l=a.length>0,c=a.find(o).length>0;c||l&&!S.allowCategorySelection||(k.is.searchSelection()&&(S.allowAdditions&&k.remove.userAddition(),k.remove.searchTerm(),k.is.focusedOnSearch()||1==n||k.focusSearch(!0)),S.useLabels||(k.remove.filteredItem(),k.set.scrollPosition(i)),k.determine.selectAction.call(this,r,s))}},document:{keydown:function(e){var t=e.which,n=k.is.inObject(t,E);if(n){var i=I.find(D.label),o=i.filter("."+T.active),a=(o.data(P.value),i.index(o)),r=i.length,s=o.length>0,l=o.length>1,c=0===a,u=a+1==r,d=k.is.searchSelection(),f=k.is.focusedOnSearch(),m=k.is.focused(),g=f&&0===k.get.caretPosition();if(d&&!s&&!f)return;t==E.leftArrow?!m&&!g||s?s&&(e.shiftKey?k.verbose("Adding previous label to selection"):(k.verbose("Selecting previous label"),i.removeClass(T.active)),c&&!l?o.addClass(T.active):o.prev(D.siblingLabel).addClass(T.active).end(),e.preventDefault()):(k.verbose("Selecting previous label"),i.last().addClass(T.active)):t==E.rightArrow?(m&&!s&&i.first().addClass(T.active),s&&(e.shiftKey?k.verbose("Adding next label to selection"):(k.verbose("Selecting next label"),i.removeClass(T.active)),u?d?f?i.removeClass(T.active):k.focusSearch():l?o.next(D.siblingLabel).addClass(T.active):o.addClass(T.active):o.next(D.siblingLabel).addClass(T.active),e.preventDefault())):t==E.deleteKey||t==E.backspace?s?(k.verbose("Removing active labels"),u&&d&&!f&&k.focusSearch(),o.last().next(D.siblingLabel).addClass(T.active),k.remove.activeLabels(o),e.preventDefault()):g&&!s&&t==E.backspace&&(k.verbose("Removing last label on input backspace"),o=i.last().addClass(T.active),k.remove.activeLabels(o)):o.removeClass(T.active)}}},keydown:function(e){var t=e.which,n=k.is.inObject(t,E);if(n){var i,o,a=X.not(D.unselectable).filter("."+T.selected).eq(0),r=Q.children("."+T.active).eq(0),s=a.length>0?a:r,l=s.length>0?s.siblings(":not(."+T.filtered+")").addBack():Q.children(":not(."+T.filtered+")"),c=s.children(D.menu),u=s.closest(D.menu),d=u.hasClass(T.visible)||u.hasClass(T.animating)||u.parent(D.menu).length>0,f=c.length>0,m=s.length>0,g=s.not(D.unselectable).length>0,p=t==E.delimiter&&S.allowAdditions&&k.is.multiple(),h=S.allowAdditions&&S.hideAdditions&&(t==E.enter||p)&&g;if(h&&(k.verbose("Selecting item from keyboard shortcut",s),k.event.item.click.call(s,e),k.is.searchSelection()&&k.remove.searchTerm()),k.is.visible()){if((t==E.enter||p)&&(t==E.enter&&m&&f&&!S.allowCategorySelection?(k.verbose("Pressed enter on unselectable category, opening sub menu"),t=E.rightArrow):g&&(k.verbose("Selecting item from keyboard shortcut",s),k.event.item.click.call(s,e),k.is.searchSelection()&&k.remove.searchTerm()),e.preventDefault()),m&&(t==E.leftArrow&&(o=u[0]!==Q[0],o&&(k.verbose("Left key pressed, closing sub-menu"),k.animate.hide(!1,u),s.removeClass(T.selected),u.closest(D.item).addClass(T.selected),e.preventDefault())),t==E.rightArrow&&f&&(k.verbose("Right key pressed, opening sub-menu"),k.animate.show(!1,c),s.removeClass(T.selected),c.find(D.item).eq(0).addClass(T.selected),e.preventDefault())),t==E.upArrow){if(i=m&&d?s.prevAll(D.item+":not("+D.unselectable+")").eq(0):X.eq(0),l.index(i)<0)return k.verbose("Up key pressed but reached top of current menu"),void e.preventDefault();k.verbose("Up key pressed, changing active item"),s.removeClass(T.selected),i.addClass(T.selected),k.set.scrollPosition(i),S.selectOnKeydown&&k.is.single()&&k.set.selectedItem(i),e.preventDefault()}if(t==E.downArrow){if(i=m&&d?i=s.nextAll(D.item+":not("+D.unselectable+")").eq(0):X.eq(0),0===i.length)return k.verbose("Down key pressed but reached bottom of current menu"),void e.preventDefault();k.verbose("Down key pressed, changing active item"),X.removeClass(T.selected),i.addClass(T.selected),k.set.scrollPosition(i),S.selectOnKeydown&&k.is.single()&&k.set.selectedItem(i),e.preventDefault()}t==E.pageUp&&(k.scrollPage("up"),e.preventDefault()),t==E.pageDown&&(k.scrollPage("down"),e.preventDefault()),t==E.escape&&(k.verbose("Escape key pressed, closing dropdown"),k.hide())}else p&&e.preventDefault(),t!=E.downArrow||k.is.visible()||(k.verbose("Down key pressed, showing dropdown"),k.select.firstUnfiltered(),k.show(),e.preventDefault())}else k.has.search()||k.set.selectedLetter(String.fromCharCode(t))}},trigger:{change:function(){var e=n.createEvent("HTMLEvents"),t=U[0];t&&(k.verbose("Triggering native change event"),e.initEvent("change",!0,!1),t.dispatchEvent(e))}},determine:{selectAction:function(t,n){k.verbose("Determining action",S.action),e.isFunction(k.action[S.action])?(k.verbose("Triggering preset action",S.action,t,n),k.action[S.action].call(Z,t,n,this)):e.isFunction(S.action)?(k.verbose("Triggering user action",S.action,t,n),S.action.call(Z,t,n,this)):k.error(q.action,S.action)},eventInModule:function(t,i){var o=e(t.target),a=o.closest(n.documentElement).length>0,r=o.closest(I).length>0;return i=e.isFunction(i)?i:function(){},a&&!r?(k.verbose("Triggering event",i),i(),!0):(k.verbose("Event occurred in dropdown, canceling callback"),!1)},eventOnElement:function(t,i){var o=e(t.target),a=o.closest(D.siblingLabel),r=n.body.contains(t.target),s=0===I.find(a).length,l=0===o.closest(Q).length;return i=e.isFunction(i)?i:function(){},r&&s&&l?(k.verbose("Triggering event",i),i(),!0):(k.verbose("Event occurred in dropdown menu, canceling callback"),!1)}},action:{nothing:function(){},activate:function(t,n,o){if(n=n!==i?n:t,k.can.activate(e(o))){if(k.set.selected(n,e(o)),k.is.multiple()&&!k.is.allFiltered())return;k.hideAndClear()}},select:function(t,n,o){if(n=n!==i?n:t,k.can.activate(e(o))){if(k.set.value(n,e(o)),k.is.multiple()&&!k.is.allFiltered())return;k.hideAndClear()}},combo:function(t,n,o){n=n!==i?n:t,k.set.selected(n,e(o)),k.hideAndClear()},hide:function(e,t,n){k.set.value(t,e),k.hideAndClear()}},get:{id:function(){return x},defaultText:function(){return I.data(P.defaultText)},defaultValue:function(){return I.data(P.defaultValue)},placeholderText:function(){return I.data(P.placeholderText)||""},text:function(){return N.text()},query:function(){return e.trim(V.val())},searchWidth:function(e){return e=e!==i?e:V.val(),H.text(e),Math.ceil(H.width()+1)},selectionCount:function(){var t,n=k.get.values();return t=k.is.multiple()?e.isArray(n)?n.length:0:""!==k.get.value()?1:0},transition:function(e){return"auto"==S.transition?k.is.upward(e)?"slide up":"slide down":S.transition},userValues:function(){var t=k.get.values();return!!t&&(t=e.isArray(t)?t:[t],e.grep(t,function(e){return k.get.item(e)===!1}))},uniqueArray:function(t){return e.grep(t,function(n,i){return e.inArray(n,t)===i})},caretPosition:function(){var e,t,i=V.get(0);return"selectionStart"in i?i.selectionStart:n.selection?(i.focus(),e=n.selection.createRange(),t=e.text.length,e.moveStart("character",-i.value.length),e.text.length-t):void 0},value:function(){var t=U.length>0?U.val():I.data(P.value),n=e.isArray(t)&&1===t.length&&""===t[0];return t===i||n?"":t},values:function(){var e=k.get.value();return""===e?"":!k.has.selectInput()&&k.is.multiple()?"string"==typeof e?e.split(S.delimiter):"":e},remoteValues:function(){var t=k.get.values(),n=!1;return t&&("string"==typeof t&&(t=[t]),e.each(t,function(e,t){var i=k.read.remoteData(t);k.verbose("Restoring value from session data",i,t),i&&(n||(n={}),n[t]=i)})),n},choiceText:function(t,n){if(n=n!==i?n:S.preserveHTML,t)return t.find(D.menu).length>0&&(k.verbose("Retrieving text of element with sub-menu"),t=t.clone(),t.find(D.menu).remove(),t.find(D.menuIcon).remove()),t.data(P.text)!==i?t.data(P.text):n?e.trim(t.html()):e.trim(t.text())},choiceValue:function(t,n){return n=n||k.get.choiceText(t),!!t&&(t.data(P.value)!==i?String(t.data(P.value)):"string"==typeof n?e.trim(n.toLowerCase()):String(n))},inputEvent:function(){var e=V[0];return!!e&&(e.oninput!==i?"input":e.onpropertychange!==i?"propertychange":"keyup")},selectValues:function(){var t={};return t.values=[],I.find("option").each(function(){var n=e(this),o=n.html(),a=n.attr("disabled"),r=n.attr("value")!==i?n.attr("value"):o;"auto"===S.placeholder&&""===r?t.placeholder=o:t.values.push({name:o,value:r,disabled:a})}),S.placeholder&&"auto"!==S.placeholder&&(k.debug("Setting placeholder value to",S.placeholder),t.placeholder=S.placeholder),S.sortSelect?(t.values.sort(function(e,t){return e.name>t.name?1:-1}),k.debug("Retrieved and sorted values from select",t)):k.debug("Retrieved values from select",t),t},activeItem:function(){return X.filter("."+T.active)},selectedItem:function(){var e=X.not(D.unselectable).filter("."+T.selected);return e.length>0?e:X.eq(0)},itemWithAdditions:function(e){var t=k.get.item(e),n=k.create.userChoice(e),i=n&&n.length>0;return i&&(t=t.length>0?t.add(n):n),t},item:function(t,n){var o,a,r=!1;return t=t!==i?t:k.get.values()!==i?k.get.values():k.get.text(),o=a?t.length>0:t!==i&&null!==t,a=k.is.multiple()&&e.isArray(t),n=""===t||0===t||(n||!1),o&&X.each(function(){var o=e(this),s=k.get.choiceText(o),l=k.get.choiceValue(o,s);if(null!==l&&l!==i)if(a)e.inArray(String(l),t)===-1&&e.inArray(s,t)===-1||(r=r?r.add(o):o);else if(n){if(k.verbose("Ambiguous dropdown value using strict type check",o,t),l===t||s===t)return r=o,!0}else if(String(l)==String(t)||s==t)return k.verbose("Found select item by value",l,t),r=o,!0}),r}},check:{maxSelections:function(e){return!S.maxSelections||(e=e!==i?e:k.get.selectionCount(),e>=S.maxSelections?(k.debug("Maximum selection count reached"),S.useLabels&&(X.addClass(T.filtered),k.add.message(A.maxSelections)),!0):(k.verbose("No longer at maximum selection count"),k.remove.message(),k.remove.filteredItem(),k.is.searchSelection()&&k.filterItems(),!1))}},restore:{defaults:function(){k.clear(),k.restore.defaultText(),k.restore.defaultValue()},defaultText:function(){var e=k.get.defaultText(),t=k.get.placeholderText;e===t?(k.debug("Restoring default placeholder text",e),k.set.placeholderText(e)):(k.debug("Restoring default text",e),k.set.text(e))},placeholderText:function(){k.set.placeholderText()},defaultValue:function(){var e=k.get.defaultValue();e!==i&&(k.debug("Restoring default value",e),""!==e?(k.set.value(e),k.set.selected()):(k.remove.activeItem(),k.remove.selectedItem()))},labels:function(){S.allowAdditions&&(S.useLabels||(k.error(q.labels),S.useLabels=!0),k.debug("Restoring selected values"),k.create.userLabels()),k.check.maxSelections()},selected:function(){k.restore.values(),k.is.multiple()?(k.debug("Restoring previously selected values and labels"),k.restore.labels()):k.debug("Restoring previously selected values")},values:function(){k.set.initialLoad(),S.apiSettings&&S.saveRemoteData&&k.get.remoteValues()?k.restore.remoteValues():k.set.selected(),k.remove.initialLoad()},remoteValues:function(){var t=k.get.remoteValues();k.debug("Recreating selected from session data",t),t&&(k.is.single()?e.each(t,function(e,t){k.set.text(t)}):e.each(t,function(e,t){k.add.label(e,t)}))}},read:{remoteData:function(e){var n;return t.Storage===i?void k.error(q.noStorage):(n=sessionStorage.getItem(e),n!==i&&n)}},save:{defaults:function(){k.save.defaultText(),k.save.placeholderText(),k.save.defaultValue()},defaultValue:function(){var e=k.get.value();k.verbose("Saving default value as",e),I.data(P.defaultValue,e)},defaultText:function(){var e=k.get.text();k.verbose("Saving default text as",e),I.data(P.defaultText,e)},placeholderText:function(){var e;S.placeholder!==!1&&N.hasClass(T.placeholder)&&(e=k.get.text(),k.verbose("Saving placeholder text as",e),I.data(P.placeholderText,e))},remoteData:function(e,n){return t.Storage===i?void k.error(q.noStorage):(k.verbose("Saving remote data to session storage",n,e),void sessionStorage.setItem(n,e))}},clear:function(){k.is.multiple()&&S.useLabels?k.remove.labels():(k.remove.activeItem(),k.remove.selectedItem()),k.set.placeholderText(),k.clearValue()},clearValue:function(){k.set.value("")},scrollPage:function(e,t){var n,i,o,a=t||k.get.selectedItem(),r=a.closest(D.menu),s=r.outerHeight(),l=r.scrollTop(),c=X.eq(0).outerHeight(),u=Math.floor(s/c),d=(r.prop("scrollHeight"),"up"==e?l-c*u:l+c*u),f=X.not(D.unselectable);o="up"==e?f.index(a)-u:f.index(a)+u,n="up"==e?o>=0:o<f.length,i=n?f.eq(o):"up"==e?f.first():f.last(),i.length>0&&(k.debug("Scrolling page",e,i),a.removeClass(T.selected),i.addClass(T.selected),S.selectOnKeydown&&k.is.single()&&k.set.selectedItem(i),r.scrollTop(d))},set:{filtered:function(){var e=k.is.multiple(),t=k.is.searchSelection(),n=e&&t,i=t?k.get.query():"",o="string"==typeof i&&i.length>0,a=k.get.searchWidth(),r=""!==i;e&&o&&(k.verbose("Adjusting input width",a,S.glyphWidth),V.css("width",a)),o||n&&r?(k.verbose("Hiding placeholder text"),N.addClass(T.filtered)):(!e||n&&!r)&&(k.verbose("Showing placeholder text"),N.removeClass(T.filtered))},empty:function(){I.addClass(T.empty)},loading:function(){I.addClass(T.loading)},placeholderText:function(e){e=e||k.get.placeholderText(),k.debug("Setting placeholder text",e),k.set.text(e),N.addClass(T.placeholder)},tabbable:function(){k.has.search()?(k.debug("Added tabindex to searchable dropdown"),V.val("").attr("tabindex",0),Q.attr("tabindex",-1)):(k.debug("Added tabindex to dropdown"),I.attr("tabindex")===i&&(I.attr("tabindex",0),Q.attr("tabindex",-1)))},initialLoad:function(){k.verbose("Setting initial load"),h=!0},activeItem:function(e){S.allowAdditions&&e.filter(D.addition).length>0?e.addClass(T.filtered):e.addClass(T.active)},partialSearch:function(e){var t=k.get.query().length;V.val(e.substr(0,t))},scrollPosition:function(e,t){var n,o,a,r,s,l,c,u,d,f=5;e=e||k.get.selectedItem(),n=e.closest(D.menu),o=e&&e.length>0,t=t!==i&&t,e&&n.length>0&&o&&(r=e.position().top,n.addClass(T.loading),l=n.scrollTop(),s=n.offset().top,r=e.offset().top,a=l-s+r,t||(c=n.height(),d=l+c<a+f,u=a-f<l),k.debug("Scrolling to active item",a),(t||u||d)&&n.scrollTop(a),n.removeClass(T.loading))},text:function(e){"select"!==S.action&&("combo"==S.action?(k.debug("Changing combo button text",e,B),S.preserveHTML?B.html(e):B.text(e)):(e!==k.get.placeholderText()&&N.removeClass(T.placeholder),k.debug("Changing text",e,N),N.removeClass(T.filtered),S.preserveHTML?N.html(e):N.text(e)))},selectedItem:function(e){var t=k.get.choiceValue(e),n=k.get.choiceText(e,!1);k.debug("Setting user selection to item",e),k.remove.activeItem(),k.set.partialSearch(n),k.set.activeItem(e),k.set.selected(t,e),k.set.text(n)},selectedLetter:function(t){var n,i=X.filter("."+T.selected),o=i.length>0&&k.has.firstLetter(i,t),a=!1;o&&(n=i.nextAll(X).eq(0),k.has.firstLetter(n,t)&&(a=n)),a||X.each(function(){if(k.has.firstLetter(e(this),t))return a=e(this),!1}),a&&(k.verbose("Scrolling to next value with letter",t),k.set.scrollPosition(a),i.removeClass(T.selected),a.addClass(T.selected),S.selectOnKeydown&&k.is.single()&&k.set.selectedItem(a))},direction:function(e){"auto"==S.direction?k.is.onScreen(e)?k.remove.upward(e):k.set.upward(e):"upward"==S.direction&&k.set.upward(e)},upward:function(e){var t=e||I;t.addClass(T.upward)},value:function(e,t,n){var o=k.escape.value(e),a=U.length>0,r=(!k.has.value(e),k.get.values()),s=e!==i?String(e):e;if(a){if(!S.allowReselection&&s==r&&(k.verbose("Skipping value update already same value",e,r),!k.is.initialLoad()))return;k.is.single()&&k.has.selectInput()&&k.can.extendSelect()&&(k.debug("Adding user option",e),k.add.optionValue(e)),k.debug("Updating input value",o,r),K=!0,U.val(o),S.fireOnInit===!1&&k.is.initialLoad()?k.debug("Input native change event ignored on initial load"):k.trigger.change(),K=!1}else k.verbose("Storing value in metadata",o,U),o!==r&&I.data(P.value,s);S.fireOnInit===!1&&k.is.initialLoad()?k.verbose("No callback on initial load",S.onChange):S.onChange.call(Z,e,t,n)},active:function(){I.addClass(T.active)},multiple:function(){I.addClass(T.multiple)},visible:function(){I.addClass(T.visible)},exactly:function(e,t){k.debug("Setting selected to exact values"),k.clear(),k.set.selected(e,t)},selected:function(t,n){var i=k.is.multiple();n=S.allowAdditions?n||k.get.itemWithAdditions(t):n||k.get.item(t),n&&(k.debug("Setting selected menu item to",n),k.is.multiple()&&k.remove.searchWidth(),k.is.single()?(k.remove.activeItem(),k.remove.selectedItem()):S.useLabels&&k.remove.selectedItem(),n.each(function(){var t=e(this),o=k.get.choiceText(t),a=k.get.choiceValue(t,o),r=t.hasClass(T.filtered),s=t.hasClass(T.active),l=t.hasClass(T.addition),c=i&&1==n.length;i?!s||l?(S.apiSettings&&S.saveRemoteData&&k.save.remoteData(o,a),S.useLabels?(k.add.value(a,o,t),k.add.label(a,o,c),k.set.activeItem(t),k.filterActive(),k.select.nextAvailable(n)):(k.add.value(a,o,t),k.set.text(k.add.variables(A.count)),k.set.activeItem(t))):r||(k.debug("Selected active value, removing label"),k.remove.selected(a)):(S.apiSettings&&S.saveRemoteData&&k.save.remoteData(o,a),k.set.text(o),k.set.value(a,o,t),t.addClass(T.active).addClass(T.selected))}))}},add:{label:function(t,n,i){var o,a=k.is.searchSelection()?V:N,r=k.escape.value(t);return o=e("<a />").addClass(T.label).attr("data-value",r).html(j.label(r,n)),o=S.onLabelCreate.call(o,r,n),k.has.label(t)?void k.debug("Label already exists, skipping",r):(S.label.variation&&o.addClass(S.label.variation),void(i===!0?(k.debug("Animating in label",o),o.addClass(T.hidden).insertBefore(a).transition(S.label.transition,S.label.duration)):(k.debug("Adding selection label",o),o.insertBefore(a))))},message:function(t){var n=Q.children(D.message),i=S.templates.message(k.add.variables(t));n.length>0?n.html(i):n=e("<div/>").html(i).addClass(T.message).appendTo(Q)},optionValue:function(t){var n=k.escape.value(t),i=U.find('option[value="'+n+'"]'),o=i.length>0;o||(k.disconnect.selectObserver(),k.is.single()&&(k.verbose("Removing previous user addition"),U.find("option."+T.addition).remove()),e("<option/>").prop("value",n).addClass(T.addition).html(t).appendTo(U),k.verbose("Adding user addition as an <option>",t),k.observe.select())},userSuggestion:function(e){var t,n=Q.children(D.addition),i=k.get.item(e),o=i&&i.not(D.addition).length,a=n.length>0;if(!S.useLabels||!k.has.maxSelections()){if(""===e||o)return void n.remove();a?(n.data(P.value,e).data(P.text,e).attr("data-"+P.value,e).attr("data-"+P.text,e).removeClass(T.filtered),S.hideAdditions||(t=S.templates.addition(k.add.variables(A.addResult,e)),n.html(t)),k.verbose("Replacing user suggestion with new value",n)):(n=k.create.userChoice(e),n.prependTo(Q),k.verbose("Adding item choice to menu corresponding with user choice addition",n)),S.hideAdditions&&!k.is.allFiltered()||n.addClass(T.selected).siblings().removeClass(T.selected),k.refreshItems()}},variables:function(e,t){var n,i,o=e.search("{count}")!==-1,a=e.search("{maxCount}")!==-1,r=e.search("{term}")!==-1;return k.verbose("Adding templated variables to message",e),o&&(n=k.get.selectionCount(),e=e.replace("{count}",n)),a&&(n=k.get.selectionCount(),e=e.replace("{maxCount}",S.maxSelections)),r&&(i=t||k.get.query(),e=e.replace("{term}",i)),e},value:function(t,n,i){var o,a=k.get.values();return""===t?void k.debug("Cannot select blank values from multiselect"):(e.isArray(a)?(o=a.concat([t]),o=k.get.uniqueArray(o)):o=[t],k.has.selectInput()?k.can.extendSelect()&&(k.debug("Adding value to select",t,o,U),k.add.optionValue(t)):(o=o.join(S.delimiter),k.debug("Setting hidden input to delimited value",o,U)),S.fireOnInit===!1&&k.is.initialLoad()?k.verbose("Skipping onadd callback on initial load",S.onAdd):S.onAdd.call(Z,t,n,i),k.set.value(o,t,n,i),void k.check.maxSelections())}},remove:{active:function(){I.removeClass(T.active)},activeLabel:function(){I.find(D.label).removeClass(T.active)},empty:function(){I.removeClass(T.empty)},loading:function(){I.removeClass(T.loading)},initialLoad:function(){h=!1},upward:function(e){var t=e||I;t.removeClass(T.upward)},visible:function(){I.removeClass(T.visible)},activeItem:function(){X.removeClass(T.active)},filteredItem:function(){S.useLabels&&k.has.maxSelections()||(S.useLabels&&k.is.multiple()?X.not("."+T.active).removeClass(T.filtered):X.removeClass(T.filtered),k.remove.empty())},optionValue:function(e){var t=k.escape.value(e),n=U.find('option[value="'+t+'"]'),i=n.length>0;i&&n.hasClass(T.addition)&&(C&&(C.disconnect(),k.verbose("Temporarily disconnecting mutation observer")),n.remove(),k.verbose("Removing user addition as an <option>",t),C&&C.observe(U[0],{childList:!0,subtree:!0}))},message:function(){Q.children(D.message).remove()},searchWidth:function(){V.css("width","")},searchTerm:function(){k.verbose("Cleared search term"),V.val(""),k.set.filtered()},userAddition:function(){X.filter(D.addition).remove()},selected:function(t,n){return!!(n=S.allowAdditions?n||k.get.itemWithAdditions(t):n||k.get.item(t))&&void n.each(function(){var t=e(this),n=k.get.choiceText(t),i=k.get.choiceValue(t,n);k.is.multiple()?S.useLabels?(k.remove.value(i,n,t),k.remove.label(i)):(k.remove.value(i,n,t),0===k.get.selectionCount()?k.set.placeholderText():k.set.text(k.add.variables(A.count))):k.remove.value(i,n,t),t.removeClass(T.filtered).removeClass(T.active),S.useLabels&&t.removeClass(T.selected)})},selectedItem:function(){X.removeClass(T.selected)},value:function(e,t,n){var i,o=k.get.values();k.has.selectInput()?(k.verbose("Input is <select> removing selected option",e),i=k.remove.arrayValue(e,o),k.remove.optionValue(e)):(k.verbose("Removing from delimited values",e),i=k.remove.arrayValue(e,o),i=i.join(S.delimiter)),S.fireOnInit===!1&&k.is.initialLoad()?k.verbose("No callback on initial load",S.onRemove):S.onRemove.call(Z,e,t,n),k.set.value(i,t,n),k.check.maxSelections()},arrayValue:function(t,n){return e.isArray(n)||(n=[n]),n=e.grep(n,function(e){return t!=e}),k.verbose("Removed value from delimited string",t,n),n},label:function(e,t){var n=I.find(D.label),i=n.filter('[data-value="'+e+'"]');k.verbose("Removing label",i),i.remove()},activeLabels:function(e){e=e||I.find(D.label).filter("."+T.active),k.verbose("Removing active label selections",e),k.remove.labels(e)},labels:function(t){t=t||I.find(D.label),k.verbose("Removing labels",t),t.each(function(){var t=e(this),n=t.data(P.value),o=n!==i?String(n):n,a=k.is.userValue(o);return S.onLabelRemove.call(t,n)===!1?void k.debug("Label remove callback cancelled removal"):(k.remove.message(),void(a?(k.remove.value(o),k.remove.label(o)):k.remove.selected(o)))})},tabbable:function(){k.has.search()?(k.debug("Searchable dropdown initialized"),V.removeAttr("tabindex"),Q.removeAttr("tabindex")):(k.debug("Simple selection dropdown initialized"),I.removeAttr("tabindex"),Q.removeAttr("tabindex"))}},has:{menuSearch:function(){return k.has.search()&&V.closest(Q).length>0},search:function(){return V.length>0},sizer:function(){return H.length>0},selectInput:function(){return U.is("select")},minCharacters:function(e){return!S.minCharacters||(e=e!==i?String(e):String(k.get.query()),e.length>=S.minCharacters)},firstLetter:function(e,t){var n,i;return!(!e||0===e.length||"string"!=typeof t)&&(n=k.get.choiceText(e,!1),t=t.toLowerCase(),i=String(n).charAt(0).toLowerCase(),t==i)},input:function(){return U.length>0},items:function(){return X.length>0},menu:function(){return Q.length>0},message:function(){return 0!==Q.children(D.message).length},label:function(e){var t=k.escape.value(e),n=I.find(D.label);return n.filter('[data-value="'+t+'"]').length>0},maxSelections:function(){return S.maxSelections&&k.get.selectionCount()>=S.maxSelections},allResultsFiltered:function(){var e=X.not(D.addition);return e.filter(D.unselectable).length===e.length},userSuggestion:function(){return Q.children(D.addition).length>0},query:function(){return""!==k.get.query()},value:function(t){var n=k.get.values(),i=e.isArray(n)?n&&e.inArray(t,n)!==-1:n==t;return!!i}},is:{active:function(){return I.hasClass(T.active)},bubbledLabelClick:function(t){return e(t.target).is("select, input")&&I.closest("label").length>0},bubbledIconClick:function(t){return e(t.target).closest(W).length>0},alreadySetup:function(){return I.is("select")&&I.parent(D.dropdown).length>0&&0===I.prev().length},animating:function(e){return e?e.transition&&e.transition("is animating"):Q.transition&&Q.transition("is animating")},disabled:function(){return I.hasClass(T.disabled)},focused:function(){return n.activeElement===I[0]},focusedOnSearch:function(){return n.activeElement===V[0]},allFiltered:function(){return(k.is.multiple()||k.has.search())&&!(0==S.hideAdditions&&k.has.userSuggestion())&&!k.has.message()&&k.has.allResultsFiltered()},hidden:function(e){return!k.is.visible(e)},initialLoad:function(){return h},onScreen:function(e){var t,n=e||Q,i=!0,o={};return n.addClass(T.loading),t={context:{scrollTop:L.scrollTop(),height:L.outerHeight()},menu:{offset:n.offset(),height:n.outerHeight()}},o={above:t.context.scrollTop<=t.menu.offset.top-t.menu.height,below:t.context.scrollTop+t.context.height>=t.menu.offset.top+t.menu.height},o.below?(k.verbose("Dropdown can fit in context downward",o),i=!0):o.below||o.above?(k.verbose("Dropdown cannot fit below, opening upward",o),i=!1):(k.verbose("Dropdown cannot fit in either direction, favoring downward",o),i=!0),n.removeClass(T.loading),i},inObject:function(t,n){var i=!1;return e.each(n,function(e,n){if(n==t)return i=!0,!0}),i},multiple:function(){return I.hasClass(T.multiple)},single:function(){return!k.is.multiple()},selectMutation:function(t){var n=!1;return e.each(t,function(t,i){if(i.target&&e(i.target).is("select"))return n=!0,!0}),n},search:function(){return I.hasClass(T.search)},searchSelection:function(){return k.has.search()&&1===V.parent(D.dropdown).length},selection:function(){return I.hasClass(T.selection)},userValue:function(t){return e.inArray(t,k.get.userValues())!==-1},upward:function(e){var t=e||I;return t.hasClass(T.upward)},visible:function(e){return e?e.hasClass(T.visible):Q.hasClass(T.visible)}},can:{activate:function(e){return!!S.useLabels||(!k.has.maxSelections()||!(!k.has.maxSelections()||!e.hasClass(T.active)))},click:function(){return c||"click"==S.on},extendSelect:function(){return S.allowAdditions||S.apiSettings},show:function(){return!k.is.disabled()&&(k.has.items()||k.has.message())},useAPI:function(){return e.fn.api!==i}},animate:{show:function(t,n){var o,a=n||Q,r=n?function(){}:function(){k.hideSubMenus(),k.hideOthers(),k.set.active()};t=e.isFunction(t)?t:function(){},k.verbose("Doing menu show animation",a),k.set.direction(n),o=k.get.transition(n),k.is.selection()&&k.set.scrollPosition(k.get.selectedItem(),!0),(k.is.hidden(a)||k.is.animating(a))&&("none"==o?(r(),a.transition("show"),t.call(Z)):e.fn.transition!==i&&I.transition("is supported")?a.transition({animation:o+" in",debug:S.debug,verbose:S.verbose,duration:S.duration,queue:!0,onStart:r,onComplete:function(){t.call(Z)}}):k.error(q.noTransition,o))},hide:function(t,n){var o=n||Q,a=(n?.9*S.duration:S.duration,n?function(){}:function(){k.can.click()&&k.unbind.intent(),k.remove.active()}),r=k.get.transition(n);t=e.isFunction(t)?t:function(){},(k.is.visible(o)||k.is.animating(o))&&(k.verbose("Doing menu hide animation",o),"none"==r?(a(),o.transition("hide"),t.call(Z)):e.fn.transition!==i&&I.transition("is supported")?o.transition({animation:r+" out",duration:S.duration,debug:S.debug,verbose:S.verbose,queue:!0,onStart:a,onComplete:function(){"auto"==S.direction&&k.remove.upward(n),t.call(Z)}}):k.error(q.transition))}},hideAndClear:function(){k.remove.searchTerm(),k.has.maxSelections()||(k.has.search()?k.hide(function(){k.remove.filteredItem()}):k.hide())},delay:{show:function(){k.verbose("Delaying show event to ensure user intent"),clearTimeout(k.timer),k.timer=setTimeout(k.show,S.delay.show)},hide:function(){k.verbose("Delaying hide event to ensure user intent"),clearTimeout(k.timer),k.timer=setTimeout(k.hide,S.delay.hide)}},escape:{value:function(t){var n=e.isArray(t),i="string"==typeof t,o=!i&&!n,a=i&&t.search(O.quote)!==-1,r=[];return k.has.selectInput()&&!o&&a?(k.debug("Encoding quote values for use in select",t),n?(e.each(t,function(e,t){r.push(t.replace(O.quote,"""))}),r):t.replace(O.quote,""")):t},regExp:function(e){return e=String(e),e.replace(O.escape,"\\$&")}},setting:function(t,n){if(k.debug("Changing setting",t,n),e.isPlainObject(t))e.extend(!0,S,t);else{if(n===i)return S[t];e.isPlainObject(S[t])?e.extend(!0,S[t],n):S[t]=n}},internal:function(t,n){if(e.isPlainObject(t))e.extend(!0,k,t);else{if(n===i)return k[t];k[t]=n}},debug:function(){!S.silent&&S.debug&&(S.performance?k.performance.log(arguments):(k.debug=Function.prototype.bind.call(console.info,console,S.name+":"),k.debug.apply(console,arguments)))},verbose:function(){!S.silent&&S.verbose&&S.debug&&(S.performance?k.performance.log(arguments):(k.verbose=Function.prototype.bind.call(console.info,console,S.name+":"),k.verbose.apply(console,arguments)))},error:function(){S.silent||(k.error=Function.prototype.bind.call(console.error,console,S.name+":"),k.error.apply(console,arguments))},performance:{log:function(e){var t,n,i;S.performance&&(t=(new Date).getTime(),i=u||t,n=t-i,u=t,d.push({Name:e[0],Arguments:[].slice.call(e,1)||"",Element:Z,"Execution Time":n})),clearTimeout(k.performance.timer),k.performance.timer=setTimeout(k.performance.display,500)},display:function(){var t=S.name+":",n=0;u=!1,clearTimeout(k.performance.timer),e.each(d,function(e,t){n+=t["Execution Time"]}),t+=" "+n+"ms",l&&(t+=" '"+l+"'"),(console.group!==i||console.table!==i)&&d.length>0&&(console.groupCollapsed(t),console.table?console.table(d):e.each(d,function(e,t){console.log(t.Name+": "+t["Execution Time"]+"ms")}),console.groupEnd()),d=[]}},invoke:function(t,n,o){var r,s,l,c=J;return n=n||g,o=Z||o,"string"==typeof t&&c!==i&&(t=t.split(/[\. ]/),r=t.length-1,e.each(t,function(n,o){var a=n!=r?o+t[n+1].charAt(0).toUpperCase()+t[n+1].slice(1):t;if(e.isPlainObject(c[a])&&n!=r)c=c[a];else{if(c[a]!==i)return s=c[a],!1;if(!e.isPlainObject(c[o])||n==r)return c[o]!==i?(s=c[o],!1):(k.error(q.method,t),!1);c=c[o]}})),e.isFunction(s)?l=s.apply(o,n):s!==i&&(l=s),e.isArray(a)?a.push(l):a!==i?a=[a,l]:l!==i&&(a=l),s}},m?(J===i&&k.initialize(),k.invoke(f)):(J!==i&&J.invoke("destroy"), +k.initialize())}),a!==i?a:r},e.fn.dropdown.settings={silent:!1,debug:!1,verbose:!1,performance:!0,on:"click",action:"activate",apiSettings:!1,selectOnKeydown:!0,minCharacters:0,saveRemoteData:!0,throttle:200,context:t,direction:"auto",keepOnScreen:!0,match:"both",fullTextSearch:!1,placeholder:"auto",preserveHTML:!0,sortSelect:!1,forceSelection:!0,allowAdditions:!1,hideAdditions:!0,maxSelections:!1,useLabels:!0,delimiter:",",showOnFocus:!0,allowReselection:!1,allowTab:!0,allowCategorySelection:!1,fireOnInit:!1,transition:"auto",duration:200,glyphWidth:1.037,label:{transition:"scale",duration:200,variation:!1},delay:{hide:300,show:200,search:20,touch:50},onChange:function(e,t,n){},onAdd:function(e,t,n){},onRemove:function(e,t,n){},onLabelSelect:function(e){},onLabelCreate:function(t,n){return e(this)},onLabelRemove:function(e){return!0},onNoResults:function(e){return!0},onShow:function(){},onHide:function(){},name:"Dropdown",namespace:"dropdown",message:{addResult:"Add <b>{term}</b>",count:"{count} selected",maxSelections:"Max {maxCount} selections",noResults:"No results found.",serverError:"There was an error contacting the server"},error:{action:"You called a dropdown action that was not defined",alreadySetup:"Once a select has been initialized behaviors must be called on the created ui dropdown",labels:"Allowing user additions currently requires the use of labels.",missingMultiple:"<select> requires multiple property to be set to correctly preserve multiple values",method:"The method you called is not defined.",noAPI:"The API module is required to load resources remotely",noStorage:"Saving remote data requires session storage",noTransition:"This module requires ui transitions <https://github.com/Semantic-Org/UI-Transition>"},regExp:{escape:/[-[\]{}()*+?.,\\^$|#\s]/g,quote:/"/g},metadata:{defaultText:"defaultText",defaultValue:"defaultValue",placeholderText:"placeholder",text:"text",value:"value"},fields:{remoteValues:"results",values:"values",disabled:"disabled",name:"name",value:"value",text:"text"},keys:{backspace:8,delimiter:188,deleteKey:46,enter:13,escape:27,pageUp:33,pageDown:34,leftArrow:37,upArrow:38,rightArrow:39,downArrow:40},selector:{addition:".addition",dropdown:".ui.dropdown",hidden:".hidden",icon:"> .dropdown.icon",input:'> input[type="hidden"], > select',item:".item",label:"> .label",remove:"> .label > .delete.icon",siblingLabel:".label",menu:".menu",message:".message",menuIcon:".dropdown.icon",search:"input.search, .menu > .search > input, .menu input.search",sizer:"> input.sizer",text:"> .text:not(.icon)",unselectable:".disabled, .filtered"},className:{active:"active",addition:"addition",animating:"animating",disabled:"disabled",empty:"empty",dropdown:"ui dropdown",filtered:"filtered",hidden:"hidden transition",item:"item",label:"ui label",loading:"loading",menu:"menu",message:"message",multiple:"multiple",placeholder:"default",sizer:"sizer",search:"search",selected:"selected",selection:"selection",upward:"upward",visible:"visible"}},e.fn.dropdown.settings.templates={dropdown:function(t){var n=t.placeholder||!1,i=(t.values||{},"");return i+='<i class="dropdown icon"></i>',i+=t.placeholder?'<div class="default text">'+n+"</div>":'<div class="text"></div>',i+='<div class="menu">',e.each(t.values,function(e,t){i+=t.disabled?'<div class="disabled item" data-value="'+t.value+'">'+t.name+"</div>":'<div class="item" data-value="'+t.value+'">'+t.name+"</div>"}),i+="</div>"},menu:function(t,n){var i=t[n.values]||{},o="";return e.each(i,function(e,t){var i=t[n.text]?'data-text="'+t[n.text]+'"':"",a=t[n.disabled]?"disabled ":"";o+='<div class="'+a+'item" data-value="'+t[n.value]+'"'+i+">",o+=t[n.name],o+="</div>"}),o},label:function(e,t){return t+'<i class="delete icon"></i>'},message:function(e){return e},addition:function(e){return e}}}(jQuery,window,document),function(e,t,n,i){"use strict";t="undefined"!=typeof t&&t.Math==Math?t:"undefined"!=typeof self&&self.Math==Math?self:Function("return this")(),e.fn.embed=function(n){var o,a=e(this),r=a.selector||"",s=(new Date).getTime(),l=[],c=arguments[0],u="string"==typeof c,d=[].slice.call(arguments,1);return a.each(function(){var f,m=e.isPlainObject(n)?e.extend(!0,{},e.fn.embed.settings,n):e.extend({},e.fn.embed.settings),g=m.selector,p=m.className,h=m.sources,v=m.error,b=m.metadata,y=m.namespace,x=m.templates,C="."+y,w="module-"+y,k=(e(t),e(this)),S=k.find(g.placeholder),T=k.find(g.icon),A=k.find(g.embed),R=this,E=k.data(w);f={initialize:function(){f.debug("Initializing embed"),f.determine.autoplay(),f.create(),f.bind.events(),f.instantiate()},instantiate:function(){f.verbose("Storing instance of module",f),E=f,k.data(w,f)},destroy:function(){f.verbose("Destroying previous instance of embed"),f.reset(),k.removeData(w).off(C)},refresh:function(){f.verbose("Refreshing selector cache"),S=k.find(g.placeholder),T=k.find(g.icon),A=k.find(g.embed)},bind:{events:function(){f.has.placeholder()&&(f.debug("Adding placeholder events"),k.on("click"+C,g.placeholder,f.createAndShow).on("click"+C,g.icon,f.createAndShow))}},create:function(){var e=f.get.placeholder();e?f.createPlaceholder():f.createAndShow()},createPlaceholder:function(e){var t=f.get.icon(),n=f.get.url();f.generate.embed(n);e=e||f.get.placeholder(),k.html(x.placeholder(e,t)),f.debug("Creating placeholder for embed",e,t)},createEmbed:function(t){f.refresh(),t=t||f.get.url(),A=e("<div/>").addClass(p.embed).html(f.generate.embed(t)).appendTo(k),m.onCreate.call(R,t),f.debug("Creating embed object",A)},changeEmbed:function(e){A.html(f.generate.embed(e))},createAndShow:function(){f.createEmbed(),f.show()},change:function(e,t,n){f.debug("Changing video to ",e,t,n),k.data(b.source,e).data(b.id,t),n?k.data(b.url,n):k.removeData(b.url),f.has.embed()?f.changeEmbed():f.create()},reset:function(){f.debug("Clearing embed and showing placeholder"),f.remove.active(),f.remove.embed(),f.showPlaceholder(),m.onReset.call(R)},show:function(){f.debug("Showing embed"),f.set.active(),m.onDisplay.call(R)},hide:function(){f.debug("Hiding embed"),f.showPlaceholder()},showPlaceholder:function(){f.debug("Showing placeholder image"),f.remove.active(),m.onPlaceholderDisplay.call(R)},get:{id:function(){return m.id||k.data(b.id)},placeholder:function(){return m.placeholder||k.data(b.placeholder)},icon:function(){return m.icon?m.icon:k.data(b.icon)!==i?k.data(b.icon):f.determine.icon()},source:function(e){return m.source?m.source:k.data(b.source)!==i?k.data(b.source):f.determine.source()},type:function(){var e=f.get.source();return h[e]!==i&&h[e].type},url:function(){return m.url?m.url:k.data(b.url)!==i?k.data(b.url):f.determine.url()}},determine:{autoplay:function(){f.should.autoplay()&&(m.autoplay=!0)},source:function(t){var n=!1;return t=t||f.get.url(),t&&e.each(h,function(e,i){if(t.search(i.domain)!==-1)return n=e,!1}),n},icon:function(){var e=f.get.source();return h[e]!==i&&h[e].icon},url:function(){var e,t=m.id||k.data(b.id),n=m.source||k.data(b.source);return e=h[n]!==i&&h[n].url.replace("{id}",t),e&&k.data(b.url,e),e}},set:{active:function(){k.addClass(p.active)}},remove:{active:function(){k.removeClass(p.active)},embed:function(){A.empty()}},encode:{parameters:function(e){var t,n=[];for(t in e)n.push(encodeURIComponent(t)+"="+encodeURIComponent(e[t]));return n.join("&")}},generate:{embed:function(e){f.debug("Generating embed html");var t,n,i=f.get.source();return e=f.get.url(e),e?(n=f.generate.parameters(i),t=x.iframe(e,n)):f.error(v.noURL,k),t},parameters:function(t,n){var o=h[t]&&h[t].parameters!==i?h[t].parameters(m):{};return n=n||m.parameters,n&&(o=e.extend({},o,n)),o=m.onEmbed(o),f.encode.parameters(o)}},has:{embed:function(){return A.length>0},placeholder:function(){return m.placeholder||k.data(b.placeholder)}},should:{autoplay:function(){return"auto"===m.autoplay?m.placeholder||k.data(b.placeholder)!==i:m.autoplay}},is:{video:function(){return"video"==f.get.type()}},setting:function(t,n){if(f.debug("Changing setting",t,n),e.isPlainObject(t))e.extend(!0,m,t);else{if(n===i)return m[t];e.isPlainObject(m[t])?e.extend(!0,m[t],n):m[t]=n}},internal:function(t,n){if(e.isPlainObject(t))e.extend(!0,f,t);else{if(n===i)return f[t];f[t]=n}},debug:function(){!m.silent&&m.debug&&(m.performance?f.performance.log(arguments):(f.debug=Function.prototype.bind.call(console.info,console,m.name+":"),f.debug.apply(console,arguments)))},verbose:function(){!m.silent&&m.verbose&&m.debug&&(m.performance?f.performance.log(arguments):(f.verbose=Function.prototype.bind.call(console.info,console,m.name+":"),f.verbose.apply(console,arguments)))},error:function(){m.silent||(f.error=Function.prototype.bind.call(console.error,console,m.name+":"),f.error.apply(console,arguments))},performance:{log:function(e){var t,n,i;m.performance&&(t=(new Date).getTime(),i=s||t,n=t-i,s=t,l.push({Name:e[0],Arguments:[].slice.call(e,1)||"",Element:R,"Execution Time":n})),clearTimeout(f.performance.timer),f.performance.timer=setTimeout(f.performance.display,500)},display:function(){var t=m.name+":",n=0;s=!1,clearTimeout(f.performance.timer),e.each(l,function(e,t){n+=t["Execution Time"]}),t+=" "+n+"ms",r&&(t+=" '"+r+"'"),a.length>1&&(t+=" ("+a.length+")"),(console.group!==i||console.table!==i)&&l.length>0&&(console.groupCollapsed(t),console.table?console.table(l):e.each(l,function(e,t){console.log(t.Name+": "+t["Execution Time"]+"ms")}),console.groupEnd()),l=[]}},invoke:function(t,n,a){var r,s,l,c=E;return n=n||d,a=R||a,"string"==typeof t&&c!==i&&(t=t.split(/[\. ]/),r=t.length-1,e.each(t,function(n,o){var a=n!=r?o+t[n+1].charAt(0).toUpperCase()+t[n+1].slice(1):t;if(e.isPlainObject(c[a])&&n!=r)c=c[a];else{if(c[a]!==i)return s=c[a],!1;if(!e.isPlainObject(c[o])||n==r)return c[o]!==i?(s=c[o],!1):(f.error(v.method,t),!1);c=c[o]}})),e.isFunction(s)?l=s.apply(a,n):s!==i&&(l=s),e.isArray(o)?o.push(l):o!==i?o=[o,l]:l!==i&&(o=l),s}},u?(E===i&&f.initialize(),f.invoke(c)):(E!==i&&E.invoke("destroy"),f.initialize())}),o!==i?o:this},e.fn.embed.settings={name:"Embed",namespace:"embed",silent:!1,debug:!1,verbose:!1,performance:!0,icon:!1,source:!1,url:!1,id:!1,autoplay:"auto",color:"#444444",hd:!0,brandedUI:!1,parameters:!1,onDisplay:function(){},onPlaceholderDisplay:function(){},onReset:function(){},onCreate:function(e){},onEmbed:function(e){return e},metadata:{id:"id",icon:"icon",placeholder:"placeholder",source:"source",url:"url"},error:{noURL:"No URL specified",method:"The method you called is not defined"},className:{active:"active",embed:"embed"},selector:{embed:".embed",placeholder:".placeholder",icon:".icon"},sources:{youtube:{name:"youtube",type:"video",icon:"video play",domain:"youtube.com",url:"//www.youtube.com/embed/{id}",parameters:function(e){return{autohide:!e.brandedUI,autoplay:e.autoplay,color:e.color||i,hq:e.hd,jsapi:e.api,modestbranding:!e.brandedUI}}},vimeo:{name:"vimeo",type:"video",icon:"video play",domain:"vimeo.com",url:"//player.vimeo.com/video/{id}",parameters:function(e){return{api:e.api,autoplay:e.autoplay,byline:e.brandedUI,color:e.color||i,portrait:e.brandedUI,title:e.brandedUI}}}},templates:{iframe:function(e,t){var n=e;return t&&(n+="?"+t),'<iframe src="'+n+'" width="100%" height="100%" frameborder="0" scrolling="no" webkitAllowFullScreen mozallowfullscreen allowFullScreen></iframe>'},placeholder:function(e,t){var n="";return t&&(n+='<i class="'+t+' icon"></i>'),e&&(n+='<img class="placeholder" src="'+e+'">'),n}},api:!1,onPause:function(){},onPlay:function(){},onStop:function(){}}}(jQuery,window,document),function(e,t,n,i){"use strict";t="undefined"!=typeof t&&t.Math==Math?t:"undefined"!=typeof self&&self.Math==Math?self:Function("return this")(),e.fn.modal=function(o){var a,r=e(this),s=e(t),l=e(n),c=e("body"),u=r.selector||"",d=(new Date).getTime(),f=[],m=arguments[0],g="string"==typeof m,p=[].slice.call(arguments,1),h=t.requestAnimationFrame||t.mozRequestAnimationFrame||t.webkitRequestAnimationFrame||t.msRequestAnimationFrame||function(e){setTimeout(e,0)};return r.each(function(){var r,v,b,y,x,C,w,k,S,T=e.isPlainObject(o)?e.extend(!0,{},e.fn.modal.settings,o):e.extend({},e.fn.modal.settings),A=T.selector,R=T.className,E=T.namespace,P=T.error,F="."+E,O="module-"+E,D=e(this),q=e(T.context),j=D.find(A.close),z=this,M=D.data(O);S={initialize:function(){S.verbose("Initializing dimmer",q),S.create.id(),S.create.dimmer(),S.refreshModals(),S.bind.events(),T.observeChanges&&S.observeChanges(),S.instantiate()},instantiate:function(){S.verbose("Storing instance of modal"),M=S,D.data(O,M)},create:{dimmer:function(){var t={debug:T.debug,dimmerName:"modals",duration:{show:T.duration,hide:T.duration}},n=e.extend(!0,t,T.dimmerSettings);return T.inverted&&(n.variation=n.variation!==i?n.variation+" inverted":"inverted"),e.fn.dimmer===i?void S.error(P.dimmer):(S.debug("Creating dimmer with settings",n),y=q.dimmer(n),T.detachable?(S.verbose("Modal is detachable, moving content into dimmer"),y.dimmer("add content",D)):S.set.undetached(),T.blurring&&y.addClass(R.blurring),void(x=y.dimmer("get dimmer")))},id:function(){w=(Math.random().toString(16)+"000000000").substr(2,8),C="."+w,S.verbose("Creating unique id for element",w)}},destroy:function(){S.verbose("Destroying previous modal"),D.removeData(O).off(F),s.off(C),x.off(C),j.off(F),q.dimmer("destroy")},observeChanges:function(){"MutationObserver"in t&&(k=new MutationObserver(function(e){S.debug("DOM tree modified, refreshing"),S.refresh()}),k.observe(z,{childList:!0,subtree:!0}),S.debug("Setting up mutation observer",k))},refresh:function(){S.remove.scrolling(),S.cacheSizes(),S.set.screenHeight(),S.set.type(),S.set.position()},refreshModals:function(){v=D.siblings(A.modal),r=v.add(D)},attachEvents:function(t,n){var i=e(t);n=e.isFunction(S[n])?S[n]:S.toggle,i.length>0?(S.debug("Attaching modal events to element",t,n),i.off(F).on("click"+F,n)):S.error(P.notFound,t)},bind:{events:function(){S.verbose("Attaching events"),D.on("click"+F,A.close,S.event.close).on("click"+F,A.approve,S.event.approve).on("click"+F,A.deny,S.event.deny),s.on("resize"+C,S.event.resize)}},get:{id:function(){return(Math.random().toString(16)+"000000000").substr(2,8)}},event:{approve:function(){return T.onApprove.call(z,e(this))===!1?void S.verbose("Approve callback returned false cancelling hide"):void S.hide()},deny:function(){return T.onDeny.call(z,e(this))===!1?void S.verbose("Deny callback returned false cancelling hide"):void S.hide()},close:function(){S.hide()},click:function(t){var i=e(t.target),o=i.closest(A.modal).length>0,a=e.contains(n.documentElement,t.target);!o&&a&&(S.debug("Dimmer clicked, hiding all modals"),S.is.active()&&(S.remove.clickaway(),T.allowMultiple?S.hide():S.hideAll()))},debounce:function(e,t){clearTimeout(S.timer),S.timer=setTimeout(e,t)},keyboard:function(e){var t=e.which,n=27;t==n&&(T.closable?(S.debug("Escape key pressed hiding modal"),S.hide()):S.debug("Escape key pressed, but closable is set to false"),e.preventDefault())},resize:function(){y.dimmer("is active")&&h(S.refresh)}},toggle:function(){S.is.active()||S.is.animating()?S.hide():S.show()},show:function(t){t=e.isFunction(t)?t:function(){},S.refreshModals(),S.showModal(t)},hide:function(t){t=e.isFunction(t)?t:function(){},S.refreshModals(),S.hideModal(t)},showModal:function(t){t=e.isFunction(t)?t:function(){},S.is.animating()||!S.is.active()?(S.showDimmer(),S.cacheSizes(),S.set.position(),S.set.screenHeight(),S.set.type(),S.set.clickaway(),!T.allowMultiple&&S.others.active()?S.hideOthers(S.showModal):(T.onShow.call(z),T.transition&&e.fn.transition!==i&&D.transition("is supported")?(S.debug("Showing modal with css animations"),D.transition({debug:T.debug,animation:T.transition+" in",queue:T.queue,duration:T.duration,useFailSafe:!0,onComplete:function(){T.onVisible.apply(z),T.keyboardShortcuts&&S.add.keyboardShortcuts(),S.save.focus(),S.set.active(),T.autofocus&&S.set.autofocus(),t()}})):S.error(P.noTransition))):S.debug("Modal is already visible")},hideModal:function(t,n){return t=e.isFunction(t)?t:function(){},S.debug("Hiding modal"),T.onHide.call(z,e(this))===!1?void S.verbose("Hide callback returned false cancelling hide"):void((S.is.animating()||S.is.active())&&(T.transition&&e.fn.transition!==i&&D.transition("is supported")?(S.remove.active(),D.transition({debug:T.debug,animation:T.transition+" out",queue:T.queue,duration:T.duration,useFailSafe:!0,onStart:function(){S.others.active()||n||S.hideDimmer(),T.keyboardShortcuts&&S.remove.keyboardShortcuts()},onComplete:function(){T.onHidden.call(z),S.restore.focus(),t()}})):S.error(P.noTransition)))},showDimmer:function(){y.dimmer("is animating")||!y.dimmer("is active")?(S.debug("Showing dimmer"),y.dimmer("show")):S.debug("Dimmer already visible")},hideDimmer:function(){return y.dimmer("is animating")||y.dimmer("is active")?void y.dimmer("hide",function(){S.remove.clickaway(),S.remove.screenHeight()}):void S.debug("Dimmer is not visible cannot hide")},hideAll:function(t){var n=r.filter("."+R.active+", ."+R.animating);t=e.isFunction(t)?t:function(){},n.length>0&&(S.debug("Hiding all visible modals"),S.hideDimmer(),n.modal("hide modal",t))},hideOthers:function(t){var n=v.filter("."+R.active+", ."+R.animating);t=e.isFunction(t)?t:function(){},n.length>0&&(S.debug("Hiding other modals",v),n.modal("hide modal",t,!0))},others:{active:function(){return v.filter("."+R.active).length>0},animating:function(){return v.filter("."+R.animating).length>0}},add:{keyboardShortcuts:function(){S.verbose("Adding keyboard shortcuts"),l.on("keyup"+F,S.event.keyboard)}},save:{focus:function(){b=e(n.activeElement).blur()}},restore:{focus:function(){b&&b.length>0&&b.focus()}},remove:{active:function(){D.removeClass(R.active)},clickaway:function(){T.closable&&x.off("click"+C)},bodyStyle:function(){""===c.attr("style")&&(S.verbose("Removing style attribute"),c.removeAttr("style"))},screenHeight:function(){S.debug("Removing page height"),c.css("height","")},keyboardShortcuts:function(){S.verbose("Removing keyboard shortcuts"),l.off("keyup"+F)},scrolling:function(){y.removeClass(R.scrolling),D.removeClass(R.scrolling)}},cacheSizes:function(){var o=D.outerHeight();S.cache!==i&&0===o||(S.cache={pageHeight:e(n).outerHeight(),height:o+T.offset,contextHeight:"body"==T.context?e(t).height():y.height()}),S.debug("Caching modal and container sizes",S.cache)},can:{fit:function(){return S.cache.height+2*T.padding<S.cache.contextHeight}},is:{active:function(){return D.hasClass(R.active)},animating:function(){return D.transition("is supported")?D.transition("is animating"):D.is(":visible")},scrolling:function(){return y.hasClass(R.scrolling)},modernBrowser:function(){return!(t.ActiveXObject||"ActiveXObject"in t)}},set:{autofocus:function(){var e=D.find("[tabindex], :input").filter(":visible"),t=e.filter("[autofocus]"),n=t.length>0?t.first():e.first();n.length>0&&n.focus()},clickaway:function(){T.closable&&x.on("click"+C,S.event.click)},screenHeight:function(){S.can.fit()?c.css("height",""):(S.debug("Modal is taller than page content, resizing page height"),c.css("height",S.cache.height+2*T.padding))},active:function(){D.addClass(R.active)},scrolling:function(){y.addClass(R.scrolling),D.addClass(R.scrolling)},type:function(){S.can.fit()?(S.verbose("Modal fits on screen"),S.others.active()||S.others.animating()||S.remove.scrolling()):(S.verbose("Modal cannot fit on screen setting to scrolling"),S.set.scrolling())},position:function(){S.verbose("Centering modal on page",S.cache),S.can.fit()?D.css({top:"",marginTop:-(S.cache.height/2)}):D.css({marginTop:"",top:l.scrollTop()})},undetached:function(){y.addClass(R.undetached)}},setting:function(t,n){if(S.debug("Changing setting",t,n),e.isPlainObject(t))e.extend(!0,T,t);else{if(n===i)return T[t];e.isPlainObject(T[t])?e.extend(!0,T[t],n):T[t]=n}},internal:function(t,n){if(e.isPlainObject(t))e.extend(!0,S,t);else{if(n===i)return S[t];S[t]=n}},debug:function(){!T.silent&&T.debug&&(T.performance?S.performance.log(arguments):(S.debug=Function.prototype.bind.call(console.info,console,T.name+":"),S.debug.apply(console,arguments)))},verbose:function(){!T.silent&&T.verbose&&T.debug&&(T.performance?S.performance.log(arguments):(S.verbose=Function.prototype.bind.call(console.info,console,T.name+":"),S.verbose.apply(console,arguments)))},error:function(){T.silent||(S.error=Function.prototype.bind.call(console.error,console,T.name+":"),S.error.apply(console,arguments))},performance:{log:function(e){var t,n,i;T.performance&&(t=(new Date).getTime(),i=d||t,n=t-i,d=t,f.push({Name:e[0],Arguments:[].slice.call(e,1)||"",Element:z,"Execution Time":n})),clearTimeout(S.performance.timer),S.performance.timer=setTimeout(S.performance.display,500)},display:function(){var t=T.name+":",n=0;d=!1,clearTimeout(S.performance.timer),e.each(f,function(e,t){n+=t["Execution Time"]}),t+=" "+n+"ms",u&&(t+=" '"+u+"'"),(console.group!==i||console.table!==i)&&f.length>0&&(console.groupCollapsed(t),console.table?console.table(f):e.each(f,function(e,t){console.log(t.Name+": "+t["Execution Time"]+"ms")}),console.groupEnd()),f=[]}},invoke:function(t,n,o){var r,s,l,c=M;return n=n||p,o=z||o,"string"==typeof t&&c!==i&&(t=t.split(/[\. ]/),r=t.length-1,e.each(t,function(n,o){var a=n!=r?o+t[n+1].charAt(0).toUpperCase()+t[n+1].slice(1):t;if(e.isPlainObject(c[a])&&n!=r)c=c[a];else{if(c[a]!==i)return s=c[a],!1;if(!e.isPlainObject(c[o])||n==r)return c[o]!==i&&(s=c[o],!1);c=c[o]}})),e.isFunction(s)?l=s.apply(o,n):s!==i&&(l=s),e.isArray(a)?a.push(l):a!==i?a=[a,l]:l!==i&&(a=l),s}},g?(M===i&&S.initialize(),S.invoke(m)):(M!==i&&M.invoke("destroy"),S.initialize())}),a!==i?a:this},e.fn.modal.settings={name:"Modal",namespace:"modal",silent:!1,debug:!1,verbose:!1,performance:!0,observeChanges:!1,allowMultiple:!1,detachable:!0,closable:!0,autofocus:!0,inverted:!1,blurring:!1,dimmerSettings:{closable:!1,useCSS:!0},keyboardShortcuts:!0,context:"body",queue:!1,duration:500,offset:0,transition:"scale",padding:50,onShow:function(){},onVisible:function(){},onHide:function(){return!0},onHidden:function(){},onApprove:function(){return!0},onDeny:function(){return!0},selector:{close:"> .close",approve:".actions .positive, .actions .approve, .actions .ok",deny:".actions .negative, .actions .deny, .actions .cancel",modal:".ui.modal"},error:{dimmer:"UI Dimmer, a required component is not included in this page",method:"The method you called is not defined.",notFound:"The element you specified could not be found"},className:{active:"active",animating:"animating",blurring:"blurring",scrolling:"scrolling",undetached:"undetached"}}}(jQuery,window,document),function(e,t,n,i){"use strict";t="undefined"!=typeof t&&t.Math==Math?t:"undefined"!=typeof self&&self.Math==Math?self:Function("return this")(),e.fn.nag=function(n){var o,a=e(this),r=a.selector||"",s=(new Date).getTime(),l=[],c=arguments[0],u="string"==typeof c,d=[].slice.call(arguments,1);return a.each(function(){var a,f=e.isPlainObject(n)?e.extend(!0,{},e.fn.nag.settings,n):e.extend({},e.fn.nag.settings),m=(f.className,f.selector),g=f.error,p=f.namespace,h="."+p,v=p+"-module",b=e(this),y=(b.find(m.close),e(f.context?f.context:"body")),x=this,C=b.data(v);t.requestAnimationFrame||t.mozRequestAnimationFrame||t.webkitRequestAnimationFrame||t.msRequestAnimationFrame||function(e){setTimeout(e,0)};a={initialize:function(){a.verbose("Initializing element"),b.on("click"+h,m.close,a.dismiss).data(v,a),f.detachable&&b.parent()[0]!==y[0]&&b.detach().prependTo(y),f.displayTime>0&&setTimeout(a.hide,f.displayTime),a.show()},destroy:function(){a.verbose("Destroying instance"),b.removeData(v).off(h)},show:function(){a.should.show()&&!b.is(":visible")&&(a.debug("Showing nag",f.animation.show),"fade"==f.animation.show?b.fadeIn(f.duration,f.easing):b.slideDown(f.duration,f.easing))},hide:function(){a.debug("Showing nag",f.animation.hide),"fade"==f.animation.show?b.fadeIn(f.duration,f.easing):b.slideUp(f.duration,f.easing)},onHide:function(){a.debug("Removing nag",f.animation.hide),b.remove(),f.onHide&&f.onHide()},dismiss:function(e){f.storageMethod&&a.storage.set(f.key,f.value),a.hide(),e.stopImmediatePropagation(),e.preventDefault()},should:{show:function(){return f.persist?(a.debug("Persistent nag is set, can show nag"),!0):a.storage.get(f.key)!=f.value.toString()?(a.debug("Stored value is not set, can show nag",a.storage.get(f.key)),!0):(a.debug("Stored value is set, cannot show nag",a.storage.get(f.key)),!1)}},get:{storageOptions:function(){var e={};return f.expires&&(e.expires=f.expires),f.domain&&(e.domain=f.domain),f.path&&(e.path=f.path),e}},clear:function(){a.storage.remove(f.key)},storage:{set:function(n,o){var r=a.get.storageOptions();if("localstorage"==f.storageMethod&&t.localStorage!==i)t.localStorage.setItem(n,o),a.debug("Value stored using local storage",n,o);else if("sessionstorage"==f.storageMethod&&t.sessionStorage!==i)t.sessionStorage.setItem(n,o),a.debug("Value stored using session storage",n,o);else{if(e.cookie===i)return void a.error(g.noCookieStorage);e.cookie(n,o,r),a.debug("Value stored using cookie",n,o,r)}},get:function(n,o){var r;return"localstorage"==f.storageMethod&&t.localStorage!==i?r=t.localStorage.getItem(n):"sessionstorage"==f.storageMethod&&t.sessionStorage!==i?r=t.sessionStorage.getItem(n):e.cookie!==i?r=e.cookie(n):a.error(g.noCookieStorage),"undefined"!=r&&"null"!=r&&r!==i&&null!==r||(r=i),r},remove:function(n){var o=a.get.storageOptions();"localstorage"==f.storageMethod&&t.localStorage!==i?t.localStorage.removeItem(n):"sessionstorage"==f.storageMethod&&t.sessionStorage!==i?t.sessionStorage.removeItem(n):e.cookie!==i?e.removeCookie(n,o):a.error(g.noStorage)}},setting:function(t,n){if(a.debug("Changing setting",t,n),e.isPlainObject(t))e.extend(!0,f,t);else{if(n===i)return f[t];e.isPlainObject(f[t])?e.extend(!0,f[t],n):f[t]=n}},internal:function(t,n){if(e.isPlainObject(t))e.extend(!0,a,t);else{if(n===i)return a[t];a[t]=n}},debug:function(){!f.silent&&f.debug&&(f.performance?a.performance.log(arguments):(a.debug=Function.prototype.bind.call(console.info,console,f.name+":"),a.debug.apply(console,arguments)))},verbose:function(){!f.silent&&f.verbose&&f.debug&&(f.performance?a.performance.log(arguments):(a.verbose=Function.prototype.bind.call(console.info,console,f.name+":"),a.verbose.apply(console,arguments)))},error:function(){f.silent||(a.error=Function.prototype.bind.call(console.error,console,f.name+":"),a.error.apply(console,arguments))},performance:{log:function(e){var t,n,i;f.performance&&(t=(new Date).getTime(),i=s||t,n=t-i,s=t,l.push({Name:e[0],Arguments:[].slice.call(e,1)||"",Element:x,"Execution Time":n})),clearTimeout(a.performance.timer),a.performance.timer=setTimeout(a.performance.display,500)},display:function(){var t=f.name+":",n=0;s=!1,clearTimeout(a.performance.timer),e.each(l,function(e,t){n+=t["Execution Time"]}),t+=" "+n+"ms",r&&(t+=" '"+r+"'"),(console.group!==i||console.table!==i)&&l.length>0&&(console.groupCollapsed(t),console.table?console.table(l):e.each(l,function(e,t){console.log(t.Name+": "+t["Execution Time"]+"ms")}),console.groupEnd()),l=[]}},invoke:function(t,n,r){var s,l,c,u=C;return n=n||d,r=x||r,"string"==typeof t&&u!==i&&(t=t.split(/[\. ]/),s=t.length-1,e.each(t,function(n,o){var r=n!=s?o+t[n+1].charAt(0).toUpperCase()+t[n+1].slice(1):t;if(e.isPlainObject(u[r])&&n!=s)u=u[r];else{if(u[r]!==i)return l=u[r],!1;if(!e.isPlainObject(u[o])||n==s)return u[o]!==i?(l=u[o],!1):(a.error(g.method,t),!1);u=u[o]}})),e.isFunction(l)?c=l.apply(r,n):l!==i&&(c=l),e.isArray(o)?o.push(c):o!==i?o=[o,c]:c!==i&&(o=c),l}},u?(C===i&&a.initialize(),a.invoke(c)):(C!==i&&C.invoke("destroy"),a.initialize())}),o!==i?o:this},e.fn.nag.settings={name:"Nag",silent:!1,debug:!1,verbose:!1,performance:!0,namespace:"Nag",persist:!1,displayTime:0,animation:{show:"slide",hide:"slide"},context:!1,detachable:!1,expires:30,domain:!1,path:"/",storageMethod:"cookie",key:"nag",value:"dismiss",error:{noCookieStorage:"$.cookie is not included. A storage solution is required.",noStorage:"Neither $.cookie or store is defined. A storage solution is required for storing state",method:"The method you called is not defined."},className:{bottom:"bottom",fixed:"fixed"},selector:{close:".close.icon"},speed:500,easing:"easeOutQuad",onHide:function(){}},e.extend(e.easing,{easeOutQuad:function(e,t,n,i,o){return-i*(t/=o)*(t-2)+n}})}(jQuery,window,document),function(e,t,n,i){"use strict";t="undefined"!=typeof t&&t.Math==Math?t:"undefined"!=typeof self&&self.Math==Math?self:Function("return this")(),e.fn.popup=function(o){var a,r=e(this),s=e(n),l=e(t),c=e("body"),u=r.selector||"",d=!0,f=(new Date).getTime(),m=[],g=arguments[0],p="string"==typeof g,h=[].slice.call(arguments,1);return r.each(function(){var r,v,b,y,x,C,w=e.isPlainObject(o)?e.extend(!0,{},e.fn.popup.settings,o):e.extend({},e.fn.popup.settings),k=w.selector,S=w.className,T=w.error,A=w.metadata,R=w.namespace,E="."+w.namespace,P="module-"+R,F=e(this),O=e(w.context),D=e(w.scrollContext),q=e(w.boundary),j=w.target?e(w.target):F,z=0,M=!1,I=!1,L=this,N=F.data(P);C={initialize:function(){C.debug("Initializing",F),C.createID(),C.bind.events(),!C.exists()&&w.preserve&&C.create(),w.observeChanges&&C.observeChanges(),C.instantiate()},instantiate:function(){C.verbose("Storing instance",C),N=C,F.data(P,N)},observeChanges:function(){"MutationObserver"in t&&(b=new MutationObserver(C.event.documentChanged),b.observe(n,{childList:!0,subtree:!0}),C.debug("Setting up mutation observer",b))},refresh:function(){w.popup?r=e(w.popup).eq(0):w.inline&&(r=j.nextAll(k.popup).eq(0),w.popup=r),w.popup?(r.addClass(S.loading),v=C.get.offsetParent(),r.removeClass(S.loading),w.movePopup&&C.has.popup()&&C.get.offsetParent(r)[0]!==v[0]&&(C.debug("Moving popup to the same offset parent as activating element"),r.detach().appendTo(v))):v=w.inline?C.get.offsetParent(j):C.has.popup()?C.get.offsetParent(r):c,v.is("html")&&v[0]!==c[0]&&(C.debug("Setting page as offset parent"),v=c),C.get.variation()&&C.set.variation()},reposition:function(){C.refresh(),C.set.position()},destroy:function(){C.debug("Destroying previous module"),b&&b.disconnect(),r&&!w.preserve&&C.removePopup(),clearTimeout(C.hideTimer),clearTimeout(C.showTimer),C.unbind.close(),C.unbind.events(),F.removeData(P)},event:{start:function(t){var n=e.isPlainObject(w.delay)?w.delay.show:w.delay;clearTimeout(C.hideTimer),I||(C.showTimer=setTimeout(C.show,n))},end:function(){var t=e.isPlainObject(w.delay)?w.delay.hide:w.delay;clearTimeout(C.showTimer),C.hideTimer=setTimeout(C.hide,t)},touchstart:function(e){I=!0,C.show()},resize:function(){C.is.visible()&&C.set.position()},documentChanged:function(t){[].forEach.call(t,function(t){t.removedNodes&&[].forEach.call(t.removedNodes,function(t){(t==L||e(t).find(L).length>0)&&(C.debug("Element removed from DOM, tearing down events"),C.destroy())})})},hideGracefully:function(t){var i=e(t.target),o=e.contains(n.documentElement,t.target),a=i.closest(k.popup).length>0;t&&!a&&o?(C.debug("Click occurred outside popup hiding popup"),C.hide()):C.debug("Click was inside popup, keeping popup open")}},create:function(){var t=C.get.html(),n=C.get.title(),i=C.get.content();t||i||n?(C.debug("Creating pop-up html"),t||(t=w.templates.popup({title:n,content:i})),r=e("<div/>").addClass(S.popup).data(A.activator,F).html(t),w.inline?(C.verbose("Inserting popup element inline",r),r.insertAfter(F)):(C.verbose("Appending popup element to body",r),r.appendTo(O)),C.refresh(),C.set.variation(),w.hoverable&&C.bind.popup(),w.onCreate.call(r,L)):0!==j.next(k.popup).length?(C.verbose("Pre-existing popup found"),w.inline=!0,w.popup=j.next(k.popup).data(A.activator,F),C.refresh(),w.hoverable&&C.bind.popup()):w.popup?(e(w.popup).data(A.activator,F),C.verbose("Used popup specified in settings"),C.refresh(),w.hoverable&&C.bind.popup()):C.debug("No content specified skipping display",L)},createID:function(){x=(Math.random().toString(16)+"000000000").substr(2,8),y="."+x,C.verbose("Creating unique id for element",x)},toggle:function(){C.debug("Toggling pop-up"),C.is.hidden()?(C.debug("Popup is hidden, showing pop-up"),C.unbind.close(),C.show()):(C.debug("Popup is visible, hiding pop-up"), +C.hide())},show:function(e){if(e=e||function(){},C.debug("Showing pop-up",w.transition),C.is.hidden()&&(!C.is.active()||!C.is.dropdown())){if(C.exists()||C.create(),w.onShow.call(r,L)===!1)return void C.debug("onShow callback returned false, cancelling popup animation");w.preserve||w.popup||C.refresh(),r&&C.set.position()&&(C.save.conditions(),w.exclusive&&C.hideAll(),C.animate.show(e))}},hide:function(e){if(e=e||function(){},C.is.visible()||C.is.animating()){if(w.onHide.call(r,L)===!1)return void C.debug("onHide callback returned false, cancelling popup animation");C.remove.visible(),C.unbind.close(),C.restore.conditions(),C.animate.hide(e)}},hideAll:function(){e(k.popup).filter("."+S.visible).each(function(){e(this).data(A.activator).popup("hide")})},exists:function(){return!!r&&(w.inline||w.popup?C.has.popup():r.closest(O).length>=1)},removePopup:function(){C.has.popup()&&!w.popup&&(C.debug("Removing popup",r),r.remove(),r=i,w.onRemove.call(r,L))},save:{conditions:function(){C.cache={title:F.attr("title")},C.cache.title&&F.removeAttr("title"),C.verbose("Saving original attributes",C.cache.title)}},restore:{conditions:function(){return C.cache&&C.cache.title&&(F.attr("title",C.cache.title),C.verbose("Restoring original attributes",C.cache.title)),!0}},supports:{svg:function(){return typeof SVGGraphicsElement===i}},animate:{show:function(t){t=e.isFunction(t)?t:function(){},w.transition&&e.fn.transition!==i&&F.transition("is supported")?(C.set.visible(),r.transition({animation:w.transition+" in",queue:!1,debug:w.debug,verbose:w.verbose,duration:w.duration,onComplete:function(){C.bind.close(),t.call(r,L),w.onVisible.call(r,L)}})):C.error(T.noTransition)},hide:function(t){return t=e.isFunction(t)?t:function(){},C.debug("Hiding pop-up"),w.onHide.call(r,L)===!1?void C.debug("onHide callback returned false, cancelling popup animation"):void(w.transition&&e.fn.transition!==i&&F.transition("is supported")?r.transition({animation:w.transition+" out",queue:!1,duration:w.duration,debug:w.debug,verbose:w.verbose,onComplete:function(){C.reset(),t.call(r,L),w.onHidden.call(r,L)}}):C.error(T.noTransition))}},change:{content:function(e){r.html(e)}},get:{html:function(){return F.removeData(A.html),F.data(A.html)||w.html},title:function(){return F.removeData(A.title),F.data(A.title)||w.title},content:function(){return F.removeData(A.content),F.data(A.content)||F.attr("title")||w.content},variation:function(){return F.removeData(A.variation),F.data(A.variation)||w.variation},popup:function(){return r},popupOffset:function(){return r.offset()},calculations:function(){var e,n=j[0],i=q[0]==t,o=w.inline||w.popup&&w.movePopup?j.position():j.offset(),a=i?{top:0,left:0}:q.offset(),s={},c=i?{top:l.scrollTop(),left:l.scrollLeft()}:{top:0,left:0};return s={target:{element:j[0],width:j.outerWidth(),height:j.outerHeight(),top:o.top,left:o.left,margin:{}},popup:{width:r.outerWidth(),height:r.outerHeight()},parent:{width:v.outerWidth(),height:v.outerHeight()},screen:{top:a.top,left:a.left,scroll:{top:c.top,left:c.left},width:q.width(),height:q.height()}},w.setFluidWidth&&C.is.fluid()&&(s.container={width:r.parent().outerWidth()},s.popup.width=s.container.width),s.target.margin.top=w.inline?parseInt(t.getComputedStyle(n).getPropertyValue("margin-top"),10):0,s.target.margin.left=w.inline?C.is.rtl()?parseInt(t.getComputedStyle(n).getPropertyValue("margin-right"),10):parseInt(t.getComputedStyle(n).getPropertyValue("margin-left"),10):0,e=s.screen,s.boundary={top:e.top+e.scroll.top,bottom:e.top+e.scroll.top+e.height,left:e.left+e.scroll.left,right:e.left+e.scroll.left+e.width},s},id:function(){return x},startEvent:function(){return"hover"==w.on?"mouseenter":"focus"==w.on&&"focus"},scrollEvent:function(){return"scroll"},endEvent:function(){return"hover"==w.on?"mouseleave":"focus"==w.on&&"blur"},distanceFromBoundary:function(e,t){var n,i,o={};return t=t||C.get.calculations(),n=t.popup,i=t.boundary,e&&(o={top:e.top-i.top,left:e.left-i.left,right:i.right-(e.left+n.width),bottom:i.bottom-(e.top+n.height)},C.verbose("Distance from boundaries determined",e,o)),o},offsetParent:function(t){var n=t!==i?t[0]:F[0],o=n.parentNode,a=e(o);if(o)for(var r="none"===a.css("transform"),s="static"===a.css("position"),l=a.is("html");o&&!l&&s&&r;)o=o.parentNode,a=e(o),r="none"===a.css("transform"),s="static"===a.css("position"),l=a.is("html");return a&&a.length>0?a:e()},positions:function(){return{"top left":!1,"top center":!1,"top right":!1,"bottom left":!1,"bottom center":!1,"bottom right":!1,"left center":!1,"right center":!1}},nextPosition:function(e){var t=e.split(" "),n=t[0],i=t[1],o={top:"bottom",bottom:"top",left:"right",right:"left"},a={left:"center",center:"right",right:"left"},r={"top left":"top center","top center":"top right","top right":"right center","right center":"bottom right","bottom right":"bottom center","bottom center":"bottom left","bottom left":"left center","left center":"top left"},s="top"==n||"bottom"==n,l=!1,c=!1,u=!1;return M||(C.verbose("All available positions available"),M=C.get.positions()),C.debug("Recording last position tried",e),M[e]=!0,"opposite"===w.prefer&&(u=[o[n],i],u=u.join(" "),l=M[u]===!0,C.debug("Trying opposite strategy",u)),"adjacent"===w.prefer&&s&&(u=[n,a[i]],u=u.join(" "),c=M[u]===!0,C.debug("Trying adjacent strategy",u)),(c||l)&&(C.debug("Using backup position",u),u=r[e]),u}},set:{position:function(e,t){if(0===j.length||0===r.length)return void C.error(T.notFound);var n,o,a,s,l,c,u,d;if(t=t||C.get.calculations(),e=e||F.data(A.position)||w.position,n=F.data(A.offset)||w.offset,o=w.distanceAway,a=t.target,s=t.popup,l=t.parent,0===a.width&&0===a.height&&!C.is.svg(a.element))return C.debug("Popup target is hidden, no action taken"),!1;switch(w.inline&&(C.debug("Adding margin to calculation",a.margin),"left center"==e||"right center"==e?(n+=a.margin.top,o+=-a.margin.left):"top left"==e||"top center"==e||"top right"==e?(n+=a.margin.left,o-=a.margin.top):(n+=a.margin.left,o+=a.margin.top)),C.debug("Determining popup position from calculations",e,t),C.is.rtl()&&(e=e.replace(/left|right/g,function(e){return"left"==e?"right":"left"}),C.debug("RTL: Popup position updated",e)),z==w.maxSearchDepth&&"string"==typeof w.lastResort&&(e=w.lastResort),e){case"top left":c={top:"auto",bottom:l.height-a.top+o,left:a.left+n,right:"auto"};break;case"top center":c={bottom:l.height-a.top+o,left:a.left+a.width/2-s.width/2+n,top:"auto",right:"auto"};break;case"top right":c={bottom:l.height-a.top+o,right:l.width-a.left-a.width-n,top:"auto",left:"auto"};break;case"left center":c={top:a.top+a.height/2-s.height/2+n,right:l.width-a.left+o,left:"auto",bottom:"auto"};break;case"right center":c={top:a.top+a.height/2-s.height/2+n,left:a.left+a.width+o,bottom:"auto",right:"auto"};break;case"bottom left":c={top:a.top+a.height+o,left:a.left+n,bottom:"auto",right:"auto"};break;case"bottom center":c={top:a.top+a.height+o,left:a.left+a.width/2-s.width/2+n,bottom:"auto",right:"auto"};break;case"bottom right":c={top:a.top+a.height+o,right:l.width-a.left-a.width-n,left:"auto",bottom:"auto"}}if(c===i&&C.error(T.invalidPosition,e),C.debug("Calculated popup positioning values",c),r.css(c).removeClass(S.position).addClass(e).addClass(S.loading),u=C.get.popupOffset(),d=C.get.distanceFromBoundary(u,t),C.is.offstage(d,e)){if(C.debug("Position is outside viewport",e),z<w.maxSearchDepth)return z++,e=C.get.nextPosition(e),C.debug("Trying new position",e),!!r&&C.set.position(e,t);if(!w.lastResort)return C.debug("Popup could not find a position to display",r),C.error(T.cannotPlace,L),C.remove.attempts(),C.remove.loading(),C.reset(),w.onUnplaceable.call(r,L),!1;C.debug("No position found, showing with last position")}return C.debug("Position is on stage",e),C.remove.attempts(),C.remove.loading(),w.setFluidWidth&&C.is.fluid()&&C.set.fluidWidth(t),!0},fluidWidth:function(e){e=e||C.get.calculations(),C.debug("Automatically setting element width to parent width",e.parent.width),r.css("width",e.container.width)},variation:function(e){e=e||C.get.variation(),e&&C.has.popup()&&(C.verbose("Adding variation to popup",e),r.addClass(e))},visible:function(){F.addClass(S.visible)}},remove:{loading:function(){r.removeClass(S.loading)},variation:function(e){e=e||C.get.variation(),e&&(C.verbose("Removing variation",e),r.removeClass(e))},visible:function(){F.removeClass(S.visible)},attempts:function(){C.verbose("Resetting all searched positions"),z=0,M=!1}},bind:{events:function(){C.debug("Binding popup events to module"),"click"==w.on&&F.on("click"+E,C.toggle),"hover"==w.on&&d&&F.on("touchstart"+E,C.event.touchstart),C.get.startEvent()&&F.on(C.get.startEvent()+E,C.event.start).on(C.get.endEvent()+E,C.event.end),w.target&&C.debug("Target set to element",j),l.on("resize"+y,C.event.resize)},popup:function(){C.verbose("Allowing hover events on popup to prevent closing"),r&&C.has.popup()&&r.on("mouseenter"+E,C.event.start).on("mouseleave"+E,C.event.end)},close:function(){(w.hideOnScroll===!0||"auto"==w.hideOnScroll&&"click"!=w.on)&&D.one(C.get.scrollEvent()+y,C.event.hideGracefully),"hover"==w.on&&I&&(C.verbose("Binding popup close event to document"),s.on("touchstart"+y,function(e){C.verbose("Touched away from popup"),C.event.hideGracefully.call(L,e)})),"click"==w.on&&w.closable&&(C.verbose("Binding popup close event to document"),s.on("click"+y,function(e){C.verbose("Clicked away from popup"),C.event.hideGracefully.call(L,e)}))}},unbind:{events:function(){l.off(y),F.off(E)},close:function(){s.off(y),D.off(y)}},has:{popup:function(){return r&&r.length>0}},is:{offstage:function(t,n){var i=[];return e.each(t,function(e,t){t<-w.jitter&&(C.debug("Position exceeds allowable distance from edge",e,t,n),i.push(e))}),i.length>0},svg:function(e){return C.supports.svg()&&e instanceof SVGGraphicsElement},active:function(){return F.hasClass(S.active)},animating:function(){return r!==i&&r.hasClass(S.animating)},fluid:function(){return r!==i&&r.hasClass(S.fluid)},visible:function(){return r!==i&&r.hasClass(S.visible)},dropdown:function(){return F.hasClass(S.dropdown)},hidden:function(){return!C.is.visible()},rtl:function(){return"rtl"==F.css("direction")}},reset:function(){C.remove.visible(),w.preserve?e.fn.transition!==i&&r.transition("remove transition"):C.removePopup()},setting:function(t,n){if(e.isPlainObject(t))e.extend(!0,w,t);else{if(n===i)return w[t];w[t]=n}},internal:function(t,n){if(e.isPlainObject(t))e.extend(!0,C,t);else{if(n===i)return C[t];C[t]=n}},debug:function(){!w.silent&&w.debug&&(w.performance?C.performance.log(arguments):(C.debug=Function.prototype.bind.call(console.info,console,w.name+":"),C.debug.apply(console,arguments)))},verbose:function(){!w.silent&&w.verbose&&w.debug&&(w.performance?C.performance.log(arguments):(C.verbose=Function.prototype.bind.call(console.info,console,w.name+":"),C.verbose.apply(console,arguments)))},error:function(){w.silent||(C.error=Function.prototype.bind.call(console.error,console,w.name+":"),C.error.apply(console,arguments))},performance:{log:function(e){var t,n,i;w.performance&&(t=(new Date).getTime(),i=f||t,n=t-i,f=t,m.push({Name:e[0],Arguments:[].slice.call(e,1)||"",Element:L,"Execution Time":n})),clearTimeout(C.performance.timer),C.performance.timer=setTimeout(C.performance.display,500)},display:function(){var t=w.name+":",n=0;f=!1,clearTimeout(C.performance.timer),e.each(m,function(e,t){n+=t["Execution Time"]}),t+=" "+n+"ms",u&&(t+=" '"+u+"'"),(console.group!==i||console.table!==i)&&m.length>0&&(console.groupCollapsed(t),console.table?console.table(m):e.each(m,function(e,t){console.log(t.Name+": "+t["Execution Time"]+"ms")}),console.groupEnd()),m=[]}},invoke:function(t,n,o){var r,s,l,c=N;return n=n||h,o=L||o,"string"==typeof t&&c!==i&&(t=t.split(/[\. ]/),r=t.length-1,e.each(t,function(n,o){var a=n!=r?o+t[n+1].charAt(0).toUpperCase()+t[n+1].slice(1):t;if(e.isPlainObject(c[a])&&n!=r)c=c[a];else{if(c[a]!==i)return s=c[a],!1;if(!e.isPlainObject(c[o])||n==r)return c[o]!==i&&(s=c[o],!1);c=c[o]}})),e.isFunction(s)?l=s.apply(o,n):s!==i&&(l=s),e.isArray(a)?a.push(l):a!==i?a=[a,l]:l!==i&&(a=l),s}},p?(N===i&&C.initialize(),C.invoke(g)):(N!==i&&N.invoke("destroy"),C.initialize())}),a!==i?a:this},e.fn.popup.settings={name:"Popup",silent:!1,debug:!1,verbose:!1,performance:!0,namespace:"popup",observeChanges:!0,onCreate:function(){},onRemove:function(){},onShow:function(){},onVisible:function(){},onHide:function(){},onUnplaceable:function(){},onHidden:function(){},on:"hover",boundary:t,addTouchEvents:!0,position:"top left",variation:"",movePopup:!0,target:!1,popup:!1,inline:!1,preserve:!1,hoverable:!1,content:!1,html:!1,title:!1,closable:!0,hideOnScroll:"auto",exclusive:!1,context:"body",scrollContext:t,prefer:"opposite",lastResort:!1,delay:{show:50,hide:70},setFluidWidth:!0,duration:200,transition:"scale",distanceAway:0,jitter:2,offset:0,maxSearchDepth:15,error:{invalidPosition:"The position you specified is not a valid position",cannotPlace:"Popup does not fit within the boundaries of the viewport",method:"The method you called is not defined.",noTransition:"This module requires ui transitions <https://github.com/Semantic-Org/UI-Transition>",notFound:"The target or popup you specified does not exist on the page"},metadata:{activator:"activator",content:"content",html:"html",offset:"offset",position:"position",title:"title",variation:"variation"},className:{active:"active",animating:"animating",dropdown:"dropdown",fluid:"fluid",loading:"loading",popup:"ui popup",position:"top left center bottom right",visible:"visible"},selector:{popup:".ui.popup"},templates:{escape:function(e){var t=/[&<>"'`]/g,n=/[&<>"'`]/,i={"&":"&","<":"<",">":">",'"':""","'":"'","`":"`"},o=function(e){return i[e]};return n.test(e)?e.replace(t,o):e},popup:function(t){var n="",o=e.fn.popup.settings.templates.escape;return typeof t!==i&&(typeof t.title!==i&&t.title&&(t.title=o(t.title),n+='<div class="header">'+t.title+"</div>"),typeof t.content!==i&&t.content&&(t.content=o(t.content),n+='<div class="content">'+t.content+"</div>")),n}}}}(jQuery,window,document),function(e,t,n,i){"use strict";t="undefined"!=typeof t&&t.Math==Math?t:"undefined"!=typeof self&&self.Math==Math?self:Function("return this")();"undefined"!=typeof t&&t.Math==Math?t:"undefined"!=typeof self&&self.Math==Math?self:Function("return this")();e.fn.progress=function(t){var o,a=e(this),r=a.selector||"",s=(new Date).getTime(),l=[],c=arguments[0],u="string"==typeof c,d=[].slice.call(arguments,1);return a.each(function(){var a,f,m=e.isPlainObject(t)?e.extend(!0,{},e.fn.progress.settings,t):e.extend({},e.fn.progress.settings),g=m.className,p=m.metadata,h=m.namespace,v=m.selector,b=m.error,y="."+h,x="module-"+h,C=e(this),w=e(this).find(v.bar),k=e(this).find(v.progress),S=e(this).find(v.label),T=this,A=C.data(x),R=!1;f={initialize:function(){f.debug("Initializing progress bar",m),f.set.duration(),f.set.transitionEvent(),f.read.metadata(),f.read.settings(),f.instantiate()},instantiate:function(){f.verbose("Storing instance of progress",f),A=f,C.data(x,f)},destroy:function(){f.verbose("Destroying previous progress for",C),clearInterval(A.interval),f.remove.state(),C.removeData(x),A=i},reset:function(){f.remove.nextValue(),f.update.progress(0)},complete:function(){(f.percent===i||f.percent<100)&&(f.remove.progressPoll(),f.set.percent(100))},read:{metadata:function(){var e={percent:C.data(p.percent),total:C.data(p.total),value:C.data(p.value)};e.percent&&(f.debug("Current percent value set from metadata",e.percent),f.set.percent(e.percent)),e.total&&(f.debug("Total value set from metadata",e.total),f.set.total(e.total)),e.value&&(f.debug("Current value set from metadata",e.value),f.set.value(e.value),f.set.progress(e.value))},settings:function(){m.total!==!1&&(f.debug("Current total set in settings",m.total),f.set.total(m.total)),m.value!==!1&&(f.debug("Current value set in settings",m.value),f.set.value(m.value),f.set.progress(f.value)),m.percent!==!1&&(f.debug("Current percent set in settings",m.percent),f.set.percent(m.percent))}},bind:{transitionEnd:function(e){var t=f.get.transitionEnd();w.one(t+y,function(t){clearTimeout(f.failSafeTimer),e.call(this,t)}),f.failSafeTimer=setTimeout(function(){w.triggerHandler(t)},m.duration+m.failSafeDelay),f.verbose("Adding fail safe timer",f.timer)}},increment:function(e){var t,n,i;f.has.total()?(n=f.get.value(),e=e||1,i=n+e):(n=f.get.percent(),e=e||f.get.randomValue(),i=n+e,t=100,f.debug("Incrementing percentage by",n,i)),i=f.get.normalizedValue(i),f.set.progress(i)},decrement:function(e){var t,n,i=f.get.total();i?(t=f.get.value(),e=e||1,n=t-e,f.debug("Decrementing value by",e,t)):(t=f.get.percent(),e=e||f.get.randomValue(),n=t-e,f.debug("Decrementing percentage by",e,t)),n=f.get.normalizedValue(n),f.set.progress(n)},has:{progressPoll:function(){return f.progressPoll},total:function(){return f.get.total()!==!1}},get:{text:function(e){var t=f.value||0,n=f.total||0,i=R?f.get.displayPercent():f.percent||0,o=f.total>0?n-t:100-i;return e=e||"",e=e.replace("{value}",t).replace("{total}",n).replace("{left}",o).replace("{percent}",i),f.verbose("Adding variables to progress bar text",e),e},normalizedValue:function(e){if(e<0)return f.debug("Value cannot decrement below 0"),0;if(f.has.total()){if(e>f.total)return f.debug("Value cannot increment above total",f.total),f.total}else if(e>100)return f.debug("Value cannot increment above 100 percent"),100;return e},updateInterval:function(){return"auto"==m.updateInterval?m.duration:m.updateInterval},randomValue:function(){return f.debug("Generating random increment percentage"),Math.floor(Math.random()*m.random.max+m.random.min)},numericValue:function(e){return"string"==typeof e?""!==e.replace(/[^\d.]/g,"")&&+e.replace(/[^\d.]/g,""):e},transitionEnd:function(){var e,t=n.createElement("element"),o={transition:"transitionend",OTransition:"oTransitionEnd",MozTransition:"transitionend",WebkitTransition:"webkitTransitionEnd"};for(e in o)if(t.style[e]!==i)return o[e]},displayPercent:function(){var e=w.width(),t=C.width(),n=parseInt(w.css("min-width"),10),i=e>n?e/t*100:f.percent;return m.precision>0?Math.round(i*(10*m.precision))/(10*m.precision):Math.round(i)},percent:function(){return f.percent||0},value:function(){return f.nextValue||f.value||0},total:function(){return f.total||!1}},create:{progressPoll:function(){f.progressPoll=setTimeout(function(){f.update.toNextValue(),f.remove.progressPoll()},f.get.updateInterval())}},is:{complete:function(){return f.is.success()||f.is.warning()||f.is.error()},success:function(){return C.hasClass(g.success)},warning:function(){return C.hasClass(g.warning)},error:function(){return C.hasClass(g.error)},active:function(){return C.hasClass(g.active)},visible:function(){return C.is(":visible")}},remove:{progressPoll:function(){f.verbose("Removing progress poll timer"),f.progressPoll&&(clearTimeout(f.progressPoll),delete f.progressPoll)},nextValue:function(){f.verbose("Removing progress value stored for next update"),delete f.nextValue},state:function(){f.verbose("Removing stored state"),delete f.total,delete f.percent,delete f.value},active:function(){f.verbose("Removing active state"),C.removeClass(g.active)},success:function(){f.verbose("Removing success state"),C.removeClass(g.success)},warning:function(){f.verbose("Removing warning state"),C.removeClass(g.warning)},error:function(){f.verbose("Removing error state"),C.removeClass(g.error)}},set:{barWidth:function(e){e>100?f.error(b.tooHigh,e):e<0?f.error(b.tooLow,e):(w.css("width",e+"%"),C.attr("data-percent",parseInt(e,10)))},duration:function(e){e=e||m.duration,e="number"==typeof e?e+"ms":e,f.verbose("Setting progress bar transition duration",e),w.css({"transition-duration":e})},percent:function(e){e="string"==typeof e?+e.replace("%",""):e,e=m.precision>0?Math.round(e*(10*m.precision))/(10*m.precision):Math.round(e),f.percent=e,f.has.total()||(f.value=m.precision>0?Math.round(e/100*f.total*(10*m.precision))/(10*m.precision):Math.round(e/100*f.total*10)/10,m.limitValues&&(f.value=f.value>100?100:f.value<0?0:f.value)),f.set.barWidth(e),f.set.labelInterval(),f.set.labels(),m.onChange.call(T,e,f.value,f.total)},labelInterval:function(){var t=function(){f.verbose("Bar finished animating, removing continuous label updates"),clearInterval(f.interval),R=!1,f.set.labels()};clearInterval(f.interval),f.bind.transitionEnd(t),R=!0,f.interval=setInterval(function(){var t=e.contains(n.documentElement,T);t||(clearInterval(f.interval),R=!1),f.set.labels()},m.framerate)},labels:function(){f.verbose("Setting both bar progress and outer label text"),f.set.barLabel(),f.set.state()},label:function(e){e=e||"",e&&(e=f.get.text(e),f.verbose("Setting label to text",e),S.text(e))},state:function(e){e=e!==i?e:f.percent,100===e?m.autoSuccess&&!(f.is.warning()||f.is.error()||f.is.success())?(f.set.success(),f.debug("Automatically triggering success at 100%")):(f.verbose("Reached 100% removing active state"),f.remove.active(),f.remove.progressPoll()):e>0?(f.verbose("Adjusting active progress bar label",e),f.set.active()):(f.remove.active(),f.set.label(m.text.active))},barLabel:function(e){e!==i?k.text(f.get.text(e)):"ratio"==m.label&&f.total?(f.verbose("Adding ratio to bar label"),k.text(f.get.text(m.text.ratio))):"percent"==m.label&&(f.verbose("Adding percentage to bar label"),k.text(f.get.text(m.text.percent)))},active:function(e){e=e||m.text.active,f.debug("Setting active state"),m.showActivity&&!f.is.active()&&C.addClass(g.active),f.remove.warning(),f.remove.error(),f.remove.success(),e=m.onLabelUpdate("active",e,f.value,f.total),e&&f.set.label(e),f.bind.transitionEnd(function(){m.onActive.call(T,f.value,f.total)})},success:function(e){e=e||m.text.success||m.text.active,f.debug("Setting success state"),C.addClass(g.success),f.remove.active(),f.remove.warning(),f.remove.error(),f.complete(),m.text.success?(e=m.onLabelUpdate("success",e,f.value,f.total),f.set.label(e)):(e=m.onLabelUpdate("active",e,f.value,f.total),f.set.label(e)),f.bind.transitionEnd(function(){m.onSuccess.call(T,f.total)})},warning:function(e){e=e||m.text.warning,f.debug("Setting warning state"),C.addClass(g.warning),f.remove.active(),f.remove.success(),f.remove.error(),f.complete(),e=m.onLabelUpdate("warning",e,f.value,f.total),e&&f.set.label(e),f.bind.transitionEnd(function(){m.onWarning.call(T,f.value,f.total)})},error:function(e){e=e||m.text.error,f.debug("Setting error state"),C.addClass(g.error),f.remove.active(),f.remove.success(),f.remove.warning(),f.complete(),e=m.onLabelUpdate("error",e,f.value,f.total),e&&f.set.label(e),f.bind.transitionEnd(function(){m.onError.call(T,f.value,f.total)})},transitionEvent:function(){a=f.get.transitionEnd()},total:function(e){f.total=e},value:function(e){f.value=e},progress:function(e){f.has.progressPoll()?(f.debug("Updated within interval, setting next update to use new value",e),f.set.nextValue(e)):(f.debug("First update in progress update interval, immediately updating",e),f.update.progress(e),f.create.progressPoll())},nextValue:function(e){f.nextValue=e}},update:{toNextValue:function(){var e=f.nextValue;e&&(f.debug("Update interval complete using last updated value",e),f.update.progress(e),f.remove.nextValue())},progress:function(e){var t;e=f.get.numericValue(e),e===!1&&f.error(b.nonNumeric,e),e=f.get.normalizedValue(e),f.has.total()?(f.set.value(e),t=e/f.total*100,f.debug("Calculating percent complete from total",t),f.set.percent(t)):(t=e,f.debug("Setting value to exact percentage value",t),f.set.percent(t))}},setting:function(t,n){if(f.debug("Changing setting",t,n),e.isPlainObject(t))e.extend(!0,m,t);else{if(n===i)return m[t];e.isPlainObject(m[t])?e.extend(!0,m[t],n):m[t]=n}},internal:function(t,n){if(e.isPlainObject(t))e.extend(!0,f,t);else{if(n===i)return f[t];f[t]=n}},debug:function(){!m.silent&&m.debug&&(m.performance?f.performance.log(arguments):(f.debug=Function.prototype.bind.call(console.info,console,m.name+":"),f.debug.apply(console,arguments)))},verbose:function(){!m.silent&&m.verbose&&m.debug&&(m.performance?f.performance.log(arguments):(f.verbose=Function.prototype.bind.call(console.info,console,m.name+":"),f.verbose.apply(console,arguments)))},error:function(){m.silent||(f.error=Function.prototype.bind.call(console.error,console,m.name+":"),f.error.apply(console,arguments))},performance:{log:function(e){var t,n,i;m.performance&&(t=(new Date).getTime(),i=s||t,n=t-i,s=t,l.push({Name:e[0],Arguments:[].slice.call(e,1)||"",Element:T,"Execution Time":n})),clearTimeout(f.performance.timer),f.performance.timer=setTimeout(f.performance.display,500)},display:function(){var t=m.name+":",n=0;s=!1,clearTimeout(f.performance.timer),e.each(l,function(e,t){n+=t["Execution Time"]}),t+=" "+n+"ms",r&&(t+=" '"+r+"'"),(console.group!==i||console.table!==i)&&l.length>0&&(console.groupCollapsed(t),console.table?console.table(l):e.each(l,function(e,t){console.log(t.Name+": "+t["Execution Time"]+"ms")}),console.groupEnd()),l=[]}},invoke:function(t,n,a){var r,s,l,c=A;return n=n||d,a=T||a,"string"==typeof t&&c!==i&&(t=t.split(/[\. ]/),r=t.length-1,e.each(t,function(n,o){var a=n!=r?o+t[n+1].charAt(0).toUpperCase()+t[n+1].slice(1):t;if(e.isPlainObject(c[a])&&n!=r)c=c[a];else{if(c[a]!==i)return s=c[a],!1;if(!e.isPlainObject(c[o])||n==r)return c[o]!==i?(s=c[o],!1):(f.error(b.method,t),!1);c=c[o]}})),e.isFunction(s)?l=s.apply(a,n):s!==i&&(l=s),e.isArray(o)?o.push(l):o!==i?o=[o,l]:l!==i&&(o=l),s}},u?(A===i&&f.initialize(),f.invoke(c)):(A!==i&&A.invoke("destroy"),f.initialize())}),o!==i?o:this},e.fn.progress.settings={name:"Progress",namespace:"progress",silent:!1,debug:!1,verbose:!1,performance:!0,random:{min:2,max:5},duration:300,updateInterval:"auto",autoSuccess:!0,showActivity:!0,limitValues:!0,label:"percent",precision:0,framerate:1e3/30,percent:!1,total:!1,value:!1,failSafeDelay:100,onLabelUpdate:function(e,t,n,i){return t},onChange:function(e,t,n){},onSuccess:function(e){},onActive:function(e,t){},onError:function(e,t){},onWarning:function(e,t){},error:{method:"The method you called is not defined.",nonNumeric:"Progress value is non numeric",tooHigh:"Value specified is above 100%",tooLow:"Value specified is below 0%"},regExp:{variable:/\{\$*[A-z0-9]+\}/g},metadata:{percent:"percent",total:"total",value:"value"},selector:{bar:"> .bar",label:"> .label",progress:".bar > .progress"},text:{active:!1,error:!1,success:!1,warning:!1,percent:"{percent}%",ratio:"{value} of {total}"},className:{active:"active",error:"error",success:"success",warning:"warning"}}}(jQuery,window,document),function(e,t,n,i){"use strict";t="undefined"!=typeof t&&t.Math==Math?t:"undefined"!=typeof self&&self.Math==Math?self:Function("return this")(),e.fn.rating=function(t){var n,o=e(this),a=o.selector||"",r=(new Date).getTime(),s=[],l=arguments[0],c="string"==typeof l,u=[].slice.call(arguments,1);return o.each(function(){var d,f,m=e.isPlainObject(t)?e.extend(!0,{},e.fn.rating.settings,t):e.extend({},e.fn.rating.settings),g=m.namespace,p=m.className,h=m.metadata,v=m.selector,b=(m.error,"."+g),y="module-"+g,x=this,C=e(this).data(y),w=e(this),k=w.find(v.icon);f={initialize:function(){f.verbose("Initializing rating module",m),0===k.length&&f.setup.layout(),m.interactive?f.enable():f.disable(),f.set.initialLoad(),f.set.rating(f.get.initialRating()),f.remove.initialLoad(),f.instantiate()},instantiate:function(){f.verbose("Instantiating module",m),C=f,w.data(y,f)},destroy:function(){f.verbose("Destroying previous instance",C),f.remove.events(),w.removeData(y)},refresh:function(){k=w.find(v.icon)},setup:{layout:function(){var t=f.get.maxRating(),n=e.fn.rating.settings.templates.icon(t);f.debug("Generating icon html dynamically"),w.html(n),f.refresh()}},event:{mouseenter:function(){var t=e(this);t.nextAll().removeClass(p.selected),w.addClass(p.selected),t.addClass(p.selected).prevAll().addClass(p.selected)},mouseleave:function(){w.removeClass(p.selected),k.removeClass(p.selected)},click:function(){var t=e(this),n=f.get.rating(),i=k.index(t)+1,o="auto"==m.clearable?1===k.length:m.clearable;o&&n==i?f.clearRating():f.set.rating(i)}},clearRating:function(){f.debug("Clearing current rating"),f.set.rating(0)},bind:{events:function(){f.verbose("Binding events"),w.on("mouseenter"+b,v.icon,f.event.mouseenter).on("mouseleave"+b,v.icon,f.event.mouseleave).on("click"+b,v.icon,f.event.click)}},remove:{events:function(){f.verbose("Removing events"),w.off(b)},initialLoad:function(){d=!1}},enable:function(){f.debug("Setting rating to interactive mode"),f.bind.events(),w.removeClass(p.disabled)},disable:function(){f.debug("Setting rating to read-only mode"),f.remove.events(),w.addClass(p.disabled)},is:{initialLoad:function(){return d}},get:{initialRating:function(){return w.data(h.rating)!==i?(w.removeData(h.rating),w.data(h.rating)):m.initialRating},maxRating:function(){return w.data(h.maxRating)!==i?(w.removeData(h.maxRating),w.data(h.maxRating)):m.maxRating},rating:function(){var e=k.filter("."+p.active).length;return f.verbose("Current rating retrieved",e),e}},set:{rating:function(e){var t=e-1>=0?e-1:0,n=k.eq(t);w.removeClass(p.selected),k.removeClass(p.selected).removeClass(p.active),e>0&&(f.verbose("Setting current rating to",e),n.prevAll().addBack().addClass(p.active)),f.is.initialLoad()||m.onRate.call(x,e)},initialLoad:function(){d=!0}},setting:function(t,n){if(f.debug("Changing setting",t,n),e.isPlainObject(t))e.extend(!0,m,t);else{if(n===i)return m[t];e.isPlainObject(m[t])?e.extend(!0,m[t],n):m[t]=n}},internal:function(t,n){if(e.isPlainObject(t))e.extend(!0,f,t);else{if(n===i)return f[t];f[t]=n}},debug:function(){!m.silent&&m.debug&&(m.performance?f.performance.log(arguments):(f.debug=Function.prototype.bind.call(console.info,console,m.name+":"),f.debug.apply(console,arguments)))},verbose:function(){!m.silent&&m.verbose&&m.debug&&(m.performance?f.performance.log(arguments):(f.verbose=Function.prototype.bind.call(console.info,console,m.name+":"),f.verbose.apply(console,arguments)))},error:function(){m.silent||(f.error=Function.prototype.bind.call(console.error,console,m.name+":"),f.error.apply(console,arguments))},performance:{log:function(e){var t,n,i;m.performance&&(t=(new Date).getTime(),i=r||t,n=t-i,r=t,s.push({Name:e[0],Arguments:[].slice.call(e,1)||"",Element:x,"Execution Time":n})),clearTimeout(f.performance.timer),f.performance.timer=setTimeout(f.performance.display,500)},display:function(){var t=m.name+":",n=0;r=!1,clearTimeout(f.performance.timer),e.each(s,function(e,t){n+=t["Execution Time"]}),t+=" "+n+"ms",a&&(t+=" '"+a+"'"),o.length>1&&(t+=" ("+o.length+")"),(console.group!==i||console.table!==i)&&s.length>0&&(console.groupCollapsed(t),console.table?console.table(s):e.each(s,function(e,t){console.log(t.Name+": "+t["Execution Time"]+"ms")}),console.groupEnd()),s=[]}},invoke:function(t,o,a){var r,s,l,c=C;return o=o||u,a=x||a,"string"==typeof t&&c!==i&&(t=t.split(/[\. ]/),r=t.length-1,e.each(t,function(n,o){var a=n!=r?o+t[n+1].charAt(0).toUpperCase()+t[n+1].slice(1):t;if(e.isPlainObject(c[a])&&n!=r)c=c[a];else{if(c[a]!==i)return s=c[a],!1;if(!e.isPlainObject(c[o])||n==r)return c[o]!==i&&(s=c[o],!1);c=c[o]}})),e.isFunction(s)?l=s.apply(a,o):s!==i&&(l=s),e.isArray(n)?n.push(l):n!==i?n=[n,l]:l!==i&&(n=l),s}},c?(C===i&&f.initialize(),f.invoke(l)):(C!==i&&C.invoke("destroy"),f.initialize())}),n!==i?n:this},e.fn.rating.settings={name:"Rating",namespace:"rating",slent:!1,debug:!1,verbose:!1,performance:!0,initialRating:0,interactive:!0,maxRating:4,clearable:"auto",fireOnInit:!1,onRate:function(e){},error:{method:"The method you called is not defined",noMaximum:"No maximum rating specified. Cannot generate HTML automatically"},metadata:{rating:"rating",maxRating:"maxRating"},className:{active:"active",disabled:"disabled",selected:"selected",loading:"loading"},selector:{icon:".icon"},templates:{icon:function(e){for(var t=1,n="";t<=e;)n+='<i class="icon"></i>',t++;return n}}}}(jQuery,window,document),function(e,t,n,i){"use strict";t="undefined"!=typeof t&&t.Math==Math?t:"undefined"!=typeof self&&self.Math==Math?self:Function("return this")(), +e.fn.search=function(o){var a,r=e(this),s=r.selector||"",l=(new Date).getTime(),c=[],u=arguments[0],d="string"==typeof u,f=[].slice.call(arguments,1);return e(this).each(function(){var m,g=e.isPlainObject(o)?e.extend(!0,{},e.fn.search.settings,o):e.extend({},e.fn.search.settings),p=g.className,h=g.metadata,v=g.regExp,b=g.fields,y=g.selector,x=g.error,C=g.namespace,w="."+C,k=C+"-module",S=e(this),T=S.find(y.prompt),A=S.find(y.searchButton),R=S.find(y.results),E=S.find(y.result),P=S.find(y.category),F=this,O=S.data(k),D=!1;m={initialize:function(){m.verbose("Initializing module"),m.determine.searchFields(),m.bind.events(),m.set.type(),m.create.results(),m.instantiate()},instantiate:function(){m.verbose("Storing instance of module",m),O=m,S.data(k,m)},destroy:function(){m.verbose("Destroying instance"),S.off(w).removeData(k)},refresh:function(){m.debug("Refreshing selector cache"),T=S.find(y.prompt),A=S.find(y.searchButton),P=S.find(y.category),R=S.find(y.results),E=S.find(y.result)},refreshResults:function(){R=S.find(y.results),E=S.find(y.result)},bind:{events:function(){m.verbose("Binding events to search"),g.automatic&&(S.on(m.get.inputEvent()+w,y.prompt,m.event.input),T.attr("autocomplete","off")),S.on("focus"+w,y.prompt,m.event.focus).on("blur"+w,y.prompt,m.event.blur).on("keydown"+w,y.prompt,m.handleKeyboard).on("click"+w,y.searchButton,m.query).on("mousedown"+w,y.results,m.event.result.mousedown).on("mouseup"+w,y.results,m.event.result.mouseup).on("click"+w,y.result,m.event.result.click)}},determine:{searchFields:function(){o&&o.searchFields!==i&&(g.searchFields=o.searchFields)}},event:{input:function(){clearTimeout(m.timer),m.timer=setTimeout(m.query,g.searchDelay)},focus:function(){m.set.focus(),m.has.minimumCharacters()&&(m.query(),m.can.show()&&m.showResults())},blur:function(e){var t=n.activeElement===this,i=function(){m.cancel.query(),m.remove.focus(),m.timer=setTimeout(m.hideResults,g.hideDelay)};t||(m.resultsClicked?(m.debug("Determining if user action caused search to close"),S.one("click.close"+w,y.results,function(e){return m.is.inMessage(e)||D?void T.focus():(D=!1,void(m.is.animating()||m.is.hidden()||i()))})):(m.debug("Input blurred without user action, closing results"),i()))},result:{mousedown:function(){m.resultsClicked=!0},mouseup:function(){m.resultsClicked=!1},click:function(n){m.debug("Search result selected");var i=e(this),o=i.find(y.title).eq(0),a=i.is("a[href]")?i:i.find("a[href]").eq(0),r=a.attr("href")||!1,s=a.attr("target")||!1,l=(o.html(),o.length>0&&o.text()),c=m.get.results(),u=i.data(h.result)||m.get.result(l,c);return e.isFunction(g.onSelect)&&g.onSelect.call(F,u,c)===!1?(m.debug("Custom onSelect callback cancelled default select action"),void(D=!0)):(m.hideResults(),l&&m.set.value(l),void(r&&(m.verbose("Opening search link found in result",a),"_blank"==s||n.ctrlKey?t.open(r):t.location.href=r)))}}},handleKeyboard:function(e){var t,n=S.find(y.result),i=S.find(y.category),o=n.filter("."+p.active),a=n.index(o),r=n.length,s=o.length>0,l=e.which,c={backspace:8,enter:13,escape:27,upArrow:38,downArrow:40};if(l==c.escape&&(m.verbose("Escape key pressed, blurring search field"),m.trigger.blur()),m.is.visible())if(l==c.enter){if(m.verbose("Enter key pressed, selecting active result"),n.filter("."+p.active).length>0)return m.event.result.click.call(n.filter("."+p.active),e),e.preventDefault(),!1}else l==c.upArrow&&s?(m.verbose("Up key pressed, changing active result"),t=a-1<0?a:a-1,i.removeClass(p.active),n.removeClass(p.active).eq(t).addClass(p.active).closest(i).addClass(p.active),e.preventDefault()):l==c.downArrow&&(m.verbose("Down key pressed, changing active result"),t=a+1>=r?a:a+1,i.removeClass(p.active),n.removeClass(p.active).eq(t).addClass(p.active).closest(i).addClass(p.active),e.preventDefault());else l==c.enter&&(m.verbose("Enter key pressed, executing query"),m.query(),m.set.buttonPressed(),T.one("keyup",m.remove.buttonFocus))},setup:{api:function(t){var n={debug:g.debug,on:!1,cache:!0,action:"search",urlData:{query:t},onSuccess:function(e){m.parse.response.call(F,e,t)},onAbort:function(e){},onFailure:function(){m.displayMessage(x.serverError)},onError:m.error};e.extend(!0,n,g.apiSettings),m.verbose("Setting up API request",n),S.api(n)}},can:{useAPI:function(){return e.fn.api!==i},show:function(){return m.is.focused()&&!m.is.visible()&&!m.is.empty()},transition:function(){return g.transition&&e.fn.transition!==i&&S.transition("is supported")}},is:{animating:function(){return R.hasClass(p.animating)},hidden:function(){return R.hasClass(p.hidden)},inMessage:function(t){if(t.target){var i=e(t.target),o=e.contains(n.documentElement,t.target);return o&&i.closest(y.message).length>0}},empty:function(){return""===R.html()},visible:function(){return R.filter(":visible").length>0},focused:function(){return T.filter(":focus").length>0}},trigger:{blur:function(){var e=n.createEvent("HTMLEvents"),t=T[0];t&&(m.verbose("Triggering native blur event"),e.initEvent("blur",!1,!1),t.dispatchEvent(e))}},get:{inputEvent:function(){var e=T[0],t=e!==i&&e.oninput!==i?"input":e!==i&&e.onpropertychange!==i?"propertychange":"keyup";return t},value:function(){return T.val()},results:function(){var e=S.data(h.results);return e},result:function(t,n){var o=["title","id"],a=!1;return t=t!==i?t:m.get.value(),n=n!==i?n:m.get.results(),"category"===g.type?(m.debug("Finding result that matches",t),e.each(n,function(n,i){if(e.isArray(i.results)&&(a=m.search.object(t,i.results,o)[0]))return!1})):(m.debug("Finding result in results object",t),a=m.search.object(t,n,o)[0]),a||!1}},select:{firstResult:function(){m.verbose("Selecting first result"),E.first().addClass(p.active)}},set:{focus:function(){S.addClass(p.focus)},loading:function(){S.addClass(p.loading)},value:function(e){m.verbose("Setting search input value",e),T.val(e)},type:function(e){e=e||g.type,"category"==g.type&&S.addClass(g.type)},buttonPressed:function(){A.addClass(p.pressed)}},remove:{loading:function(){S.removeClass(p.loading)},focus:function(){S.removeClass(p.focus)},buttonPressed:function(){A.removeClass(p.pressed)}},query:function(){var t=m.get.value(),n=m.read.cache(t);m.has.minimumCharacters()?(n?(m.debug("Reading result from cache",t),m.save.results(n.results),m.addResults(n.html),m.inject.id(n.results)):(m.debug("Querying for",t),e.isPlainObject(g.source)||e.isArray(g.source)?m.search.local(t):m.can.useAPI()?m.search.remote(t):m.error(x.source)),g.onSearchQuery.call(F,t)):m.hideResults()},search:{local:function(e){var t,n=m.search.object(e,g.content);m.set.loading(),m.save.results(n),m.debug("Returned local search results",n),t=m.generateResults({results:n}),m.remove.loading(),m.addResults(t),m.inject.id(n),m.write.cache(e,{html:t,results:n})},remote:function(e){S.api("is loading")&&S.api("abort"),m.setup.api(e),S.api("query")},object:function(t,n,o){var a=[],r=[],s=t.toString().replace(v.escape,"\\$&"),l=new RegExp(v.beginsWith+s,"i"),c=function(t,n){var i=e.inArray(n,a)==-1,o=e.inArray(n,r)==-1;i&&o&&t.push(n)};return n=n||g.source,o=o!==i?o:g.searchFields,e.isArray(o)||(o=[o]),n===i||n===!1?(m.error(x.source),[]):(e.each(o,function(i,o){e.each(n,function(e,n){var i="string"==typeof n[o];i&&(n[o].search(l)!==-1?c(a,n):g.searchFullText&&m.fuzzySearch(t,n[o])&&c(r,n))})}),e.merge(a,r))}},fuzzySearch:function(e,t){var n=t.length,i=e.length;if("string"!=typeof e)return!1;if(e=e.toLowerCase(),t=t.toLowerCase(),i>n)return!1;if(i===n)return e===t;e:for(var o=0,a=0;o<i;o++){for(var r=e.charCodeAt(o);a<n;)if(t.charCodeAt(a++)===r)continue e;return!1}return!0},parse:{response:function(e,t){var n=m.generateResults(e);m.verbose("Parsing server response",e),e!==i&&t!==i&&e[b.results]!==i&&(m.addResults(n),m.inject.id(e[b.results]),m.write.cache(t,{html:n,results:e[b.results]}),m.save.results(e[b.results]))}},cancel:{query:function(){m.can.useAPI()&&S.api("abort")}},has:{minimumCharacters:function(){var e=m.get.value(),t=e.length;return t>=g.minCharacters}},clear:{cache:function(e){var t=S.data(h.cache);e?e&&t&&t[e]&&(m.debug("Removing value from cache",e),delete t[e],S.data(h.cache,t)):(m.debug("Clearing cache",e),S.removeData(h.cache))}},read:{cache:function(e){var t=S.data(h.cache);return!!g.cache&&(m.verbose("Checking cache for generated html for query",e),"object"==typeof t&&t[e]!==i&&t[e])}},create:{id:function(e,t){var n,o,a=e+1;return t!==i?(n=String.fromCharCode(97+t),o=n+a,m.verbose("Creating category result id",o)):(o=a,m.verbose("Creating result id",o)),o},results:function(){0===R.length&&(R=e("<div />").addClass(p.results).appendTo(S))}},inject:{result:function(e,t,n){m.verbose("Injecting result into results");var o=n!==i?R.children().eq(n).children(y.result).eq(t):R.children(y.result).eq(t);m.verbose("Injecting results metadata",o),o.data(h.result,e)},id:function(t){m.debug("Injecting unique ids into results");var n=0,o=0;return"category"===g.type?e.each(t,function(t,a){o=0,e.each(a.results,function(e,t){var r=a.results[e];r.id===i&&(r.id=m.create.id(o,n)),m.inject.result(r,o,n),o++}),n++}):e.each(t,function(e,n){var a=t[e];a.id===i&&(a.id=m.create.id(o)),m.inject.result(a,o),o++}),t}},save:{results:function(e){m.verbose("Saving current search results to metadata",e),S.data(h.results,e)}},write:{cache:function(e,t){var n=S.data(h.cache)!==i?S.data(h.cache):{};g.cache&&(m.verbose("Writing generated html to cache",e,t),n[e]=t,S.data(h.cache,n))}},addResults:function(t){return e.isFunction(g.onResultsAdd)&&g.onResultsAdd.call(R,t)===!1?(m.debug("onResultsAdd callback cancelled default action"),!1):void(t?(R.html(t),m.refreshResults(),g.selectFirstResult&&m.select.firstResult(),m.showResults()):m.hideResults())},showResults:function(){m.is.visible()||(m.can.transition()?(m.debug("Showing results with css animations"),R.transition({animation:g.transition+" in",debug:g.debug,verbose:g.verbose,duration:g.duration,queue:!0})):(m.debug("Showing results with javascript"),R.stop().fadeIn(g.duration,g.easing)),g.onResultsOpen.call(R))},hideResults:function(){m.is.visible()&&(m.can.transition()?(m.debug("Hiding results with css animations"),R.transition({animation:g.transition+" out",debug:g.debug,verbose:g.verbose,duration:g.duration,queue:!0})):(m.debug("Hiding results with javascript"),R.stop().fadeOut(g.duration,g.easing)),g.onResultsClose.call(R))},generateResults:function(t){m.debug("Generating html from response",t);var n=g.templates[g.type],i=e.isPlainObject(t[b.results])&&!e.isEmptyObject(t[b.results]),o=e.isArray(t[b.results])&&t[b.results].length>0,a="";return i||o?(g.maxResults>0&&(i?"standard"==g.type&&m.error(x.maxResults):t[b.results]=t[b.results].slice(0,g.maxResults)),e.isFunction(n)?a=n(t,b):m.error(x.noTemplate,!1)):g.showNoResults&&(a=m.displayMessage(x.noResults,"empty")),g.onResults.call(F,t),a},displayMessage:function(e,t){return t=t||"standard",m.debug("Displaying message",e,t),m.addResults(g.templates.message(e,t)),g.templates.message(e,t)},setting:function(t,n){if(e.isPlainObject(t))e.extend(!0,g,t);else{if(n===i)return g[t];g[t]=n}},internal:function(t,n){if(e.isPlainObject(t))e.extend(!0,m,t);else{if(n===i)return m[t];m[t]=n}},debug:function(){!g.silent&&g.debug&&(g.performance?m.performance.log(arguments):(m.debug=Function.prototype.bind.call(console.info,console,g.name+":"),m.debug.apply(console,arguments)))},verbose:function(){!g.silent&&g.verbose&&g.debug&&(g.performance?m.performance.log(arguments):(m.verbose=Function.prototype.bind.call(console.info,console,g.name+":"),m.verbose.apply(console,arguments)))},error:function(){g.silent||(m.error=Function.prototype.bind.call(console.error,console,g.name+":"),m.error.apply(console,arguments))},performance:{log:function(e){var t,n,i;g.performance&&(t=(new Date).getTime(),i=l||t,n=t-i,l=t,c.push({Name:e[0],Arguments:[].slice.call(e,1)||"",Element:F,"Execution Time":n})),clearTimeout(m.performance.timer),m.performance.timer=setTimeout(m.performance.display,500)},display:function(){var t=g.name+":",n=0;l=!1,clearTimeout(m.performance.timer),e.each(c,function(e,t){n+=t["Execution Time"]}),t+=" "+n+"ms",s&&(t+=" '"+s+"'"),r.length>1&&(t+=" ("+r.length+")"),(console.group!==i||console.table!==i)&&c.length>0&&(console.groupCollapsed(t),console.table?console.table(c):e.each(c,function(e,t){console.log(t.Name+": "+t["Execution Time"]+"ms")}),console.groupEnd()),c=[]}},invoke:function(t,n,o){var r,s,l,c=O;return n=n||f,o=F||o,"string"==typeof t&&c!==i&&(t=t.split(/[\. ]/),r=t.length-1,e.each(t,function(n,o){var a=n!=r?o+t[n+1].charAt(0).toUpperCase()+t[n+1].slice(1):t;if(e.isPlainObject(c[a])&&n!=r)c=c[a];else{if(c[a]!==i)return s=c[a],!1;if(!e.isPlainObject(c[o])||n==r)return c[o]!==i&&(s=c[o],!1);c=c[o]}})),e.isFunction(s)?l=s.apply(o,n):s!==i&&(l=s),e.isArray(a)?a.push(l):a!==i?a=[a,l]:l!==i&&(a=l),s}},d?(O===i&&m.initialize(),m.invoke(u)):(O!==i&&O.invoke("destroy"),m.initialize())}),a!==i?a:this},e.fn.search.settings={name:"Search",namespace:"search",silent:!1,debug:!1,verbose:!1,performance:!0,type:"standard",minCharacters:1,selectFirstResult:!1,apiSettings:!1,source:!1,searchFields:["title","description"],displayField:"",searchFullText:!0,automatic:!0,hideDelay:0,searchDelay:200,maxResults:7,cache:!0,showNoResults:!0,transition:"scale",duration:200,easing:"easeOutExpo",onSelect:!1,onResultsAdd:!1,onSearchQuery:function(e){},onResults:function(e){},onResultsOpen:function(){},onResultsClose:function(){},className:{animating:"animating",active:"active",empty:"empty",focus:"focus",hidden:"hidden",loading:"loading",results:"results",pressed:"down"},error:{source:"Cannot search. No source used, and Semantic API module was not included",noResults:"Your search returned no results",logging:"Error in debug logging, exiting.",noEndpoint:"No search endpoint was specified",noTemplate:"A valid template name was not specified.",serverError:"There was an issue querying the server.",maxResults:"Results must be an array to use maxResults setting",method:"The method you called is not defined."},metadata:{cache:"cache",results:"results",result:"result"},regExp:{escape:/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g,beginsWith:"(?:s|^)"},fields:{categories:"results",categoryName:"name",categoryResults:"results",description:"description",image:"image",price:"price",results:"results",title:"title",url:"url",action:"action",actionText:"text",actionURL:"url"},selector:{prompt:".prompt",searchButton:".search.button",results:".results",message:".results > .message",category:".category",result:".result",title:".title, .name"},templates:{escape:function(e){var t=/[&<>"'`]/g,n=/[&<>"'`]/,i={"&":"&","<":"<",">":">",'"':""","'":"'","`":"`"},o=function(e){return i[e]};return n.test(e)?e.replace(t,o):e},message:function(e,t){var n="";return e!==i&&t!==i&&(n+='<div class="message '+t+'">',n+="empty"==t?'<div class="header">No Results</div class="header"><div class="description">'+e+'</div class="description">':' <div class="description">'+e+"</div>",n+="</div>"),n},category:function(t,n){var o="";e.fn.search.settings.templates.escape;return t[n.categoryResults]!==i&&(e.each(t[n.categoryResults],function(t,a){a[n.results]!==i&&a.results.length>0&&(o+='<div class="category">',a[n.categoryName]!==i&&(o+='<div class="name">'+a[n.categoryName]+"</div>"),e.each(a.results,function(e,t){o+=t[n.url]?'<a class="result" href="'+t[n.url]+'">':'<a class="result">',t[n.image]!==i&&(o+='<div class="image"> <img src="'+t[n.image]+'"></div>'),o+='<div class="content">',t[n.price]!==i&&(o+='<div class="price">'+t[n.price]+"</div>"),t[n.title]!==i&&(o+='<div class="title">'+t[n.title]+"</div>"),t[n.description]!==i&&(o+='<div class="description">'+t[n.description]+"</div>"),o+="</div>",o+="</a>"}),o+="</div>")}),t[n.action]&&(o+='<a href="'+t[n.action][n.actionURL]+'" class="action">'+t[n.action][n.actionText]+"</a>"),o)},standard:function(t,n){var o="";return t[n.results]!==i&&(e.each(t[n.results],function(e,t){o+=t[n.url]?'<a class="result" href="'+t[n.url]+'">':'<a class="result">',t[n.image]!==i&&(o+='<div class="image"> <img src="'+t[n.image]+'"></div>'),o+='<div class="content">',t[n.price]!==i&&(o+='<div class="price">'+t[n.price]+"</div>"),t[n.title]!==i&&(o+='<div class="title">'+t[n.title]+"</div>"),t[n.description]!==i&&(o+='<div class="description">'+t[n.description]+"</div>"),o+="</div>",o+="</a>"}),t[n.action]&&(o+='<a href="'+t[n.action][n.actionURL]+'" class="action">'+t[n.action][n.actionText]+"</a>"),o)}}}}(jQuery,window,document),function(e,t,n,i){"use strict";t="undefined"!=typeof t&&t.Math==Math?t:"undefined"!=typeof self&&self.Math==Math?self:Function("return this")(),e.fn.shape=function(o){var a,r=e(this),s=(e("body"),(new Date).getTime()),l=[],c=arguments[0],u="string"==typeof c,d=[].slice.call(arguments,1),f=t.requestAnimationFrame||t.mozRequestAnimationFrame||t.webkitRequestAnimationFrame||t.msRequestAnimationFrame||function(e){setTimeout(e,0)};return r.each(function(){var t,m,g,p=r.selector||"",h=e.isPlainObject(o)?e.extend(!0,{},e.fn.shape.settings,o):e.extend({},e.fn.shape.settings),v=h.namespace,b=h.selector,y=h.error,x=h.className,C="."+v,w="module-"+v,k=e(this),S=k.find(b.sides),T=k.find(b.side),A=!1,R=this,E=k.data(w);g={initialize:function(){g.verbose("Initializing module for",R),g.set.defaultSide(),g.instantiate()},instantiate:function(){g.verbose("Storing instance of module",g),E=g,k.data(w,E)},destroy:function(){g.verbose("Destroying previous module for",R),k.removeData(w).off(C)},refresh:function(){g.verbose("Refreshing selector cache for",R),k=e(R),S=e(this).find(b.shape),T=e(this).find(b.side)},repaint:function(){g.verbose("Forcing repaint event");var e=S[0]||n.createElement("div");e.offsetWidth},animate:function(e,n){g.verbose("Animating box with properties",e),n=n||function(e){g.verbose("Executing animation callback"),e!==i&&e.stopPropagation(),g.reset(),g.set.active()},h.beforeChange.call(m[0]),g.get.transitionEvent()?(g.verbose("Starting CSS animation"),k.addClass(x.animating),S.css(e).one(g.get.transitionEvent(),n),g.set.duration(h.duration),f(function(){k.addClass(x.animating),t.addClass(x.hidden)})):n()},queue:function(e){g.debug("Queueing animation of",e),S.one(g.get.transitionEvent(),function(){g.debug("Executing queued animation"),setTimeout(function(){k.shape(e)},0)})},reset:function(){g.verbose("Animating states reset"),k.removeClass(x.animating).attr("style","").removeAttr("style"),S.attr("style","").removeAttr("style"),T.attr("style","").removeAttr("style").removeClass(x.hidden),m.removeClass(x.animating).attr("style","").removeAttr("style")},is:{complete:function(){return T.filter("."+x.active)[0]==m[0]},animating:function(){return k.hasClass(x.animating)}},set:{defaultSide:function(){t=k.find("."+h.className.active),m=t.next(b.side).length>0?t.next(b.side):k.find(b.side).first(),A=!1,g.verbose("Active side set to",t),g.verbose("Next side set to",m)},duration:function(e){e=e||h.duration,e="number"==typeof e?e+"ms":e,g.verbose("Setting animation duration",e),(h.duration||0===h.duration)&&S.add(T).css({"-webkit-transition-duration":e,"-moz-transition-duration":e,"-ms-transition-duration":e,"-o-transition-duration":e,"transition-duration":e})},currentStageSize:function(){var e=k.find("."+h.className.active),t=e.outerWidth(!0),n=e.outerHeight(!0);k.css({width:t,height:n})},stageSize:function(){var e=k.clone().addClass(x.loading),t=e.find("."+h.className.active),n=A?e.find(b.side).eq(A):t.next(b.side).length>0?t.next(b.side):e.find(b.side).first(),i="next"==h.width?n.outerWidth(!0):"initial"==h.width?k.width():h.width,o="next"==h.height?n.outerHeight(!0):"initial"==h.height?k.height():h.height;t.removeClass(x.active),n.addClass(x.active),e.insertAfter(k),e.remove(),"auto"!=h.width&&(k.css("width",i+h.jitter),g.verbose("Specifying width during animation",i)),"auto"!=h.height&&(k.css("height",o+h.jitter),g.verbose("Specifying height during animation",o))},nextSide:function(e){A=e,m=T.filter(e),A=T.index(m),0===m.length&&(g.set.defaultSide(),g.error(y.side)),g.verbose("Next side manually set to",m)},active:function(){g.verbose("Setting new side to active",m),T.removeClass(x.active),m.addClass(x.active),h.onChange.call(m[0]),g.set.defaultSide()}},flip:{up:function(){if(g.is.complete()&&!g.is.animating()&&!h.allowRepeats)return void g.debug("Side already visible",m);if(g.is.animating())g.queue("flip up");else{g.debug("Flipping up",m);var e=g.get.transform.up();g.set.stageSize(),g.stage.above(),g.animate(e)}},down:function(){if(g.is.complete()&&!g.is.animating()&&!h.allowRepeats)return void g.debug("Side already visible",m);if(g.is.animating())g.queue("flip down");else{g.debug("Flipping down",m);var e=g.get.transform.down();g.set.stageSize(),g.stage.below(),g.animate(e)}},left:function(){if(g.is.complete()&&!g.is.animating()&&!h.allowRepeats)return void g.debug("Side already visible",m);if(g.is.animating())g.queue("flip left");else{g.debug("Flipping left",m);var e=g.get.transform.left();g.set.stageSize(),g.stage.left(),g.animate(e)}},right:function(){if(g.is.complete()&&!g.is.animating()&&!h.allowRepeats)return void g.debug("Side already visible",m);if(g.is.animating())g.queue("flip right");else{g.debug("Flipping right",m);var e=g.get.transform.right();g.set.stageSize(),g.stage.right(),g.animate(e)}},over:function(){return!g.is.complete()||g.is.animating()||h.allowRepeats?void(g.is.animating()?g.queue("flip over"):(g.debug("Flipping over",m),g.set.stageSize(),g.stage.behind(),g.animate(g.get.transform.over()))):void g.debug("Side already visible",m)},back:function(){return!g.is.complete()||g.is.animating()||h.allowRepeats?void(g.is.animating()?g.queue("flip back"):(g.debug("Flipping back",m),g.set.stageSize(),g.stage.behind(),g.animate(g.get.transform.back()))):void g.debug("Side already visible",m)}},get:{transform:{up:function(){var e={y:-((t.outerHeight(!0)-m.outerHeight(!0))/2),z:-(t.outerHeight(!0)/2)};return{transform:"translateY("+e.y+"px) translateZ("+e.z+"px) rotateX(-90deg)"}},down:function(){var e={y:-((t.outerHeight(!0)-m.outerHeight(!0))/2),z:-(t.outerHeight(!0)/2)};return{transform:"translateY("+e.y+"px) translateZ("+e.z+"px) rotateX(90deg)"}},left:function(){var e={x:-((t.outerWidth(!0)-m.outerWidth(!0))/2),z:-(t.outerWidth(!0)/2)};return{transform:"translateX("+e.x+"px) translateZ("+e.z+"px) rotateY(90deg)"}},right:function(){var e={x:-((t.outerWidth(!0)-m.outerWidth(!0))/2),z:-(t.outerWidth(!0)/2)};return{transform:"translateX("+e.x+"px) translateZ("+e.z+"px) rotateY(-90deg)"}},over:function(){var e={x:-((t.outerWidth(!0)-m.outerWidth(!0))/2)};return{transform:"translateX("+e.x+"px) rotateY(180deg)"}},back:function(){var e={x:-((t.outerWidth(!0)-m.outerWidth(!0))/2)};return{transform:"translateX("+e.x+"px) rotateY(-180deg)"}}},transitionEvent:function(){var e,t=n.createElement("element"),o={transition:"transitionend",OTransition:"oTransitionEnd",MozTransition:"transitionend",WebkitTransition:"webkitTransitionEnd"};for(e in o)if(t.style[e]!==i)return o[e]},nextSide:function(){return t.next(b.side).length>0?t.next(b.side):k.find(b.side).first()}},stage:{above:function(){var e={origin:(t.outerHeight(!0)-m.outerHeight(!0))/2,depth:{active:m.outerHeight(!0)/2,next:t.outerHeight(!0)/2}};g.verbose("Setting the initial animation position as above",m,e),S.css({transform:"translateZ(-"+e.depth.active+"px)"}),t.css({transform:"rotateY(0deg) translateZ("+e.depth.active+"px)"}),m.addClass(x.animating).css({top:e.origin+"px",transform:"rotateX(90deg) translateZ("+e.depth.next+"px)"})},below:function(){var e={origin:(t.outerHeight(!0)-m.outerHeight(!0))/2,depth:{active:m.outerHeight(!0)/2,next:t.outerHeight(!0)/2}};g.verbose("Setting the initial animation position as below",m,e),S.css({transform:"translateZ(-"+e.depth.active+"px)"}),t.css({transform:"rotateY(0deg) translateZ("+e.depth.active+"px)"}),m.addClass(x.animating).css({top:e.origin+"px",transform:"rotateX(-90deg) translateZ("+e.depth.next+"px)"})},left:function(){var e={active:t.outerWidth(!0),next:m.outerWidth(!0)},n={origin:(e.active-e.next)/2,depth:{active:e.next/2,next:e.active/2}};g.verbose("Setting the initial animation position as left",m,n),S.css({transform:"translateZ(-"+n.depth.active+"px)"}),t.css({transform:"rotateY(0deg) translateZ("+n.depth.active+"px)"}),m.addClass(x.animating).css({left:n.origin+"px",transform:"rotateY(-90deg) translateZ("+n.depth.next+"px)"})},right:function(){var e={active:t.outerWidth(!0),next:m.outerWidth(!0)},n={origin:(e.active-e.next)/2,depth:{active:e.next/2,next:e.active/2}};g.verbose("Setting the initial animation position as left",m,n),S.css({transform:"translateZ(-"+n.depth.active+"px)"}),t.css({transform:"rotateY(0deg) translateZ("+n.depth.active+"px)"}),m.addClass(x.animating).css({left:n.origin+"px",transform:"rotateY(90deg) translateZ("+n.depth.next+"px)"})},behind:function(){var e={active:t.outerWidth(!0),next:m.outerWidth(!0)},n={origin:(e.active-e.next)/2,depth:{active:e.next/2,next:e.active/2}};g.verbose("Setting the initial animation position as behind",m,n),t.css({transform:"rotateY(0deg)"}),m.addClass(x.animating).css({left:n.origin+"px",transform:"rotateY(-180deg)"})}},setting:function(t,n){if(g.debug("Changing setting",t,n),e.isPlainObject(t))e.extend(!0,h,t);else{if(n===i)return h[t];e.isPlainObject(h[t])?e.extend(!0,h[t],n):h[t]=n}},internal:function(t,n){if(e.isPlainObject(t))e.extend(!0,g,t);else{if(n===i)return g[t];g[t]=n}},debug:function(){!h.silent&&h.debug&&(h.performance?g.performance.log(arguments):(g.debug=Function.prototype.bind.call(console.info,console,h.name+":"),g.debug.apply(console,arguments)))},verbose:function(){!h.silent&&h.verbose&&h.debug&&(h.performance?g.performance.log(arguments):(g.verbose=Function.prototype.bind.call(console.info,console,h.name+":"),g.verbose.apply(console,arguments)))},error:function(){h.silent||(g.error=Function.prototype.bind.call(console.error,console,h.name+":"),g.error.apply(console,arguments))},performance:{log:function(e){var t,n,i;h.performance&&(t=(new Date).getTime(),i=s||t,n=t-i,s=t,l.push({Name:e[0],Arguments:[].slice.call(e,1)||"",Element:R,"Execution Time":n})),clearTimeout(g.performance.timer),g.performance.timer=setTimeout(g.performance.display,500)},display:function(){var t=h.name+":",n=0;s=!1,clearTimeout(g.performance.timer),e.each(l,function(e,t){n+=t["Execution Time"]}),t+=" "+n+"ms",p&&(t+=" '"+p+"'"),r.length>1&&(t+=" ("+r.length+")"),(console.group!==i||console.table!==i)&&l.length>0&&(console.groupCollapsed(t),console.table?console.table(l):e.each(l,function(e,t){console.log(t.Name+": "+t["Execution Time"]+"ms")}),console.groupEnd()),l=[]}},invoke:function(t,n,o){var r,s,l,c=E;return n=n||d,o=R||o,"string"==typeof t&&c!==i&&(t=t.split(/[\. ]/),r=t.length-1,e.each(t,function(n,o){var a=n!=r?o+t[n+1].charAt(0).toUpperCase()+t[n+1].slice(1):t;if(e.isPlainObject(c[a])&&n!=r)c=c[a];else{if(c[a]!==i)return s=c[a],!1;if(!e.isPlainObject(c[o])||n==r)return c[o]!==i&&(s=c[o],!1);c=c[o]}})),e.isFunction(s)?l=s.apply(o,n):s!==i&&(l=s),e.isArray(a)?a.push(l):a!==i?a=[a,l]:l!==i&&(a=l),s}},u?(E===i&&g.initialize(),g.invoke(c)):(E!==i&&E.invoke("destroy"),g.initialize())}),a!==i?a:this},e.fn.shape.settings={name:"Shape",silent:!1,debug:!1,verbose:!1,jitter:0,performance:!0,namespace:"shape",width:"initial",height:"initial",beforeChange:function(){},onChange:function(){},allowRepeats:!1,duration:!1,error:{side:"You tried to switch to a side that does not exist.",method:"The method you called is not defined"},className:{animating:"animating",hidden:"hidden",loading:"loading",active:"active"},selector:{sides:".sides",side:".side"}}}(jQuery,window,document),function(e,t,n,i){"use strict";t="undefined"!=typeof t&&t.Math==Math?t:"undefined"!=typeof self&&self.Math==Math?self:Function("return this")(),e.fn.sidebar=function(o){var a,r=e(this),s=e(t),l=e(n),c=e("html"),u=e("head"),d=r.selector||"",f=(new Date).getTime(),m=[],g=arguments[0],p="string"==typeof g,h=[].slice.call(arguments,1),v=t.requestAnimationFrame||t.mozRequestAnimationFrame||t.webkitRequestAnimationFrame||t.msRequestAnimationFrame||function(e){setTimeout(e,0)};return r.each(function(){var r,b,y,x,C,w,k=e.isPlainObject(o)?e.extend(!0,{},e.fn.sidebar.settings,o):e.extend({},e.fn.sidebar.settings),S=k.selector,T=k.className,A=k.namespace,R=k.regExp,E=k.error,P="."+A,F="module-"+A,O=e(this),D=e(k.context),q=O.children(S.sidebar),j=D.children(S.fixed),z=D.children(S.pusher),M=this,I=O.data(F);w={initialize:function(){w.debug("Initializing sidebar",o),w.create.id(),C=w.get.transitionEvent(),w.is.ios()&&w.set.ios(),k.delaySetup?v(w.setup.layout):w.setup.layout(),v(function(){w.setup.cache()}),w.instantiate()},instantiate:function(){w.verbose("Storing instance of module",w),I=w,O.data(F,w)},create:{id:function(){y=(Math.random().toString(16)+"000000000").substr(2,8),b="."+y,w.verbose("Creating unique id for element",y)}},destroy:function(){w.verbose("Destroying previous module for",O),O.off(P).removeData(F),w.is.ios()&&w.remove.ios(),D.off(b),s.off(b),l.off(b)},event:{clickaway:function(e){var t=z.find(e.target).length>0||z.is(e.target),n=D.is(e.target);t&&(w.verbose("User clicked on dimmed page"),w.hide()),n&&(w.verbose("User clicked on dimmable context (scaled out page)"),w.hide())},touch:function(e){},containScroll:function(e){M.scrollTop<=0&&(M.scrollTop=1),M.scrollTop+M.offsetHeight>=M.scrollHeight&&(M.scrollTop=M.scrollHeight-M.offsetHeight-1)},scroll:function(t){0===e(t.target).closest(S.sidebar).length&&t.preventDefault()}},bind:{clickaway:function(){w.verbose("Adding clickaway events to context",D),k.closable&&D.on("click"+b,w.event.clickaway).on("touchend"+b,w.event.clickaway)},scrollLock:function(){k.scrollLock&&(w.debug("Disabling page scroll"),s.on("DOMMouseScroll"+b,w.event.scroll)),w.verbose("Adding events to contain sidebar scroll"),l.on("touchmove"+b,w.event.touch),O.on("scroll"+P,w.event.containScroll)}},unbind:{clickaway:function(){w.verbose("Removing clickaway events from context",D),D.off(b)},scrollLock:function(){w.verbose("Removing scroll lock from page"),l.off(b),s.off(b),O.off("scroll"+P)}},add:{inlineCSS:function(){var t,n=w.cache.width||O.outerWidth(),i=w.cache.height||O.outerHeight(),o=w.is.rtl(),a=w.get.direction(),s={left:n,right:-n,top:i,bottom:-i};o&&(w.verbose("RTL detected, flipping widths"),s.left=-n,s.right=n),t="<style>","left"===a||"right"===a?(w.debug("Adding CSS rules for animation distance",n),t+=" .ui.visible."+a+".sidebar ~ .fixed, .ui.visible."+a+".sidebar ~ .pusher { -webkit-transform: translate3d("+s[a]+"px, 0, 0); transform: translate3d("+s[a]+"px, 0, 0); }"):"top"!==a&&"bottom"!=a||(t+=" .ui.visible."+a+".sidebar ~ .fixed, .ui.visible."+a+".sidebar ~ .pusher { -webkit-transform: translate3d(0, "+s[a]+"px, 0); transform: translate3d(0, "+s[a]+"px, 0); }"),w.is.ie()&&("left"===a||"right"===a?(w.debug("Adding CSS rules for animation distance",n),t+=" body.pushable > .ui.visible."+a+".sidebar ~ .pusher:after { -webkit-transform: translate3d("+s[a]+"px, 0, 0); transform: translate3d("+s[a]+"px, 0, 0); }"):"top"!==a&&"bottom"!=a||(t+=" body.pushable > .ui.visible."+a+".sidebar ~ .pusher:after { -webkit-transform: translate3d(0, "+s[a]+"px, 0); transform: translate3d(0, "+s[a]+"px, 0); }"),t+=" body.pushable > .ui.visible.left.sidebar ~ .ui.visible.right.sidebar ~ .pusher:after, body.pushable > .ui.visible.right.sidebar ~ .ui.visible.left.sidebar ~ .pusher:after { -webkit-transform: translate3d(0px, 0, 0); transform: translate3d(0px, 0, 0); }"),t+="</style>",r=e(t).appendTo(u),w.debug("Adding sizing css to head",r)}},refresh:function(){w.verbose("Refreshing selector cache"),D=e(k.context),q=D.children(S.sidebar),z=D.children(S.pusher),j=D.children(S.fixed),w.clear.cache()},refreshSidebars:function(){w.verbose("Refreshing other sidebars"),q=D.children(S.sidebar)},repaint:function(){w.verbose("Forcing repaint event"),M.style.display="none";M.offsetHeight;M.scrollTop=M.scrollTop,M.style.display=""},setup:{cache:function(){ +w.cache={width:O.outerWidth(),height:O.outerHeight(),rtl:"rtl"==O.css("direction")}},layout:function(){0===D.children(S.pusher).length&&(w.debug("Adding wrapper element for sidebar"),w.error(E.pusher),z=e('<div class="pusher" />'),D.children().not(S.omitted).not(q).wrapAll(z),w.refresh()),0!==O.nextAll(S.pusher).length&&O.nextAll(S.pusher)[0]===z[0]||(w.debug("Moved sidebar to correct parent element"),w.error(E.movedSidebar,M),O.detach().prependTo(D),w.refresh()),w.clear.cache(),w.set.pushable(),w.set.direction()}},attachEvents:function(t,n){var i=e(t);n=e.isFunction(w[n])?w[n]:w.toggle,i.length>0?(w.debug("Attaching sidebar events to element",t,n),i.on("click"+P,n)):w.error(E.notFound,t)},show:function(t){if(t=e.isFunction(t)?t:function(){},w.is.hidden()){if(w.refreshSidebars(),k.overlay&&(w.error(E.overlay),k.transition="overlay"),w.refresh(),w.othersActive())if(w.debug("Other sidebars currently visible"),k.exclusive){if("overlay"!=k.transition)return void w.hideOthers(w.show);w.hideOthers()}else k.transition="overlay";w.pushPage(function(){t.call(M),k.onShow.call(M)}),k.onChange.call(M),k.onVisible.call(M)}else w.debug("Sidebar is already visible")},hide:function(t){t=e.isFunction(t)?t:function(){},(w.is.visible()||w.is.animating())&&(w.debug("Hiding sidebar",t),w.refreshSidebars(),w.pullPage(function(){t.call(M),k.onHidden.call(M)}),k.onChange.call(M),k.onHide.call(M))},othersAnimating:function(){return q.not(O).filter("."+T.animating).length>0},othersVisible:function(){return q.not(O).filter("."+T.visible).length>0},othersActive:function(){return w.othersVisible()||w.othersAnimating()},hideOthers:function(e){var t=q.not(O).filter("."+T.visible),n=t.length,i=0;e=e||function(){},t.sidebar("hide",function(){i++,i==n&&e()})},toggle:function(){w.verbose("Determining toggled direction"),w.is.hidden()?w.show():w.hide()},pushPage:function(t){var n,i,o,a=w.get.transition(),r="overlay"===a||w.othersActive()?O:z;t=e.isFunction(t)?t:function(){},"scale down"==k.transition&&w.scrollToTop(),w.set.transition(a),w.repaint(),n=function(){w.bind.clickaway(),w.add.inlineCSS(),w.set.animating(),w.set.visible()},i=function(){w.set.dimmed()},o=function(e){e.target==r[0]&&(r.off(C+b,o),w.remove.animating(),w.bind.scrollLock(),t.call(M))},r.off(C+b),r.on(C+b,o),v(n),k.dimPage&&!w.othersVisible()&&v(i)},pullPage:function(t){var n,i,o=w.get.transition(),a="overlay"==o||w.othersActive()?O:z;t=e.isFunction(t)?t:function(){},w.verbose("Removing context push state",w.get.direction()),w.unbind.clickaway(),w.unbind.scrollLock(),n=function(){w.set.transition(o),w.set.animating(),w.remove.visible(),k.dimPage&&!w.othersVisible()&&z.removeClass(T.dimmed)},i=function(e){e.target==a[0]&&(a.off(C+b,i),w.remove.animating(),w.remove.transition(),w.remove.inlineCSS(),("scale down"==o||k.returnScroll&&w.is.mobile())&&w.scrollBack(),t.call(M))},a.off(C+b),a.on(C+b,i),v(n)},scrollToTop:function(){w.verbose("Scrolling to top of page to avoid animation issues"),x=e(t).scrollTop(),O.scrollTop(0),t.scrollTo(0,0)},scrollBack:function(){w.verbose("Scrolling back to original page position"),t.scrollTo(0,x)},clear:{cache:function(){w.verbose("Clearing cached dimensions"),w.cache={}}},set:{ios:function(){c.addClass(T.ios)},pushed:function(){D.addClass(T.pushed)},pushable:function(){D.addClass(T.pushable)},dimmed:function(){z.addClass(T.dimmed)},active:function(){O.addClass(T.active)},animating:function(){O.addClass(T.animating)},transition:function(e){e=e||w.get.transition(),O.addClass(e)},direction:function(e){e=e||w.get.direction(),O.addClass(T[e])},visible:function(){O.addClass(T.visible)},overlay:function(){O.addClass(T.overlay)}},remove:{inlineCSS:function(){w.debug("Removing inline css styles",r),r&&r.length>0&&r.remove()},ios:function(){c.removeClass(T.ios)},pushed:function(){D.removeClass(T.pushed)},pushable:function(){D.removeClass(T.pushable)},active:function(){O.removeClass(T.active)},animating:function(){O.removeClass(T.animating)},transition:function(e){e=e||w.get.transition(),O.removeClass(e)},direction:function(e){e=e||w.get.direction(),O.removeClass(T[e])},visible:function(){O.removeClass(T.visible)},overlay:function(){O.removeClass(T.overlay)}},get:{direction:function(){return O.hasClass(T.top)?T.top:O.hasClass(T.right)?T.right:O.hasClass(T.bottom)?T.bottom:T.left},transition:function(){var e,t=w.get.direction();return e=w.is.mobile()?"auto"==k.mobileTransition?k.defaultTransition.mobile[t]:k.mobileTransition:"auto"==k.transition?k.defaultTransition.computer[t]:k.transition,w.verbose("Determined transition",e),e},transitionEvent:function(){var e,t=n.createElement("element"),o={transition:"transitionend",OTransition:"oTransitionEnd",MozTransition:"transitionend",WebkitTransition:"webkitTransitionEnd"};for(e in o)if(t.style[e]!==i)return o[e]}},is:{ie:function(){var e=!t.ActiveXObject&&"ActiveXObject"in t,n="ActiveXObject"in t;return e||n},ios:function(){var e=navigator.userAgent,t=e.match(R.ios),n=e.match(R.mobileChrome);return!(!t||n)&&(w.verbose("Browser was found to be iOS",e),!0)},mobile:function(){var e=navigator.userAgent,t=e.match(R.mobile);return t?(w.verbose("Browser was found to be mobile",e),!0):(w.verbose("Browser is not mobile, using regular transition",e),!1)},hidden:function(){return!w.is.visible()},visible:function(){return O.hasClass(T.visible)},open:function(){return w.is.visible()},closed:function(){return w.is.hidden()},vertical:function(){return O.hasClass(T.top)},animating:function(){return D.hasClass(T.animating)},rtl:function(){return w.cache.rtl===i&&(w.cache.rtl="rtl"==O.css("direction")),w.cache.rtl}},setting:function(t,n){if(w.debug("Changing setting",t,n),e.isPlainObject(t))e.extend(!0,k,t);else{if(n===i)return k[t];e.isPlainObject(k[t])?e.extend(!0,k[t],n):k[t]=n}},internal:function(t,n){if(e.isPlainObject(t))e.extend(!0,w,t);else{if(n===i)return w[t];w[t]=n}},debug:function(){!k.silent&&k.debug&&(k.performance?w.performance.log(arguments):(w.debug=Function.prototype.bind.call(console.info,console,k.name+":"),w.debug.apply(console,arguments)))},verbose:function(){!k.silent&&k.verbose&&k.debug&&(k.performance?w.performance.log(arguments):(w.verbose=Function.prototype.bind.call(console.info,console,k.name+":"),w.verbose.apply(console,arguments)))},error:function(){k.silent||(w.error=Function.prototype.bind.call(console.error,console,k.name+":"),w.error.apply(console,arguments))},performance:{log:function(e){var t,n,i;k.performance&&(t=(new Date).getTime(),i=f||t,n=t-i,f=t,m.push({Name:e[0],Arguments:[].slice.call(e,1)||"",Element:M,"Execution Time":n})),clearTimeout(w.performance.timer),w.performance.timer=setTimeout(w.performance.display,500)},display:function(){var t=k.name+":",n=0;f=!1,clearTimeout(w.performance.timer),e.each(m,function(e,t){n+=t["Execution Time"]}),t+=" "+n+"ms",d&&(t+=" '"+d+"'"),(console.group!==i||console.table!==i)&&m.length>0&&(console.groupCollapsed(t),console.table?console.table(m):e.each(m,function(e,t){console.log(t.Name+": "+t["Execution Time"]+"ms")}),console.groupEnd()),m=[]}},invoke:function(t,n,o){var r,s,l,c=I;return n=n||h,o=M||o,"string"==typeof t&&c!==i&&(t=t.split(/[\. ]/),r=t.length-1,e.each(t,function(n,o){var a=n!=r?o+t[n+1].charAt(0).toUpperCase()+t[n+1].slice(1):t;if(e.isPlainObject(c[a])&&n!=r)c=c[a];else{if(c[a]!==i)return s=c[a],!1;if(!e.isPlainObject(c[o])||n==r)return c[o]!==i?(s=c[o],!1):(w.error(E.method,t),!1);c=c[o]}})),e.isFunction(s)?l=s.apply(o,n):s!==i&&(l=s),e.isArray(a)?a.push(l):a!==i?a=[a,l]:l!==i&&(a=l),s}},p?(I===i&&w.initialize(),w.invoke(g)):(I!==i&&w.invoke("destroy"),w.initialize())}),a!==i?a:this},e.fn.sidebar.settings={name:"Sidebar",namespace:"sidebar",silent:!1,debug:!1,verbose:!1,performance:!0,transition:"auto",mobileTransition:"auto",defaultTransition:{computer:{left:"uncover",right:"uncover",top:"overlay",bottom:"overlay"},mobile:{left:"uncover",right:"uncover",top:"overlay",bottom:"overlay"}},context:"body",exclusive:!1,closable:!0,dimPage:!0,scrollLock:!1,returnScroll:!1,delaySetup:!1,duration:500,onChange:function(){},onShow:function(){},onHide:function(){},onHidden:function(){},onVisible:function(){},className:{active:"active",animating:"animating",dimmed:"dimmed",ios:"ios",pushable:"pushable",pushed:"pushed",right:"right",top:"top",left:"left",bottom:"bottom",visible:"visible"},selector:{fixed:".fixed",omitted:"script, link, style, .ui.modal, .ui.dimmer, .ui.nag, .ui.fixed",pusher:".pusher",sidebar:".ui.sidebar"},regExp:{ios:/(iPad|iPhone|iPod)/g,mobileChrome:/(CriOS)/g,mobile:/Mobile|iP(hone|od|ad)|Android|BlackBerry|IEMobile|Kindle|NetFront|Silk-Accelerated|(hpw|web)OS|Fennec|Minimo|Opera M(obi|ini)|Blazer|Dolfin|Dolphin|Skyfire|Zune/g},error:{method:"The method you called is not defined.",pusher:"Had to add pusher element. For optimal performance make sure body content is inside a pusher element",movedSidebar:"Had to move sidebar. For optimal performance make sure sidebar and pusher are direct children of your body tag",overlay:"The overlay setting is no longer supported, use animation: overlay",notFound:"There were no elements that matched the specified selector"}}}(jQuery,window,document),function(e,t,n,i){"use strict";t="undefined"!=typeof t&&t.Math==Math?t:"undefined"!=typeof self&&self.Math==Math?self:Function("return this")(),e.fn.sticky=function(o){var a,r=e(this),s=r.selector||"",l=(new Date).getTime(),c=[],u=arguments[0],d="string"==typeof u,f=[].slice.call(arguments,1);return r.each(function(){var r,m,g,p,h,v=e.isPlainObject(o)?e.extend(!0,{},e.fn.sticky.settings,o):e.extend({},e.fn.sticky.settings),b=v.className,y=v.namespace,x=v.error,C="."+y,w="module-"+y,k=e(this),S=e(t),T=e(v.scrollContext),A=(k.selector||"",k.data(w)),R=t.requestAnimationFrame||t.mozRequestAnimationFrame||t.webkitRequestAnimationFrame||t.msRequestAnimationFrame||function(e){setTimeout(e,0)},E=this;h={initialize:function(){h.determineContainer(),h.determineContext(),h.verbose("Initializing sticky",v,r),h.save.positions(),h.checkErrors(),h.bind.events(),v.observeChanges&&h.observeChanges(),h.instantiate()},instantiate:function(){h.verbose("Storing instance of module",h),A=h,k.data(w,h)},destroy:function(){h.verbose("Destroying previous instance"),h.reset(),g&&g.disconnect(),p&&p.disconnect(),S.off("load"+C,h.event.load).off("resize"+C,h.event.resize),T.off("scrollchange"+C,h.event.scrollchange),k.removeData(w)},observeChanges:function(){"MutationObserver"in t&&(g=new MutationObserver(h.event.documentChanged),p=new MutationObserver(h.event.changed),g.observe(n,{childList:!0,subtree:!0}),p.observe(E,{childList:!0,subtree:!0}),p.observe(m[0],{childList:!0,subtree:!0}),h.debug("Setting up mutation observer",p))},determineContainer:function(){r=v.container?e(v.container):k.offsetParent()},determineContext:function(){if(m=v.context?e(v.context):r,0===m.length)return void h.error(x.invalidContext,v.context,k)},checkErrors:function(){if(h.is.hidden()&&h.error(x.visible,k),h.cache.element.height>h.cache.context.height)return h.reset(),void h.error(x.elementSize,k)},bind:{events:function(){S.on("load"+C,h.event.load).on("resize"+C,h.event.resize),T.off("scroll"+C).on("scroll"+C,h.event.scroll).on("scrollchange"+C,h.event.scrollchange)}},event:{changed:function(e){clearTimeout(h.timer),h.timer=setTimeout(function(){h.verbose("DOM tree modified, updating sticky menu",e),h.refresh()},100)},documentChanged:function(t){[].forEach.call(t,function(t){t.removedNodes&&[].forEach.call(t.removedNodes,function(t){(t==E||e(t).find(E).length>0)&&(h.debug("Element removed from DOM, tearing down events"),h.destroy())})})},load:function(){h.verbose("Page contents finished loading"),R(h.refresh)},resize:function(){h.verbose("Window resized"),R(h.refresh)},scroll:function(){R(function(){T.triggerHandler("scrollchange"+C,T.scrollTop())})},scrollchange:function(e,t){h.stick(t),v.onScroll.call(E)}},refresh:function(e){h.reset(),v.context||h.determineContext(),e&&h.determineContainer(),h.save.positions(),h.stick(),v.onReposition.call(E)},supports:{sticky:function(){var t=e("<div/>");t[0];return t.addClass(b.supported),t.css("position").match("sticky")}},save:{lastScroll:function(e){h.lastScroll=e},elementScroll:function(e){h.elementScroll=e},positions:function(){var e={height:T.height()},t={margin:{top:parseInt(k.css("margin-top"),10),bottom:parseInt(k.css("margin-bottom"),10)},offset:k.offset(),width:k.outerWidth(),height:k.outerHeight()},n={offset:m.offset(),height:m.outerHeight()};({height:r.outerHeight()});h.is.standardScroll()||(h.debug("Non-standard scroll. Removing scroll offset from element offset"),e.top=T.scrollTop(),e.left=T.scrollLeft(),t.offset.top+=e.top,n.offset.top+=e.top,t.offset.left+=e.left,n.offset.left+=e.left),h.cache={fits:t.height<e.height,scrollContext:{height:e.height},element:{margin:t.margin,top:t.offset.top-t.margin.top,left:t.offset.left,width:t.width,height:t.height,bottom:t.offset.top+t.height},context:{top:n.offset.top,height:n.height,bottom:n.offset.top+n.height}},h.set.containerSize(),h.set.size(),h.stick(),h.debug("Caching element positions",h.cache)}},get:{direction:function(e){var t="down";return e=e||T.scrollTop(),h.lastScroll!==i&&(h.lastScroll<e?t="down":h.lastScroll>e&&(t="up")),t},scrollChange:function(e){return e=e||T.scrollTop(),h.lastScroll?e-h.lastScroll:0},currentElementScroll:function(){return h.elementScroll?h.elementScroll:h.is.top()?Math.abs(parseInt(k.css("top"),10))||0:Math.abs(parseInt(k.css("bottom"),10))||0},elementScroll:function(e){e=e||T.scrollTop();var t=h.cache.element,n=h.cache.scrollContext,i=h.get.scrollChange(e),o=t.height-n.height+v.offset,a=h.get.currentElementScroll(),r=a+i;return a=h.cache.fits||r<0?0:r>o?o:r}},remove:{lastScroll:function(){delete h.lastScroll},elementScroll:function(e){delete h.elementScroll},offset:function(){k.css("margin-top","")}},set:{offset:function(){h.verbose("Setting offset on element",v.offset),k.css("margin-top",v.offset)},containerSize:function(){var e=r.get(0).tagName;"HTML"===e||"body"==e?h.determineContainer():Math.abs(r.outerHeight()-h.cache.context.height)>v.jitter&&(h.debug("Context has padding, specifying exact height for container",h.cache.context.height),r.css({height:h.cache.context.height}))},minimumSize:function(){var e=h.cache.element;r.css("min-height",e.height)},scroll:function(e){h.debug("Setting scroll on element",e),h.elementScroll!=e&&(h.is.top()&&k.css("bottom","").css("top",-e),h.is.bottom()&&k.css("top","").css("bottom",e))},size:function(){0!==h.cache.element.height&&0!==h.cache.element.width&&(E.style.setProperty("width",h.cache.element.width+"px","important"),E.style.setProperty("height",h.cache.element.height+"px","important"))}},is:{standardScroll:function(){return T[0]==t},top:function(){return k.hasClass(b.top)},bottom:function(){return k.hasClass(b.bottom)},initialPosition:function(){return!h.is.fixed()&&!h.is.bound()},hidden:function(){return!k.is(":visible")},bound:function(){return k.hasClass(b.bound)},fixed:function(){return k.hasClass(b.fixed)}},stick:function(e){var t=e||T.scrollTop(),n=h.cache,i=n.fits,o=n.element,a=n.scrollContext,r=n.context,s=h.is.bottom()&&v.pushing?v.bottomOffset:v.offset,e={top:t+s,bottom:t+s+a.height},l=(h.get.direction(e.top),i?0:h.get.elementScroll(e.top)),c=!i,u=0!==o.height;u&&(h.is.initialPosition()?e.top>=r.bottom?(h.debug("Initial element position is bottom of container"),h.bindBottom()):e.top>o.top&&(o.height+e.top-l>=r.bottom?(h.debug("Initial element position is bottom of container"),h.bindBottom()):(h.debug("Initial element position is fixed"),h.fixTop())):h.is.fixed()?h.is.top()?e.top<=o.top?(h.debug("Fixed element reached top of container"),h.setInitialPosition()):o.height+e.top-l>=r.bottom?(h.debug("Fixed element reached bottom of container"),h.bindBottom()):c&&(h.set.scroll(l),h.save.lastScroll(e.top),h.save.elementScroll(l)):h.is.bottom()&&(e.bottom-o.height<=o.top?(h.debug("Bottom fixed rail has reached top of container"),h.setInitialPosition()):e.bottom>=r.bottom?(h.debug("Bottom fixed rail has reached bottom of container"),h.bindBottom()):c&&(h.set.scroll(l),h.save.lastScroll(e.top),h.save.elementScroll(l))):h.is.bottom()&&(e.top<=o.top?(h.debug("Jumped from bottom fixed to top fixed, most likely used home/end button"),h.setInitialPosition()):v.pushing?h.is.bound()&&e.bottom<=r.bottom&&(h.debug("Fixing bottom attached element to bottom of browser."),h.fixBottom()):h.is.bound()&&e.top<=r.bottom-o.height&&(h.debug("Fixing bottom attached element to top of browser."),h.fixTop())))},bindTop:function(){h.debug("Binding element to top of parent container"),h.remove.offset(),k.css({left:"",top:"",marginBottom:""}).removeClass(b.fixed).removeClass(b.bottom).addClass(b.bound).addClass(b.top),v.onTop.call(E),v.onUnstick.call(E)},bindBottom:function(){h.debug("Binding element to bottom of parent container"),h.remove.offset(),k.css({left:"",top:""}).removeClass(b.fixed).removeClass(b.top).addClass(b.bound).addClass(b.bottom),v.onBottom.call(E),v.onUnstick.call(E)},setInitialPosition:function(){h.debug("Returning to initial position"),h.unfix(),h.unbind()},fixTop:function(){h.debug("Fixing element to top of page"),h.set.minimumSize(),h.set.offset(),k.css({left:h.cache.element.left,bottom:"",marginBottom:""}).removeClass(b.bound).removeClass(b.bottom).addClass(b.fixed).addClass(b.top),v.onStick.call(E)},fixBottom:function(){h.debug("Sticking element to bottom of page"),h.set.minimumSize(),h.set.offset(),k.css({left:h.cache.element.left,bottom:"",marginBottom:""}).removeClass(b.bound).removeClass(b.top).addClass(b.fixed).addClass(b.bottom),v.onStick.call(E)},unbind:function(){h.is.bound()&&(h.debug("Removing container bound position on element"),h.remove.offset(),k.removeClass(b.bound).removeClass(b.top).removeClass(b.bottom))},unfix:function(){h.is.fixed()&&(h.debug("Removing fixed position on element"),h.remove.offset(),k.removeClass(b.fixed).removeClass(b.top).removeClass(b.bottom),v.onUnstick.call(E))},reset:function(){h.debug("Resetting elements position"),h.unbind(),h.unfix(),h.resetCSS(),h.remove.offset(),h.remove.lastScroll()},resetCSS:function(){k.css({width:"",height:""}),r.css({height:""})},setting:function(t,n){if(e.isPlainObject(t))e.extend(!0,v,t);else{if(n===i)return v[t];v[t]=n}},internal:function(t,n){if(e.isPlainObject(t))e.extend(!0,h,t);else{if(n===i)return h[t];h[t]=n}},debug:function(){!v.silent&&v.debug&&(v.performance?h.performance.log(arguments):(h.debug=Function.prototype.bind.call(console.info,console,v.name+":"),h.debug.apply(console,arguments)))},verbose:function(){!v.silent&&v.verbose&&v.debug&&(v.performance?h.performance.log(arguments):(h.verbose=Function.prototype.bind.call(console.info,console,v.name+":"),h.verbose.apply(console,arguments)))},error:function(){v.silent||(h.error=Function.prototype.bind.call(console.error,console,v.name+":"),h.error.apply(console,arguments))},performance:{log:function(e){var t,n,i;v.performance&&(t=(new Date).getTime(),i=l||t,n=t-i,l=t,c.push({Name:e[0],Arguments:[].slice.call(e,1)||"",Element:E,"Execution Time":n})),clearTimeout(h.performance.timer),h.performance.timer=setTimeout(h.performance.display,0)},display:function(){var t=v.name+":",n=0;l=!1,clearTimeout(h.performance.timer),e.each(c,function(e,t){n+=t["Execution Time"]}),t+=" "+n+"ms",s&&(t+=" '"+s+"'"),(console.group!==i||console.table!==i)&&c.length>0&&(console.groupCollapsed(t),console.table?console.table(c):e.each(c,function(e,t){console.log(t.Name+": "+t["Execution Time"]+"ms")}),console.groupEnd()),c=[]}},invoke:function(t,n,o){var r,s,l,c=A;return n=n||f,o=E||o,"string"==typeof t&&c!==i&&(t=t.split(/[\. ]/),r=t.length-1,e.each(t,function(n,o){var a=n!=r?o+t[n+1].charAt(0).toUpperCase()+t[n+1].slice(1):t;if(e.isPlainObject(c[a])&&n!=r)c=c[a];else{if(c[a]!==i)return s=c[a],!1;if(!e.isPlainObject(c[o])||n==r)return c[o]!==i&&(s=c[o],!1);c=c[o]}})),e.isFunction(s)?l=s.apply(o,n):s!==i&&(l=s),e.isArray(a)?a.push(l):a!==i?a=[a,l]:l!==i&&(a=l),s}},d?(A===i&&h.initialize(),h.invoke(u)):(A!==i&&A.invoke("destroy"),h.initialize())}),a!==i?a:this},e.fn.sticky.settings={name:"Sticky",namespace:"sticky",silent:!1,debug:!1,verbose:!0,performance:!0,pushing:!1,context:!1,container:!1,scrollContext:t,offset:0,bottomOffset:0,jitter:5,observeChanges:!1,onReposition:function(){},onScroll:function(){},onStick:function(){},onUnstick:function(){},onTop:function(){},onBottom:function(){},error:{container:"Sticky element must be inside a relative container",visible:"Element is hidden, you must call refresh after element becomes visible. Use silent setting to surpress this warning in production.",method:"The method you called is not defined.",invalidContext:"Context specified does not exist",elementSize:"Sticky element is larger than its container, cannot create sticky."},className:{bound:"bound",fixed:"fixed",supported:"native",top:"top",bottom:"bottom"}}}(jQuery,window,document),function(e,t,n,i){"use strict";t="undefined"!=typeof t&&t.Math==Math?t:"undefined"!=typeof self&&self.Math==Math?self:Function("return this")(),e.fn.tab=function(o){var a,r=e(e.isFunction(this)?t:this),s=r.selector||"",l=(new Date).getTime(),c=[],u=arguments[0],d="string"==typeof u,f=[].slice.call(arguments,1),m=!1;return r.each(function(){var g,p,h,v,b,y,x=e.isPlainObject(o)?e.extend(!0,{},e.fn.tab.settings,o):e.extend({},e.fn.tab.settings),C=x.className,w=x.metadata,k=x.selector,S=x.error,T="."+x.namespace,A="module-"+x.namespace,R=e(this),E={},P=!0,F=0,O=this,D=R.data(A);b={initialize:function(){b.debug("Initializing tab menu item",R),b.fix.callbacks(),b.determineTabs(),b.debug("Determining tabs",x.context,p),x.auto&&b.set.auto(),b.bind.events(),x.history&&!m&&(b.initializeHistory(),m=!0),b.instantiate()},instantiate:function(){b.verbose("Storing instance of module",b),D=b,R.data(A,b)},destroy:function(){b.debug("Destroying tabs",R),R.removeData(A).off(T)},bind:{events:function(){e.isWindow(O)||(b.debug("Attaching tab activation events to element",R),R.on("click"+T,b.event.click))}},determineTabs:function(){var t;"parent"===x.context?(R.closest(k.ui).length>0?(t=R.closest(k.ui),b.verbose("Using closest UI element as parent",t)):t=R,g=t.parent(),b.verbose("Determined parent element for creating context",g)):x.context?(g=e(x.context),b.verbose("Using selector for tab context",x.context,g)):g=e("body"),x.childrenOnly?(p=g.children(k.tabs),b.debug("Searching tab context children for tabs",g,p)):(p=g.find(k.tabs),b.debug("Searching tab context for tabs",g,p))},fix:{callbacks:function(){e.isPlainObject(o)&&(o.onTabLoad||o.onTabInit)&&(o.onTabLoad&&(o.onLoad=o.onTabLoad,delete o.onTabLoad,b.error(S.legacyLoad,o.onLoad)),o.onTabInit&&(o.onFirstLoad=o.onTabInit,delete o.onTabInit,b.error(S.legacyInit,o.onFirstLoad)),x=e.extend(!0,{},e.fn.tab.settings,o))}},initializeHistory:function(){if(b.debug("Initializing page state"),e.address===i)return b.error(S.state),!1;if("state"==x.historyType){if(b.debug("Using HTML5 to manage state"),x.path===!1)return b.error(S.path),!1;e.address.history(!0).state(x.path)}e.address.bind("change",b.event.history.change)},event:{click:function(t){var n=e(this).data(w.tab);n!==i?(x.history?(b.verbose("Updating page state",t),e.address.value(n)):(b.verbose("Changing tab",t),b.changeTab(n)),t.preventDefault()):b.debug("No tab specified")},history:{change:function(t){var n=t.pathNames.join("/")||b.get.initialPath(),o=x.templates.determineTitle(n)||!1;b.performance.display(),b.debug("History change event",n,t),y=t,n!==i&&b.changeTab(n),o&&e.address.title(o)}}},refresh:function(){h&&(b.debug("Refreshing tab",h),b.changeTab(h))},cache:{read:function(e){return e!==i&&E[e]},add:function(e,t){e=e||h,b.debug("Adding cached content for",e),E[e]=t},remove:function(e){e=e||h,b.debug("Removing cached content for",e),delete E[e]}},set:{auto:function(){var t="string"==typeof x.path?x.path.replace(/\/$/,"")+"/{$tab}":"/{$tab}";b.verbose("Setting up automatic tab retrieval from server",t),e.isPlainObject(x.apiSettings)?x.apiSettings.url=t:x.apiSettings={url:t}},loading:function(e){var t=b.get.tabElement(e),n=t.hasClass(C.loading);n||(b.verbose("Setting loading state for",t),t.addClass(C.loading).siblings(p).removeClass(C.active+" "+C.loading),t.length>0&&x.onRequest.call(t[0],e))},state:function(t){e.address.value(t)}},changeTab:function(n){var i=t.history&&t.history.pushState,o=i&&x.ignoreFirstLoad&&P,a=x.auto||e.isPlainObject(x.apiSettings),r=a&&!o?b.utilities.pathToArray(n):b.get.defaultPathArray(n);n=b.utilities.arrayToPath(r),e.each(r,function(t,i){var s,l,c,u,d=r.slice(0,t+1),f=b.utilities.arrayToPath(d),m=b.is.tab(f),p=t+1==r.length,k=b.get.tabElement(f);if(b.verbose("Looking for tab",i),m){if(b.verbose("Tab was found",i),h=f,v=b.utilities.filterArray(r,d),p?u=!0:(l=r.slice(0,t+2),c=b.utilities.arrayToPath(l),u=!b.is.tab(c),u&&b.verbose("Tab parameters found",l)),u&&a)return o?(b.debug("Ignoring remote content on first tab load",f),P=!1,b.cache.add(n,k.html()),b.activate.all(f),x.onFirstLoad.call(k[0],f,v,y),x.onLoad.call(k[0],f,v,y)):(b.activate.navigation(f),b.fetch.content(f,n)),!1;b.debug("Opened local tab",f),b.activate.all(f),b.cache.read(f)||(b.cache.add(f,!0),b.debug("First time tab loaded calling tab init"),x.onFirstLoad.call(k[0],f,v,y)),x.onLoad.call(k[0],f,v,y)}else{if(n.search("/")!=-1||""===n)return b.error(S.missingTab,R,g,f),!1;if(s=e("#"+n+', a[name="'+n+'"]'),f=s.closest("[data-tab]").data(w.tab),k=b.get.tabElement(f),s&&s.length>0&&f)return b.debug("Anchor link used, opening parent tab",k,s),k.hasClass(C.active)||setTimeout(function(){b.scrollTo(s)},0),b.activate.all(f),b.cache.read(f)||(b.cache.add(f,!0),b.debug("First time tab loaded calling tab init"),x.onFirstLoad.call(k[0],f,v,y)),x.onLoad.call(k[0],f,v,y),!1}})},scrollTo:function(t){var i=!!(t&&t.length>0)&&t.offset().top;i!==!1&&(b.debug("Forcing scroll to an in-page link in a hidden tab",i,t),e(n).scrollTop(i))},update:{content:function(t,n,o){var a=b.get.tabElement(t),r=a[0];o=o!==i?o:x.evaluateScripts,"string"==typeof x.cacheType&&"dom"==x.cacheType.toLowerCase()&&"string"!=typeof n?a.empty().append(e(n).clone(!0)):o?(b.debug("Updating HTML and evaluating inline scripts",t,n),a.html(n)):(b.debug("Updating HTML",t,n),r.innerHTML=n)}},fetch:{content:function(t,n){var o,a,r=b.get.tabElement(t),s={dataType:"html",encodeParameters:!1,on:"now",cache:x.alwaysRefresh,headers:{"X-Remote":!0},onSuccess:function(e){"response"==x.cacheType&&b.cache.add(n,e),b.update.content(t,e),t==h?(b.debug("Content loaded",t),b.activate.tab(t)):b.debug("Content loaded in background",t),x.onFirstLoad.call(r[0],t,v,y),x.onLoad.call(r[0],t,v,y),"string"==typeof x.cacheType&&"dom"==x.cacheType.toLowerCase()&&r.children().length>0?setTimeout(function(){var e=r.children().clone(!0);e=e.not("script"),b.cache.add(n,e)},0):b.cache.add(n,r.html())},urlData:{tab:n}},l=r.api("get request")||!1,c=l&&"pending"===l.state();n=n||t,a=b.cache.read(n),x.cache&&a?(b.activate.tab(t),b.debug("Adding cached content",n),"once"==x.evaluateScripts?b.update.content(t,a,!1):b.update.content(t,a),x.onLoad.call(r[0],t,v,y)):c?(b.set.loading(t),b.debug("Content is already loading",n)):e.api!==i?(o=e.extend(!0,{},x.apiSettings,s),b.debug("Retrieving remote content",n,o),b.set.loading(t),r.api(o)):b.error(S.api)}},activate:{all:function(e){b.activate.tab(e),b.activate.navigation(e)},tab:function(e){var t=b.get.tabElement(e),n="siblings"==x.deactivate?t.siblings(p):p.not(t),i=t.hasClass(C.active);b.verbose("Showing tab content for",t),i||(t.addClass(C.active),n.removeClass(C.active+" "+C.loading),t.length>0&&x.onVisible.call(t[0],e))},navigation:function(e){var t=b.get.navElement(e),n="siblings"==x.deactivate?t.siblings(r):r.not(t),i=t.hasClass(C.active);b.verbose("Activating tab navigation for",t,e),i||(t.addClass(C.active),n.removeClass(C.active+" "+C.loading))}},deactivate:{all:function(){b.deactivate.navigation(),b.deactivate.tabs()},navigation:function(){r.removeClass(C.active)},tabs:function(){p.removeClass(C.active+" "+C.loading)}},is:{tab:function(e){return e!==i&&b.get.tabElement(e).length>0}},get:{initialPath:function(){return r.eq(0).data(w.tab)||p.eq(0).data(w.tab)},path:function(){return e.address.value()},defaultPathArray:function(e){return b.utilities.pathToArray(b.get.defaultPath(e))},defaultPath:function(e){var t=r.filter("[data-"+w.tab+'^="'+e+'/"]').eq(0),n=t.data(w.tab)||!1;if(n){if(b.debug("Found default tab",n),F<x.maxDepth)return F++,b.get.defaultPath(n);b.error(S.recursion)}else b.debug("No default tabs found for",e,p);return F=0,e},navElement:function(e){return e=e||h,r.filter("[data-"+w.tab+'="'+e+'"]')},tabElement:function(e){var t,n,i,o;return e=e||h,i=b.utilities.pathToArray(e),o=b.utilities.last(i),t=p.filter("[data-"+w.tab+'="'+e+'"]'),n=p.filter("[data-"+w.tab+'="'+o+'"]'),t.length>0?t:n},tab:function(){return h}},utilities:{filterArray:function(t,n){return e.grep(t,function(t){return e.inArray(t,n)==-1})},last:function(t){return!!e.isArray(t)&&t[t.length-1]},pathToArray:function(e){return e===i&&(e=h),"string"==typeof e?e.split("/"):[e]},arrayToPath:function(t){return!!e.isArray(t)&&t.join("/")}},setting:function(t,n){if(b.debug("Changing setting",t,n),e.isPlainObject(t))e.extend(!0,x,t);else{if(n===i)return x[t];e.isPlainObject(x[t])?e.extend(!0,x[t],n):x[t]=n}},internal:function(t,n){if(e.isPlainObject(t))e.extend(!0,b,t);else{if(n===i)return b[t];b[t]=n}},debug:function(){!x.silent&&x.debug&&(x.performance?b.performance.log(arguments):(b.debug=Function.prototype.bind.call(console.info,console,x.name+":"),b.debug.apply(console,arguments)))},verbose:function(){!x.silent&&x.verbose&&x.debug&&(x.performance?b.performance.log(arguments):(b.verbose=Function.prototype.bind.call(console.info,console,x.name+":"),b.verbose.apply(console,arguments)))},error:function(){x.silent||(b.error=Function.prototype.bind.call(console.error,console,x.name+":"),b.error.apply(console,arguments))},performance:{log:function(e){var t,n,i;x.performance&&(t=(new Date).getTime(),i=l||t,n=t-i,l=t,c.push({Name:e[0],Arguments:[].slice.call(e,1)||"",Element:O,"Execution Time":n})),clearTimeout(b.performance.timer),b.performance.timer=setTimeout(b.performance.display,500)},display:function(){var t=x.name+":",n=0;l=!1,clearTimeout(b.performance.timer),e.each(c,function(e,t){n+=t["Execution Time"]}),t+=" "+n+"ms",s&&(t+=" '"+s+"'"),(console.group!==i||console.table!==i)&&c.length>0&&(console.groupCollapsed(t),console.table?console.table(c):e.each(c,function(e,t){console.log(t.Name+": "+t["Execution Time"]+"ms")}),console.groupEnd()),c=[]}},invoke:function(t,n,o){var r,s,l,c=D;return n=n||f,o=O||o,"string"==typeof t&&c!==i&&(t=t.split(/[\. ]/),r=t.length-1,e.each(t,function(n,o){var a=n!=r?o+t[n+1].charAt(0).toUpperCase()+t[n+1].slice(1):t;if(e.isPlainObject(c[a])&&n!=r)c=c[a];else{if(c[a]!==i)return s=c[a],!1;if(!e.isPlainObject(c[o])||n==r)return c[o]!==i?(s=c[o],!1):(b.error(S.method,t),!1);c=c[o]}})),e.isFunction(s)?l=s.apply(o,n):s!==i&&(l=s),e.isArray(a)?a.push(l):a!==i?a=[a,l]:l!==i&&(a=l),s}},d?(D===i&&b.initialize(),b.invoke(u)):(D!==i&&D.invoke("destroy"),b.initialize())}),a!==i?a:this},e.tab=function(){e(t).tab.apply(this,arguments)},e.fn.tab.settings={name:"Tab",namespace:"tab",silent:!1,debug:!1,verbose:!1,performance:!0,auto:!1,history:!1,historyType:"hash",path:!1,context:!1,childrenOnly:!1,maxDepth:25,deactivate:"siblings",alwaysRefresh:!1,cache:!0,cacheType:"response",ignoreFirstLoad:!1,apiSettings:!1,evaluateScripts:"once",onFirstLoad:function(e,t,n){},onLoad:function(e,t,n){},onVisible:function(e,t,n){},onRequest:function(e,t,n){},templates:{determineTitle:function(e){}},error:{api:"You attempted to load content without API module",method:"The method you called is not defined",missingTab:"Activated tab cannot be found. Tabs are case-sensitive.",noContent:"The tab you specified is missing a content url.",path:"History enabled, but no path was specified",recursion:"Max recursive depth reached",legacyInit:"onTabInit has been renamed to onFirstLoad in 2.0, please adjust your code.", +legacyLoad:"onTabLoad has been renamed to onLoad in 2.0. Please adjust your code",state:"History requires Asual's Address library <https://github.com/asual/jquery-address>"},metadata:{tab:"tab",loaded:"loaded",promise:"promise"},className:{loading:"loading",active:"active"},selector:{tabs:".ui.tab",ui:".ui"}}}(jQuery,window,document),function(e,t,n,i){"use strict";t="undefined"!=typeof t&&t.Math==Math?t:"undefined"!=typeof self&&self.Math==Math?self:Function("return this")(),e.fn.transition=function(){var o,a=e(this),r=a.selector||"",s=(new Date).getTime(),l=[],c=arguments,u=c[0],d=[].slice.call(arguments,1),f="string"==typeof u;t.requestAnimationFrame||t.mozRequestAnimationFrame||t.webkitRequestAnimationFrame||t.msRequestAnimationFrame||function(e){setTimeout(e,0)};return a.each(function(t){var m,g,p,h,v,b,y,x,C,w=e(this),k=this;C={initialize:function(){m=C.get.settings.apply(k,c),h=m.className,p=m.error,v=m.metadata,x="."+m.namespace,y="module-"+m.namespace,g=w.data(y)||C,b=C.get.animationEndEvent(),f&&(f=C.invoke(u)),f===!1&&(C.verbose("Converted arguments into settings object",m),m.interval?C.delay(m.animate):C.animate(),C.instantiate())},instantiate:function(){C.verbose("Storing instance of module",C),g=C,w.data(y,g)},destroy:function(){C.verbose("Destroying previous module for",k),w.removeData(y)},refresh:function(){C.verbose("Refreshing display type on next animation"),delete C.displayType},forceRepaint:function(){C.verbose("Forcing element repaint");var e=w.parent(),t=w.next();0===t.length?w.detach().appendTo(e):w.detach().insertBefore(t)},repaint:function(){C.verbose("Repainting element");k.offsetWidth},delay:function(e){var n,o,r=C.get.animationDirection();r||(r=C.can.transition()?C.get.direction():"static"),e=e!==i?e:m.interval,n="auto"==m.reverse&&r==h.outward,o=n||1==m.reverse?(a.length-t)*m.interval:t*m.interval,C.debug("Delaying animation by",o),setTimeout(C.animate,o)},animate:function(e){if(m=e||m,!C.is.supported())return C.error(p.support),!1;if(C.debug("Preparing animation",m.animation),C.is.animating()){if(m.queue)return!m.allowRepeats&&C.has.direction()&&C.is.occurring()&&C.queuing!==!0?C.debug("Animation is currently occurring, preventing queueing same animation",m.animation):C.queue(m.animation),!1;if(!m.allowRepeats&&C.is.occurring())return C.debug("Animation is already occurring, will not execute repeated animation",m.animation),!1;C.debug("New animation started, completing previous early",m.animation),g.complete()}C.can.animate()?C.set.animating(m.animation):C.error(p.noAnimation,m.animation,k)},reset:function(){C.debug("Resetting animation to beginning conditions"),C.remove.animationCallbacks(),C.restore.conditions(),C.remove.animating()},queue:function(e){C.debug("Queueing animation of",e),C.queuing=!0,w.one(b+".queue"+x,function(){C.queuing=!1,C.repaint(),C.animate.apply(this,m)})},complete:function(e){C.debug("Animation complete",m.animation),C.remove.completeCallback(),C.remove.failSafe(),C.is.looping()||(C.is.outward()?(C.verbose("Animation is outward, hiding element"),C.restore.conditions(),C.hide()):C.is.inward()?(C.verbose("Animation is outward, showing element"),C.restore.conditions(),C.show()):(C.verbose("Static animation completed"),C.restore.conditions(),m.onComplete.call(k)))},force:{visible:function(){var e=w.attr("style"),t=C.get.userStyle(),n=C.get.displayType(),o=t+"display: "+n+" !important;",a=w.css("display"),r=e===i||""===e;a!==n?(C.verbose("Overriding default display to show element",n),w.attr("style",o)):r&&w.removeAttr("style")},hidden:function(){var e=w.attr("style"),t=w.css("display"),n=e===i||""===e;"none"===t||C.is.hidden()?n&&w.removeAttr("style"):(C.verbose("Overriding default display to hide element"),w.css("display","none"))}},has:{direction:function(t){var n=!1;return t=t||m.animation,"string"==typeof t&&(t=t.split(" "),e.each(t,function(e,t){t!==h.inward&&t!==h.outward||(n=!0)})),n},inlineDisplay:function(){var t=w.attr("style")||"";return e.isArray(t.match(/display.*?;/,""))}},set:{animating:function(e){var t;C.remove.completeCallback(),e=e||m.animation,t=C.get.animationClass(e),C.save.animation(t),C.force.visible(),C.remove.hidden(),C.remove.direction(),C.start.animation(t)},duration:function(e,t){t=t||m.duration,t="number"==typeof t?t+"ms":t,(t||0===t)&&(C.verbose("Setting animation duration",t),w.css({"animation-duration":t}))},direction:function(e){e=e||C.get.direction(),e==h.inward?C.set.inward():C.set.outward()},looping:function(){C.debug("Transition set to loop"),w.addClass(h.looping)},hidden:function(){w.addClass(h.transition).addClass(h.hidden)},inward:function(){C.debug("Setting direction to inward"),w.removeClass(h.outward).addClass(h.inward)},outward:function(){C.debug("Setting direction to outward"),w.removeClass(h.inward).addClass(h.outward)},visible:function(){w.addClass(h.transition).addClass(h.visible)}},start:{animation:function(e){e=e||C.get.animationClass(),C.debug("Starting tween",e),w.addClass(e).one(b+".complete"+x,C.complete),m.useFailSafe&&C.add.failSafe(),C.set.duration(m.duration),m.onStart.call(k)}},save:{animation:function(e){C.cache||(C.cache={}),C.cache.animation=e},displayType:function(e){"none"!==e&&w.data(v.displayType,e)},transitionExists:function(t,n){e.fn.transition.exists[t]=n,C.verbose("Saving existence of transition",t,n)}},restore:{conditions:function(){var e=C.get.currentAnimation();e&&(w.removeClass(e),C.verbose("Removing animation class",C.cache)),C.remove.duration()}},add:{failSafe:function(){var e=C.get.duration();C.timer=setTimeout(function(){w.triggerHandler(b)},e+m.failSafeDelay),C.verbose("Adding fail safe timer",C.timer)}},remove:{animating:function(){w.removeClass(h.animating)},animationCallbacks:function(){C.remove.queueCallback(),C.remove.completeCallback()},queueCallback:function(){w.off(".queue"+x)},completeCallback:function(){w.off(".complete"+x)},display:function(){w.css("display","")},direction:function(){w.removeClass(h.inward).removeClass(h.outward)},duration:function(){w.css("animation-duration","")},failSafe:function(){C.verbose("Removing fail safe timer",C.timer),C.timer&&clearTimeout(C.timer)},hidden:function(){w.removeClass(h.hidden)},visible:function(){w.removeClass(h.visible)},looping:function(){C.debug("Transitions are no longer looping"),C.is.looping()&&(C.reset(),w.removeClass(h.looping))},transition:function(){w.removeClass(h.visible).removeClass(h.hidden)}},get:{settings:function(t,n,i){return"object"==typeof t?e.extend(!0,{},e.fn.transition.settings,t):"function"==typeof i?e.extend({},e.fn.transition.settings,{animation:t,onComplete:i,duration:n}):"string"==typeof n||"number"==typeof n?e.extend({},e.fn.transition.settings,{animation:t,duration:n}):"object"==typeof n?e.extend({},e.fn.transition.settings,n,{animation:t}):"function"==typeof n?e.extend({},e.fn.transition.settings,{animation:t,onComplete:n}):e.extend({},e.fn.transition.settings,{animation:t})},animationClass:function(e){var t=e||m.animation,n=C.can.transition()&&!C.has.direction()?C.get.direction()+" ":"";return h.animating+" "+h.transition+" "+n+t},currentAnimation:function(){return!(!C.cache||C.cache.animation===i)&&C.cache.animation},currentDirection:function(){return C.is.inward()?h.inward:h.outward},direction:function(){return C.is.hidden()||!C.is.visible()?h.inward:h.outward},animationDirection:function(t){var n;return t=t||m.animation,"string"==typeof t&&(t=t.split(" "),e.each(t,function(e,t){t===h.inward?n=h.inward:t===h.outward&&(n=h.outward)})),!!n&&n},duration:function(e){return e=e||m.duration,e===!1&&(e=w.css("animation-duration")||0),"string"==typeof e?e.indexOf("ms")>-1?parseFloat(e):1e3*parseFloat(e):e},displayType:function(e){return e=e===i||e,m.displayType?m.displayType:(e&&w.data(v.displayType)===i&&C.can.transition(!0),w.data(v.displayType))},userStyle:function(e){return e=e||w.attr("style")||"",e.replace(/display.*?;/,"")},transitionExists:function(t){return e.fn.transition.exists[t]},animationStartEvent:function(){var e,t=n.createElement("div"),o={animation:"animationstart",OAnimation:"oAnimationStart",MozAnimation:"mozAnimationStart",WebkitAnimation:"webkitAnimationStart"};for(e in o)if(t.style[e]!==i)return o[e];return!1},animationEndEvent:function(){var e,t=n.createElement("div"),o={animation:"animationend",OAnimation:"oAnimationEnd",MozAnimation:"mozAnimationEnd",WebkitAnimation:"webkitAnimationEnd"};for(e in o)if(t.style[e]!==i)return o[e];return!1}},can:{transition:function(t){var n,o,a,r,s,l,c=m.animation,u=C.get.transitionExists(c),d=C.get.displayType(!1);if(u===i||t){if(C.verbose("Determining whether animation exists"),n=w.attr("class"),o=w.prop("tagName"),a=e("<"+o+" />").addClass(n).insertAfter(w),r=a.addClass(c).removeClass(h.inward).removeClass(h.outward).addClass(h.animating).addClass(h.transition).css("animationName"),s=a.addClass(h.inward).css("animationName"),d||(d=a.attr("class",n).removeAttr("style").removeClass(h.hidden).removeClass(h.visible).show().css("display"),C.verbose("Determining final display state",d),C.save.displayType(d)),a.remove(),r!=s)C.debug("Direction exists for animation",c),l=!0;else{if("none"==r||!r)return void C.debug("No animation defined in css",c);C.debug("Static animation found",c,d),l=!1}C.save.transitionExists(c,l)}return u!==i?u:l},animate:function(){return C.can.transition()!==i}},is:{animating:function(){return w.hasClass(h.animating)},inward:function(){return w.hasClass(h.inward)},outward:function(){return w.hasClass(h.outward)},looping:function(){return w.hasClass(h.looping)},occurring:function(e){return e=e||m.animation,e="."+e.replace(" ","."),w.filter(e).length>0},visible:function(){return w.is(":visible")},hidden:function(){return"hidden"===w.css("visibility")},supported:function(){return b!==!1}},hide:function(){C.verbose("Hiding element"),C.is.animating()&&C.reset(),k.blur(),C.remove.display(),C.remove.visible(),C.set.hidden(),C.force.hidden(),m.onHide.call(k),m.onComplete.call(k)},show:function(e){C.verbose("Showing element",e),C.remove.hidden(),C.set.visible(),C.force.visible(),m.onShow.call(k),m.onComplete.call(k)},toggle:function(){C.is.visible()?C.hide():C.show()},stop:function(){C.debug("Stopping current animation"),w.triggerHandler(b)},stopAll:function(){C.debug("Stopping all animation"),C.remove.queueCallback(),w.triggerHandler(b)},clear:{queue:function(){C.debug("Clearing animation queue"),C.remove.queueCallback()}},enable:function(){C.verbose("Starting animation"),w.removeClass(h.disabled)},disable:function(){C.debug("Stopping animation"),w.addClass(h.disabled)},setting:function(t,n){if(C.debug("Changing setting",t,n),e.isPlainObject(t))e.extend(!0,m,t);else{if(n===i)return m[t];e.isPlainObject(m[t])?e.extend(!0,m[t],n):m[t]=n}},internal:function(t,n){if(e.isPlainObject(t))e.extend(!0,C,t);else{if(n===i)return C[t];C[t]=n}},debug:function(){!m.silent&&m.debug&&(m.performance?C.performance.log(arguments):(C.debug=Function.prototype.bind.call(console.info,console,m.name+":"),C.debug.apply(console,arguments)))},verbose:function(){!m.silent&&m.verbose&&m.debug&&(m.performance?C.performance.log(arguments):(C.verbose=Function.prototype.bind.call(console.info,console,m.name+":"),C.verbose.apply(console,arguments)))},error:function(){m.silent||(C.error=Function.prototype.bind.call(console.error,console,m.name+":"),C.error.apply(console,arguments))},performance:{log:function(e){var t,n,i;m.performance&&(t=(new Date).getTime(),i=s||t,n=t-i,s=t,l.push({Name:e[0],Arguments:[].slice.call(e,1)||"",Element:k,"Execution Time":n})),clearTimeout(C.performance.timer),C.performance.timer=setTimeout(C.performance.display,500)},display:function(){var t=m.name+":",n=0;s=!1,clearTimeout(C.performance.timer),e.each(l,function(e,t){n+=t["Execution Time"]}),t+=" "+n+"ms",r&&(t+=" '"+r+"'"),a.length>1&&(t+=" ("+a.length+")"),(console.group!==i||console.table!==i)&&l.length>0&&(console.groupCollapsed(t),console.table?console.table(l):e.each(l,function(e,t){console.log(t.Name+": "+t["Execution Time"]+"ms")}),console.groupEnd()),l=[]}},invoke:function(t,n,a){var r,s,l,c=g;return n=n||d,a=k||a,"string"==typeof t&&c!==i&&(t=t.split(/[\. ]/),r=t.length-1,e.each(t,function(n,o){var a=n!=r?o+t[n+1].charAt(0).toUpperCase()+t[n+1].slice(1):t;if(e.isPlainObject(c[a])&&n!=r)c=c[a];else{if(c[a]!==i)return s=c[a],!1;if(!e.isPlainObject(c[o])||n==r)return c[o]!==i&&(s=c[o],!1);c=c[o]}})),e.isFunction(s)?l=s.apply(a,n):s!==i&&(l=s),e.isArray(o)?o.push(l):o!==i?o=[o,l]:l!==i&&(o=l),s!==i&&s}},C.initialize()}),o!==i?o:this},e.fn.transition.exists={},e.fn.transition.settings={name:"Transition",silent:!1,debug:!1,verbose:!1,performance:!0,namespace:"transition",interval:0,reverse:"auto",onStart:function(){},onComplete:function(){},onShow:function(){},onHide:function(){},useFailSafe:!0,failSafeDelay:100,allowRepeats:!1,displayType:!1,animation:"fade",duration:!1,queue:!0,metadata:{displayType:"display"},className:{animating:"animating",disabled:"disabled",hidden:"hidden",inward:"in",loading:"loading",looping:"looping",outward:"out",transition:"transition",visible:"visible"},error:{noAnimation:"Element is no longer attached to DOM. Unable to animate. Use silent setting to surpress this warning in production.",repeated:"That animation is already occurring, cancelling repeated animation",method:"The method you called is not defined",support:"This browser does not support CSS animations"}}}(jQuery,window,document),function(e,t,n,i){"use strict";var t="undefined"!=typeof t&&t.Math==Math?t:"undefined"!=typeof self&&self.Math==Math?self:Function("return this")();e.api=e.fn.api=function(n){var o,a=e(e.isFunction(this)?t:this),r=a.selector||"",s=(new Date).getTime(),l=[],c=arguments[0],u="string"==typeof c,d=[].slice.call(arguments,1);return a.each(function(){var a,f,m,g,p,h,v=e.isPlainObject(n)?e.extend(!0,{},e.fn.api.settings,n):e.extend({},e.fn.api.settings),b=v.namespace,y=v.metadata,x=v.selector,C=v.error,w=v.className,k="."+b,S="module-"+b,T=e(this),A=T.closest(x.form),R=v.stateContext?e(v.stateContext):T,E=this,P=R[0],F=T.data(S);h={initialize:function(){u||h.bind.events(),h.instantiate()},instantiate:function(){h.verbose("Storing instance of module",h),F=h,T.data(S,F)},destroy:function(){h.verbose("Destroying previous module for",E),T.removeData(S).off(k)},bind:{events:function(){var e=h.get.event();e?(h.verbose("Attaching API events to element",e),T.on(e+k,h.event.trigger)):"now"==v.on&&(h.debug("Querying API endpoint immediately"),h.query())}},decode:{json:function(e){if(e!==i&&"string"==typeof e)try{e=JSON.parse(e)}catch(t){}return e}},read:{cachedResponse:function(e){var n;return t.Storage===i?void h.error(C.noStorage):(n=sessionStorage.getItem(e),h.debug("Using cached response",e,n),n=h.decode.json(n))}},write:{cachedResponse:function(n,o){return o&&""===o?void h.debug("Response empty, not caching",o):t.Storage===i?void h.error(C.noStorage):(e.isPlainObject(o)&&(o=JSON.stringify(o)),sessionStorage.setItem(n,o),void h.verbose("Storing cached response for url",n,o))}},query:function(){if(h.is.disabled())return void h.debug("Element is disabled API request aborted");if(h.is.loading()){if(!v.interruptRequests)return void h.debug("Cancelling request, previous request is still pending");h.debug("Interrupting previous request"),h.abort()}return v.defaultData&&e.extend(!0,v.urlData,h.get.defaultData()),v.serializeForm&&(v.data=h.add.formData(v.data)),f=h.get.settings(),f===!1?(h.cancelled=!0,void h.error(C.beforeSend)):(h.cancelled=!1,m=h.get.templatedURL(),m||h.is.mocked()?(m=h.add.urlData(m),m||h.is.mocked()?(f.url=v.base+m,a=e.extend(!0,{},v,{type:v.method||v.type,data:g,url:v.base+m,beforeSend:v.beforeXHR,success:function(){},failure:function(){},complete:function(){}}),h.debug("Querying URL",a.url),h.verbose("Using AJAX settings",a),"local"===v.cache&&h.read.cachedResponse(m)?(h.debug("Response returned from local cache"),h.request=h.create.request(),void h.request.resolveWith(P,[h.read.cachedResponse(m)])):void(v.throttle?v.throttleFirstRequest||h.timer?(h.debug("Throttling request",v.throttle),clearTimeout(h.timer),h.timer=setTimeout(function(){h.timer&&delete h.timer,h.debug("Sending throttled request",g,a.method),h.send.request()},v.throttle)):(h.debug("Sending request",g,a.method),h.send.request(),h.timer=setTimeout(function(){},v.throttle)):(h.debug("Sending request",g,a.method),h.send.request()))):void 0):void h.error(C.missingURL))},should:{removeError:function(){return v.hideError===!0||"auto"===v.hideError&&!h.is.form()}},is:{disabled:function(){return T.filter(x.disabled).length>0},expectingJSON:function(){return"json"===v.dataType||"jsonp"===v.dataType},form:function(){return T.is("form")||R.is("form")},mocked:function(){return v.mockResponse||v.mockResponseAsync||v.response||v.responseAsync},input:function(){return T.is("input")},loading:function(){return!!h.request&&"pending"==h.request.state()},abortedRequest:function(e){return e&&e.readyState!==i&&0===e.readyState?(h.verbose("XHR request determined to be aborted"),!0):(h.verbose("XHR request was not aborted"),!1)},validResponse:function(t){return h.is.expectingJSON()&&e.isFunction(v.successTest)?(h.debug("Checking JSON returned success",v.successTest,t),v.successTest(t)?(h.debug("Response passed success test",t),!0):(h.debug("Response failed success test",t),!1)):(h.verbose("Response is not JSON, skipping validation",v.successTest,t),!0)}},was:{cancelled:function(){return h.cancelled||!1},succesful:function(){return h.request&&"resolved"==h.request.state()},failure:function(){return h.request&&"rejected"==h.request.state()},complete:function(){return h.request&&("resolved"==h.request.state()||"rejected"==h.request.state())}},add:{urlData:function(t,n){var o,a;return t&&(o=t.match(v.regExp.required),a=t.match(v.regExp.optional),n=n||v.urlData,o&&(h.debug("Looking for required URL variables",o),e.each(o,function(o,a){var r=a.indexOf("$")!==-1?a.substr(2,a.length-3):a.substr(1,a.length-2),s=e.isPlainObject(n)&&n[r]!==i?n[r]:T.data(r)!==i?T.data(r):R.data(r)!==i?R.data(r):n[r];return s===i?(h.error(C.requiredParameter,r,t),t=!1,!1):(h.verbose("Found required variable",r,s),s=v.encodeParameters?h.get.urlEncodedValue(s):s,t=t.replace(a,s),void 0)})),a&&(h.debug("Looking for optional URL variables",o),e.each(a,function(o,a){var r=a.indexOf("$")!==-1?a.substr(3,a.length-4):a.substr(2,a.length-3),s=e.isPlainObject(n)&&n[r]!==i?n[r]:T.data(r)!==i?T.data(r):R.data(r)!==i?R.data(r):n[r];s!==i?(h.verbose("Optional variable Found",r,s),t=t.replace(a,s)):(h.verbose("Optional variable not found",r),t=t.indexOf("/"+a)!==-1?t.replace("/"+a,""):t.replace(a,""))}))),t},formData:function(t){var n,o=e.fn.serializeObject!==i,a=o?A.serializeObject():A.serialize();return t=t||v.data,n=e.isPlainObject(t),n?o?(h.debug("Extending existing data with form data",t,a),t=e.extend(!0,{},t,a)):(h.error(C.missingSerialize),h.debug("Cant extend data. Replacing data with form data",t,a),t=a):(h.debug("Adding form data",a),t=a),t}},send:{request:function(){h.set.loading(),h.request=h.create.request(),h.is.mocked()?h.mockedXHR=h.create.mockedXHR():h.xhr=h.create.xhr(),v.onRequest.call(P,h.request,h.xhr)}},event:{trigger:function(e){h.query(),"submit"!=e.type&&"click"!=e.type||e.preventDefault()},xhr:{always:function(){},done:function(t,n,i){var o=this,a=(new Date).getTime()-p,r=v.loadingDuration-a,s=!!e.isFunction(v.onResponse)&&(h.is.expectingJSON()?v.onResponse.call(o,e.extend(!0,{},t)):v.onResponse.call(o,t));r=r>0?r:0,s&&(h.debug("Modified API response in onResponse callback",v.onResponse,s,t),t=s),r>0&&h.debug("Response completed early delaying state change by",r),setTimeout(function(){h.is.validResponse(t)?h.request.resolveWith(o,[t,i]):h.request.rejectWith(o,[i,"invalid"])},r)},fail:function(e,t,n){var i=this,o=(new Date).getTime()-p,a=v.loadingDuration-o;a=a>0?a:0,a>0&&h.debug("Response completed early delaying state change by",a),setTimeout(function(){h.is.abortedRequest(e)?h.request.rejectWith(i,[e,"aborted",n]):h.request.rejectWith(i,[e,"error",t,n])},a)}},request:{done:function(e,t){h.debug("Successful API Response",e),"local"===v.cache&&m&&(h.write.cachedResponse(m,e),h.debug("Saving server response locally",h.cache)),v.onSuccess.call(P,e,T,t)},complete:function(e,t){var n,i;h.was.succesful()?(i=e,n=t):(n=e,i=h.get.responseFromXHR(n)),h.remove.loading(),v.onComplete.call(P,i,T,n)},fail:function(e,t,n){var o=h.get.responseFromXHR(e),r=h.get.errorFromRequest(o,t,n);return"aborted"==t?(h.debug("XHR Aborted (Most likely caused by page navigation or CORS Policy)",t,n),v.onAbort.call(P,t,T,e),!0):("invalid"==t?h.debug("JSON did not pass success test. A server-side error has most likely occurred",o):"error"==t&&e!==i&&(h.debug("XHR produced a server error",t,n),200!=e.status&&n!==i&&""!==n&&h.error(C.statusMessage+n,a.url),v.onError.call(P,r,T,e)),v.errorDuration&&"aborted"!==t&&(h.debug("Adding error state"),h.set.error(),h.should.removeError()&&setTimeout(h.remove.error,v.errorDuration)),h.debug("API Request failed",r,e),void v.onFailure.call(P,o,T,e))}}},create:{request:function(){return e.Deferred().always(h.event.request.complete).done(h.event.request.done).fail(h.event.request.fail)},mockedXHR:function(){var t,n,i,o=!1,a=!1,r=!1,s=v.mockResponse||v.response,l=v.mockResponseAsync||v.responseAsync;return i=e.Deferred().always(h.event.xhr.complete).done(h.event.xhr.done).fail(h.event.xhr.fail),s?(e.isFunction(s)?(h.debug("Using specified synchronous callback",s),n=s.call(P,f)):(h.debug("Using settings specified response",s),n=s),i.resolveWith(P,[n,o,{responseText:n}])):e.isFunction(l)&&(t=function(e){h.debug("Async callback returned response",e),e?i.resolveWith(P,[e,o,{responseText:e}]):i.rejectWith(P,[{responseText:e},a,r])},h.debug("Using specified async response callback",l),l.call(P,f,t)),i},xhr:function(){var t;return t=e.ajax(a).always(h.event.xhr.always).done(h.event.xhr.done).fail(h.event.xhr.fail),h.verbose("Created server request",t,a),t}},set:{error:function(){h.verbose("Adding error state to element",R),R.addClass(w.error)},loading:function(){h.verbose("Adding loading state to element",R),R.addClass(w.loading),p=(new Date).getTime()}},remove:{error:function(){h.verbose("Removing error state from element",R),R.removeClass(w.error)},loading:function(){h.verbose("Removing loading state from element",R),R.removeClass(w.loading)}},get:{responseFromXHR:function(t){return!!e.isPlainObject(t)&&(h.is.expectingJSON()?h.decode.json(t.responseText):t.responseText)},errorFromRequest:function(t,n,o){return e.isPlainObject(t)&&t.error!==i?t.error:v.error[n]!==i?v.error[n]:o},request:function(){return h.request||!1},xhr:function(){return h.xhr||!1},settings:function(){var t;return t=v.beforeSend.call(P,v),t&&(t.success!==i&&(h.debug("Legacy success callback detected",t),h.error(C.legacyParameters,t.success),t.onSuccess=t.success),t.failure!==i&&(h.debug("Legacy failure callback detected",t),h.error(C.legacyParameters,t.failure),t.onFailure=t.failure),t.complete!==i&&(h.debug("Legacy complete callback detected",t),h.error(C.legacyParameters,t.complete),t.onComplete=t.complete)),t===i&&h.error(C.noReturnedValue),t===!1?t:t!==i?e.extend(!0,{},t):e.extend(!0,{},v)},urlEncodedValue:function(e){var n=t.decodeURIComponent(e),i=t.encodeURIComponent(e),o=n!==e;return o?(h.debug("URL value is already encoded, avoiding double encoding",e),e):(h.verbose("Encoding value using encodeURIComponent",e,i),i)},defaultData:function(){var t={};return e.isWindow(E)||(h.is.input()?t.value=T.val():h.is.form()||(t.text=T.text())),t},event:function(){return e.isWindow(E)||"now"==v.on?(h.debug("API called without element, no events attached"),!1):"auto"==v.on?T.is("input")?E.oninput!==i?"input":E.onpropertychange!==i?"propertychange":"keyup":T.is("form")?"submit":"click":v.on},templatedURL:function(e){if(e=e||T.data(y.action)||v.action||!1,m=T.data(y.url)||v.url||!1)return h.debug("Using specified url",m),m;if(e){if(h.debug("Looking up url for action",e,v.api),v.api[e]===i&&!h.is.mocked())return void h.error(C.missingAction,v.action,v.api);m=v.api[e]}else h.is.form()&&(m=T.attr("action")||R.attr("action")||!1,h.debug("No url or action specified, defaulting to form action",m));return m}},abort:function(){var e=h.get.xhr();e&&"resolved"!==e.state()&&(h.debug("Cancelling API request"),e.abort())},reset:function(){h.remove.error(),h.remove.loading()},setting:function(t,n){if(h.debug("Changing setting",t,n),e.isPlainObject(t))e.extend(!0,v,t);else{if(n===i)return v[t];e.isPlainObject(v[t])?e.extend(!0,v[t],n):v[t]=n}},internal:function(t,n){if(e.isPlainObject(t))e.extend(!0,h,t);else{if(n===i)return h[t];h[t]=n}},debug:function(){!v.silent&&v.debug&&(v.performance?h.performance.log(arguments):(h.debug=Function.prototype.bind.call(console.info,console,v.name+":"),h.debug.apply(console,arguments)))},verbose:function(){!v.silent&&v.verbose&&v.debug&&(v.performance?h.performance.log(arguments):(h.verbose=Function.prototype.bind.call(console.info,console,v.name+":"),h.verbose.apply(console,arguments)))},error:function(){v.silent||(h.error=Function.prototype.bind.call(console.error,console,v.name+":"),h.error.apply(console,arguments))},performance:{log:function(e){var t,n,i;v.performance&&(t=(new Date).getTime(),i=s||t,n=t-i,s=t,l.push({Name:e[0],Arguments:[].slice.call(e,1)||"","Execution Time":n})),clearTimeout(h.performance.timer),h.performance.timer=setTimeout(h.performance.display,500)},display:function(){var t=v.name+":",n=0;s=!1,clearTimeout(h.performance.timer),e.each(l,function(e,t){n+=t["Execution Time"]}),t+=" "+n+"ms",r&&(t+=" '"+r+"'"),(console.group!==i||console.table!==i)&&l.length>0&&(console.groupCollapsed(t),console.table?console.table(l):e.each(l,function(e,t){console.log(t.Name+": "+t["Execution Time"]+"ms")}),console.groupEnd()),l=[]}},invoke:function(t,n,a){var r,s,l,c=F;return n=n||d,a=E||a,"string"==typeof t&&c!==i&&(t=t.split(/[\. ]/),r=t.length-1,e.each(t,function(n,o){var a=n!=r?o+t[n+1].charAt(0).toUpperCase()+t[n+1].slice(1):t;if(e.isPlainObject(c[a])&&n!=r)c=c[a];else{if(c[a]!==i)return s=c[a],!1;if(!e.isPlainObject(c[o])||n==r)return c[o]!==i?(s=c[o],!1):(h.error(C.method,t),!1);c=c[o]}})),e.isFunction(s)?l=s.apply(a,n):s!==i&&(l=s),e.isArray(o)?o.push(l):o!==i?o=[o,l]:l!==i&&(o=l),s}},u?(F===i&&h.initialize(),h.invoke(c)):(F!==i&&F.invoke("destroy"),h.initialize())}),o!==i?o:this},e.api.settings={name:"API",namespace:"api",debug:!1,verbose:!1,performance:!0,api:{},cache:!0,interruptRequests:!0,on:"auto",stateContext:!1,loadingDuration:0,hideError:"auto",errorDuration:2e3,encodeParameters:!0,action:!1,url:!1,base:"",urlData:{},defaultData:!0,serializeForm:!1,throttle:0,throttleFirstRequest:!0,method:"get",data:{},dataType:"json",mockResponse:!1,mockResponseAsync:!1,response:!1,responseAsync:!1,beforeSend:function(e){return e},beforeXHR:function(e){},onRequest:function(e,t){},onResponse:!1,onSuccess:function(e,t){},onComplete:function(e,t){},onFailure:function(e,t){},onError:function(e,t){},onAbort:function(e,t){},successTest:!1,error:{beforeSend:"The before send function has aborted the request",error:"There was an error with your request",exitConditions:"API Request Aborted. Exit conditions met",JSONParse:"JSON could not be parsed during error handling",legacyParameters:"You are using legacy API success callback names",method:"The method you called is not defined",missingAction:"API action used but no url was defined",missingSerialize:"jquery-serialize-object is required to add form data to an existing data object",missingURL:"No URL specified for api event",noReturnedValue:"The beforeSend callback must return a settings object, beforeSend ignored.",noStorage:"Caching responses locally requires session storage",parseError:"There was an error parsing your request",requiredParameter:"Missing a required URL parameter: ",statusMessage:"Server gave an error: ",timeout:"Your request timed out"},regExp:{required:/\{\$*[A-z0-9]+\}/g,optional:/\{\/\$*[A-z0-9]+\}/g},className:{loading:"loading",error:"error"},selector:{disabled:".disabled",form:"form"},metadata:{action:"action",url:"url"}}}(jQuery,window,document),function(e,t,n,i){"use strict";t="undefined"!=typeof t&&t.Math==Math?t:"undefined"!=typeof self&&self.Math==Math?self:Function("return this")(),e.fn.state=function(t){var o,a=e(this),r=a.selector||"",s=("ontouchstart"in n.documentElement,(new Date).getTime()),l=[],c=arguments[0],u="string"==typeof c,d=[].slice.call(arguments,1);return a.each(function(){var n,f=e.isPlainObject(t)?e.extend(!0,{},e.fn.state.settings,t):e.extend({},e.fn.state.settings),m=f.error,g=f.metadata,p=f.className,h=f.namespace,v=f.states,b=f.text,y="."+h,x=h+"-module",C=e(this),w=this,k=C.data(x);n={initialize:function(){n.verbose("Initializing module"),f.automatic&&n.add.defaults(),f.context&&""!==r?e(f.context).on(r,"mouseenter"+y,n.change.text).on(r,"mouseleave"+y,n.reset.text).on(r,"click"+y,n.toggle.state):C.on("mouseenter"+y,n.change.text).on("mouseleave"+y,n.reset.text).on("click"+y,n.toggle.state),n.instantiate()},instantiate:function(){n.verbose("Storing instance of module",n),k=n,C.data(x,n)},destroy:function(){n.verbose("Destroying previous module",k),C.off(y).removeData(x)},refresh:function(){n.verbose("Refreshing selector cache"),C=e(w)},add:{defaults:function(){var o=t&&e.isPlainObject(t.states)?t.states:{};e.each(f.defaults,function(t,a){n.is[t]!==i&&n.is[t]()&&(n.verbose("Adding default states",t,w),e.extend(f.states,a,o))})}},is:{active:function(){return C.hasClass(p.active)},loading:function(){return C.hasClass(p.loading)},inactive:function(){return!C.hasClass(p.active)},state:function(e){return p[e]!==i&&C.hasClass(p[e])},enabled:function(){return!C.is(f.filter.active)},disabled:function(){return C.is(f.filter.active)},textEnabled:function(){return!C.is(f.filter.text)},button:function(){return C.is(".button:not(a, .submit)")},input:function(){return C.is("input")},progress:function(){return C.is(".ui.progress")}},allow:function(e){n.debug("Now allowing state",e),v[e]=!0},disallow:function(e){n.debug("No longer allowing",e),v[e]=!1},allows:function(e){return v[e]||!1},enable:function(){C.removeClass(p.disabled)},disable:function(){C.addClass(p.disabled)},setState:function(e){n.allows(e)&&C.addClass(p[e])},removeState:function(e){n.allows(e)&&C.removeClass(p[e])},toggle:{state:function(){var t,o;if(n.allows("active")&&n.is.enabled()){if(n.refresh(),e.fn.api!==i)if(t=C.api("get request"),o=C.api("was cancelled"))n.debug("API Request cancelled by beforesend"),f.activateTest=function(){return!1},f.deactivateTest=function(){return!1};else if(t)return void n.listenTo(t);n.change.state()}}},listenTo:function(t){n.debug("API request detected, waiting for state signal",t),t&&(b.loading&&n.update.text(b.loading),e.when(t).then(function(){"resolved"==t.state()?(n.debug("API request succeeded"),f.activateTest=function(){return!0},f.deactivateTest=function(){return!0}):(n.debug("API request failed"),f.activateTest=function(){return!1},f.deactivateTest=function(){return!1}),n.change.state()}))},change:{state:function(){n.debug("Determining state change direction"),n.is.inactive()?n.activate():n.deactivate(),f.sync&&n.sync(),f.onChange.call(w)},text:function(){n.is.textEnabled()&&(n.is.disabled()?(n.verbose("Changing text to disabled text",b.hover),n.update.text(b.disabled)):n.is.active()?b.hover?(n.verbose("Changing text to hover text",b.hover),n.update.text(b.hover)):b.deactivate&&(n.verbose("Changing text to deactivating text",b.deactivate),n.update.text(b.deactivate)):b.hover?(n.verbose("Changing text to hover text",b.hover),n.update.text(b.hover)):b.activate&&(n.verbose("Changing text to activating text",b.activate),n.update.text(b.activate)))}},activate:function(){f.activateTest.call(w)&&(n.debug("Setting state to active"),C.addClass(p.active),n.update.text(b.active),f.onActivate.call(w))},deactivate:function(){f.deactivateTest.call(w)&&(n.debug("Setting state to inactive"),C.removeClass(p.active),n.update.text(b.inactive),f.onDeactivate.call(w)); +},sync:function(){n.verbose("Syncing other buttons to current state"),n.is.active()?a.not(C).state("activate"):a.not(C).state("deactivate")},get:{text:function(){return f.selector.text?C.find(f.selector.text).text():C.html()},textFor:function(e){return b[e]||!1}},flash:{text:function(e,t,i){var o=n.get.text();n.debug("Flashing text message",e,t),e=e||f.text.flash,t=t||f.flashDuration,i=i||function(){},n.update.text(e),setTimeout(function(){n.update.text(o),i.call(w)},t)}},reset:{text:function(){var e=b.active||C.data(g.storedText),t=b.inactive||C.data(g.storedText);n.is.textEnabled()&&(n.is.active()&&e?(n.verbose("Resetting active text",e),n.update.text(e)):t&&(n.verbose("Resetting inactive text",e),n.update.text(t)))}},update:{text:function(e){var t=n.get.text();e&&e!==t?(n.debug("Updating text",e),f.selector.text?C.data(g.storedText,e).find(f.selector.text).text(e):C.data(g.storedText,e).html(e)):n.debug("Text is already set, ignoring update",e)}},setting:function(t,o){if(n.debug("Changing setting",t,o),e.isPlainObject(t))e.extend(!0,f,t);else{if(o===i)return f[t];e.isPlainObject(f[t])?e.extend(!0,f[t],o):f[t]=o}},internal:function(t,o){if(e.isPlainObject(t))e.extend(!0,n,t);else{if(o===i)return n[t];n[t]=o}},debug:function(){!f.silent&&f.debug&&(f.performance?n.performance.log(arguments):(n.debug=Function.prototype.bind.call(console.info,console,f.name+":"),n.debug.apply(console,arguments)))},verbose:function(){!f.silent&&f.verbose&&f.debug&&(f.performance?n.performance.log(arguments):(n.verbose=Function.prototype.bind.call(console.info,console,f.name+":"),n.verbose.apply(console,arguments)))},error:function(){f.silent||(n.error=Function.prototype.bind.call(console.error,console,f.name+":"),n.error.apply(console,arguments))},performance:{log:function(e){var t,i,o;f.performance&&(t=(new Date).getTime(),o=s||t,i=t-o,s=t,l.push({Name:e[0],Arguments:[].slice.call(e,1)||"",Element:w,"Execution Time":i})),clearTimeout(n.performance.timer),n.performance.timer=setTimeout(n.performance.display,500)},display:function(){var t=f.name+":",o=0;s=!1,clearTimeout(n.performance.timer),e.each(l,function(e,t){o+=t["Execution Time"]}),t+=" "+o+"ms",r&&(t+=" '"+r+"'"),(console.group!==i||console.table!==i)&&l.length>0&&(console.groupCollapsed(t),console.table?console.table(l):e.each(l,function(e,t){console.log(t.Name+": "+t["Execution Time"]+"ms")}),console.groupEnd()),l=[]}},invoke:function(t,a,r){var s,l,c,u=k;return a=a||d,r=w||r,"string"==typeof t&&u!==i&&(t=t.split(/[\. ]/),s=t.length-1,e.each(t,function(o,a){var r=o!=s?a+t[o+1].charAt(0).toUpperCase()+t[o+1].slice(1):t;if(e.isPlainObject(u[r])&&o!=s)u=u[r];else{if(u[r]!==i)return l=u[r],!1;if(!e.isPlainObject(u[a])||o==s)return u[a]!==i?(l=u[a],!1):(n.error(m.method,t),!1);u=u[a]}})),e.isFunction(l)?c=l.apply(r,a):l!==i&&(c=l),e.isArray(o)?o.push(c):o!==i?o=[o,c]:c!==i&&(o=c),l}},u?(k===i&&n.initialize(),n.invoke(c)):(k!==i&&k.invoke("destroy"),n.initialize())}),o!==i?o:this},e.fn.state.settings={name:"State",debug:!1,verbose:!1,namespace:"state",performance:!0,onActivate:function(){},onDeactivate:function(){},onChange:function(){},activateTest:function(){return!0},deactivateTest:function(){return!0},automatic:!0,sync:!1,flashDuration:1e3,filter:{text:".loading, .disabled",active:".disabled"},context:!1,error:{beforeSend:"The before send function has cancelled state change",method:"The method you called is not defined."},metadata:{promise:"promise",storedText:"stored-text"},className:{active:"active",disabled:"disabled",error:"error",loading:"loading",success:"success",warning:"warning"},selector:{text:!1},defaults:{input:{disabled:!0,loading:!0,active:!0},button:{disabled:!0,loading:!0,active:!0},progress:{active:!0,success:!0,warning:!0,error:!0}},states:{active:!0,disabled:!0,error:!0,loading:!0,success:!0,warning:!0},text:{disabled:!1,flash:!1,hover:!1,active:!1,inactive:!1,activate:!1,deactivate:!1}}}(jQuery,window,document),function(e,t,n,i){"use strict";t="undefined"!=typeof t&&t.Math==Math?t:"undefined"!=typeof self&&self.Math==Math?self:Function("return this")(),e.fn.visibility=function(o){var a,r=e(this),s=r.selector||"",l=(new Date).getTime(),c=[],u=arguments[0],d="string"==typeof u,f=[].slice.call(arguments,1),m=r.length,g=0;return r.each(function(){var r,p,h,v,b=e.isPlainObject(o)?e.extend(!0,{},e.fn.visibility.settings,o):e.extend({},e.fn.visibility.settings),y=b.className,x=b.namespace,C=b.error,w=b.metadata,k="."+x,S="module-"+x,T=e(t),A=e(this),R=e(b.context),E=(A.selector||"",A.data(S)),P=t.requestAnimationFrame||t.mozRequestAnimationFrame||t.webkitRequestAnimationFrame||t.msRequestAnimationFrame||function(e){setTimeout(e,0)},F=this,O=!1;v={initialize:function(){v.debug("Initializing",b),v.setup.cache(),v.should.trackChanges()&&("image"==b.type&&v.setup.image(),"fixed"==b.type&&v.setup.fixed(),b.observeChanges&&v.observeChanges(),v.bind.events()),v.save.position(),v.is.visible()||v.error(C.visible,A),b.initialCheck&&v.checkVisibility(),v.instantiate()},instantiate:function(){v.debug("Storing instance",v),A.data(S,v),E=v},destroy:function(){v.verbose("Destroying previous module"),h&&h.disconnect(),p&&p.disconnect(),T.off("load"+k,v.event.load).off("resize"+k,v.event.resize),R.off("scroll"+k,v.event.scroll).off("scrollchange"+k,v.event.scrollchange),"fixed"==b.type&&(v.resetFixed(),v.remove.placeholder()),A.off(k).removeData(S)},observeChanges:function(){"MutationObserver"in t&&(p=new MutationObserver(v.event.contextChanged),h=new MutationObserver(v.event.changed),p.observe(n,{childList:!0,subtree:!0}),h.observe(F,{childList:!0,subtree:!0}),v.debug("Setting up mutation observer",h))},bind:{events:function(){v.verbose("Binding visibility events to scroll and resize"),b.refreshOnLoad&&T.on("load"+k,v.event.load),T.on("resize"+k,v.event.resize),R.off("scroll"+k).on("scroll"+k,v.event.scroll).on("scrollchange"+k,v.event.scrollchange)}},event:{changed:function(e){v.verbose("DOM tree modified, updating visibility calculations"),v.timer=setTimeout(function(){v.verbose("DOM tree modified, updating sticky menu"),v.refresh()},100)},contextChanged:function(t){[].forEach.call(t,function(t){t.removedNodes&&[].forEach.call(t.removedNodes,function(t){(t==F||e(t).find(F).length>0)&&(v.debug("Element removed from DOM, tearing down events"),v.destroy())})})},resize:function(){v.debug("Window resized"),b.refreshOnResize&&P(v.refresh)},load:function(){v.debug("Page finished loading"),P(v.refresh)},scroll:function(){b.throttle?(clearTimeout(v.timer),v.timer=setTimeout(function(){R.triggerHandler("scrollchange"+k,[R.scrollTop()])},b.throttle)):P(function(){R.triggerHandler("scrollchange"+k,[R.scrollTop()])})},scrollchange:function(e,t){v.checkVisibility(t)}},precache:function(t,i){t instanceof Array||(t=[t]);for(var o=t.length,a=0,r=[],s=n.createElement("img"),l=function(){a++,a>=t.length&&e.isFunction(i)&&i()};o--;)s=n.createElement("img"),s.onload=l,s.onerror=l,s.src=t[o],r.push(s)},enableCallbacks:function(){v.debug("Allowing callbacks to occur"),O=!1},disableCallbacks:function(){v.debug("Disabling all callbacks temporarily"),O=!0},should:{trackChanges:function(){return d?(v.debug("One time query, no need to bind events"),!1):(v.debug("Callbacks being attached"),!0)}},setup:{cache:function(){v.cache={occurred:{},screen:{},element:{}}},image:function(){var e=A.data(w.src);e&&(v.verbose("Lazy loading image",e),b.once=!0,b.observeChanges=!1,b.onOnScreen=function(){v.debug("Image on screen",F),v.precache(e,function(){v.set.image(e,function(){g++,g==m&&b.onAllLoaded.call(this),b.onLoad.call(this)})})})},fixed:function(){v.debug("Setting up fixed"),b.once=!1,b.observeChanges=!1,b.initialCheck=!0,b.refreshOnLoad=!0,o.transition||(b.transition=!1),v.create.placeholder(),v.debug("Added placeholder",r),b.onTopPassed=function(){v.debug("Element passed, adding fixed position",A),v.show.placeholder(),v.set.fixed(),b.transition&&e.fn.transition!==i&&A.transition(b.transition,b.duration)},b.onTopPassedReverse=function(){v.debug("Element returned to position, removing fixed",A),v.hide.placeholder(),v.remove.fixed()}}},create:{placeholder:function(){v.verbose("Creating fixed position placeholder"),r=A.clone(!1).css("display","none").addClass(y.placeholder).insertAfter(A)}},show:{placeholder:function(){v.verbose("Showing placeholder"),r.css("display","block").css("visibility","hidden")}},hide:{placeholder:function(){v.verbose("Hiding placeholder"),r.css("display","none").css("visibility","")}},set:{fixed:function(){v.verbose("Setting element to fixed position"),A.addClass(y.fixed).css({position:"fixed",top:b.offset+"px",left:"auto",zIndex:b.zIndex}),b.onFixed.call(F)},image:function(t,n){A.attr("src",t),b.transition?e.fn.transition!==i?A.transition(b.transition,b.duration,n):A.fadeIn(b.duration,n):A.show()}},is:{onScreen:function(){var e=v.get.elementCalculations();return e.onScreen},offScreen:function(){var e=v.get.elementCalculations();return e.offScreen},visible:function(){return!(!v.cache||!v.cache.element)&&!(0===v.cache.element.width&&0===v.cache.element.offset.top)}},refresh:function(){v.debug("Refreshing constants (width/height)"),"fixed"==b.type&&v.resetFixed(),v.reset(),v.save.position(),b.checkOnRefresh&&v.checkVisibility(),b.onRefresh.call(F)},resetFixed:function(){v.remove.fixed(),v.remove.occurred()},reset:function(){v.verbose("Resetting all cached values"),e.isPlainObject(v.cache)&&(v.cache.screen={},v.cache.element={})},checkVisibility:function(e){v.verbose("Checking visibility of element",v.cache.element),!O&&v.is.visible()&&(v.save.scroll(e),v.save.calculations(),v.passed(),v.passingReverse(),v.topVisibleReverse(),v.bottomVisibleReverse(),v.topPassedReverse(),v.bottomPassedReverse(),v.onScreen(),v.offScreen(),v.passing(),v.topVisible(),v.bottomVisible(),v.topPassed(),v.bottomPassed(),b.onUpdate&&b.onUpdate.call(F,v.get.elementCalculations()))},passed:function(t,n){var o=v.get.elementCalculations();if(t&&n)b.onPassed[t]=n;else{if(t!==i)return v.get.pixelsPassed(t)>o.pixelsPassed;o.passing&&e.each(b.onPassed,function(e,t){o.bottomVisible||o.pixelsPassed>v.get.pixelsPassed(e)?v.execute(t,e):b.once||v.remove.occurred(t)})}},onScreen:function(e){var t=v.get.elementCalculations(),n=e||b.onOnScreen,o="onScreen";if(e&&(v.debug("Adding callback for onScreen",e),b.onOnScreen=e),t.onScreen?v.execute(n,o):b.once||v.remove.occurred(o),e!==i)return t.onOnScreen},offScreen:function(e){var t=v.get.elementCalculations(),n=e||b.onOffScreen,o="offScreen";if(e&&(v.debug("Adding callback for offScreen",e),b.onOffScreen=e),t.offScreen?v.execute(n,o):b.once||v.remove.occurred(o),e!==i)return t.onOffScreen},passing:function(e){var t=v.get.elementCalculations(),n=e||b.onPassing,o="passing";if(e&&(v.debug("Adding callback for passing",e),b.onPassing=e),t.passing?v.execute(n,o):b.once||v.remove.occurred(o),e!==i)return t.passing},topVisible:function(e){var t=v.get.elementCalculations(),n=e||b.onTopVisible,o="topVisible";if(e&&(v.debug("Adding callback for top visible",e),b.onTopVisible=e),t.topVisible?v.execute(n,o):b.once||v.remove.occurred(o),e===i)return t.topVisible},bottomVisible:function(e){var t=v.get.elementCalculations(),n=e||b.onBottomVisible,o="bottomVisible";if(e&&(v.debug("Adding callback for bottom visible",e),b.onBottomVisible=e),t.bottomVisible?v.execute(n,o):b.once||v.remove.occurred(o),e===i)return t.bottomVisible},topPassed:function(e){var t=v.get.elementCalculations(),n=e||b.onTopPassed,o="topPassed";if(e&&(v.debug("Adding callback for top passed",e),b.onTopPassed=e),t.topPassed?v.execute(n,o):b.once||v.remove.occurred(o),e===i)return t.topPassed},bottomPassed:function(e){var t=v.get.elementCalculations(),n=e||b.onBottomPassed,o="bottomPassed";if(e&&(v.debug("Adding callback for bottom passed",e),b.onBottomPassed=e),t.bottomPassed?v.execute(n,o):b.once||v.remove.occurred(o),e===i)return t.bottomPassed},passingReverse:function(e){var t=v.get.elementCalculations(),n=e||b.onPassingReverse,o="passingReverse";if(e&&(v.debug("Adding callback for passing reverse",e),b.onPassingReverse=e),t.passing?b.once||v.remove.occurred(o):v.get.occurred("passing")&&v.execute(n,o),e!==i)return!t.passing},topVisibleReverse:function(e){var t=v.get.elementCalculations(),n=e||b.onTopVisibleReverse,o="topVisibleReverse";if(e&&(v.debug("Adding callback for top visible reverse",e),b.onTopVisibleReverse=e),t.topVisible?b.once||v.remove.occurred(o):v.get.occurred("topVisible")&&v.execute(n,o),e===i)return!t.topVisible},bottomVisibleReverse:function(e){var t=v.get.elementCalculations(),n=e||b.onBottomVisibleReverse,o="bottomVisibleReverse";if(e&&(v.debug("Adding callback for bottom visible reverse",e),b.onBottomVisibleReverse=e),t.bottomVisible?b.once||v.remove.occurred(o):v.get.occurred("bottomVisible")&&v.execute(n,o),e===i)return!t.bottomVisible},topPassedReverse:function(e){var t=v.get.elementCalculations(),n=e||b.onTopPassedReverse,o="topPassedReverse";if(e&&(v.debug("Adding callback for top passed reverse",e),b.onTopPassedReverse=e),t.topPassed?b.once||v.remove.occurred(o):v.get.occurred("topPassed")&&v.execute(n,o),e===i)return!t.onTopPassed},bottomPassedReverse:function(e){var t=v.get.elementCalculations(),n=e||b.onBottomPassedReverse,o="bottomPassedReverse";if(e&&(v.debug("Adding callback for bottom passed reverse",e),b.onBottomPassedReverse=e),t.bottomPassed?b.once||v.remove.occurred(o):v.get.occurred("bottomPassed")&&v.execute(n,o),e===i)return!t.bottomPassed},execute:function(e,t){var n=v.get.elementCalculations(),i=v.get.screenCalculations();e=e||!1,e&&(b.continuous?(v.debug("Callback being called continuously",t,n),e.call(F,n,i)):v.get.occurred(t)||(v.debug("Conditions met",t,n),e.call(F,n,i))),v.save.occurred(t)},remove:{fixed:function(){v.debug("Removing fixed position"),A.removeClass(y.fixed).css({position:"",top:"",left:"",zIndex:""}),b.onUnfixed.call(F)},placeholder:function(){v.debug("Removing placeholder content"),r&&r.remove()},occurred:function(e){if(e){var t=v.cache.occurred;t[e]!==i&&t[e]===!0&&(v.debug("Callback can now be called again",e),v.cache.occurred[e]=!1)}else v.cache.occurred={}}},save:{calculations:function(){v.verbose("Saving all calculations necessary to determine positioning"),v.save.direction(),v.save.screenCalculations(),v.save.elementCalculations()},occurred:function(e){e&&(v.cache.occurred[e]!==i&&v.cache.occurred[e]===!0||(v.verbose("Saving callback occurred",e),v.cache.occurred[e]=!0))},scroll:function(e){e=e+b.offset||R.scrollTop()+b.offset,v.cache.scroll=e},direction:function(){var e,t=v.get.scroll(),n=v.get.lastScroll();return e=t>n&&n?"down":t<n&&n?"up":"static",v.cache.direction=e,v.cache.direction},elementPosition:function(){var e=v.cache.element,t=v.get.screenSize();return v.verbose("Saving element position"),e.fits=e.height<t.height,e.offset=A.offset(),e.width=A.outerWidth(),e.height=A.outerHeight(),v.cache.element=e,e},elementCalculations:function(){var e=v.get.screenCalculations(),t=v.get.elementPosition();return b.includeMargin?(t.margin={},t.margin.top=parseInt(A.css("margin-top"),10),t.margin.bottom=parseInt(A.css("margin-bottom"),10),t.top=t.offset.top-t.margin.top,t.bottom=t.offset.top+t.height+t.margin.bottom):(t.top=t.offset.top,t.bottom=t.offset.top+t.height),t.topVisible=e.bottom>=t.top,t.topPassed=e.top>=t.top,t.bottomVisible=e.bottom>=t.bottom,t.bottomPassed=e.top>=t.bottom,t.pixelsPassed=0,t.percentagePassed=0,t.onScreen=t.topVisible&&!t.bottomPassed,t.passing=t.topPassed&&!t.bottomPassed,t.offScreen=!t.onScreen,t.passing&&(t.pixelsPassed=e.top-t.top,t.percentagePassed=(e.top-t.top)/t.height),v.cache.element=t,v.verbose("Updated element calculations",t),t},screenCalculations:function(){var e=v.get.scroll();return v.save.direction(),v.cache.screen.top=e,v.cache.screen.bottom=e+v.cache.screen.height,v.cache.screen},screenSize:function(){v.verbose("Saving window position"),v.cache.screen={height:R.height()}},position:function(){v.save.screenSize(),v.save.elementPosition()}},get:{pixelsPassed:function(e){var t=v.get.elementCalculations();return e.search("%")>-1?t.height*(parseInt(e,10)/100):parseInt(e,10)},occurred:function(e){return v.cache.occurred!==i&&(v.cache.occurred[e]||!1)},direction:function(){return v.cache.direction===i&&v.save.direction(),v.cache.direction},elementPosition:function(){return v.cache.element===i&&v.save.elementPosition(),v.cache.element},elementCalculations:function(){return v.cache.element===i&&v.save.elementCalculations(),v.cache.element},screenCalculations:function(){return v.cache.screen===i&&v.save.screenCalculations(),v.cache.screen},screenSize:function(){return v.cache.screen===i&&v.save.screenSize(),v.cache.screen},scroll:function(){return v.cache.scroll===i&&v.save.scroll(),v.cache.scroll},lastScroll:function(){return v.cache.screen===i?(v.debug("First scroll event, no last scroll could be found"),!1):v.cache.screen.top}},setting:function(t,n){if(e.isPlainObject(t))e.extend(!0,b,t);else{if(n===i)return b[t];b[t]=n}},internal:function(t,n){if(e.isPlainObject(t))e.extend(!0,v,t);else{if(n===i)return v[t];v[t]=n}},debug:function(){!b.silent&&b.debug&&(b.performance?v.performance.log(arguments):(v.debug=Function.prototype.bind.call(console.info,console,b.name+":"),v.debug.apply(console,arguments)))},verbose:function(){!b.silent&&b.verbose&&b.debug&&(b.performance?v.performance.log(arguments):(v.verbose=Function.prototype.bind.call(console.info,console,b.name+":"),v.verbose.apply(console,arguments)))},error:function(){b.silent||(v.error=Function.prototype.bind.call(console.error,console,b.name+":"),v.error.apply(console,arguments))},performance:{log:function(e){var t,n,i;b.performance&&(t=(new Date).getTime(),i=l||t,n=t-i,l=t,c.push({Name:e[0],Arguments:[].slice.call(e,1)||"",Element:F,"Execution Time":n})),clearTimeout(v.performance.timer),v.performance.timer=setTimeout(v.performance.display,500)},display:function(){var t=b.name+":",n=0;l=!1,clearTimeout(v.performance.timer),e.each(c,function(e,t){n+=t["Execution Time"]}),t+=" "+n+"ms",s&&(t+=" '"+s+"'"),(console.group!==i||console.table!==i)&&c.length>0&&(console.groupCollapsed(t),console.table?console.table(c):e.each(c,function(e,t){console.log(t.Name+": "+t["Execution Time"]+"ms")}),console.groupEnd()),c=[]}},invoke:function(t,n,o){var r,s,l,c=E;return n=n||f,o=F||o,"string"==typeof t&&c!==i&&(t=t.split(/[\. ]/),r=t.length-1,e.each(t,function(n,o){var a=n!=r?o+t[n+1].charAt(0).toUpperCase()+t[n+1].slice(1):t;if(e.isPlainObject(c[a])&&n!=r)c=c[a];else{if(c[a]!==i)return s=c[a],!1;if(!e.isPlainObject(c[o])||n==r)return c[o]!==i?(s=c[o],!1):(v.error(C.method,t),!1);c=c[o]}})),e.isFunction(s)?l=s.apply(o,n):s!==i&&(l=s),e.isArray(a)?a.push(l):a!==i?a=[a,l]:l!==i&&(a=l),s}},d?(E===i&&v.initialize(),E.save.scroll(),E.save.calculations(),v.invoke(u)):(E!==i&&E.invoke("destroy"),v.initialize())}),a!==i?a:this},e.fn.visibility.settings={name:"Visibility",namespace:"visibility",debug:!1,verbose:!1,performance:!0,observeChanges:!0,initialCheck:!0,refreshOnLoad:!0,refreshOnResize:!0,checkOnRefresh:!0,once:!0,continuous:!1,offset:0,includeMargin:!1,context:t,throttle:!1,type:!1,zIndex:"10",transition:"fade in",duration:1e3,onPassed:{},onOnScreen:!1,onOffScreen:!1,onPassing:!1,onTopVisible:!1,onBottomVisible:!1,onTopPassed:!1,onBottomPassed:!1,onPassingReverse:!1,onTopVisibleReverse:!1,onBottomVisibleReverse:!1,onTopPassedReverse:!1,onBottomPassedReverse:!1,onLoad:function(){},onAllLoaded:function(){},onFixed:function(){},onUnfixed:function(){},onUpdate:!1,onRefresh:function(){},metadata:{src:"src"},className:{fixed:"fixed",placeholder:"placeholder"},error:{method:"The method you called is not defined.",visible:"Element is hidden, you must call refresh after element becomes visible"}}}(jQuery,window,document);
\ No newline at end of file |