Index: lams_tool_mindmap/.externalToolBuilders/SASS Mindmap.launch =================================================================== diff -u --- lams_tool_mindmap/.externalToolBuilders/SASS Mindmap.launch (revision 0) +++ lams_tool_mindmap/.externalToolBuilders/SASS Mindmap.launch (revision 9cda52bbbc8ef19343bd53f563c0734560af1a3a) @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + Index: lams_tool_mindmap/.gitignore =================================================================== diff -u -ra83b0eec89979dce7415b02afdda324b14018dbb -r9cda52bbbc8ef19343bd53f563c0734560af1a3a --- lams_tool_mindmap/.gitignore (.../.gitignore) (revision a83b0eec89979dce7415b02afdda324b14018dbb) +++ lams_tool_mindmap/.gitignore (.../.gitignore) (revision 9cda52bbbc8ef19343bd53f563c0734560af1a3a) @@ -1,3 +1,4 @@ /bin/ /build/ /.sass-cache/ +/web/includes/css/mindmap.css Index: lams_tool_mindmap/.project =================================================================== diff -u -r65ae437b8b63c7e70b6c63380e3d25086d6ca101 -r9cda52bbbc8ef19343bd53f563c0734560af1a3a --- lams_tool_mindmap/.project (.../.project) (revision 65ae437b8b63c7e70b6c63380e3d25086d6ca101) +++ lams_tool_mindmap/.project (.../.project) (revision 9cda52bbbc8ef19343bd53f563c0734560af1a3a) @@ -10,6 +10,15 @@ + + org.eclipse.ui.externaltools.ExternalToolBuilder + + + LaunchConfigHandle + <project>/.externalToolBuilders/SASS Mindmap.launch + + + com.genuitec.eclipse.hibernate.hibernatenature Index: lams_tool_mindmap/build.properties =================================================================== diff -u -r5df0d44b0e94fca808f5d4f70fc01c0c428c3dfe -r9cda52bbbc8ef19343bd53f563c0734560af1a3a --- lams_tool_mindmap/build.properties (.../build.properties) (revision 5df0d44b0e94fca808f5d4f70fc01c0c428c3dfe) +++ lams_tool_mindmap/build.properties (.../build.properties) (revision 9cda52bbbc8ef19343bd53f563c0734560af1a3a) @@ -4,4 +4,6 @@ signature=lamind10 tool.version=20170101 tool.hide=false -tool.short.name=mindmap \ No newline at end of file +tool.short.name=mindmap + +scss.css.dir=${basedir}/web/includes/css Fisheye: Tag 9cda52bbbc8ef19343bd53f563c0734560af1a3a refers to a dead (removed) revision in file `lams_tool_mindmap/conf/language/ApplicationResources.properties'. Fisheye: No comparison available. Pass `N' to diff? Index: lams_tool_mindmap/conf/language/lams/ApplicationResources.properties =================================================================== diff -u -r67fb8eb3f9790623d3e9201b6a21a67d03e4573e -r9cda52bbbc8ef19343bd53f563c0734560af1a3a --- lams_tool_mindmap/conf/language/lams/ApplicationResources.properties (.../ApplicationResources.properties) (revision 67fb8eb3f9790623d3e9201b6a21a67d03e4573e) +++ lams_tool_mindmap/conf/language/lams/ApplicationResources.properties (.../ApplicationResources.properties) (revision 9cda52bbbc8ef19343bd53f563c0734560af1a3a) @@ -75,11 +75,6 @@ node.child2.defaultName =Sub idea 2 node.instructor.label =Instructor node.learner.label =Learner -local.title =Text -local.delete_question =Are sure you want to delete? -local.yes =Yes -local.no =No -local.node_creator =Node creator label.planner.editing.advice =Editing advice label.nogroups =No groups found for activity. monitor.summary.date.restriction =Deadline @@ -94,9 +89,18 @@ button.submit =Finish label.mindmap.options =Mindmap options label.activity.completion =End of activity -message.enableFlash =Sorry, this activity requires the Adobe Flash Player plugin, which does not seem to be present in your browser. Nevertheless, you can continue with the next activities. message.enableJavaScript =Sorry, enable javascript on your browser to use this activity. -message.enableFlashAuthorMonitor =Sorry, this activity requires the Adobe Flash Player plugin, which does not seem to be present in your browser. - +error.unable.to.load.mindmap=Unable to load data due from the server. Please reload your page. +error.occured.during.save=An error occurred while saving. Please reload your page. +label.your.mindmap.saved.every.minute=Your Mindmap will be saved automatically every minute and when you press Finish. +label.zoom=Zoom +label.zoom.reset=Reset +label.zoom.increase=+ +label.zoom.decrease=- +label.add.idea=Add Idea +label.edit.idea.text=Edit Idea Text +label.delete.idea=Delete Idea +label.expand.collapse.idea=Expand/Collapse Idea +label.no.changes.can.be.made.reloading.ideas=No changes can be made to the mindmap. Reloading ideas. #======= End labels: Exported 91 labels for en AU ===== Index: lams_tool_mindmap/src/java/org/lamsfoundation/lams/tool/mindmap/dbupdates/patch20180517.sql =================================================================== diff -u --- lams_tool_mindmap/src/java/org/lamsfoundation/lams/tool/mindmap/dbupdates/patch20180517.sql (revision 0) +++ lams_tool_mindmap/src/java/org/lamsfoundation/lams/tool/mindmap/dbupdates/patch20180517.sql (revision 9cda52bbbc8ef19343bd53f563c0734560af1a3a) @@ -0,0 +1,15 @@ +-- Turn off autocommit, so nothing is committed if there is an error +SET AUTOCOMMIT = 0; +SET FOREIGN_KEY_CHECKS=0; +----------------------Put all sql statements below here------------------------- + +-- Convert to mindmup. Color now #xxxxxx not just xxxxxx +ALTER TABLE tl_lamind10_node MODIFY node_color VARCHAR(7); +UPDATE tl_lamind10_node SET node_color = CONCAT('#',node_color) WHERE NOT node_color LIKE ('#%'); + +----------------------Put all sql statements above here------------------------- + +-- If there were no errors, commit and restore autocommit to on +COMMIT; +SET AUTOCOMMIT = 1; +SET FOREIGN_KEY_CHECKS=1; \ No newline at end of file Index: lams_tool_mindmap/src/java/org/lamsfoundation/lams/tool/mindmap/dto/IdeaJSON.java =================================================================== diff -u --- lams_tool_mindmap/src/java/org/lamsfoundation/lams/tool/mindmap/dto/IdeaJSON.java (revision 0) +++ lams_tool_mindmap/src/java/org/lamsfoundation/lams/tool/mindmap/dto/IdeaJSON.java (revision 9cda52bbbc8ef19343bd53f563c0734560af1a3a) @@ -0,0 +1,141 @@ +/**************************************************************** + * Copyright (C) 2005 LAMS Foundation (http://lamsfoundation.org) + * ============================================================= + * License Information: http://lamsfoundation.org/licensing/lams/2.0/ + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2.0 + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + * + * http://www.gnu.org/licenses/gpl.txt + * **************************************************************** + */ + +package org.lamsfoundation.lams.tool.mindmap.dto; + +import java.util.Iterator; + +import org.apache.log4j.Logger; +import org.apache.tomcat.util.json.JSONException; +import org.apache.tomcat.util.json.JSONObject; +import org.lamsfoundation.lams.tool.mindmap.util.xmlmodel.NodeConceptModel; +import org.lamsfoundation.lams.tool.mindmap.util.xmlmodel.NodeModel; + +/** A JSONObject in the MapJs v3 format as per https://github.com/mindmup/mapjs/wiki/Data-Format. + * Converts from our existing XML Model to JSON and back again. */ + +// invididual ideas +// +// { +// id: _idea id_, /*alphanumeric */ +// title: _idea title_, /* string */ +// attr : { /* key-value map of idea attributes, optional */ +// style: { }, /* key-value map of style properties, optional */ +// collapsed: true/false /* optional */ +// attachment: { contentType: _content type_, content: _content_ }, +// icon: { url: _icon url_, position: _icon position_, width: _icon width_, height: _icon height_ } +// } + +public class IdeaJSON extends JSONObject { + + private static Logger logger = Logger.getLogger(IdeaJSON.class); + + public static final String MAPJS_JSON_ID_KEY = "id"; + public static final String MAPJS_JSON_TITLE_KEY = "title"; + public static final String MAPJS_JSON_IDEAS_KEY = "ideas"; + public static final String MAPJS_JSON_STYLE_KEY = "style"; + public static final String MAPJS_JSON_ATTRIBUTES_KEY = "attr"; + public static final String MAPJS_JSON_BACKGROUND_COLOR_KEY = "background"; + public static final String MAPJS_JSON_WIDTH_KEY = "width"; + public static final String MAPJS_JSON_ATTRIBUTES_CONTENTLOCKED = "contentLocked"; + + public static final String DEFAULT_COLOR = "#ffffff"; + + // TODO support more than one main node. mindmup does it, our database code does not + /** Create JSON objects from the database model */ + public IdeaJSON(NodeModel node, int level) throws JSONException { + super(); + + this.put(MAPJS_JSON_ID_KEY, node.getConcept().getId()); + this.put(MAPJS_JSON_TITLE_KEY, node.getConcept().getText()); + this.put("creator", node.getConcept().getCreator()); // LAMS custom value + + JSONObject attr = new JSONObject(); + if ( node.getConcept().isEdit() == 0) { + attr.put(MAPJS_JSON_ATTRIBUTES_CONTENTLOCKED, true); + } + JSONObject style = new JSONObject(); + + int nextLevel = level+1; + if (!node.getBranch().isEmpty()) { + JSONObject ideasList = new JSONObject(); + if (level == 0) { + int rank = 1; + for (NodeModel childNode : node.getBranch()) { + ideasList.put(String.valueOf(rank), new IdeaJSON(childNode, nextLevel)); + rank = -rank; + if (rank > 0) + rank++; + } + } else { + int rank = 1; + for (NodeModel childNode : node.getBranch()) { + ideasList.put(String.valueOf(rank++), new IdeaJSON(childNode, nextLevel)); + } + } + this.put(MAPJS_JSON_IDEAS_KEY, ideasList); + } + + String color = node.getConcept().getColor(); + if (color == null || color.length() == 0 ) + color = DEFAULT_COLOR; + else if ( color != null && color.length() > 0 && !color.startsWith("#")) + color = "#" + color; + style.put(MAPJS_JSON_BACKGROUND_COLOR_KEY, color); + attr.put(MAPJS_JSON_STYLE_KEY, style); + this.put(MAPJS_JSON_ATTRIBUTES_KEY, attr); + } + + public static NodeModel toNodeModel(JSONObject idea) throws JSONException { + + // Pull out the style attributes + String color = null; + if ( idea.has(MAPJS_JSON_ATTRIBUTES_KEY) ) { + JSONObject attributes = idea.getJSONObject(MAPJS_JSON_ATTRIBUTES_KEY); + if ( attributes.has(MAPJS_JSON_STYLE_KEY)) { + JSONObject styles = attributes.getJSONObject(MAPJS_JSON_STYLE_KEY); + color = styles.getString(MAPJS_JSON_BACKGROUND_COLOR_KEY); + } + } + if (color == null || color.length() == 0) { + color = DEFAULT_COLOR; + } + + String creator = idea.has("creator") ? idea.getString("creator") : null; + NodeConceptModel nodeConceptModel = new NodeConceptModel(idea.getLong(MAPJS_JSON_ID_KEY), + idea.getString(MAPJS_JSON_TITLE_KEY), color, creator); + NodeModel nodeModel = new NodeModel(nodeConceptModel); + + if (idea.has(MAPJS_JSON_IDEAS_KEY)) { + JSONObject ideas = idea.getJSONObject(MAPJS_JSON_IDEAS_KEY); + Iterator> keys = ideas.sortedKeys(); + while (keys.hasNext()) { + String key = (String) keys.next(); + nodeModel.addNode(IdeaJSON.toNodeModel((JSONObject) ideas.get(key))); + } + } + + return nodeModel; + } + +} Index: lams_tool_mindmap/src/java/org/lamsfoundation/lams/tool/mindmap/dto/NotifyActionJSON.java =================================================================== diff -u --- lams_tool_mindmap/src/java/org/lamsfoundation/lams/tool/mindmap/dto/NotifyActionJSON.java (revision 0) +++ lams_tool_mindmap/src/java/org/lamsfoundation/lams/tool/mindmap/dto/NotifyActionJSON.java (revision 9cda52bbbc8ef19343bd53f563c0734560af1a3a) @@ -0,0 +1,57 @@ +/**************************************************************** + * Copyright (C) 2005 LAMS Foundation (http://lamsfoundation.org) + * ============================================================= + * License Information: http://lamsfoundation.org/licensing/lams/2.0/ + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2.0 + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + * + * http://www.gnu.org/licenses/gpl.txt + * **************************************************************** + */ + + + +package org.lamsfoundation.lams.tool.mindmap.dto; + +import org.apache.tomcat.util.json.JSONException; +import org.apache.tomcat.util.json.JSONObject; + +/** + * JSON Class for returning update actions to the client for multiuser mode + */ +public class NotifyActionJSON extends JSONObject { + + // Poll server response + public static final String REQUEST_ID_KEY = "actionId"; // Global ID from request table. + public static final String NODE_ID_KEY = "nodeId"; // Node ID + public static final String TYPE_KEY = "type"; // 0 - delete; 1 - create node; 2 - change color; 3 - change text + public static final String CHILD_NODE_ID_KEY = "childNodeId"; // Child Node ID // 1 - create node + public static final String TITLE_KEY = "title"; // Text (if type is 3, 1) + public static final String BACKGROUND_COLOR = "color"; // Color (if type is 2, maybe 1) + public static final String CREATOR = "creator"; // Creator description (type 1) - not used at present + + public NotifyActionJSON(Long requestId, Long nodeId, int type, Long childNodeId, String text, String color, String creator) throws JSONException { + super(); + this.put(REQUEST_ID_KEY, requestId); + this.put(NODE_ID_KEY, nodeId); + this.put(TYPE_KEY, type); + this.put(CHILD_NODE_ID_KEY, childNodeId); + this.putOpt(TITLE_KEY, text); + this.putOpt(BACKGROUND_COLOR, color); + this.putOpt(CREATOR, creator); + } + + +} Index: lams_tool_mindmap/src/java/org/lamsfoundation/lams/tool/mindmap/dto/NotifyResponseJSON.java =================================================================== diff -u --- lams_tool_mindmap/src/java/org/lamsfoundation/lams/tool/mindmap/dto/NotifyResponseJSON.java (revision 0) +++ lams_tool_mindmap/src/java/org/lamsfoundation/lams/tool/mindmap/dto/NotifyResponseJSON.java (revision 9cda52bbbc8ef19343bd53f563c0734560af1a3a) @@ -0,0 +1,48 @@ +/**************************************************************** + * Copyright (C) 2005 LAMS Foundation (http://lamsfoundation.org) + * ============================================================= + * License Information: http://lamsfoundation.org/licensing/lams/2.0/ + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2.0 + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + * + * http://www.gnu.org/licenses/gpl.txt + * **************************************************************** + */ + + + +package org.lamsfoundation.lams.tool.mindmap.dto; + +import org.apache.tomcat.util.json.JSONException; +import org.apache.tomcat.util.json.JSONObject; + +/** + * JSON Class for returning responding to a multi user change sent from the client. + */ +public class NotifyResponseJSON extends JSONObject { + + // Poll server response + public static final String REQUEST_ID_KEY = "requestId"; // Request ID + public static final String NODE_ID_KEY = "nodeId"; // Node ID + public static final String OK = "ok"; + + public NotifyResponseJSON(int ok, Long requestId, Long nodeId) throws JSONException { + super(); + this.put(OK, ok); + this.put(REQUEST_ID_KEY, requestId); + this.putOpt(NODE_ID_KEY, nodeId); + } + +} Index: lams_tool_mindmap/src/java/org/lamsfoundation/lams/tool/mindmap/dto/RootJSON.java =================================================================== diff -u --- lams_tool_mindmap/src/java/org/lamsfoundation/lams/tool/mindmap/dto/RootJSON.java (revision 0) +++ lams_tool_mindmap/src/java/org/lamsfoundation/lams/tool/mindmap/dto/RootJSON.java (revision 9cda52bbbc8ef19343bd53f563c0734560af1a3a) @@ -0,0 +1,102 @@ +/**************************************************************** + * Copyright (C) 2005 LAMS Foundation (http://lamsfoundation.org) + * ============================================================= + * License Information: http://lamsfoundation.org/licensing/lams/2.0/ + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2.0 + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + * + * http://www.gnu.org/licenses/gpl.txt + * **************************************************************** + */ + +package org.lamsfoundation.lams.tool.mindmap.dto; + +import java.util.Iterator; + +import org.apache.log4j.Logger; +import org.apache.tomcat.util.json.JSONArray; +import org.apache.tomcat.util.json.JSONException; +import org.apache.tomcat.util.json.JSONObject; +import org.lamsfoundation.lams.tool.mindmap.util.xmlmodel.NodeConceptModel; +import org.lamsfoundation.lams.tool.mindmap.util.xmlmodel.NodeModel; + +/** A JSONObject in the MapJs v3 format as per https://github.com/mindmup/mapjs/wiki/Data-Format. + * Converts from our existing XML Model to JSON and back again. + * + * If matchUserId is not null then check if this user can edit. If it is null, any user can edit. */ + +// aggregate root +// +// { +// formatVersion:3, /*numeric, only applicable to root idea*/ +// id: _aggregate idea id_, /* alphanumeric */ +// attr: {}, /* aggregate attributes, such as the map theme */ +// ideas: { _rank_: {_root idea_}, _rank2_: {_root idea 2_} ... }, /* key-value map of root nodes */ +// } +// + +public class RootJSON extends JSONObject { + + public static final String MAPJS_JSON_ROOT_ID_VALUE = "root"; + + + private static Logger logger = Logger.getLogger(RootJSON.class); + + // TODO support more than one main node. mindmup does it, our database code does not + /** Create JSON objects from the database model */ + public RootJSON(NodeModel node) throws JSONException { + super(); + + // create special root level JSON object + this.put(IdeaJSON.MAPJS_JSON_ID_KEY, MAPJS_JSON_ROOT_ID_VALUE); + this.put("formatVersion", "3"); + + if ( node != null) { + this.put(IdeaJSON.MAPJS_JSON_TITLE_KEY, node.getConcept().getText()); + } + + // start the recursion to create the ideas objects + JSONObject ideas = new JSONObject(); + ideas.put("1", new IdeaJSON(node, 0) ); + this.put(IdeaJSON.MAPJS_JSON_IDEAS_KEY, ideas); + } + + /** Deserialise the JSON from the client */ + public static NodeModel toNodeModel(String jsonContent) throws JSONException { + + JSONObject rootModel = new JSONObject(jsonContent); + if ( ! MAPJS_JSON_ROOT_ID_VALUE.equals(rootModel.get(IdeaJSON.MAPJS_JSON_ID_KEY)) ) + throw new JSONException("Root idea missing. Unable to parse mindmap. "+jsonContent); + + JSONObject ideasObject = rootModel.getJSONObject(IdeaJSON.MAPJS_JSON_IDEAS_KEY); + + if ( ideasObject.length() == 0 ) { + logger.error("No ideas to save!!!!!"); + return null; + } + + if ( ideasObject.length() > 1 ) { + logger.warn("More than node found at top level. Saving random node and children only."+jsonContent); + } + + Iterator> keys = ideasObject.sortedKeys(); + if ( keys.hasNext() ) { + String key = (String)keys.next(); + return IdeaJSON.toNodeModel((JSONObject) ideasObject.get(key)); + } + return null; + } + +} Index: lams_tool_mindmap/src/java/org/lamsfoundation/lams/tool/mindmap/service/IMindmapService.java =================================================================== diff -u -r2abc3485dc2d24ea02044a64271f3ee0d3b8c11b -r9cda52bbbc8ef19343bd53f563c0734560af1a3a --- lams_tool_mindmap/src/java/org/lamsfoundation/lams/tool/mindmap/service/IMindmapService.java (.../IMindmapService.java) (revision 2abc3485dc2d24ea02044a64271f3ee0d3b8c11b) +++ lams_tool_mindmap/src/java/org/lamsfoundation/lams/tool/mindmap/service/IMindmapService.java (.../IMindmapService.java) (revision 9cda52bbbc8ef19343bd53f563c0734560af1a3a) @@ -88,8 +88,6 @@ public MessageService getMindmapMessageService(); - public String getLanguageXML(); - public void deleteNodeByUniqueMindmapUser(Long uniqueId, Long mindmapId, Long userId, Long sessionId); public void deleteNodes(String nodesToDeleteCondition); @@ -124,7 +122,7 @@ String text, String color, MindmapUser mindmapUser, Mindmap mindmap, MindmapSession session); public NodeModel getMindmapXMLFromDatabase(Long rootNodeId, Long mindmapId, NodeModel rootNodeModel, - MindmapUser mindmapUser); + MindmapUser mindmapUser, boolean isMonitor, boolean isAuthor, boolean isUserLocked); public void getChildMindmapNodes(List branches, MindmapNode rootMindmapNode, MindmapUser mindmapUser, Mindmap mindmap, MindmapSession mindmapSession); Index: lams_tool_mindmap/src/java/org/lamsfoundation/lams/tool/mindmap/service/MindmapService.java =================================================================== diff -u -r045ebfd1d11d9ed0a1f81a00abb1a2ea373e8d93 -r9cda52bbbc8ef19343bd53f563c0734560af1a3a --- lams_tool_mindmap/src/java/org/lamsfoundation/lams/tool/mindmap/service/MindmapService.java (.../MindmapService.java) (revision 045ebfd1d11d9ed0a1f81a00abb1a2ea373e8d93) +++ lams_tool_mindmap/src/java/org/lamsfoundation/lams/tool/mindmap/service/MindmapService.java (.../MindmapService.java) (revision 9cda52bbbc8ef19343bd53f563c0734560af1a3a) @@ -318,7 +318,7 @@ */ @Override public NodeModel getMindmapXMLFromDatabase(Long rootNodeId, Long mindmapId, NodeModel rootNodeModel, - MindmapUser mindmapUser) { + MindmapUser mindmapUser, boolean isMonitor, boolean isAuthor, boolean isUserLocked) { List mindmapNodes = getMindmapNodeByParentId(rootNodeId, mindmapId); if ((mindmapNodes != null) && (mindmapNodes.size() > 0)) { @@ -332,24 +332,20 @@ mindmapUserName = mindmapNode.getUser().getFirstName() + " " + mindmapNode.getUser().getLastName(); } - NodeModel nodeModel = null; - if (mindmapUser != null) { - int edit = 1; - if (mindmapNode.getUser() == mindmapUser) { - edit = 1; - } else { - edit = 0; - } - - nodeModel = new NodeModel(new NodeConceptModel(mindmapNode.getUniqueId(), mindmapNode.getText(), - mindmapNode.getColor(), mindmapUserName, edit)); + int edit; + if ( isAuthor ){ + edit = 1; + } else if ( isMonitor || isUserLocked || mindmapUser == null) { + edit = 0; } else { - nodeModel = new NodeModel(new NodeConceptModel(mindmapNode.getUniqueId(), mindmapNode.getText(), - mindmapNode.getColor(), mindmapUserName, 1)); + edit = mindmapUser.equals(mindmapNode.getUser()) ? 1 : 0; } + NodeModel nodeModel = new NodeModel(new NodeConceptModel(mindmapNode.getUniqueId(), mindmapNode.getText(), + mindmapNode.getColor(), mindmapUserName, edit)); + rootNodeModel.addNode(nodeModel); - getMindmapXMLFromDatabase(mindmapNode.getNodeId(), mindmapId, nodeModel, mindmapUser); + getMindmapXMLFromDatabase(mindmapNode.getNodeId(), mindmapId, nodeModel, mindmapUser, isMonitor, isAuthor, isUserLocked); } } @@ -388,27 +384,6 @@ } @Override - public String getLanguageXML() { - ArrayList languageCollection = new ArrayList(); - languageCollection.add(new String("local.title")); - languageCollection.add(new String("local.delete_question")); - languageCollection.add(new String("local.yes")); - languageCollection.add(new String("local.no")); - languageCollection.add(new String("local.node_creator")); - - String languageOutput = ""; - - for (int i = 0; i < languageCollection.size(); i++) { - languageOutput += "" - + mindmapMessageService.getMessage(languageCollection.get(i)) + ""; - } - - languageOutput += ""; - - return languageOutput; - } - - @Override public void resetDefineLater(Long toolContentId) throws DataMissingException, ToolException { Mindmap mindmap = mindmapDAO.getByContentId(toolContentId); if (mindmap == null) { @@ -533,7 +508,7 @@ NodeModel rootNodeModel = new NodeModel(new NodeConceptModel(rootMindmapNode.getUniqueId(), rootMindmapNode.getText(), rootMindmapNode.getColor(), rootMindmapUser, 1)); NodeModel currentNodeModel = getMindmapXMLFromDatabase(rootMindmapNode.getNodeId(), mindmap.getUid(), - rootNodeModel, null); + rootNodeModel, null, false, false, false); mindmapContent = xstream.toXML(currentNodeModel); } Index: lams_tool_mindmap/src/java/org/lamsfoundation/lams/tool/mindmap/util/xmlmodel/PollResponseModel.java =================================================================== diff -u -r2f725f8ef2aa09a2663b2335bf67213074426d11 -r9cda52bbbc8ef19343bd53f563c0734560af1a3a --- lams_tool_mindmap/src/java/org/lamsfoundation/lams/tool/mindmap/util/xmlmodel/PollResponseModel.java (.../PollResponseModel.java) (revision 2f725f8ef2aa09a2663b2335bf67213074426d11) +++ lams_tool_mindmap/src/java/org/lamsfoundation/lams/tool/mindmap/util/xmlmodel/PollResponseModel.java (.../PollResponseModel.java) (revision 9cda52bbbc8ef19343bd53f563c0734560af1a3a) @@ -37,6 +37,10 @@ // list of Requests (Actions) ArrayList actions = new ArrayList(); + public ArrayList getActions() { + return actions; + } + /** Default Constructor */ public PollResponseModel() { } Index: lams_tool_mindmap/src/java/org/lamsfoundation/lams/tool/mindmap/web/actions/AuthoringAction.java =================================================================== diff -u -r6cfcb91c5526d4bbb22cd98dbd9d04c175cba1eb -r9cda52bbbc8ef19343bd53f563c0734560af1a3a --- lams_tool_mindmap/src/java/org/lamsfoundation/lams/tool/mindmap/web/actions/AuthoringAction.java (.../AuthoringAction.java) (revision 6cfcb91c5526d4bbb22cd98dbd9d04c175cba1eb) +++ lams_tool_mindmap/src/java/org/lamsfoundation/lams/tool/mindmap/web/actions/AuthoringAction.java (.../AuthoringAction.java) (revision 9cda52bbbc8ef19343bd53f563c0734560af1a3a) @@ -31,13 +31,15 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import org.apache.commons.lang.StringUtils; import org.apache.log4j.Logger; import org.apache.struts.action.ActionForm; import org.apache.struts.action.ActionForward; import org.apache.struts.action.ActionMapping; +import org.apache.tomcat.util.json.JSONException; +import org.apache.tomcat.util.json.JSONObject; import org.lamsfoundation.lams.authoring.web.AuthoringConstants; import org.lamsfoundation.lams.tool.ToolAccessMode; +import org.lamsfoundation.lams.tool.mindmap.dto.RootJSON; import org.lamsfoundation.lams.tool.mindmap.model.Mindmap; import org.lamsfoundation.lams.tool.mindmap.model.MindmapNode; import org.lamsfoundation.lams.tool.mindmap.model.MindmapUser; @@ -47,8 +49,6 @@ import org.lamsfoundation.lams.tool.mindmap.util.xmlmodel.NodeConceptModel; import org.lamsfoundation.lams.tool.mindmap.util.xmlmodel.NodeModel; import org.lamsfoundation.lams.tool.mindmap.web.forms.AuthoringForm; -import org.lamsfoundation.lams.util.Configuration; -import org.lamsfoundation.lams.util.ConfigurationKeys; import org.lamsfoundation.lams.util.WebUtil; import org.lamsfoundation.lams.web.action.LamsDispatchAction; import org.lamsfoundation.lams.web.util.AttributeNames; @@ -104,11 +104,11 @@ String childNodeName1 = mindmapService.getMindmapMessageService().getMessage("node.child1.defaultName"); String childNodeName2 = mindmapService.getMindmapMessageService().getMessage("node.child2.defaultName"); - MindmapNode rootMindmapNode = mindmapService.saveMindmapNode(null, null, 1l, rootNodeName, "ffffff", null, + MindmapNode rootMindmapNode = mindmapService.saveMindmapNode(null, null, 1l, rootNodeName, "#ffffff", null, mindmap, null); mindmapService.saveOrUpdateMindmapNode(rootMindmapNode); - mindmapService.saveMindmapNode(null, rootMindmapNode, 2l, childNodeName1, "ffffff", null, mindmap, null); - mindmapService.saveMindmapNode(null, rootMindmapNode, 3l, childNodeName2, "ffffff", null, mindmap, null); + mindmapService.saveMindmapNode(null, rootMindmapNode, 2l, childNodeName1, "#ffffff", null, mindmap, null); + mindmapService.saveMindmapNode(null, rootMindmapNode, 3l, childNodeName2, "#ffffff", null, mindmap, null); } if (mode.isTeacher()) { @@ -122,21 +122,8 @@ } /* Mindmap Attributes */ - - String mindmapContentPath = Configuration.get(ConfigurationKeys.SERVER_URL) - + "tool/lamind10/authoring.do?dispatch=setMindmapContent%26mindmapId=" + mindmap.getUid(); - request.setAttribute("mindmapContentPath", mindmapContentPath); - - String localizationPath = Configuration.get(ConfigurationKeys.SERVER_URL) - + "tool/lamind10/authoring.do?dispatch=setLocale"; - request.setAttribute("localizationPath", localizationPath); - - String currentMindmapUser = mindmapService.getMindmapMessageService().getMessage("node.instructor.label"); - request.setAttribute("currentMindmapUser", currentMindmapUser); - - String mindmapType = "images/mindmap_singleuser.swf"; - request.setAttribute("mindmapType", mindmapType); - + request.setAttribute("mindmapId", mindmap.getUid()); + // Set up the authForm. AuthoringForm authForm = (AuthoringForm) form; updateAuthForm(authForm, mindmap); @@ -155,18 +142,16 @@ /** * Returns the serialized XML of the Mindmap Nodes from Database - * - * @param mapping - * @param form - * @param request - * @param response - * @return null */ - public ActionForward setMindmapContent(ActionMapping mapping, ActionForm form, HttpServletRequest request, - HttpServletResponse response) { + public ActionForward setMindmapContentJSON(ActionMapping mapping, ActionForm form, HttpServletRequest request, + HttpServletResponse response) throws JSONException, IOException { - Long mindmapId = WebUtil.readLongParam(request, "mindmapId", false); + // set up mindmapService + if (mindmapService == null) { + mindmapService = MindmapServiceProxy.getMindmapService(this.getServlet().getServletContext()); + } + Long mindmapId = WebUtil.readLongParam(request, "mindmapId", false); List mindmapNodeList = mindmapService.getAuthorRootNodeByMindmapId(mindmapId); if (mindmapNodeList != null && mindmapNodeList.size() > 0) { @@ -177,56 +162,24 @@ NodeModel rootNodeModel = new NodeModel(new NodeConceptModel(rootMindmapNode.getUniqueId(), rootMindmapNode.getText(), rootMindmapNode.getColor(), rootMindmapUser, 1)); NodeModel currentNodeModel = mindmapService.getMindmapXMLFromDatabase(rootMindmapNode.getNodeId(), - mindmapId, rootNodeModel, null); + mindmapId, rootNodeModel, null, false, true, false); - String mindmapContent = mindmapService.getXStream().toXML(currentNodeModel); + JSONObject jsonObject = new JSONObject(); + jsonObject.put("mindmap", new RootJSON(currentNodeModel)); - try { - response.setContentType("text/xml"); - response.setCharacterEncoding("utf-8"); - response.getWriter().write(mindmapContent); - } catch (IOException e) { - e.printStackTrace(); - } + response.setContentType("application/x-json;charset=utf-8"); + response.getWriter().print(jsonObject.toString()); + return null; } return null; } /** - * Returns the serialized XML of the Mindmap Nodes from Database - * - * @param mapping - * @param form - * @param request - * @param response - * @return null - */ - public ActionForward setLocale(ActionMapping mapping, ActionForm form, HttpServletRequest request, - HttpServletResponse response) { - - try { - response.setContentType("text/xml"); - response.setCharacterEncoding("utf-8"); - response.getWriter().write(mindmapService.getLanguageXML()); - } catch (IOException e) { - e.printStackTrace(); - } - - return null; - } - - /** * Saves Mindmap Nodes to Database - * - * @param mapping - * @param form - * @param request - * @param response - * @return null */ public ActionForward updateContent(ActionMapping mapping, ActionForm form, HttpServletRequest request, - HttpServletResponse response) { + HttpServletResponse response) throws JSONException { // get authForm and session map. AuthoringForm authForm = (AuthoringForm) form; SessionMap map = getSessionMap(request, authForm); @@ -253,13 +206,20 @@ request.setAttribute(MindmapConstants.ATTR_SESSION_MAP, map); /* Saving Minmdap Nodes */ - - // getting xml data from SWF - String mindmapContent = authForm.getMindmapContent(); MindmapUser mindmapUser = mindmapService.getUserByUID(mindmap.getCreateBy()); + String mindmapContent = authForm.getMindmapContent(); - // Saving Mindmap data to XML - NodeModel rootNodeModel = (NodeModel) mindmapService.getXStream().fromXML(mindmapContent); + NodeModel rootNodeModel = RootJSON.toNodeModel(mindmapContent); + if ( rootNodeModel == null ) { + String error = new StringBuilder("Unable to save mindmap for authoring. User:") + .append(mindmapUser.getLoginName()) + .append("("+mindmapUser.getUserId()) + .append("). No root node. JSON: ") + .append(mindmapContent) + .toString(); + throw new JSONException(error); + } + NodeConceptModel nodeConceptModel = rootNodeModel.getConcept(); List branches = rootNodeModel.getBranch(); Index: lams_tool_mindmap/src/java/org/lamsfoundation/lams/tool/mindmap/web/actions/LearningAction.java =================================================================== diff -u -rc460c4454266a5b356af92096468e48613939da9 -r9cda52bbbc8ef19343bd53f563c0734560af1a3a --- lams_tool_mindmap/src/java/org/lamsfoundation/lams/tool/mindmap/web/actions/LearningAction.java (.../LearningAction.java) (revision c460c4454266a5b356af92096468e48613939da9) +++ lams_tool_mindmap/src/java/org/lamsfoundation/lams/tool/mindmap/web/actions/LearningAction.java (.../LearningAction.java) (revision 9cda52bbbc8ef19343bd53f563c0734560af1a3a) @@ -38,14 +38,21 @@ import org.apache.struts.action.ActionForm; import org.apache.struts.action.ActionForward; import org.apache.struts.action.ActionMapping; +import org.apache.tomcat.util.json.JSONArray; +import org.apache.tomcat.util.json.JSONException; +import org.apache.tomcat.util.json.JSONObject; import org.lamsfoundation.lams.learning.web.util.LearningWebUtil; import org.lamsfoundation.lams.notebook.model.NotebookEntry; import org.lamsfoundation.lams.notebook.service.CoreNotebookConstants; import org.lamsfoundation.lams.tool.ToolAccessMode; import org.lamsfoundation.lams.tool.ToolSessionManager; import org.lamsfoundation.lams.tool.exception.DataMissingException; import org.lamsfoundation.lams.tool.exception.ToolException; +import org.lamsfoundation.lams.tool.mindmap.dto.IdeaJSON; import org.lamsfoundation.lams.tool.mindmap.dto.MindmapDTO; +import org.lamsfoundation.lams.tool.mindmap.dto.NotifyActionJSON; +import org.lamsfoundation.lams.tool.mindmap.dto.NotifyResponseJSON; +import org.lamsfoundation.lams.tool.mindmap.dto.RootJSON; import org.lamsfoundation.lams.tool.mindmap.model.Mindmap; import org.lamsfoundation.lams.tool.mindmap.model.MindmapNode; import org.lamsfoundation.lams.tool.mindmap.model.MindmapRequest; @@ -85,7 +92,15 @@ private static Logger log = Logger.getLogger(LearningAction.class); private static final boolean MODE_OPTIONAL = false; private IMindmapService mindmapService; + + private static final String REQUEST_JSON_TYPE = "type"; // Expected to be int: 0 - delete; 1 - create node; 2 - change color; 3 - change text + private static final String REQUEST_JSON_REQUEST_ID = "requestId"; // Expected to be long + private static final String REQUEST_JSON_PARENT_NODE_ID = "parentId"; // Expected to be long + + private static final String RESPONSE_JSON_ACTIONS = "actions"; + + /** * Default action on page load. Clones Mindmap Nodes for each Learner in single-user mode. Uses shared (runtime * created in CopyToolContent method) Mindmap Nodes in multi-user mode. @@ -149,15 +164,15 @@ LearningWebUtil.putActivityPositionInRequestByToolSessionId(toolSessionID, request, getServlet().getServletContext()); + HttpSession ss = SessionManager.getSession(); + UserDTO userDto = (UserDTO) ss.getAttribute(AttributeNames.USER); + // check if there is submission deadline Date submissionDeadline = mindmap.getSubmissionDeadline(); if (submissionDeadline != null) { // store submission deadline to sessionMap request.setAttribute(MindmapConstants.ATTR_SUBMISSION_DEADLINE, submissionDeadline); - - HttpSession ss = SessionManager.getSession(); - UserDTO learnerDto = (UserDTO) ss.getAttribute(AttributeNames.USER); - TimeZone learnerTimeZone = learnerDto.getTimeZone(); + TimeZone learnerTimeZone = userDto.getTimeZone(); Date tzSubmissionDeadline = DateUtil.convertToTimeZoneFromDefault(learnerTimeZone, submissionDeadline); Date currentLearnerDate = DateUtil.convertToTimeZoneFromDefault(learnerTimeZone, new Date()); @@ -169,8 +184,7 @@ MindmapUser mindmapUser; if (mode.equals(ToolAccessMode.TEACHER)) { - Long userID = WebUtil.readLongParam(request, AttributeNames.PARAM_USER_ID, false); - mindmapUser = mindmapService.getUserByUserIdAndSessionId(userID, toolSessionID); + mindmapUser = mindmapService.getUserByUserIdAndSessionId(userDto.getUserID().longValue(), toolSessionID); } else { mindmapUser = getCurrentUser(toolSessionID); } @@ -193,19 +207,6 @@ String currentMindmapUser = mindmapUser.getFirstName() + " " + mindmapUser.getLastName(); request.setAttribute("currentMindmapUser", currentMindmapUser); - // mindmapType Parameter - String mindmapType = null; - if (mindmap.isLockOnFinished() && mindmapUser.isFinishedActivity()) { - mindmapType = "images/mindmap_locked.swf"; - } else { - if (mindmap.isMultiUserMode() == true) { - mindmapType = "images/mindmap_multiuser.swf"; - } else { - mindmapType = "images/mindmap_singleuser.swf"; - } - } - request.setAttribute("mindmapType", mindmapType); - // pollServer Parameter String pollServerParam = Configuration.get(ConfigurationKeys.SERVER_URL) + "tool/lamind10/learning.do?dispatch=pollServerAction%26mindmapId=" + mindmap.getUid() + "%26userId=" @@ -423,7 +424,150 @@ return null; } + /** + * Gets the Notify Requests (Actions) from Flash and returns proper Notify Responses + * + * @param mapping + * @param form + * @param request + * @param response + * @return null + * @throws JSONException + */ + public ActionForward notifyServerActionJSON(ActionMapping mapping, ActionForm form, HttpServletRequest request, + HttpServletResponse response) throws JSONException { + + Long userId = WebUtil.readLongParam(request, "userId", false); + Long mindmapId = WebUtil.readLongParam(request, "mindmapId", false); + Long toolSessionId = WebUtil.readLongParam(request, "sessionId", false); + String requestAction = WebUtil.readStrParam(request, "actionJSON", false); + + MindmapSession mindmapSession = mindmapService.getSessionBySessionId(toolSessionId); + + JSONObject notifyRequest = new JSONObject(requestAction); + int requestType = notifyRequest.getInt(REQUEST_JSON_TYPE); + + MindmapRequest mindmapRequest = null; + if ( notifyRequest.has(REQUEST_JSON_REQUEST_ID)) { + Long lastActionId = WebUtil.readLongParam(request, "lastActionId", false); + mindmapRequest = mindmapService.getRequestByUniqueId(notifyRequest.getLong(REQUEST_JSON_REQUEST_ID), userId, mindmapId, lastActionId); + } + + NotifyResponseJSON notifyResponse = null; + + MindmapUser currentUser = mindmapService.getUserByUID(userId); + if ( currentUser == null || ( mindmapSession.getMindmap().isLockOnFinished() && currentUser.isFinishedActivity() ) ) { + notifyResponse = new NotifyResponseJSON(0, null, null); + } + + // if request wasn't created before, create it + else if (mindmapRequest == null) { + // getting node to which changes will be applied + MindmapNode mindmapNode = null; + if ( notifyRequest.has(IdeaJSON.MAPJS_JSON_ID_KEY) && requestType != 1 ) { + List mindmapNodeList = mindmapService.getMindmapNodeByUniqueIdSessionId(notifyRequest.getLong(IdeaJSON.MAPJS_JSON_ID_KEY), mindmapId, toolSessionId); + if ((mindmapNodeList != null) && (mindmapNodeList.size() > 0)) { + mindmapNode = (MindmapNode) mindmapNodeList.get(0); + } else { + LearningAction.log.error("notifyServerAction(): Error finding node!"); + return null; + } + } + + // delete node + if (requestType == 0) { + // if node is created not by author or by other user... cannot delete + if (mindmapNode.getUser() == mindmapService.getUserByUID(userId)) { + + List childNodes = mindmapService.getMindmapNodeByParentIdMindmapIdSessionId(mindmapNode.getNodeId(), + mindmapId, toolSessionId); + + if ((childNodes == null) || (childNodes.size() == 0)) // check if node has any children + { + mindmapService.deleteNodeByUniqueMindmapUser(mindmapNode.getUniqueId(), mindmapId, userId, mindmapSession.getUid()); + mindmapRequest = saveMindmapRequestJSON(mindmapRequest, requestType, null, mindmapNode.getUniqueId(), userId, + mindmapId, null, toolSessionId); + notifyResponse = new NotifyResponseJSON(1, mindmapRequest.getGlobalId(), null); + } else { + notifyResponse = new NotifyResponseJSON(0, null, null); + } + } else { + notifyResponse = new NotifyResponseJSON(0, null, null); + } + } + // create node + else if (requestType == 1) { + // no checking... users can create nodes everywhere. + + // node unique ID - keep the biggest out of (next database value, client node). this ensures + // that deletions don't go back and fill holes with nodes that may conflict with something on a client. + Long uniqueId = mindmapService.getNodeLastUniqueIdByMindmapUidSessionId(mindmapId, toolSessionId) + 1; + Long childIdFromRequest = notifyRequest.getLong(IdeaJSON.MAPJS_JSON_ID_KEY); + if ( childIdFromRequest.longValue() > uniqueId.longValue() ) + uniqueId = childIdFromRequest; + + Long parentNodeId = notifyRequest.getLong(REQUEST_JSON_PARENT_NODE_ID); + List parentNodes = mindmapService.getMindmapNodeByUniqueIdSessionId(parentNodeId, mindmapId, toolSessionId); + if (parentNodes == null || parentNodes.size() == 0 ) { + LearningAction.log.error("notifyServerAction(): Unable to find parent node: "+parentNodeId+" toolSessionId "+toolSessionId); + } + + MindmapNode newMindmapNode = mindmapService.saveMindmapNode(null, (MindmapNode) parentNodes.get(0), uniqueId, + notifyRequest.optString(IdeaJSON.MAPJS_JSON_TITLE_KEY), + notifyRequest.optString(IdeaJSON.MAPJS_JSON_BACKGROUND_COLOR_KEY), mindmapService.getUserByUID(userId), + mindmapService.getMindmapByUid(mindmapId), mindmapSession); + + mindmapRequest = saveMindmapRequestJSON(mindmapRequest, requestType, null, parentNodeId, userId, mindmapId, + uniqueId, toolSessionId); + notifyResponse = new NotifyResponseJSON(1, mindmapRequest.getGlobalId(), uniqueId); + } + // change color + else if (requestType == 2) { + if (mindmapNode.getUser() == mindmapService.getUserByUID(userId)) { + mindmapNode.setColor(notifyRequest.optString(IdeaJSON.MAPJS_JSON_BACKGROUND_COLOR_KEY)); + mindmapNode.setUser(mindmapService.getUserByUID(userId)); + mindmapService.saveOrUpdateMindmapNode(mindmapNode); + mindmapRequest = saveMindmapRequestJSON(mindmapRequest, requestType, null, mindmapNode.getUniqueId(), userId, + mindmapId, null, toolSessionId); + notifyResponse = new NotifyResponseJSON(1, mindmapRequest.getGlobalId(), null); + } else { + notifyResponse = new NotifyResponseJSON(0, null, null); + } + } + // change text + else if (requestType == 3) { + if (mindmapNode.getUser() == mindmapService.getUserByUID(userId)) { + mindmapNode.setText(notifyRequest.optString(IdeaJSON.MAPJS_JSON_TITLE_KEY)); + mindmapNode.setUser(mindmapService.getUserByUID(userId)); + mindmapService.saveOrUpdateMindmapNode(mindmapNode); + mindmapRequest = saveMindmapRequestJSON(mindmapRequest, requestType, null, mindmapNode.getUniqueId(), userId, + mindmapId, null, toolSessionId); + notifyResponse = new NotifyResponseJSON(1, mindmapRequest.getGlobalId(), null); + } else { + notifyResponse = new NotifyResponseJSON(0, null, null); + } + } + } else { + if (requestType == 1) { + notifyResponse = new NotifyResponseJSON(1, mindmapRequest.getGlobalId(), + mindmapRequest.getNodeChildId()); + } else { + notifyResponse = new NotifyResponseJSON(1, mindmapRequest.getGlobalId(), null); + } + } + + try { + response.setContentType("application/x-json;charset=utf-8"); + response.getWriter().write(notifyResponse.toString()); + } catch (IOException e) { + e.printStackTrace(); + } + + return null; + } + + /** * Saves Notify Requests to database * * @param mindmapRequest @@ -449,6 +593,31 @@ } /** + * Saves Notify Requests to database + * + * @param mindmapRequest + * @param requestType + * @param notifyRequestModel + * @param userId + * @param mindmapId + * @param nodeChildId + */ + private MindmapRequest saveMindmapRequestJSON(MindmapRequest mindmapRequest, int requestType, Long requestId, + Long nodeId, Long userId, Long mindmapId, Long nodeChildId, Long sessionId) { + mindmapRequest = new MindmapRequest(); + mindmapRequest.setType(requestType); + mindmapRequest.setUniqueId(requestId); + // incrementing lastRequestId + mindmapRequest.setGlobalId(mindmapService.getLastGlobalIdByMindmapId(mindmapId, sessionId) + 1); + mindmapRequest.setUser(mindmapService.getUserByUID(userId)); + mindmapRequest.setMindmap(mindmapService.getMindmapByUid(mindmapId)); + mindmapRequest.setNodeId(nodeId); + mindmapRequest.setNodeChildId(nodeChildId); // nodeChildId + mindmapService.saveOrUpdateMindmapRequest(mindmapRequest); + return mindmapRequest; + } + + /** * Generated Notify Responses * * @param ok @@ -474,7 +643,101 @@ * @param request * @param response * @return null + * @throws JSONException */ + public ActionForward pollServerActionJSON(ActionMapping mapping, ActionForm form, HttpServletRequest request, + HttpServletResponse response) throws JSONException { + + Long mindmapId = WebUtil.readLongParam(request, "mindmapId", false); + Long userId = WebUtil.readLongParam(request, "userId", false); + Long toolSessionId = WebUtil.readLongParam(request, "sessionId", false); + Long lastActionId = WebUtil.readLongParam(request, "lastActionID", false); + + JSONObject pollResponseModel = new JSONObject(); + JSONArray actions = new JSONArray(); + pollResponseModel.put(RESPONSE_JSON_ACTIONS, actions); + + List requestsList = mindmapService.getLastRequestsAfterGlobalId(lastActionId, mindmapId, userId, toolSessionId); + for (Iterator iterator = requestsList.iterator(); iterator.hasNext();) { + MindmapRequest mindmapRequest = (MindmapRequest) iterator.next(); + int requestType = mindmapRequest.getType(); + + JSONObject notifyRequestModel = null; + MindmapNode rootMindmapNode = null; + if ((requestType != 0) && (requestType != 1)) { + List nodesList = mindmapService.getMindmapNodeByUniqueIdSessionId(mindmapRequest.getNodeId(), mindmapId, + toolSessionId); + + if ((nodesList != null) && (nodesList.size() > 0)) { + rootMindmapNode = (MindmapNode) nodesList.get(0); + } else { + LearningAction.log.error("pollServerAction(): Error finding node while changing text or color!"); + } + } + + MindmapNode mindmapNode = null; + if (requestType == 1) { + List nodesList = mindmapService.getMindmapNodeByUniqueIdSessionId(mindmapRequest.getNodeChildId(), + mindmapId, toolSessionId); + + if ((nodesList != null) && (nodesList.size() > 0)) { + mindmapNode = (MindmapNode) nodesList.get(0); + } else { + LearningAction.log.error("pollServerAction(): Error finding node while creating a node!"); + } + } + + // delete node + if (requestType == 0) { + notifyRequestModel = new NotifyActionJSON(mindmapRequest.getGlobalId(), mindmapRequest.getNodeId(), + mindmapRequest.getType(), null, null, null, null); + } + // create node + else if (requestType == 1) { + String creator = null; + MindmapUser mindmapUser = mindmapNode.getUser(); + if (mindmapUser != null) { + creator = mindmapUser.getFirstName() + " " + mindmapUser.getLastName(); + } else { + creator = "Student"; + } + notifyRequestModel = new NotifyActionJSON(mindmapRequest.getGlobalId(), mindmapRequest.getNodeId(), + mindmapRequest.getType(), mindmapRequest.getNodeChildId(), mindmapNode.getText(), + mindmapNode.getColor(), creator); + } + // change color + else if (requestType == 2) { + notifyRequestModel = new NotifyActionJSON(mindmapRequest.getGlobalId(), mindmapRequest.getNodeId(), + mindmapRequest.getType(), null, null, rootMindmapNode.getColor(), null); + } + // change text + else if (requestType == 3) { + notifyRequestModel = new NotifyActionJSON(mindmapRequest.getGlobalId(), mindmapRequest.getNodeId(), + mindmapRequest.getType(), null, rootMindmapNode.getText(), null, null); + } + + actions.put(notifyRequestModel); + } + + try { + response.setContentType("application/x-json;charset=utf-8"); + response.getWriter().write(pollResponseModel.toString()); + } catch (IOException e) { + e.printStackTrace(); + } + + return null; + } + + /** + * Returns lists of Poll Requests (Actions) on Mindmap Nodes made by other learners + * + * @param mapping + * @param form + * @param request + * @param response + * @return null + */ public ActionForward pollServerAction(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) { @@ -569,23 +832,45 @@ } /** - * Returns the serialized XML of the Mindmap Nodes from Database + * Returns the serialized JSON of the Mindmap Nodes from Database * * @param mapping * @param form * @param request * @param response * @return null + * @throws JSONException + * @throws IOException */ - public ActionForward setMindmapContent(ActionMapping mapping, ActionForm form, HttpServletRequest request, - HttpServletResponse response) { + public ActionForward setMindmapContentJSON(ActionMapping mapping, ActionForm form, HttpServletRequest request, + HttpServletResponse response) throws JSONException, IOException { + // set up mindmapService + if (mindmapService == null) { + mindmapService = MindmapServiceProxy.getMindmapService(this.getServlet().getServletContext()); + } + Long mindmapId = WebUtil.readLongParam(request, "mindmapId", false); - Long userId = WebUtil.readLongParam(request, "userId", false); Long toolSessionId = WebUtil.readLongParam(request, "sessionId", true); Mindmap mindmap = mindmapService.getMindmapByUid(mindmapId); - MindmapUser mindmapUser = mindmapService.getUserByUID(userId); + ToolAccessMode mode = WebUtil.readToolAccessModeParam(request, AttributeNames.PARAM_MODE, + LearningAction.MODE_OPTIONAL); + boolean monitoring = mode.equals(ToolAccessMode.TEACHER); + + // In monitoring we do not need the user when accessing multi mode mindmap + Long userId = WebUtil.readLongParam(request, "userId", true); + if (userId == null && !(monitoring && mindmap.isMultiUserMode())) { + log.error( + "Unable to display mindmap as no user id is supplied. Can only skip user id if this is monitoring and multiuser mode."); + return null; + } + + MindmapUser mindmapUser = null; + if (userId != null) { + mindmapUser = mindmapService.getUserByUID(userId); + } + List mindmapNodeList = null; if (mindmap.isMultiUserMode()) { mindmapNodeList = mindmapService.getAuthorRootNodeByMindmapSession(mindmapId, toolSessionId); @@ -604,74 +889,73 @@ + rootMindmapNode.getUser().getLastName(); } - int edit = 1; - if (rootMindmapNode.getUser() == mindmapUser) { - edit = 1; - } else { - edit = 0; - } - NodeModel rootNodeModel = new NodeModel(new NodeConceptModel(rootMindmapNode.getUniqueId(), - rootMindmapNode.getText(), rootMindmapNode.getColor(), mindmapUserName, edit)); + rootMindmapNode.getText(), rootMindmapNode.getColor(), mindmapUserName, 0)); NodeModel currentNodeModel = mindmapService.getMindmapXMLFromDatabase(rootMindmapNode.getNodeId(), - mindmapId, rootNodeModel, mindmapUser); + mindmapId, rootNodeModel, mindmapUser, monitoring, false, + mindmap.isLockOnFinished() && mindmapUser.isFinishedActivity()); - String mindmapContent = mindmapService.getXStream().toXML(currentNodeModel); - - // Saving lastActionID - Long lastActionId = mindmapService.getLastGlobalIdByMindmapId(mindmap.getUid(), toolSessionId); - //mindmap.setLastActionId(lastActionId); - + JSONObject jsonObject = new JSONObject(); + jsonObject.put("mindmap", new RootJSON(currentNodeModel)); // adding lastActionId if (mindmap.isMultiUserMode()) { - if (mindmap.isLockOnFinished() && !mindmapUser.isFinishedActivity()) { - mindmapContent = "\n" + mindmapContent + "\n" + lastActionId - + "\n"; - } else if (!mindmap.isLockOnFinished()) { - mindmapContent = "\n" + mindmapContent + "\n" + lastActionId - + "\n"; - } + Long lastActionId = mindmapService.getLastGlobalIdByMindmapId(mindmap.getUid(), toolSessionId); + jsonObject.put("lastActionId", lastActionId); } - try { - response.setContentType("text/xml"); - response.setCharacterEncoding("utf-8"); - response.getWriter().write(mindmapContent); - } catch (IOException e) { - e.printStackTrace(); - } - + response.setContentType("application/x-json;charset=utf-8"); + response.getWriter().print(jsonObject.toString()); + return null; } return null; } + public ActionForward saveLastMindmapChanges(ActionMapping mapping, ActionForm form, HttpServletRequest request, - HttpServletResponse response) { + HttpServletResponse response) throws JSONException, IOException { Long userId = WebUtil.readLongParam(request, "userId", false); Long toolContentId = WebUtil.readLongParam(request, "mindmapId", false); - Long toolSessionId = WebUtil.readLongParam(request, "sessionId", true); + Long toolSessionId = WebUtil.readLongParam(request, "toolSessionID", true); MindmapUser mindmapUser = mindmapService.getUserByUID(userId); Mindmap mindmap = mindmapService.getMindmapByUid(toolContentId); MindmapSession mindmapSession = mindmapService.getSessionBySessionId(toolSessionId); + NotifyResponseJSON responseJSON = null; // Saving Mindmap Nodes if (!mindmap.isMultiUserMode()) { - // getting xml data from SWF + // getting JSON from Mindmup String mindmapContent = WebUtil.readStrParam(request, "content", false); - // learningForm.getMindmapContent(); - - saveMindmapXML(mindmap, mindmapUser, mindmapContent, mindmapSession); + Long rootNodeId = saveMapJsJSON(mindmap, mindmapUser, mindmapContent, mindmapSession); + responseJSON = new NotifyResponseJSON( rootNodeId != null ? 1 : 0, null, rootNodeId); + } else { + responseJSON = new NotifyResponseJSON(0, null, 0L); } + response.setContentType("application/x-json;charset=utf-8"); + response.getWriter().print(responseJSON.toString()); return null; } - public void saveMindmapXML(Mindmap mindmap, MindmapUser mindmapUser, String mindmapContent, - MindmapSession mindmapSession) { - // Saving Mindmap data to XML - NodeModel rootNodeModel = (NodeModel) mindmapService.getXStream().fromXML(mindmapContent); + private Long saveMapJsJSON(Mindmap mindmap, MindmapUser mindmapUser, String mindmapContent, + MindmapSession mindmapSession) throws JSONException { + + NodeModel rootNodeModel = RootJSON.toNodeModel(mindmapContent); + if ( rootNodeModel == null ) { + String error = new StringBuilder("Unable to save mindmap for session:") + .append(mindmapSession.getSessionName()) + .append("(") + .append(mindmapSession.getSessionId()) + .append(") user:") + .append(mindmapUser.getLoginName()) + .append("("+mindmapUser.getUserId()) + .append("). No root node. JSON: ") + .append(mindmapContent) + .toString(); + throw new JSONException(error); + } + NodeConceptModel nodeConceptModel = rootNodeModel.getConcept(); List branches = rootNodeModel.getBranch(); @@ -693,32 +977,11 @@ nodesToDeleteCondition += mindmapService.getNodesToDeleteCondition() + " and mindmap_id = " + mindmap.getUid() + " and user_id = " + mindmapUser.getUid(); mindmapService.deleteNodes(nodesToDeleteCondition); + + return rootMindmapNode.getNodeId(); } /** - * Returns the serialized XML of the Mindmap Nodes from Database - * - * @param mapping - * @param form - * @param request - * @param response - * @return null - */ - public ActionForward setLocale(ActionMapping mapping, ActionForm form, HttpServletRequest request, - HttpServletResponse response) { - - try { - response.setContentType("text/xml"); - response.setCharacterEncoding("utf-8"); - response.getWriter().write(mindmapService.getLanguageXML()); - } catch (IOException e) { - e.printStackTrace(); - } - - return null; - } - - /** * Returns current learner * * @param toolSessionId @@ -747,9 +1010,10 @@ * @param request * @param response * @return + * @throws JSONException */ public ActionForward reflect(ActionMapping mapping, ActionForm form, HttpServletRequest request, - HttpServletResponse response) { + HttpServletResponse response) throws JSONException { LearningForm learningForm = (LearningForm) form; @@ -771,7 +1035,7 @@ // Saving Mindmap Nodes if (!mindmap.isMultiUserMode() && !StringUtils.isBlank(learningForm.getMindmapContent())) { - saveMindmapXML(mindmap, mindmapUser, learningForm.getMindmapContent(), mindmapSession); + saveMapJsJSON(mindmap, mindmapUser, learningForm.getMindmapContent(), mindmapSession); } // Reflection @@ -794,48 +1058,53 @@ * @param request * @param response * @return null + * @throws JSONException */ public ActionForward finishActivity(ActionMapping mapping, ActionForm form, HttpServletRequest request, - HttpServletResponse response) { + HttpServletResponse response) throws JSONException { Long toolSessionID = WebUtil.readLongParam(request, "toolSessionID"); MindmapUser mindmapUser = getCurrentUser(toolSessionID); - if (mindmapUser != null) { - LearningForm learningForm = (LearningForm) form; + // Retrieve the session and content + MindmapSession mindmapSession = mindmapService.getSessionBySessionId(toolSessionID); + if (mindmapSession == null) { + throw new MindmapException("Cannot retrieve session with toolSessionID" + toolSessionID); + } + Mindmap mindmap = mindmapSession.getMindmap(); - mindmapUser.setFinishedActivity(true); - mindmapService.saveOrUpdateMindmapUser(mindmapUser); + // if locked do not update anything - do not want to risk having accidently changed the mindmap + boolean contentLocked = mindmap.isLockOnFinished() && mindmapUser.isFinishedActivity(); - // Retrieve the session and content - MindmapSession mindmapSession = mindmapService.getSessionBySessionId(toolSessionID); - if (mindmapSession == null) { - throw new MindmapException("Cannot retrieve session with toolSessionID" + toolSessionID); - } + if (mindmapUser != null) { + if (!contentLocked) { + LearningForm learningForm = (LearningForm) form; - Mindmap mindmap = mindmapSession.getMindmap(); + mindmapUser.setFinishedActivity(true); + mindmapService.saveOrUpdateMindmapUser(mindmapUser); - // save the reflection entry and call the notebook. - if (mindmap.isReflectOnActivity()) { - // check for existing notebook entry - NotebookEntry entry = mindmapService.getEntry(mindmapUser.getEntryUID()); - if (entry == null) { - // create new entry - Long entryUID = mindmapService.createNotebookEntry(toolSessionID, - CoreNotebookConstants.NOTEBOOK_TOOL, MindmapConstants.TOOL_SIGNATURE, - mindmapUser.getUserId().intValue(), learningForm.getEntryText()); - mindmapUser.setEntryUID(entryUID); - mindmapService.saveOrUpdateMindmapUser(mindmapUser); + // save the reflection entry and call the notebook. + if (mindmap.isReflectOnActivity()) { + // check for existing notebook entry + NotebookEntry entry = mindmapService.getEntry(mindmapUser.getEntryUID()); + if (entry == null) { + // create new entry + Long entryUID = mindmapService.createNotebookEntry(toolSessionID, + CoreNotebookConstants.NOTEBOOK_TOOL, MindmapConstants.TOOL_SIGNATURE, + mindmapUser.getUserId().intValue(), learningForm.getEntryText()); + mindmapUser.setEntryUID(entryUID); + mindmapService.saveOrUpdateMindmapUser(mindmapUser); + } else { + // update existing entry + entry.setEntry(learningForm.getEntryText()); + entry.setLastModified(new Date()); + mindmapService.updateEntry(entry); + } } else { - // update existing entry - entry.setEntry(learningForm.getEntryText()); - entry.setLastModified(new Date()); - mindmapService.updateEntry(entry); + if (!mindmap.isMultiUserMode() && !StringUtils.isBlank(learningForm.getMindmapContent())) { + saveMapJsJSON(mindmap, mindmapUser, learningForm.getMindmapContent(), mindmapSession); + } } - } else { - if (!mindmap.isMultiUserMode() && !StringUtils.isBlank(learningForm.getMindmapContent())) { - saveMindmapXML(mindmap, mindmapUser, learningForm.getMindmapContent(), mindmapSession); - } } } else { Index: lams_tool_mindmap/src/java/org/lamsfoundation/lams/tool/mindmap/web/actions/MonitoringAction.java =================================================================== diff -u -r7e8d2e658c8363cb1878c6f9270b5e1f64c3afac -r9cda52bbbc8ef19343bd53f563c0734560af1a3a --- lams_tool_mindmap/src/java/org/lamsfoundation/lams/tool/mindmap/web/actions/MonitoringAction.java (.../MonitoringAction.java) (revision 7e8d2e658c8363cb1878c6f9270b5e1f64c3afac) +++ lams_tool_mindmap/src/java/org/lamsfoundation/lams/tool/mindmap/web/actions/MonitoringAction.java (.../MonitoringAction.java) (revision 9cda52bbbc8ef19343bd53f563c0734560af1a3a) @@ -26,7 +26,6 @@ import java.io.IOException; import java.util.Date; -import java.util.List; import java.util.TimeZone; import javax.servlet.http.HttpServletRequest; @@ -38,20 +37,15 @@ import org.apache.struts.action.ActionForward; import org.apache.struts.action.ActionMapping; import org.lamsfoundation.lams.notebook.model.NotebookEntry; +import org.lamsfoundation.lams.tool.ToolAccessMode; import org.lamsfoundation.lams.tool.mindmap.dto.MindmapDTO; import org.lamsfoundation.lams.tool.mindmap.dto.MindmapUserDTO; import org.lamsfoundation.lams.tool.mindmap.model.Mindmap; -import org.lamsfoundation.lams.tool.mindmap.model.MindmapNode; -import org.lamsfoundation.lams.tool.mindmap.model.MindmapSession; import org.lamsfoundation.lams.tool.mindmap.model.MindmapUser; import org.lamsfoundation.lams.tool.mindmap.service.IMindmapService; import org.lamsfoundation.lams.tool.mindmap.service.MindmapServiceProxy; import org.lamsfoundation.lams.tool.mindmap.util.MindmapConstants; -import org.lamsfoundation.lams.tool.mindmap.util.xmlmodel.NodeConceptModel; -import org.lamsfoundation.lams.tool.mindmap.util.xmlmodel.NodeModel; import org.lamsfoundation.lams.usermanagement.dto.UserDTO; -import org.lamsfoundation.lams.util.Configuration; -import org.lamsfoundation.lams.util.ConfigurationKeys; import org.lamsfoundation.lams.util.DateUtil; import org.lamsfoundation.lams.util.WebUtil; import org.lamsfoundation.lams.web.action.LamsDispatchAction; @@ -120,79 +114,6 @@ } /** - * Returns the serialized XML of the Mindmap Nodes from Database - * - * @param mapping - * @param form - * @param request - * @param response - * @return null - */ - public ActionForward setMindmapContent(ActionMapping mapping, ActionForm form, HttpServletRequest request, - HttpServletResponse response) { - - Long mindmapId = WebUtil.readLongParam(request, "mindmapId", false); - Long userId = WebUtil.readLongParam(request, "userId", false); - Mindmap mindmap = mindmapService.getMindmapByUid(mindmapId); - - List mindmapNodeList = null; - if (!mindmap.isMultiUserMode()) { - mindmapNodeList = mindmapService.getRootNodeByMindmapIdAndUserId(mindmapId, userId); - } - - if (mindmapNodeList != null && mindmapNodeList.size() > 0) { - MindmapNode rootMindmapNode = (MindmapNode) mindmapNodeList.get(0); - - String mindmapUserName = null; - if (rootMindmapNode.getUser() == null) { - mindmapUserName = mindmapService.getMindmapMessageService().getMessage("node.learner.label"); - } else { - mindmapUserName = rootMindmapNode.getUser().getFirstName() + " " - + rootMindmapNode.getUser().getLastName(); - } - - NodeModel rootNodeModel = new NodeModel(new NodeConceptModel(rootMindmapNode.getUniqueId(), - rootMindmapNode.getText(), rootMindmapNode.getColor(), mindmapUserName)); - NodeModel currentNodeModel = mindmapService.getMindmapXMLFromDatabase(rootMindmapNode.getNodeId(), - mindmapId, rootNodeModel, null); - - String mindmapContent = mindmapService.getXStream().toXML(currentNodeModel); - - try { - response.setContentType("text/xml"); - response.setCharacterEncoding("utf-8"); - response.getWriter().write(mindmapContent); - } catch (IOException e) { - e.printStackTrace(); - } - } - return null; - } - - /** - * Returns the serialized XML of the Mindmap Nodes from Database - * - * @param mapping - * @param form - * @param request - * @param response - * @return null - */ - public ActionForward setLocale(ActionMapping mapping, ActionForm form, HttpServletRequest request, - HttpServletResponse response) { - - try { - response.setContentType("text/xml"); - response.setCharacterEncoding("utf-8"); - response.getWriter().write(mindmapService.getLanguageXML()); - } catch (IOException e) { - e.printStackTrace(); - } - - return null; - } - - /** * Shows Mindmap Nodes for each learner * * @param mapping @@ -206,46 +127,23 @@ setupService(); - Long userId = new Long(WebUtil.readLongParam(request, "userUID")); Long toolContentID = new Long(WebUtil.readLongParam(request, AttributeNames.PARAM_TOOL_CONTENT_ID)); - String contentFolderID = WebUtil.readStrParam(request, AttributeNames.PARAM_CONTENT_FOLDER_ID); Mindmap mindmap = mindmapService.getMindmapByContentId(toolContentID); - MindmapUser mindmapUser = mindmapService.getUserByUID(userId); - - MindmapUserDTO userDTO = new MindmapUserDTO(mindmapUser); - request.setAttribute("userDTO", userDTO); - - // mindmapType Parameter - String mindmapType = "images/mindmap_singleuser.swf"; - request.setAttribute("mindmapType", mindmapType); - - // Mindmap path Parameter - String mindmapContentPath = Configuration.get(ConfigurationKeys.SERVER_URL) - + "tool/lamind10/monitoring.do?dispatch=setMindmapContent%26mindmapId=" + mindmap.getUid() - + "%26userId=" + userId; - request.setAttribute("mindmapContentPath", mindmapContentPath); - - // Current user Parameter - String currentMindmapUser = mindmapService.getMindmapMessageService().getMessage("node.instructor.label"); - request.setAttribute("currentMindmapUser", currentMindmapUser); - - String localizationPath = Configuration.get(ConfigurationKeys.SERVER_URL) - + "tool/lamind10/monitoring.do?dispatch=setLocale"; - request.setAttribute("localizationPath", localizationPath); - - request.setAttribute("toolContentID", toolContentID); - request.setAttribute("contentFolderID", contentFolderID); request.setAttribute("isMultiUserMode", mindmap.isMultiUserMode()); - - // Attributes for AJAX calls while saving Mindmap - request.setAttribute("get", Configuration.get(ConfigurationKeys.SERVER_URL) + "tool/lamind10/monitoring.do"); - request.setAttribute("dispatch", "updateContent"); request.setAttribute("mindmapId", mindmap.getUid()); - request.setAttribute("userId", mindmapUser.getUid()); + request.setAttribute("sessionId", WebUtil.readLongParam(request, AttributeNames.PARAM_TOOL_SESSION_ID)); + request.setAttribute("mode", ToolAccessMode.TEACHER); + if ( ! mindmap.isMultiUserMode() ) { + Long userId = new Long(WebUtil.readLongParam(request, "userUID")); + MindmapUser mindmapUser = mindmapService.getUserByUID(userId); + MindmapUserDTO userDTO = new MindmapUserDTO(mindmapUser); + request.setAttribute("userDTO", userDTO); + request.setAttribute("userId", mindmapUser.getUid()); + } + return mapping.findForward("mindmap_display"); } - /** * Shows Notebook reflection that Learner has done. * @@ -265,6 +163,7 @@ request.setAttribute("reflectTitle", mindmap.getTitle()); request.setAttribute("mindmapUser", mindmapUser.getFirstName() + " " + mindmapUser.getLastName()); + request.setAttribute("mode", ToolAccessMode.TEACHER); // Reflection NotebookEntry entry = mindmapService.getEntry(mindmapUser.getEntryUID()); @@ -276,59 +175,6 @@ } /** - * Saves Mindmap Nodes to Database - * - * @param mapping - * @param form - * @param request - * @param response - * @return null - */ - public ActionForward updateContent(ActionMapping mapping, ActionForm form, HttpServletRequest request, - HttpServletResponse response) { - - Long toolContentId = WebUtil.readLongParam(request, "mindmapId", false); - Mindmap mindmap = mindmapService.getMindmapByUid(toolContentId); - - Long userId = WebUtil.readLongParam(request, "userId", false); - MindmapUser mindmapUser = mindmapService.getUserByUID(userId); - - MindmapSession mindmapSession = mindmapUser.getMindmapSession(); - - String mindmapContent = WebUtil.readStrParam(request, "content", false); - - if (!mindmap.isMultiUserMode()) { - // Saving Mindmap data to XML - - NodeModel rootNodeModel = (NodeModel) mindmapService.getXStream().fromXML(mindmapContent); - NodeConceptModel nodeConceptModel = rootNodeModel.getConcept(); - List branches = rootNodeModel.getBranch(); - - // saving root Node into database - MindmapNode rootMindmapNode = (MindmapNode) mindmapService - .getRootNodeByMindmapIdAndUserId(mindmap.getUid(), userId).get(0); - - rootMindmapNode = mindmapService.saveMindmapNode(rootMindmapNode, null, nodeConceptModel.getId(), - nodeConceptModel.getText(), nodeConceptModel.getColor(), mindmapUser, mindmap, mindmapSession); - - // string to accumulate deleted nodes for query - String nodesToDeleteCondition = " where uniqueId <> " + rootMindmapNode.getUniqueId(); - - // saving child Nodes into database - if (branches != null) { - mindmapService.setNodesToDeleteCondition(""); - mindmapService.getChildMindmapNodes(branches, rootMindmapNode, mindmapUser, mindmap, mindmapSession); - } - - nodesToDeleteCondition += mindmapService.getNodesToDeleteCondition() + " and mindmap_id = " - + mindmap.getUid() + " and user_id = " + mindmapUser.getUid(); - mindmapService.deleteNodes(nodesToDeleteCondition); - } - - return null; - } - - /** * Set Submission Deadline * * @param mapping Index: lams_tool_mindmap/web/common/learnerheader.jsp =================================================================== diff -u -r59300dcfe778d065bfdf068dc3d301a549843c78 -r9cda52bbbc8ef19343bd53f563c0734560af1a3a --- lams_tool_mindmap/web/common/learnerheader.jsp (.../learnerheader.jsp) (revision 59300dcfe778d065bfdf068dc3d301a549843c78) +++ lams_tool_mindmap/web/common/learnerheader.jsp (.../learnerheader.jsp) (revision 9cda52bbbc8ef19343bd53f563c0734560af1a3a) @@ -8,6 +8,7 @@ + Index: lams_tool_mindmap/web/common/mapjs.jsp =================================================================== diff -u --- lams_tool_mindmap/web/common/mapjs.jsp (revision 0) +++ lams_tool_mindmap/web/common/mapjs.jsp (revision 9cda52bbbc8ef19343bd53f563c0734560af1a3a) @@ -0,0 +1,346 @@ + + + + + + + + + + + + + + + + + + + + + + + Background: + + + + + Index: lams_tool_mindmap/web/includes/css/mapjs.css =================================================================== diff -u --- lams_tool_mindmap/web/includes/css/mapjs.css (revision 0) +++ lams_tool_mindmap/web/includes/css/mapjs.css (revision 9cda52bbbc8ef19343bd53f563c0734560af1a3a) @@ -0,0 +1,140 @@ +.mapjs-node { + margin: 0; + padding: 7px; + z-index: 2; + user-select: none; + -moz-user-select: none; + -webkit-user-select: none; + -ms-user-select: none; + color: #4F4F4F; + cursor: hand; +} +.mapjs-node .resize-node { + position: absolute; + height: 100%; + width: 20px; + right: -10px; + top: 0; + border-radius: 10px; + background-color: transparent; + cursor: ew-resize; +} +.resize-node:hover { + background-color: black; + opacity: 0.3; +} +.mapjs-node:focus { + outline: none; +} +.mapjs-node.droppable { + border: 1px solid red; +} +.mapjs-add-link { + cursor: crosshair; +} +.mapjs-add-link .mapjs-node { + cursor: alias; +} +.mapjs-node span[contenteditable=true] { + user-select: text; + -moz-user-select: text; + -webkit-user-select: text; + -ms-user-select: text; +} +.mapjs-node { + font-family: -apple-system, "Helvetica Neue", Roboto, Helvetica, Arial, sans-serif; + font-weight: 500; + font-size: 12px; + line-height: 15px; +} +.mapjs-node span { + white-space: pre-wrap; + display: block; + max-width: 146px; + min-height: 1em; + min-width: 1em; + outline: none; +} +.mapjs-node.attr_group span { + min-height: 1.5em; +} +.mapjs-node.dragging { + opacity: 0.4; +} +.mapjs-node[mapjs-level="1"] { + background-color:#22AAE0; +} +.mapjs-decorations { + position: absolute; + display: block; + white-space: nowrap; +} +.mapjs-label { + background: black; + opacity: 0.5; + color: white; + display: inline-block; +} +.mapjs-hyperlink { + background-image: url(icon-link-inactive.svg); + width: 32px; + height: 32px; + background-size: 32px 32px; + background-repeat: no-repeat no-repeat; + display: inline-block; +} +.mapjs-hyperlink:hover { + background-image:url(icon-link-active.svg); +} +.mapjs-attachment { + background-image: url(icon-paperclip-inactive.svg); + width: 16px; + height: 32px; + background-size: 16px 32px; + background-repeat: no-repeat no-repeat; + display: inline-block; +} +.mapjs-attachment:hover{ + background-image:url(icon-paperclip-active.svg); + +} +.mapjs-draw-container { + position: absolute; + margin: 0px; + padding: 0px; + z-index: 1; +} +.mapjs-link { + stroke-width: 1.5px; + fill: none; +} +.mapjs-link-hit { + stroke: transparent; + stroke-width: 15; + cursor: crosshair; +} +.mapjs-arrow { +} +.drag-shadow { + opacity: 0.5; +} +.mapjs-reorder-bounds { + stroke-width: 5px; + fill: none; + stroke: #000; +} +.mapjs-reorder-bounds { + z-index: 999; + background-image:url(chevron-left.svg); + width: 11px; + background-height: 100%; + background-width: 100%; + height: 20px; + background-repeat: no-repeat; +} +.mapjs-reorder-bounds[mapjs-edge="left"] { + background-image:url(chevron-right.svg); +} +.mapjs-reorder-bounds[mapjs-edge="top"] { + transform: rotate(-90deg); +} Index: lams_tool_mindmap/web/includes/css/mindmap.scss =================================================================== diff -u --- lams_tool_mindmap/web/includes/css/mindmap.scss (revision 0) +++ lams_tool_mindmap/web/includes/css/mindmap.scss (revision 9cda52bbbc8ef19343bd53f563c0734560af1a3a) @@ -0,0 +1,14 @@ +@import "_lams_variables.scss"; + +#mindmap-controls { + height: 50px; + overflow-y: scroll; + padding: 5px; +} + +#mindmap-container { + background-color: $body-bg; + height: calc(100vh - 300px); + width: 100%; + border: $border-thin-solid; +} Index: lams_tool_mindmap/web/includes/javascript/authoring.js =================================================================== diff -u -rbe07c35c372d904a65581d98660e73f3b13b69db -r9cda52bbbc8ef19343bd53f563c0734560af1a3a --- lams_tool_mindmap/web/includes/javascript/authoring.js (.../authoring.js) (revision be07c35c372d904a65581d98660e73f3b13b69db) +++ lams_tool_mindmap/web/includes/javascript/authoring.js (.../authoring.js) (revision 9cda52bbbc8ef19343bd53f563c0734560af1a3a) @@ -1,30 +1,9 @@ function init() { - // open the current tab - var tag = document.getElementById("currentTab"); - if (tag.value != "") { - selectTab(tag.value); - } else { - selectTab(1); - } + selectTab(1); } function doSelectTab(tabId) { - var tag = document.getElementById("currentTab"); - - var mindmapContent = document.getElementById("mindmapContent"); - - if (tag.value == "" || tag.value == 1) { - mindmapContent.value = document['flashContent'].getMindmap(); - } - - tag.value = tabId; - - if (tag.value == "" || tag.value == 1) { - flashvars["xml"] = ""; - embedFlashObject(700, 525); - } - selectTab(tabId); } Index: lams_tool_mindmap/web/includes/javascript/mapjs/main.js =================================================================== diff -u --- lams_tool_mindmap/web/includes/javascript/mapjs/main.js (revision 0) +++ lams_tool_mindmap/web/includes/javascript/mapjs/main.js (revision 9cda52bbbc8ef19343bd53f563c0734560af1a3a) @@ -0,0 +1,24307 @@ +/******/ (function(modules) { // webpackBootstrap +/******/ // The module cache +/******/ var installedModules = {}; +/******/ +/******/ // The require function +/******/ function __webpack_require__(moduleId) { +/******/ +/******/ // Check if module is in cache +/******/ if(installedModules[moduleId]) { +/******/ return installedModules[moduleId].exports; +/******/ } +/******/ // Create a new module (and put it into the cache) +/******/ var module = installedModules[moduleId] = { +/******/ i: moduleId, +/******/ l: false, +/******/ exports: {} +/******/ }; +/******/ +/******/ // Execute the module function +/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); +/******/ +/******/ // Flag the module as loaded +/******/ module.l = true; +/******/ +/******/ // Return the exports of the module +/******/ return module.exports; +/******/ } +/******/ +/******/ +/******/ // expose the modules object (__webpack_modules__) +/******/ __webpack_require__.m = modules; +/******/ +/******/ // expose the module cache +/******/ __webpack_require__.c = installedModules; +/******/ +/******/ // identity function for calling harmony imports with the correct context +/******/ __webpack_require__.i = function(value) { return value; }; +/******/ +/******/ // define getter function for harmony exports +/******/ __webpack_require__.d = function(exports, name, getter) { +/******/ if(!__webpack_require__.o(exports, name)) { +/******/ Object.defineProperty(exports, name, { +/******/ configurable: false, +/******/ enumerable: true, +/******/ get: getter +/******/ }); +/******/ } +/******/ }; +/******/ +/******/ // getDefaultExport function for compatibility with non-harmony modules +/******/ __webpack_require__.n = function(module) { +/******/ var getter = module && module.__esModule ? +/******/ function getDefault() { return module['default']; } : +/******/ function getModuleExports() { return module; }; +/******/ __webpack_require__.d(getter, 'a', getter); +/******/ return getter; +/******/ }; +/******/ +/******/ // Object.prototype.hasOwnProperty.call +/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; +/******/ +/******/ // __webpack_public_path__ +/******/ __webpack_require__.p = ""; +/******/ +/******/ // Load entry module and return exports +/******/ return __webpack_require__(__webpack_require__.s = 20); +/******/ }) +/************************************************************************/ +/******/ ([ +/* 0 */ +/***/ (function(module, exports, __webpack_require__) { + +var __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;// Underscore.js 1.8.3 +// http://underscorejs.org +// (c) 2009-2015 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors +// Underscore may be freely distributed under the MIT license. + +(function() { + + // Baseline setup + // -------------- + + // Establish the root object, `window` in the browser, or `exports` on the server. + var root = this; + + // Save the previous value of the `_` variable. + var previousUnderscore = root._; + + // Save bytes in the minified (but not gzipped) version: + var ArrayProto = Array.prototype, ObjProto = Object.prototype, FuncProto = Function.prototype; + + // Create quick reference variables for speed access to core prototypes. + var + push = ArrayProto.push, + slice = ArrayProto.slice, + toString = ObjProto.toString, + hasOwnProperty = ObjProto.hasOwnProperty; + + // All **ECMAScript 5** native function implementations that we hope to use + // are declared here. + var + nativeIsArray = Array.isArray, + nativeKeys = Object.keys, + nativeBind = FuncProto.bind, + nativeCreate = Object.create; + + // Naked function reference for surrogate-prototype-swapping. + var Ctor = function(){}; + + // Create a safe reference to the Underscore object for use below. + var _ = function(obj) { + if (obj instanceof _) return obj; + if (!(this instanceof _)) return new _(obj); + this._wrapped = obj; + }; + + // Export the Underscore object for **Node.js**, with + // backwards-compatibility for the old `require()` API. If we're in + // the browser, add `_` as a global object. + if (true) { + if (typeof module !== 'undefined' && module.exports) { + exports = module.exports = _; + } + exports._ = _; + } else { + root._ = _; + } + + // Current version. + _.VERSION = '1.8.3'; + + // Internal function that returns an efficient (for current engines) version + // of the passed-in callback, to be repeatedly applied in other Underscore + // functions. + var optimizeCb = function(func, context, argCount) { + if (context === void 0) return func; + switch (argCount == null ? 3 : argCount) { + case 1: return function(value) { + return func.call(context, value); + }; + case 2: return function(value, other) { + return func.call(context, value, other); + }; + case 3: return function(value, index, collection) { + return func.call(context, value, index, collection); + }; + case 4: return function(accumulator, value, index, collection) { + return func.call(context, accumulator, value, index, collection); + }; + } + return function() { + return func.apply(context, arguments); + }; + }; + + // A mostly-internal function to generate callbacks that can be applied + // to each element in a collection, returning the desired result — either + // identity, an arbitrary callback, a property matcher, or a property accessor. + var cb = function(value, context, argCount) { + if (value == null) return _.identity; + if (_.isFunction(value)) return optimizeCb(value, context, argCount); + if (_.isObject(value)) return _.matcher(value); + return _.property(value); + }; + _.iteratee = function(value, context) { + return cb(value, context, Infinity); + }; + + // An internal function for creating assigner functions. + var createAssigner = function(keysFunc, undefinedOnly) { + return function(obj) { + var length = arguments.length; + if (length < 2 || obj == null) return obj; + for (var index = 1; index < length; index++) { + var source = arguments[index], + keys = keysFunc(source), + l = keys.length; + for (var i = 0; i < l; i++) { + var key = keys[i]; + if (!undefinedOnly || obj[key] === void 0) obj[key] = source[key]; + } + } + return obj; + }; + }; + + // An internal function for creating a new object that inherits from another. + var baseCreate = function(prototype) { + if (!_.isObject(prototype)) return {}; + if (nativeCreate) return nativeCreate(prototype); + Ctor.prototype = prototype; + var result = new Ctor; + Ctor.prototype = null; + return result; + }; + + var property = function(key) { + return function(obj) { + return obj == null ? void 0 : obj[key]; + }; + }; + + // Helper for collection methods to determine whether a collection + // should be iterated as an array or as an object + // Related: http://people.mozilla.org/~jorendorff/es6-draft.html#sec-tolength + // Avoids a very nasty iOS 8 JIT bug on ARM-64. #2094 + var MAX_ARRAY_INDEX = Math.pow(2, 53) - 1; + var getLength = property('length'); + var isArrayLike = function(collection) { + var length = getLength(collection); + return typeof length == 'number' && length >= 0 && length <= MAX_ARRAY_INDEX; + }; + + // Collection Functions + // -------------------- + + // The cornerstone, an `each` implementation, aka `forEach`. + // Handles raw objects in addition to array-likes. Treats all + // sparse array-likes as if they were dense. + _.each = _.forEach = function(obj, iteratee, context) { + iteratee = optimizeCb(iteratee, context); + var i, length; + if (isArrayLike(obj)) { + for (i = 0, length = obj.length; i < length; i++) { + iteratee(obj[i], i, obj); + } + } else { + var keys = _.keys(obj); + for (i = 0, length = keys.length; i < length; i++) { + iteratee(obj[keys[i]], keys[i], obj); + } + } + return obj; + }; + + // Return the results of applying the iteratee to each element. + _.map = _.collect = function(obj, iteratee, context) { + iteratee = cb(iteratee, context); + var keys = !isArrayLike(obj) && _.keys(obj), + length = (keys || obj).length, + results = Array(length); + for (var index = 0; index < length; index++) { + var currentKey = keys ? keys[index] : index; + results[index] = iteratee(obj[currentKey], currentKey, obj); + } + return results; + }; + + // Create a reducing function iterating left or right. + function createReduce(dir) { + // Optimized iterator function as using arguments.length + // in the main function will deoptimize the, see #1991. + function iterator(obj, iteratee, memo, keys, index, length) { + for (; index >= 0 && index < length; index += dir) { + var currentKey = keys ? keys[index] : index; + memo = iteratee(memo, obj[currentKey], currentKey, obj); + } + return memo; + } + + return function(obj, iteratee, memo, context) { + iteratee = optimizeCb(iteratee, context, 4); + var keys = !isArrayLike(obj) && _.keys(obj), + length = (keys || obj).length, + index = dir > 0 ? 0 : length - 1; + // Determine the initial value if none is provided. + if (arguments.length < 3) { + memo = obj[keys ? keys[index] : index]; + index += dir; + } + return iterator(obj, iteratee, memo, keys, index, length); + }; + } + + // **Reduce** builds up a single result from a list of values, aka `inject`, + // or `foldl`. + _.reduce = _.foldl = _.inject = createReduce(1); + + // The right-associative version of reduce, also known as `foldr`. + _.reduceRight = _.foldr = createReduce(-1); + + // Return the first value which passes a truth test. Aliased as `detect`. + _.find = _.detect = function(obj, predicate, context) { + var key; + if (isArrayLike(obj)) { + key = _.findIndex(obj, predicate, context); + } else { + key = _.findKey(obj, predicate, context); + } + if (key !== void 0 && key !== -1) return obj[key]; + }; + + // Return all the elements that pass a truth test. + // Aliased as `select`. + _.filter = _.select = function(obj, predicate, context) { + var results = []; + predicate = cb(predicate, context); + _.each(obj, function(value, index, list) { + if (predicate(value, index, list)) results.push(value); + }); + return results; + }; + + // Return all the elements for which a truth test fails. + _.reject = function(obj, predicate, context) { + return _.filter(obj, _.negate(cb(predicate)), context); + }; + + // Determine whether all of the elements match a truth test. + // Aliased as `all`. + _.every = _.all = function(obj, predicate, context) { + predicate = cb(predicate, context); + var keys = !isArrayLike(obj) && _.keys(obj), + length = (keys || obj).length; + for (var index = 0; index < length; index++) { + var currentKey = keys ? keys[index] : index; + if (!predicate(obj[currentKey], currentKey, obj)) return false; + } + return true; + }; + + // Determine if at least one element in the object matches a truth test. + // Aliased as `any`. + _.some = _.any = function(obj, predicate, context) { + predicate = cb(predicate, context); + var keys = !isArrayLike(obj) && _.keys(obj), + length = (keys || obj).length; + for (var index = 0; index < length; index++) { + var currentKey = keys ? keys[index] : index; + if (predicate(obj[currentKey], currentKey, obj)) return true; + } + return false; + }; + + // Determine if the array or object contains a given item (using `===`). + // Aliased as `includes` and `include`. + _.contains = _.includes = _.include = function(obj, item, fromIndex, guard) { + if (!isArrayLike(obj)) obj = _.values(obj); + if (typeof fromIndex != 'number' || guard) fromIndex = 0; + return _.indexOf(obj, item, fromIndex) >= 0; + }; + + // Invoke a method (with arguments) on every item in a collection. + _.invoke = function(obj, method) { + var args = slice.call(arguments, 2); + var isFunc = _.isFunction(method); + return _.map(obj, function(value) { + var func = isFunc ? method : value[method]; + return func == null ? func : func.apply(value, args); + }); + }; + + // Convenience version of a common use case of `map`: fetching a property. + _.pluck = function(obj, key) { + return _.map(obj, _.property(key)); + }; + + // Convenience version of a common use case of `filter`: selecting only objects + // containing specific `key:value` pairs. + _.where = function(obj, attrs) { + return _.filter(obj, _.matcher(attrs)); + }; + + // Convenience version of a common use case of `find`: getting the first object + // containing specific `key:value` pairs. + _.findWhere = function(obj, attrs) { + return _.find(obj, _.matcher(attrs)); + }; + + // Return the maximum element (or element-based computation). + _.max = function(obj, iteratee, context) { + var result = -Infinity, lastComputed = -Infinity, + value, computed; + if (iteratee == null && obj != null) { + obj = isArrayLike(obj) ? obj : _.values(obj); + for (var i = 0, length = obj.length; i < length; i++) { + value = obj[i]; + if (value > result) { + result = value; + } + } + } else { + iteratee = cb(iteratee, context); + _.each(obj, function(value, index, list) { + computed = iteratee(value, index, list); + if (computed > lastComputed || computed === -Infinity && result === -Infinity) { + result = value; + lastComputed = computed; + } + }); + } + return result; + }; + + // Return the minimum element (or element-based computation). + _.min = function(obj, iteratee, context) { + var result = Infinity, lastComputed = Infinity, + value, computed; + if (iteratee == null && obj != null) { + obj = isArrayLike(obj) ? obj : _.values(obj); + for (var i = 0, length = obj.length; i < length; i++) { + value = obj[i]; + if (value < result) { + result = value; + } + } + } else { + iteratee = cb(iteratee, context); + _.each(obj, function(value, index, list) { + computed = iteratee(value, index, list); + if (computed < lastComputed || computed === Infinity && result === Infinity) { + result = value; + lastComputed = computed; + } + }); + } + return result; + }; + + // Shuffle a collection, using the modern version of the + // [Fisher-Yates shuffle](http://en.wikipedia.org/wiki/Fisher–Yates_shuffle). + _.shuffle = function(obj) { + var set = isArrayLike(obj) ? obj : _.values(obj); + var length = set.length; + var shuffled = Array(length); + for (var index = 0, rand; index < length; index++) { + rand = _.random(0, index); + if (rand !== index) shuffled[index] = shuffled[rand]; + shuffled[rand] = set[index]; + } + return shuffled; + }; + + // Sample **n** random values from a collection. + // If **n** is not specified, returns a single random element. + // The internal `guard` argument allows it to work with `map`. + _.sample = function(obj, n, guard) { + if (n == null || guard) { + if (!isArrayLike(obj)) obj = _.values(obj); + return obj[_.random(obj.length - 1)]; + } + return _.shuffle(obj).slice(0, Math.max(0, n)); + }; + + // Sort the object's values by a criterion produced by an iteratee. + _.sortBy = function(obj, iteratee, context) { + iteratee = cb(iteratee, context); + return _.pluck(_.map(obj, function(value, index, list) { + return { + value: value, + index: index, + criteria: iteratee(value, index, list) + }; + }).sort(function(left, right) { + var a = left.criteria; + var b = right.criteria; + if (a !== b) { + if (a > b || a === void 0) return 1; + if (a < b || b === void 0) return -1; + } + return left.index - right.index; + }), 'value'); + }; + + // An internal function used for aggregate "group by" operations. + var group = function(behavior) { + return function(obj, iteratee, context) { + var result = {}; + iteratee = cb(iteratee, context); + _.each(obj, function(value, index) { + var key = iteratee(value, index, obj); + behavior(result, value, key); + }); + return result; + }; + }; + + // Groups the object's values by a criterion. Pass either a string attribute + // to group by, or a function that returns the criterion. + _.groupBy = group(function(result, value, key) { + if (_.has(result, key)) result[key].push(value); else result[key] = [value]; + }); + + // Indexes the object's values by a criterion, similar to `groupBy`, but for + // when you know that your index values will be unique. + _.indexBy = group(function(result, value, key) { + result[key] = value; + }); + + // Counts instances of an object that group by a certain criterion. Pass + // either a string attribute to count by, or a function that returns the + // criterion. + _.countBy = group(function(result, value, key) { + if (_.has(result, key)) result[key]++; else result[key] = 1; + }); + + // Safely create a real, live array from anything iterable. + _.toArray = function(obj) { + if (!obj) return []; + if (_.isArray(obj)) return slice.call(obj); + if (isArrayLike(obj)) return _.map(obj, _.identity); + return _.values(obj); + }; + + // Return the number of elements in an object. + _.size = function(obj) { + if (obj == null) return 0; + return isArrayLike(obj) ? obj.length : _.keys(obj).length; + }; + + // Split a collection into two arrays: one whose elements all satisfy the given + // predicate, and one whose elements all do not satisfy the predicate. + _.partition = function(obj, predicate, context) { + predicate = cb(predicate, context); + var pass = [], fail = []; + _.each(obj, function(value, key, obj) { + (predicate(value, key, obj) ? pass : fail).push(value); + }); + return [pass, fail]; + }; + + // Array Functions + // --------------- + + // Get the first element of an array. Passing **n** will return the first N + // values in the array. Aliased as `head` and `take`. The **guard** check + // allows it to work with `_.map`. + _.first = _.head = _.take = function(array, n, guard) { + if (array == null) return void 0; + if (n == null || guard) return array[0]; + return _.initial(array, array.length - n); + }; + + // Returns everything but the last entry of the array. Especially useful on + // the arguments object. Passing **n** will return all the values in + // the array, excluding the last N. + _.initial = function(array, n, guard) { + return slice.call(array, 0, Math.max(0, array.length - (n == null || guard ? 1 : n))); + }; + + // Get the last element of an array. Passing **n** will return the last N + // values in the array. + _.last = function(array, n, guard) { + if (array == null) return void 0; + if (n == null || guard) return array[array.length - 1]; + return _.rest(array, Math.max(0, array.length - n)); + }; + + // Returns everything but the first entry of the array. Aliased as `tail` and `drop`. + // Especially useful on the arguments object. Passing an **n** will return + // the rest N values in the array. + _.rest = _.tail = _.drop = function(array, n, guard) { + return slice.call(array, n == null || guard ? 1 : n); + }; + + // Trim out all falsy values from an array. + _.compact = function(array) { + return _.filter(array, _.identity); + }; + + // Internal implementation of a recursive `flatten` function. + var flatten = function(input, shallow, strict, startIndex) { + var output = [], idx = 0; + for (var i = startIndex || 0, length = getLength(input); i < length; i++) { + var value = input[i]; + if (isArrayLike(value) && (_.isArray(value) || _.isArguments(value))) { + //flatten current level of array or arguments object + if (!shallow) value = flatten(value, shallow, strict); + var j = 0, len = value.length; + output.length += len; + while (j < len) { + output[idx++] = value[j++]; + } + } else if (!strict) { + output[idx++] = value; + } + } + return output; + }; + + // Flatten out an array, either recursively (by default), or just one level. + _.flatten = function(array, shallow) { + return flatten(array, shallow, false); + }; + + // Return a version of the array that does not contain the specified value(s). + _.without = function(array) { + return _.difference(array, slice.call(arguments, 1)); + }; + + // Produce a duplicate-free version of the array. If the array has already + // been sorted, you have the option of using a faster algorithm. + // Aliased as `unique`. + _.uniq = _.unique = function(array, isSorted, iteratee, context) { + if (!_.isBoolean(isSorted)) { + context = iteratee; + iteratee = isSorted; + isSorted = false; + } + if (iteratee != null) iteratee = cb(iteratee, context); + var result = []; + var seen = []; + for (var i = 0, length = getLength(array); i < length; i++) { + var value = array[i], + computed = iteratee ? iteratee(value, i, array) : value; + if (isSorted) { + if (!i || seen !== computed) result.push(value); + seen = computed; + } else if (iteratee) { + if (!_.contains(seen, computed)) { + seen.push(computed); + result.push(value); + } + } else if (!_.contains(result, value)) { + result.push(value); + } + } + return result; + }; + + // Produce an array that contains the union: each distinct element from all of + // the passed-in arrays. + _.union = function() { + return _.uniq(flatten(arguments, true, true)); + }; + + // Produce an array that contains every item shared between all the + // passed-in arrays. + _.intersection = function(array) { + var result = []; + var argsLength = arguments.length; + for (var i = 0, length = getLength(array); i < length; i++) { + var item = array[i]; + if (_.contains(result, item)) continue; + for (var j = 1; j < argsLength; j++) { + if (!_.contains(arguments[j], item)) break; + } + if (j === argsLength) result.push(item); + } + return result; + }; + + // Take the difference between one array and a number of other arrays. + // Only the elements present in just the first array will remain. + _.difference = function(array) { + var rest = flatten(arguments, true, true, 1); + return _.filter(array, function(value){ + return !_.contains(rest, value); + }); + }; + + // Zip together multiple lists into a single array -- elements that share + // an index go together. + _.zip = function() { + return _.unzip(arguments); + }; + + // Complement of _.zip. Unzip accepts an array of arrays and groups + // each array's elements on shared indices + _.unzip = function(array) { + var length = array && _.max(array, getLength).length || 0; + var result = Array(length); + + for (var index = 0; index < length; index++) { + result[index] = _.pluck(array, index); + } + return result; + }; + + // Converts lists into objects. Pass either a single array of `[key, value]` + // pairs, or two parallel arrays of the same length -- one of keys, and one of + // the corresponding values. + _.object = function(list, values) { + var result = {}; + for (var i = 0, length = getLength(list); i < length; i++) { + if (values) { + result[list[i]] = values[i]; + } else { + result[list[i][0]] = list[i][1]; + } + } + return result; + }; + + // Generator function to create the findIndex and findLastIndex functions + function createPredicateIndexFinder(dir) { + return function(array, predicate, context) { + predicate = cb(predicate, context); + var length = getLength(array); + var index = dir > 0 ? 0 : length - 1; + for (; index >= 0 && index < length; index += dir) { + if (predicate(array[index], index, array)) return index; + } + return -1; + }; + } + + // Returns the first index on an array-like that passes a predicate test + _.findIndex = createPredicateIndexFinder(1); + _.findLastIndex = createPredicateIndexFinder(-1); + + // Use a comparator function to figure out the smallest index at which + // an object should be inserted so as to maintain order. Uses binary search. + _.sortedIndex = function(array, obj, iteratee, context) { + iteratee = cb(iteratee, context, 1); + var value = iteratee(obj); + var low = 0, high = getLength(array); + while (low < high) { + var mid = Math.floor((low + high) / 2); + if (iteratee(array[mid]) < value) low = mid + 1; else high = mid; + } + return low; + }; + + // Generator function to create the indexOf and lastIndexOf functions + function createIndexFinder(dir, predicateFind, sortedIndex) { + return function(array, item, idx) { + var i = 0, length = getLength(array); + if (typeof idx == 'number') { + if (dir > 0) { + i = idx >= 0 ? idx : Math.max(idx + length, i); + } else { + length = idx >= 0 ? Math.min(idx + 1, length) : idx + length + 1; + } + } else if (sortedIndex && idx && length) { + idx = sortedIndex(array, item); + return array[idx] === item ? idx : -1; + } + if (item !== item) { + idx = predicateFind(slice.call(array, i, length), _.isNaN); + return idx >= 0 ? idx + i : -1; + } + for (idx = dir > 0 ? i : length - 1; idx >= 0 && idx < length; idx += dir) { + if (array[idx] === item) return idx; + } + return -1; + }; + } + + // Return the position of the first occurrence of an item in an array, + // or -1 if the item is not included in the array. + // If the array is large and already in sort order, pass `true` + // for **isSorted** to use binary search. + _.indexOf = createIndexFinder(1, _.findIndex, _.sortedIndex); + _.lastIndexOf = createIndexFinder(-1, _.findLastIndex); + + // Generate an integer Array containing an arithmetic progression. A port of + // the native Python `range()` function. See + // [the Python documentation](http://docs.python.org/library/functions.html#range). + _.range = function(start, stop, step) { + if (stop == null) { + stop = start || 0; + start = 0; + } + step = step || 1; + + var length = Math.max(Math.ceil((stop - start) / step), 0); + var range = Array(length); + + for (var idx = 0; idx < length; idx++, start += step) { + range[idx] = start; + } + + return range; + }; + + // Function (ahem) Functions + // ------------------ + + // Determines whether to execute a function as a constructor + // or a normal function with the provided arguments + var executeBound = function(sourceFunc, boundFunc, context, callingContext, args) { + if (!(callingContext instanceof boundFunc)) return sourceFunc.apply(context, args); + var self = baseCreate(sourceFunc.prototype); + var result = sourceFunc.apply(self, args); + if (_.isObject(result)) return result; + return self; + }; + + // Create a function bound to a given object (assigning `this`, and arguments, + // optionally). Delegates to **ECMAScript 5**'s native `Function.bind` if + // available. + _.bind = function(func, context) { + if (nativeBind && func.bind === nativeBind) return nativeBind.apply(func, slice.call(arguments, 1)); + if (!_.isFunction(func)) throw new TypeError('Bind must be called on a function'); + var args = slice.call(arguments, 2); + var bound = function() { + return executeBound(func, bound, context, this, args.concat(slice.call(arguments))); + }; + return bound; + }; + + // Partially apply a function by creating a version that has had some of its + // arguments pre-filled, without changing its dynamic `this` context. _ acts + // as a placeholder, allowing any combination of arguments to be pre-filled. + _.partial = function(func) { + var boundArgs = slice.call(arguments, 1); + var bound = function() { + var position = 0, length = boundArgs.length; + var args = Array(length); + for (var i = 0; i < length; i++) { + args[i] = boundArgs[i] === _ ? arguments[position++] : boundArgs[i]; + } + while (position < arguments.length) args.push(arguments[position++]); + return executeBound(func, bound, this, this, args); + }; + return bound; + }; + + // Bind a number of an object's methods to that object. Remaining arguments + // are the method names to be bound. Useful for ensuring that all callbacks + // defined on an object belong to it. + _.bindAll = function(obj) { + var i, length = arguments.length, key; + if (length <= 1) throw new Error('bindAll must be passed function names'); + for (i = 1; i < length; i++) { + key = arguments[i]; + obj[key] = _.bind(obj[key], obj); + } + return obj; + }; + + // Memoize an expensive function by storing its results. + _.memoize = function(func, hasher) { + var memoize = function(key) { + var cache = memoize.cache; + var address = '' + (hasher ? hasher.apply(this, arguments) : key); + if (!_.has(cache, address)) cache[address] = func.apply(this, arguments); + return cache[address]; + }; + memoize.cache = {}; + return memoize; + }; + + // Delays a function for the given number of milliseconds, and then calls + // it with the arguments supplied. + _.delay = function(func, wait) { + var args = slice.call(arguments, 2); + return setTimeout(function(){ + return func.apply(null, args); + }, wait); + }; + + // Defers a function, scheduling it to run after the current call stack has + // cleared. + _.defer = _.partial(_.delay, _, 1); + + // Returns a function, that, when invoked, will only be triggered at most once + // during a given window of time. Normally, the throttled function will run + // as much as it can, without ever going more than once per `wait` duration; + // but if you'd like to disable the execution on the leading edge, pass + // `{leading: false}`. To disable execution on the trailing edge, ditto. + _.throttle = function(func, wait, options) { + var context, args, result; + var timeout = null; + var previous = 0; + if (!options) options = {}; + var later = function() { + previous = options.leading === false ? 0 : _.now(); + timeout = null; + result = func.apply(context, args); + if (!timeout) context = args = null; + }; + return function() { + var now = _.now(); + if (!previous && options.leading === false) previous = now; + var remaining = wait - (now - previous); + context = this; + args = arguments; + if (remaining <= 0 || remaining > wait) { + if (timeout) { + clearTimeout(timeout); + timeout = null; + } + previous = now; + result = func.apply(context, args); + if (!timeout) context = args = null; + } else if (!timeout && options.trailing !== false) { + timeout = setTimeout(later, remaining); + } + return result; + }; + }; + + // Returns a function, that, as long as it continues to be invoked, will not + // be triggered. The function will be called after it stops being called for + // N milliseconds. If `immediate` is passed, trigger the function on the + // leading edge, instead of the trailing. + _.debounce = function(func, wait, immediate) { + var timeout, args, context, timestamp, result; + + var later = function() { + var last = _.now() - timestamp; + + if (last < wait && last >= 0) { + timeout = setTimeout(later, wait - last); + } else { + timeout = null; + if (!immediate) { + result = func.apply(context, args); + if (!timeout) context = args = null; + } + } + }; + + return function() { + context = this; + args = arguments; + timestamp = _.now(); + var callNow = immediate && !timeout; + if (!timeout) timeout = setTimeout(later, wait); + if (callNow) { + result = func.apply(context, args); + context = args = null; + } + + return result; + }; + }; + + // Returns the first function passed as an argument to the second, + // allowing you to adjust arguments, run code before and after, and + // conditionally execute the original function. + _.wrap = function(func, wrapper) { + return _.partial(wrapper, func); + }; + + // Returns a negated version of the passed-in predicate. + _.negate = function(predicate) { + return function() { + return !predicate.apply(this, arguments); + }; + }; + + // Returns a function that is the composition of a list of functions, each + // consuming the return value of the function that follows. + _.compose = function() { + var args = arguments; + var start = args.length - 1; + return function() { + var i = start; + var result = args[start].apply(this, arguments); + while (i--) result = args[i].call(this, result); + return result; + }; + }; + + // Returns a function that will only be executed on and after the Nth call. + _.after = function(times, func) { + return function() { + if (--times < 1) { + return func.apply(this, arguments); + } + }; + }; + + // Returns a function that will only be executed up to (but not including) the Nth call. + _.before = function(times, func) { + var memo; + return function() { + if (--times > 0) { + memo = func.apply(this, arguments); + } + if (times <= 1) func = null; + return memo; + }; + }; + + // Returns a function that will be executed at most one time, no matter how + // often you call it. Useful for lazy initialization. + _.once = _.partial(_.before, 2); + + // Object Functions + // ---------------- + + // Keys in IE < 9 that won't be iterated by `for key in ...` and thus missed. + var hasEnumBug = !{toString: null}.propertyIsEnumerable('toString'); + var nonEnumerableProps = ['valueOf', 'isPrototypeOf', 'toString', + 'propertyIsEnumerable', 'hasOwnProperty', 'toLocaleString']; + + function collectNonEnumProps(obj, keys) { + var nonEnumIdx = nonEnumerableProps.length; + var constructor = obj.constructor; + var proto = (_.isFunction(constructor) && constructor.prototype) || ObjProto; + + // Constructor is a special case. + var prop = 'constructor'; + if (_.has(obj, prop) && !_.contains(keys, prop)) keys.push(prop); + + while (nonEnumIdx--) { + prop = nonEnumerableProps[nonEnumIdx]; + if (prop in obj && obj[prop] !== proto[prop] && !_.contains(keys, prop)) { + keys.push(prop); + } + } + } + + // Retrieve the names of an object's own properties. + // Delegates to **ECMAScript 5**'s native `Object.keys` + _.keys = function(obj) { + if (!_.isObject(obj)) return []; + if (nativeKeys) return nativeKeys(obj); + var keys = []; + for (var key in obj) if (_.has(obj, key)) keys.push(key); + // Ahem, IE < 9. + if (hasEnumBug) collectNonEnumProps(obj, keys); + return keys; + }; + + // Retrieve all the property names of an object. + _.allKeys = function(obj) { + if (!_.isObject(obj)) return []; + var keys = []; + for (var key in obj) keys.push(key); + // Ahem, IE < 9. + if (hasEnumBug) collectNonEnumProps(obj, keys); + return keys; + }; + + // Retrieve the values of an object's properties. + _.values = function(obj) { + var keys = _.keys(obj); + var length = keys.length; + var values = Array(length); + for (var i = 0; i < length; i++) { + values[i] = obj[keys[i]]; + } + return values; + }; + + // Returns the results of applying the iteratee to each element of the object + // In contrast to _.map it returns an object + _.mapObject = function(obj, iteratee, context) { + iteratee = cb(iteratee, context); + var keys = _.keys(obj), + length = keys.length, + results = {}, + currentKey; + for (var index = 0; index < length; index++) { + currentKey = keys[index]; + results[currentKey] = iteratee(obj[currentKey], currentKey, obj); + } + return results; + }; + + // Convert an object into a list of `[key, value]` pairs. + _.pairs = function(obj) { + var keys = _.keys(obj); + var length = keys.length; + var pairs = Array(length); + for (var i = 0; i < length; i++) { + pairs[i] = [keys[i], obj[keys[i]]]; + } + return pairs; + }; + + // Invert the keys and values of an object. The values must be serializable. + _.invert = function(obj) { + var result = {}; + var keys = _.keys(obj); + for (var i = 0, length = keys.length; i < length; i++) { + result[obj[keys[i]]] = keys[i]; + } + return result; + }; + + // Return a sorted list of the function names available on the object. + // Aliased as `methods` + _.functions = _.methods = function(obj) { + var names = []; + for (var key in obj) { + if (_.isFunction(obj[key])) names.push(key); + } + return names.sort(); + }; + + // Extend a given object with all the properties in passed-in object(s). + _.extend = createAssigner(_.allKeys); + + // Assigns a given object with all the own properties in the passed-in object(s) + // (https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object/assign) + _.extendOwn = _.assign = createAssigner(_.keys); + + // Returns the first key on an object that passes a predicate test + _.findKey = function(obj, predicate, context) { + predicate = cb(predicate, context); + var keys = _.keys(obj), key; + for (var i = 0, length = keys.length; i < length; i++) { + key = keys[i]; + if (predicate(obj[key], key, obj)) return key; + } + }; + + // Return a copy of the object only containing the whitelisted properties. + _.pick = function(object, oiteratee, context) { + var result = {}, obj = object, iteratee, keys; + if (obj == null) return result; + if (_.isFunction(oiteratee)) { + keys = _.allKeys(obj); + iteratee = optimizeCb(oiteratee, context); + } else { + keys = flatten(arguments, false, false, 1); + iteratee = function(value, key, obj) { return key in obj; }; + obj = Object(obj); + } + for (var i = 0, length = keys.length; i < length; i++) { + var key = keys[i]; + var value = obj[key]; + if (iteratee(value, key, obj)) result[key] = value; + } + return result; + }; + + // Return a copy of the object without the blacklisted properties. + _.omit = function(obj, iteratee, context) { + if (_.isFunction(iteratee)) { + iteratee = _.negate(iteratee); + } else { + var keys = _.map(flatten(arguments, false, false, 1), String); + iteratee = function(value, key) { + return !_.contains(keys, key); + }; + } + return _.pick(obj, iteratee, context); + }; + + // Fill in a given object with default properties. + _.defaults = createAssigner(_.allKeys, true); + + // Creates an object that inherits from the given prototype object. + // If additional properties are provided then they will be added to the + // created object. + _.create = function(prototype, props) { + var result = baseCreate(prototype); + if (props) _.extendOwn(result, props); + return result; + }; + + // Create a (shallow-cloned) duplicate of an object. + _.clone = function(obj) { + if (!_.isObject(obj)) return obj; + return _.isArray(obj) ? obj.slice() : _.extend({}, obj); + }; + + // Invokes interceptor with the obj, and then returns obj. + // The primary purpose of this method is to "tap into" a method chain, in + // order to perform operations on intermediate results within the chain. + _.tap = function(obj, interceptor) { + interceptor(obj); + return obj; + }; + + // Returns whether an object has a given set of `key:value` pairs. + _.isMatch = function(object, attrs) { + var keys = _.keys(attrs), length = keys.length; + if (object == null) return !length; + var obj = Object(object); + for (var i = 0; i < length; i++) { + var key = keys[i]; + if (attrs[key] !== obj[key] || !(key in obj)) return false; + } + return true; + }; + + + // Internal recursive comparison function for `isEqual`. + var eq = function(a, b, aStack, bStack) { + // Identical objects are equal. `0 === -0`, but they aren't identical. + // See the [Harmony `egal` proposal](http://wiki.ecmascript.org/doku.php?id=harmony:egal). + if (a === b) return a !== 0 || 1 / a === 1 / b; + // A strict comparison is necessary because `null == undefined`. + if (a == null || b == null) return a === b; + // Unwrap any wrapped objects. + if (a instanceof _) a = a._wrapped; + if (b instanceof _) b = b._wrapped; + // Compare `[[Class]]` names. + var className = toString.call(a); + if (className !== toString.call(b)) return false; + switch (className) { + // Strings, numbers, regular expressions, dates, and booleans are compared by value. + case '[object RegExp]': + // RegExps are coerced to strings for comparison (Note: '' + /a/i === '/a/i') + case '[object String]': + // Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is + // equivalent to `new String("5")`. + return '' + a === '' + b; + case '[object Number]': + // `NaN`s are equivalent, but non-reflexive. + // Object(NaN) is equivalent to NaN + if (+a !== +a) return +b !== +b; + // An `egal` comparison is performed for other numeric values. + return +a === 0 ? 1 / +a === 1 / b : +a === +b; + case '[object Date]': + case '[object Boolean]': + // Coerce dates and booleans to numeric primitive values. Dates are compared by their + // millisecond representations. Note that invalid dates with millisecond representations + // of `NaN` are not equivalent. + return +a === +b; + } + + var areArrays = className === '[object Array]'; + if (!areArrays) { + if (typeof a != 'object' || typeof b != 'object') return false; + + // Objects with different constructors are not equivalent, but `Object`s or `Array`s + // from different frames are. + var aCtor = a.constructor, bCtor = b.constructor; + if (aCtor !== bCtor && !(_.isFunction(aCtor) && aCtor instanceof aCtor && + _.isFunction(bCtor) && bCtor instanceof bCtor) + && ('constructor' in a && 'constructor' in b)) { + return false; + } + } + // Assume equality for cyclic structures. The algorithm for detecting cyclic + // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`. + + // Initializing stack of traversed objects. + // It's done here since we only need them for objects and arrays comparison. + aStack = aStack || []; + bStack = bStack || []; + var length = aStack.length; + while (length--) { + // Linear search. Performance is inversely proportional to the number of + // unique nested structures. + if (aStack[length] === a) return bStack[length] === b; + } + + // Add the first object to the stack of traversed objects. + aStack.push(a); + bStack.push(b); + + // Recursively compare objects and arrays. + if (areArrays) { + // Compare array lengths to determine if a deep comparison is necessary. + length = a.length; + if (length !== b.length) return false; + // Deep compare the contents, ignoring non-numeric properties. + while (length--) { + if (!eq(a[length], b[length], aStack, bStack)) return false; + } + } else { + // Deep compare objects. + var keys = _.keys(a), key; + length = keys.length; + // Ensure that both objects contain the same number of properties before comparing deep equality. + if (_.keys(b).length !== length) return false; + while (length--) { + // Deep compare each member + key = keys[length]; + if (!(_.has(b, key) && eq(a[key], b[key], aStack, bStack))) return false; + } + } + // Remove the first object from the stack of traversed objects. + aStack.pop(); + bStack.pop(); + return true; + }; + + // Perform a deep comparison to check if two objects are equal. + _.isEqual = function(a, b) { + return eq(a, b); + }; + + // Is a given array, string, or object empty? + // An "empty" object has no enumerable own-properties. + _.isEmpty = function(obj) { + if (obj == null) return true; + if (isArrayLike(obj) && (_.isArray(obj) || _.isString(obj) || _.isArguments(obj))) return obj.length === 0; + return _.keys(obj).length === 0; + }; + + // Is a given value a DOM element? + _.isElement = function(obj) { + return !!(obj && obj.nodeType === 1); + }; + + // Is a given value an array? + // Delegates to ECMA5's native Array.isArray + _.isArray = nativeIsArray || function(obj) { + return toString.call(obj) === '[object Array]'; + }; + + // Is a given variable an object? + _.isObject = function(obj) { + var type = typeof obj; + return type === 'function' || type === 'object' && !!obj; + }; + + // Add some isType methods: isArguments, isFunction, isString, isNumber, isDate, isRegExp, isError. + _.each(['Arguments', 'Function', 'String', 'Number', 'Date', 'RegExp', 'Error'], function(name) { + _['is' + name] = function(obj) { + return toString.call(obj) === '[object ' + name + ']'; + }; + }); + + // Define a fallback version of the method in browsers (ahem, IE < 9), where + // there isn't any inspectable "Arguments" type. + if (!_.isArguments(arguments)) { + _.isArguments = function(obj) { + return _.has(obj, 'callee'); + }; + } + + // Optimize `isFunction` if appropriate. Work around some typeof bugs in old v8, + // IE 11 (#1621), and in Safari 8 (#1929). + if (typeof /./ != 'function' && typeof Int8Array != 'object') { + _.isFunction = function(obj) { + return typeof obj == 'function' || false; + }; + } + + // Is a given object a finite number? + _.isFinite = function(obj) { + return isFinite(obj) && !isNaN(parseFloat(obj)); + }; + + // Is the given value `NaN`? (NaN is the only number which does not equal itself). + _.isNaN = function(obj) { + return _.isNumber(obj) && obj !== +obj; + }; + + // Is a given value a boolean? + _.isBoolean = function(obj) { + return obj === true || obj === false || toString.call(obj) === '[object Boolean]'; + }; + + // Is a given value equal to null? + _.isNull = function(obj) { + return obj === null; + }; + + // Is a given variable undefined? + _.isUndefined = function(obj) { + return obj === void 0; + }; + + // Shortcut function for checking if an object has a given property directly + // on itself (in other words, not on a prototype). + _.has = function(obj, key) { + return obj != null && hasOwnProperty.call(obj, key); + }; + + // Utility Functions + // ----------------- + + // Run Underscore.js in *noConflict* mode, returning the `_` variable to its + // previous owner. Returns a reference to the Underscore object. + _.noConflict = function() { + root._ = previousUnderscore; + return this; + }; + + // Keep the identity function around for default iteratees. + _.identity = function(value) { + return value; + }; + + // Predicate-generating functions. Often useful outside of Underscore. + _.constant = function(value) { + return function() { + return value; + }; + }; + + _.noop = function(){}; + + _.property = property; + + // Generates a function for a given object that returns a given property. + _.propertyOf = function(obj) { + return obj == null ? function(){} : function(key) { + return obj[key]; + }; + }; + + // Returns a predicate for checking whether an object has a given set of + // `key:value` pairs. + _.matcher = _.matches = function(attrs) { + attrs = _.extendOwn({}, attrs); + return function(obj) { + return _.isMatch(obj, attrs); + }; + }; + + // Run a function **n** times. + _.times = function(n, iteratee, context) { + var accum = Array(Math.max(0, n)); + iteratee = optimizeCb(iteratee, context, 1); + for (var i = 0; i < n; i++) accum[i] = iteratee(i); + return accum; + }; + + // Return a random integer between min and max (inclusive). + _.random = function(min, max) { + if (max == null) { + max = min; + min = 0; + } + return min + Math.floor(Math.random() * (max - min + 1)); + }; + + // A (possibly faster) way to get the current timestamp as an integer. + _.now = Date.now || function() { + return new Date().getTime(); + }; + + // List of HTML entities for escaping. + var escapeMap = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''', + '`': '`' + }; + var unescapeMap = _.invert(escapeMap); + + // Functions for escaping and unescaping strings to/from HTML interpolation. + var createEscaper = function(map) { + var escaper = function(match) { + return map[match]; + }; + // Regexes for identifying a key that needs to be escaped + var source = '(?:' + _.keys(map).join('|') + ')'; + var testRegexp = RegExp(source); + var replaceRegexp = RegExp(source, 'g'); + return function(string) { + string = string == null ? '' : '' + string; + return testRegexp.test(string) ? string.replace(replaceRegexp, escaper) : string; + }; + }; + _.escape = createEscaper(escapeMap); + _.unescape = createEscaper(unescapeMap); + + // If the value of the named `property` is a function then invoke it with the + // `object` as context; otherwise, return it. + _.result = function(object, property, fallback) { + var value = object == null ? void 0 : object[property]; + if (value === void 0) { + value = fallback; + } + return _.isFunction(value) ? value.call(object) : value; + }; + + // Generate a unique integer id (unique within the entire client session). + // Useful for temporary DOM ids. + var idCounter = 0; + _.uniqueId = function(prefix) { + var id = ++idCounter + ''; + return prefix ? prefix + id : id; + }; + + // By default, Underscore uses ERB-style template delimiters, change the + // following template settings to use alternative delimiters. + _.templateSettings = { + evaluate : /<%([\s\S]+?)%>/g, + interpolate : /<%=([\s\S]+?)%>/g, + escape : /<%-([\s\S]+?)%>/g + }; + + // When customizing `templateSettings`, if you don't want to define an + // interpolation, evaluation or escaping regex, we need one that is + // guaranteed not to match. + var noMatch = /(.)^/; + + // Certain characters need to be escaped so that they can be put into a + // string literal. + var escapes = { + "'": "'", + '\\': '\\', + '\r': 'r', + '\n': 'n', + '\u2028': 'u2028', + '\u2029': 'u2029' + }; + + var escaper = /\\|'|\r|\n|\u2028|\u2029/g; + + var escapeChar = function(match) { + return '\\' + escapes[match]; + }; + + // JavaScript micro-templating, similar to John Resig's implementation. + // Underscore templating handles arbitrary delimiters, preserves whitespace, + // and correctly escapes quotes within interpolated code. + // NB: `oldSettings` only exists for backwards compatibility. + _.template = function(text, settings, oldSettings) { + if (!settings && oldSettings) settings = oldSettings; + settings = _.defaults({}, settings, _.templateSettings); + + // Combine delimiters into one regular expression via alternation. + var matcher = RegExp([ + (settings.escape || noMatch).source, + (settings.interpolate || noMatch).source, + (settings.evaluate || noMatch).source + ].join('|') + '|$', 'g'); + + // Compile the template source, escaping string literals appropriately. + var index = 0; + var source = "__p+='"; + text.replace(matcher, function(match, escape, interpolate, evaluate, offset) { + source += text.slice(index, offset).replace(escaper, escapeChar); + index = offset + match.length; + + if (escape) { + source += "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'"; + } else if (interpolate) { + source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'"; + } else if (evaluate) { + source += "';\n" + evaluate + "\n__p+='"; + } + + // Adobe VMs need the match returned to produce the correct offest. + return match; + }); + source += "';\n"; + + // If a variable is not specified, place data values in local scope. + if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n'; + + source = "var __t,__p='',__j=Array.prototype.join," + + "print=function(){__p+=__j.call(arguments,'');};\n" + + source + 'return __p;\n'; + + try { + var render = new Function(settings.variable || 'obj', '_', source); + } catch (e) { + e.source = source; + throw e; + } + + var template = function(data) { + return render.call(this, data, _); + }; + + // Provide the compiled source as a convenience for precompilation. + var argument = settings.variable || 'obj'; + template.source = 'function(' + argument + '){\n' + source + '}'; + + return template; + }; + + // Add a "chain" function. Start chaining a wrapped Underscore object. + _.chain = function(obj) { + var instance = _(obj); + instance._chain = true; + return instance; + }; + + // OOP + // --------------- + // If Underscore is called as a function, it returns a wrapped object that + // can be used OO-style. This wrapper holds altered versions of all the + // underscore functions. Wrapped objects may be chained. + + // Helper function to continue chaining intermediate results. + var result = function(instance, obj) { + return instance._chain ? _(obj).chain() : obj; + }; + + // Add your own custom functions to the Underscore object. + _.mixin = function(obj) { + _.each(_.functions(obj), function(name) { + var func = _[name] = obj[name]; + _.prototype[name] = function() { + var args = [this._wrapped]; + push.apply(args, arguments); + return result(this, func.apply(_, args)); + }; + }); + }; + + // Add all of the Underscore functions to the wrapper object. + _.mixin(_); + + // Add all mutator Array functions to the wrapper. + _.each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) { + var method = ArrayProto[name]; + _.prototype[name] = function() { + var obj = this._wrapped; + method.apply(obj, arguments); + if ((name === 'shift' || name === 'splice') && obj.length === 0) delete obj[0]; + return result(this, obj); + }; + }); + + // Add all accessor Array functions to the wrapper. + _.each(['concat', 'join', 'slice'], function(name) { + var method = ArrayProto[name]; + _.prototype[name] = function() { + return result(this, method.apply(this._wrapped, arguments)); + }; + }); + + // Extracts the result from a wrapped and chained object. + _.prototype.value = function() { + return this._wrapped; + }; + + // Provide unwrapping proxy for some methods used in engine operations + // such as arithmetic and JSON stringification. + _.prototype.valueOf = _.prototype.toJSON = _.prototype.value; + + _.prototype.toString = function() { + return '' + this._wrapped; + }; + + // AMD registration happens at the end for compatibility with AMD loaders + // that may not enforce next-turn semantics on modules. Even though general + // practice for AMD registration is to be anonymous, underscore registers + // as a named module because, like jQuery, it is a base library that is + // popular enough to be bundled in a third party lib, but not be part of + // an AMD load request. Those cases could generate an error when an + // anonymous define() is called outside of a loader request. + if (true) { + !(__WEBPACK_AMD_DEFINE_ARRAY__ = [], __WEBPACK_AMD_DEFINE_RESULT__ = function() { + return _; + }.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__), + __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); + } +}.call(this)); + + +/***/ }), +/* 1 */ +/***/ (function(module, exports, __webpack_require__) { + +var __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;/*! + * jQuery JavaScript Library v2.2.4 + * http://jquery.com/ + * + * Includes Sizzle.js + * http://sizzlejs.com/ + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license + * http://jquery.org/license + * + * Date: 2016-05-20T17:23Z + */ + +(function( global, factory ) { + + if ( typeof module === "object" && typeof module.exports === "object" ) { + // For CommonJS and CommonJS-like environments where a proper `window` + // is present, execute the factory and get jQuery. + // For environments that do not have a `window` with a `document` + // (such as Node.js), expose a factory as module.exports. + // This accentuates the need for the creation of a real `window`. + // e.g. var jQuery = require("jquery")(window); + // See ticket #14549 for more info. + module.exports = global.document ? + factory( global, true ) : + function( w ) { + if ( !w.document ) { + throw new Error( "jQuery requires a window with a document" ); + } + return factory( w ); + }; + } else { + factory( global ); + } + +// Pass this if window is not defined yet +}(typeof window !== "undefined" ? window : this, function( window, noGlobal ) { + +// Support: Firefox 18+ +// Can't be in strict mode, several libs including ASP.NET trace +// the stack via arguments.caller.callee and Firefox dies if +// you try to trace through "use strict" call chains. (#13335) +//"use strict"; +var arr = []; + +var document = window.document; + +var slice = arr.slice; + +var concat = arr.concat; + +var push = arr.push; + +var indexOf = arr.indexOf; + +var class2type = {}; + +var toString = class2type.toString; + +var hasOwn = class2type.hasOwnProperty; + +var support = {}; + + + +var + version = "2.2.4", + + // Define a local copy of jQuery + jQuery = function( selector, context ) { + + // The jQuery object is actually just the init constructor 'enhanced' + // Need init if jQuery is called (just allow error to be thrown if not included) + return new jQuery.fn.init( selector, context ); + }, + + // Support: Android<4.1 + // Make sure we trim BOM and NBSP + rtrim = /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, + + // Matches dashed string for camelizing + rmsPrefix = /^-ms-/, + rdashAlpha = /-([\da-z])/gi, + + // Used by jQuery.camelCase as callback to replace() + fcamelCase = function( all, letter ) { + return letter.toUpperCase(); + }; + +jQuery.fn = jQuery.prototype = { + + // The current version of jQuery being used + jquery: version, + + constructor: jQuery, + + // Start with an empty selector + selector: "", + + // The default length of a jQuery object is 0 + length: 0, + + toArray: function() { + return slice.call( this ); + }, + + // Get the Nth element in the matched element set OR + // Get the whole matched element set as a clean array + get: function( num ) { + return num != null ? + + // Return just the one element from the set + ( num < 0 ? this[ num + this.length ] : this[ num ] ) : + + // Return all the elements in a clean array + slice.call( this ); + }, + + // Take an array of elements and push it onto the stack + // (returning the new matched element set) + pushStack: function( elems ) { + + // Build a new jQuery matched element set + var ret = jQuery.merge( this.constructor(), elems ); + + // Add the old object onto the stack (as a reference) + ret.prevObject = this; + ret.context = this.context; + + // Return the newly-formed element set + return ret; + }, + + // Execute a callback for every element in the matched set. + each: function( callback ) { + return jQuery.each( this, callback ); + }, + + map: function( callback ) { + return this.pushStack( jQuery.map( this, function( elem, i ) { + return callback.call( elem, i, elem ); + } ) ); + }, + + slice: function() { + return this.pushStack( slice.apply( this, arguments ) ); + }, + + first: function() { + return this.eq( 0 ); + }, + + last: function() { + return this.eq( -1 ); + }, + + eq: function( i ) { + var len = this.length, + j = +i + ( i < 0 ? len : 0 ); + return this.pushStack( j >= 0 && j < len ? [ this[ j ] ] : [] ); + }, + + end: function() { + return this.prevObject || this.constructor(); + }, + + // For internal use only. + // Behaves like an Array's method, not like a jQuery method. + push: push, + sort: arr.sort, + splice: arr.splice +}; + +jQuery.extend = jQuery.fn.extend = function() { + var options, name, src, copy, copyIsArray, clone, + target = arguments[ 0 ] || {}, + i = 1, + length = arguments.length, + deep = false; + + // Handle a deep copy situation + if ( typeof target === "boolean" ) { + deep = target; + + // Skip the boolean and the target + target = arguments[ i ] || {}; + i++; + } + + // Handle case when target is a string or something (possible in deep copy) + if ( typeof target !== "object" && !jQuery.isFunction( target ) ) { + target = {}; + } + + // Extend jQuery itself if only one argument is passed + if ( i === length ) { + target = this; + i--; + } + + for ( ; i < length; i++ ) { + + // Only deal with non-null/undefined values + if ( ( options = arguments[ i ] ) != null ) { + + // Extend the base object + for ( name in options ) { + src = target[ name ]; + copy = options[ name ]; + + // Prevent never-ending loop + if ( target === copy ) { + continue; + } + + // Recurse if we're merging plain objects or arrays + if ( deep && copy && ( jQuery.isPlainObject( copy ) || + ( copyIsArray = jQuery.isArray( copy ) ) ) ) { + + if ( copyIsArray ) { + copyIsArray = false; + clone = src && jQuery.isArray( src ) ? src : []; + + } else { + clone = src && jQuery.isPlainObject( src ) ? src : {}; + } + + // Never move original objects, clone them + target[ name ] = jQuery.extend( deep, clone, copy ); + + // Don't bring in undefined values + } else if ( copy !== undefined ) { + target[ name ] = copy; + } + } + } + } + + // Return the modified object + return target; +}; + +jQuery.extend( { + + // Unique for each copy of jQuery on the page + expando: "jQuery" + ( version + Math.random() ).replace( /\D/g, "" ), + + // Assume jQuery is ready without the ready module + isReady: true, + + error: function( msg ) { + throw new Error( msg ); + }, + + noop: function() {}, + + isFunction: function( obj ) { + return jQuery.type( obj ) === "function"; + }, + + isArray: Array.isArray, + + isWindow: function( obj ) { + return obj != null && obj === obj.window; + }, + + isNumeric: function( obj ) { + + // parseFloat NaNs numeric-cast false positives (null|true|false|"") + // ...but misinterprets leading-number strings, particularly hex literals ("0x...") + // subtraction forces infinities to NaN + // adding 1 corrects loss of precision from parseFloat (#15100) + var realStringObj = obj && obj.toString(); + return !jQuery.isArray( obj ) && ( realStringObj - parseFloat( realStringObj ) + 1 ) >= 0; + }, + + isPlainObject: function( obj ) { + var key; + + // Not plain objects: + // - Any object or value whose internal [[Class]] property is not "[object Object]" + // - DOM nodes + // - window + if ( jQuery.type( obj ) !== "object" || obj.nodeType || jQuery.isWindow( obj ) ) { + return false; + } + + // Not own constructor property must be Object + if ( obj.constructor && + !hasOwn.call( obj, "constructor" ) && + !hasOwn.call( obj.constructor.prototype || {}, "isPrototypeOf" ) ) { + return false; + } + + // Own properties are enumerated firstly, so to speed up, + // if last one is own, then all properties are own + for ( key in obj ) {} + + return key === undefined || hasOwn.call( obj, key ); + }, + + isEmptyObject: function( obj ) { + var name; + for ( name in obj ) { + return false; + } + return true; + }, + + type: function( obj ) { + if ( obj == null ) { + return obj + ""; + } + + // Support: Android<4.0, iOS<6 (functionish RegExp) + return typeof obj === "object" || typeof obj === "function" ? + class2type[ toString.call( obj ) ] || "object" : + typeof obj; + }, + + // Evaluates a script in a global context + globalEval: function( code ) { + var script, + indirect = eval; + + code = jQuery.trim( code ); + + if ( code ) { + + // If the code includes a valid, prologue position + // strict mode pragma, execute code by injecting a + // script tag into the document. + if ( code.indexOf( "use strict" ) === 1 ) { + script = document.createElement( "script" ); + script.text = code; + document.head.appendChild( script ).parentNode.removeChild( script ); + } else { + + // Otherwise, avoid the DOM node creation, insertion + // and removal by using an indirect global eval + + indirect( code ); + } + } + }, + + // Convert dashed to camelCase; used by the css and data modules + // Support: IE9-11+ + // Microsoft forgot to hump their vendor prefix (#9572) + camelCase: function( string ) { + return string.replace( rmsPrefix, "ms-" ).replace( rdashAlpha, fcamelCase ); + }, + + nodeName: function( elem, name ) { + return elem.nodeName && elem.nodeName.toLowerCase() === name.toLowerCase(); + }, + + each: function( obj, callback ) { + var length, i = 0; + + if ( isArrayLike( obj ) ) { + length = obj.length; + for ( ; i < length; i++ ) { + if ( callback.call( obj[ i ], i, obj[ i ] ) === false ) { + break; + } + } + } else { + for ( i in obj ) { + if ( callback.call( obj[ i ], i, obj[ i ] ) === false ) { + break; + } + } + } + + return obj; + }, + + // Support: Android<4.1 + trim: function( text ) { + return text == null ? + "" : + ( text + "" ).replace( rtrim, "" ); + }, + + // results is for internal usage only + makeArray: function( arr, results ) { + var ret = results || []; + + if ( arr != null ) { + if ( isArrayLike( Object( arr ) ) ) { + jQuery.merge( ret, + typeof arr === "string" ? + [ arr ] : arr + ); + } else { + push.call( ret, arr ); + } + } + + return ret; + }, + + inArray: function( elem, arr, i ) { + return arr == null ? -1 : indexOf.call( arr, elem, i ); + }, + + merge: function( first, second ) { + var len = +second.length, + j = 0, + i = first.length; + + for ( ; j < len; j++ ) { + first[ i++ ] = second[ j ]; + } + + first.length = i; + + return first; + }, + + grep: function( elems, callback, invert ) { + var callbackInverse, + matches = [], + i = 0, + length = elems.length, + callbackExpect = !invert; + + // Go through the array, only saving the items + // that pass the validator function + for ( ; i < length; i++ ) { + callbackInverse = !callback( elems[ i ], i ); + if ( callbackInverse !== callbackExpect ) { + matches.push( elems[ i ] ); + } + } + + return matches; + }, + + // arg is for internal usage only + map: function( elems, callback, arg ) { + var length, value, + i = 0, + ret = []; + + // Go through the array, translating each of the items to their new values + if ( isArrayLike( elems ) ) { + length = elems.length; + for ( ; i < length; i++ ) { + value = callback( elems[ i ], i, arg ); + + if ( value != null ) { + ret.push( value ); + } + } + + // Go through every key on the object, + } else { + for ( i in elems ) { + value = callback( elems[ i ], i, arg ); + + if ( value != null ) { + ret.push( value ); + } + } + } + + // Flatten any nested arrays + return concat.apply( [], ret ); + }, + + // A global GUID counter for objects + guid: 1, + + // Bind a function to a context, optionally partially applying any + // arguments. + proxy: function( fn, context ) { + var tmp, args, proxy; + + if ( typeof context === "string" ) { + tmp = fn[ context ]; + context = fn; + fn = tmp; + } + + // Quick check to determine if target is callable, in the spec + // this throws a TypeError, but we will just return undefined. + if ( !jQuery.isFunction( fn ) ) { + return undefined; + } + + // Simulated bind + args = slice.call( arguments, 2 ); + proxy = function() { + return fn.apply( context || this, args.concat( slice.call( arguments ) ) ); + }; + + // Set the guid of unique handler to the same of original handler, so it can be removed + proxy.guid = fn.guid = fn.guid || jQuery.guid++; + + return proxy; + }, + + now: Date.now, + + // jQuery.support is not used in Core but other projects attach their + // properties to it so it needs to exist. + support: support +} ); + +// JSHint would error on this code due to the Symbol not being defined in ES5. +// Defining this global in .jshintrc would create a danger of using the global +// unguarded in another place, it seems safer to just disable JSHint for these +// three lines. +/* jshint ignore: start */ +if ( typeof Symbol === "function" ) { + jQuery.fn[ Symbol.iterator ] = arr[ Symbol.iterator ]; +} +/* jshint ignore: end */ + +// Populate the class2type map +jQuery.each( "Boolean Number String Function Array Date RegExp Object Error Symbol".split( " " ), +function( i, name ) { + class2type[ "[object " + name + "]" ] = name.toLowerCase(); +} ); + +function isArrayLike( obj ) { + + // Support: iOS 8.2 (not reproducible in simulator) + // `in` check used to prevent JIT error (gh-2145) + // hasOwn isn't used here due to false negatives + // regarding Nodelist length in IE + var length = !!obj && "length" in obj && obj.length, + type = jQuery.type( obj ); + + if ( type === "function" || jQuery.isWindow( obj ) ) { + return false; + } + + return type === "array" || length === 0 || + typeof length === "number" && length > 0 && ( length - 1 ) in obj; +} +var Sizzle = +/*! + * Sizzle CSS Selector Engine v2.2.1 + * http://sizzlejs.com/ + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license + * http://jquery.org/license + * + * Date: 2015-10-17 + */ +(function( window ) { + +var i, + support, + Expr, + getText, + isXML, + tokenize, + compile, + select, + outermostContext, + sortInput, + hasDuplicate, + + // Local document vars + setDocument, + document, + docElem, + documentIsHTML, + rbuggyQSA, + rbuggyMatches, + matches, + contains, + + // Instance-specific data + expando = "sizzle" + 1 * new Date(), + preferredDoc = window.document, + dirruns = 0, + done = 0, + classCache = createCache(), + tokenCache = createCache(), + compilerCache = createCache(), + sortOrder = function( a, b ) { + if ( a === b ) { + hasDuplicate = true; + } + return 0; + }, + + // General-purpose constants + MAX_NEGATIVE = 1 << 31, + + // Instance methods + hasOwn = ({}).hasOwnProperty, + arr = [], + pop = arr.pop, + push_native = arr.push, + push = arr.push, + slice = arr.slice, + // Use a stripped-down indexOf as it's faster than native + // http://jsperf.com/thor-indexof-vs-for/5 + indexOf = function( list, elem ) { + var i = 0, + len = list.length; + for ( ; i < len; i++ ) { + if ( list[i] === elem ) { + return i; + } + } + return -1; + }, + + booleans = "checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped", + + // Regular expressions + + // http://www.w3.org/TR/css3-selectors/#whitespace + whitespace = "[\\x20\\t\\r\\n\\f]", + + // http://www.w3.org/TR/CSS21/syndata.html#value-def-identifier + identifier = "(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+", + + // Attribute selectors: http://www.w3.org/TR/selectors/#attribute-selectors + attributes = "\\[" + whitespace + "*(" + identifier + ")(?:" + whitespace + + // Operator (capture 2) + "*([*^$|!~]?=)" + whitespace + + // "Attribute values must be CSS identifiers [capture 5] or strings [capture 3 or capture 4]" + "*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|(" + identifier + "))|)" + whitespace + + "*\\]", + + pseudos = ":(" + identifier + ")(?:\\((" + + // To reduce the number of selectors needing tokenize in the preFilter, prefer arguments: + // 1. quoted (capture 3; capture 4 or capture 5) + "('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|" + + // 2. simple (capture 6) + "((?:\\\\.|[^\\\\()[\\]]|" + attributes + ")*)|" + + // 3. anything else (capture 2) + ".*" + + ")\\)|)", + + // Leading and non-escaped trailing whitespace, capturing some non-whitespace characters preceding the latter + rwhitespace = new RegExp( whitespace + "+", "g" ), + rtrim = new RegExp( "^" + whitespace + "+|((?:^|[^\\\\])(?:\\\\.)*)" + whitespace + "+$", "g" ), + + rcomma = new RegExp( "^" + whitespace + "*," + whitespace + "*" ), + rcombinators = new RegExp( "^" + whitespace + "*([>+~]|" + whitespace + ")" + whitespace + "*" ), + + rattributeQuotes = new RegExp( "=" + whitespace + "*([^\\]'\"]*?)" + whitespace + "*\\]", "g" ), + + rpseudo = new RegExp( pseudos ), + ridentifier = new RegExp( "^" + identifier + "$" ), + + matchExpr = { + "ID": new RegExp( "^#(" + identifier + ")" ), + "CLASS": new RegExp( "^\\.(" + identifier + ")" ), + "TAG": new RegExp( "^(" + identifier + "|[*])" ), + "ATTR": new RegExp( "^" + attributes ), + "PSEUDO": new RegExp( "^" + pseudos ), + "CHILD": new RegExp( "^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\(" + whitespace + + "*(even|odd|(([+-]|)(\\d*)n|)" + whitespace + "*(?:([+-]|)" + whitespace + + "*(\\d+)|))" + whitespace + "*\\)|)", "i" ), + "bool": new RegExp( "^(?:" + booleans + ")$", "i" ), + // For use in libraries implementing .is() + // We use this for POS matching in `select` + "needsContext": new RegExp( "^" + whitespace + "*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\(" + + whitespace + "*((?:-\\d)?\\d*)" + whitespace + "*\\)|)(?=[^-]|$)", "i" ) + }, + + rinputs = /^(?:input|select|textarea|button)$/i, + rheader = /^h\d$/i, + + rnative = /^[^{]+\{\s*\[native \w/, + + // Easily-parseable/retrievable ID or TAG or CLASS selectors + rquickExpr = /^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/, + + rsibling = /[+~]/, + rescape = /'|\\/g, + + // CSS escapes http://www.w3.org/TR/CSS21/syndata.html#escaped-characters + runescape = new RegExp( "\\\\([\\da-f]{1,6}" + whitespace + "?|(" + whitespace + ")|.)", "ig" ), + funescape = function( _, escaped, escapedWhitespace ) { + var high = "0x" + escaped - 0x10000; + // NaN means non-codepoint + // Support: Firefox<24 + // Workaround erroneous numeric interpretation of +"0x" + return high !== high || escapedWhitespace ? + escaped : + high < 0 ? + // BMP codepoint + String.fromCharCode( high + 0x10000 ) : + // Supplemental Plane codepoint (surrogate pair) + String.fromCharCode( high >> 10 | 0xD800, high & 0x3FF | 0xDC00 ); + }, + + // Used for iframes + // See setDocument() + // Removing the function wrapper causes a "Permission Denied" + // error in IE + unloadHandler = function() { + setDocument(); + }; + +// Optimize for push.apply( _, NodeList ) +try { + push.apply( + (arr = slice.call( preferredDoc.childNodes )), + preferredDoc.childNodes + ); + // Support: Android<4.0 + // Detect silently failing push.apply + arr[ preferredDoc.childNodes.length ].nodeType; +} catch ( e ) { + push = { apply: arr.length ? + + // Leverage slice if possible + function( target, els ) { + push_native.apply( target, slice.call(els) ); + } : + + // Support: IE<9 + // Otherwise append directly + function( target, els ) { + var j = target.length, + i = 0; + // Can't trust NodeList.length + while ( (target[j++] = els[i++]) ) {} + target.length = j - 1; + } + }; +} + +function Sizzle( selector, context, results, seed ) { + var m, i, elem, nid, nidselect, match, groups, newSelector, + newContext = context && context.ownerDocument, + + // nodeType defaults to 9, since context defaults to document + nodeType = context ? context.nodeType : 9; + + results = results || []; + + // Return early from calls with invalid selector or context + if ( typeof selector !== "string" || !selector || + nodeType !== 1 && nodeType !== 9 && nodeType !== 11 ) { + + return results; + } + + // Try to shortcut find operations (as opposed to filters) in HTML documents + if ( !seed ) { + + if ( ( context ? context.ownerDocument || context : preferredDoc ) !== document ) { + setDocument( context ); + } + context = context || document; + + if ( documentIsHTML ) { + + // If the selector is sufficiently simple, try using a "get*By*" DOM method + // (excepting DocumentFragment context, where the methods don't exist) + if ( nodeType !== 11 && (match = rquickExpr.exec( selector )) ) { + + // ID selector + if ( (m = match[1]) ) { + + // Document context + if ( nodeType === 9 ) { + if ( (elem = context.getElementById( m )) ) { + + // Support: IE, Opera, Webkit + // TODO: identify versions + // getElementById can match elements by name instead of ID + if ( elem.id === m ) { + results.push( elem ); + return results; + } + } else { + return results; + } + + // Element context + } else { + + // Support: IE, Opera, Webkit + // TODO: identify versions + // getElementById can match elements by name instead of ID + if ( newContext && (elem = newContext.getElementById( m )) && + contains( context, elem ) && + elem.id === m ) { + + results.push( elem ); + return results; + } + } + + // Type selector + } else if ( match[2] ) { + push.apply( results, context.getElementsByTagName( selector ) ); + return results; + + // Class selector + } else if ( (m = match[3]) && support.getElementsByClassName && + context.getElementsByClassName ) { + + push.apply( results, context.getElementsByClassName( m ) ); + return results; + } + } + + // Take advantage of querySelectorAll + if ( support.qsa && + !compilerCache[ selector + " " ] && + (!rbuggyQSA || !rbuggyQSA.test( selector )) ) { + + if ( nodeType !== 1 ) { + newContext = context; + newSelector = selector; + + // qSA looks outside Element context, which is not what we want + // Thanks to Andrew Dupont for this workaround technique + // Support: IE <=8 + // Exclude object elements + } else if ( context.nodeName.toLowerCase() !== "object" ) { + + // Capture the context ID, setting it first if necessary + if ( (nid = context.getAttribute( "id" )) ) { + nid = nid.replace( rescape, "\\$&" ); + } else { + context.setAttribute( "id", (nid = expando) ); + } + + // Prefix every selector in the list + groups = tokenize( selector ); + i = groups.length; + nidselect = ridentifier.test( nid ) ? "#" + nid : "[id='" + nid + "']"; + while ( i-- ) { + groups[i] = nidselect + " " + toSelector( groups[i] ); + } + newSelector = groups.join( "," ); + + // Expand context for sibling selectors + newContext = rsibling.test( selector ) && testContext( context.parentNode ) || + context; + } + + if ( newSelector ) { + try { + push.apply( results, + newContext.querySelectorAll( newSelector ) + ); + return results; + } catch ( qsaError ) { + } finally { + if ( nid === expando ) { + context.removeAttribute( "id" ); + } + } + } + } + } + } + + // All others + return select( selector.replace( rtrim, "$1" ), context, results, seed ); +} + +/** + * Create key-value caches of limited size + * @returns {function(string, object)} Returns the Object data after storing it on itself with + * property name the (space-suffixed) string and (if the cache is larger than Expr.cacheLength) + * deleting the oldest entry + */ +function createCache() { + var keys = []; + + function cache( key, value ) { + // Use (key + " ") to avoid collision with native prototype properties (see Issue #157) + if ( keys.push( key + " " ) > Expr.cacheLength ) { + // Only keep the most recent entries + delete cache[ keys.shift() ]; + } + return (cache[ key + " " ] = value); + } + return cache; +} + +/** + * Mark a function for special use by Sizzle + * @param {Function} fn The function to mark + */ +function markFunction( fn ) { + fn[ expando ] = true; + return fn; +} + +/** + * Support testing using an element + * @param {Function} fn Passed the created div and expects a boolean result + */ +function assert( fn ) { + var div = document.createElement("div"); + + try { + return !!fn( div ); + } catch (e) { + return false; + } finally { + // Remove from its parent by default + if ( div.parentNode ) { + div.parentNode.removeChild( div ); + } + // release memory in IE + div = null; + } +} + +/** + * Adds the same handler for all of the specified attrs + * @param {String} attrs Pipe-separated list of attributes + * @param {Function} handler The method that will be applied + */ +function addHandle( attrs, handler ) { + var arr = attrs.split("|"), + i = arr.length; + + while ( i-- ) { + Expr.attrHandle[ arr[i] ] = handler; + } +} + +/** + * Checks document order of two siblings + * @param {Element} a + * @param {Element} b + * @returns {Number} Returns less than 0 if a precedes b, greater than 0 if a follows b + */ +function siblingCheck( a, b ) { + var cur = b && a, + diff = cur && a.nodeType === 1 && b.nodeType === 1 && + ( ~b.sourceIndex || MAX_NEGATIVE ) - + ( ~a.sourceIndex || MAX_NEGATIVE ); + + // Use IE sourceIndex if available on both nodes + if ( diff ) { + return diff; + } + + // Check if b follows a + if ( cur ) { + while ( (cur = cur.nextSibling) ) { + if ( cur === b ) { + return -1; + } + } + } + + return a ? 1 : -1; +} + +/** + * Returns a function to use in pseudos for input types + * @param {String} type + */ +function createInputPseudo( type ) { + return function( elem ) { + var name = elem.nodeName.toLowerCase(); + return name === "input" && elem.type === type; + }; +} + +/** + * Returns a function to use in pseudos for buttons + * @param {String} type + */ +function createButtonPseudo( type ) { + return function( elem ) { + var name = elem.nodeName.toLowerCase(); + return (name === "input" || name === "button") && elem.type === type; + }; +} + +/** + * Returns a function to use in pseudos for positionals + * @param {Function} fn + */ +function createPositionalPseudo( fn ) { + return markFunction(function( argument ) { + argument = +argument; + return markFunction(function( seed, matches ) { + var j, + matchIndexes = fn( [], seed.length, argument ), + i = matchIndexes.length; + + // Match elements found at the specified indexes + while ( i-- ) { + if ( seed[ (j = matchIndexes[i]) ] ) { + seed[j] = !(matches[j] = seed[j]); + } + } + }); + }); +} + +/** + * Checks a node for validity as a Sizzle context + * @param {Element|Object=} context + * @returns {Element|Object|Boolean} The input node if acceptable, otherwise a falsy value + */ +function testContext( context ) { + return context && typeof context.getElementsByTagName !== "undefined" && context; +} + +// Expose support vars for convenience +support = Sizzle.support = {}; + +/** + * Detects XML nodes + * @param {Element|Object} elem An element or a document + * @returns {Boolean} True iff elem is a non-HTML XML node + */ +isXML = Sizzle.isXML = function( elem ) { + // documentElement is verified for cases where it doesn't yet exist + // (such as loading iframes in IE - #4833) + var documentElement = elem && (elem.ownerDocument || elem).documentElement; + return documentElement ? documentElement.nodeName !== "HTML" : false; +}; + +/** + * Sets document-related variables once based on the current document + * @param {Element|Object} [doc] An element or document object to use to set the document + * @returns {Object} Returns the current document + */ +setDocument = Sizzle.setDocument = function( node ) { + var hasCompare, parent, + doc = node ? node.ownerDocument || node : preferredDoc; + + // Return early if doc is invalid or already selected + if ( doc === document || doc.nodeType !== 9 || !doc.documentElement ) { + return document; + } + + // Update global variables + document = doc; + docElem = document.documentElement; + documentIsHTML = !isXML( document ); + + // Support: IE 9-11, Edge + // Accessing iframe documents after unload throws "permission denied" errors (jQuery #13936) + if ( (parent = document.defaultView) && parent.top !== parent ) { + // Support: IE 11 + if ( parent.addEventListener ) { + parent.addEventListener( "unload", unloadHandler, false ); + + // Support: IE 9 - 10 only + } else if ( parent.attachEvent ) { + parent.attachEvent( "onunload", unloadHandler ); + } + } + + /* Attributes + ---------------------------------------------------------------------- */ + + // Support: IE<8 + // Verify that getAttribute really returns attributes and not properties + // (excepting IE8 booleans) + support.attributes = assert(function( div ) { + div.className = "i"; + return !div.getAttribute("className"); + }); + + /* getElement(s)By* + ---------------------------------------------------------------------- */ + + // Check if getElementsByTagName("*") returns only elements + support.getElementsByTagName = assert(function( div ) { + div.appendChild( document.createComment("") ); + return !div.getElementsByTagName("*").length; + }); + + // Support: IE<9 + support.getElementsByClassName = rnative.test( document.getElementsByClassName ); + + // Support: IE<10 + // Check if getElementById returns elements by name + // The broken getElementById methods don't pick up programatically-set names, + // so use a roundabout getElementsByName test + support.getById = assert(function( div ) { + docElem.appendChild( div ).id = expando; + return !document.getElementsByName || !document.getElementsByName( expando ).length; + }); + + // ID find and filter + if ( support.getById ) { + Expr.find["ID"] = function( id, context ) { + if ( typeof context.getElementById !== "undefined" && documentIsHTML ) { + var m = context.getElementById( id ); + return m ? [ m ] : []; + } + }; + Expr.filter["ID"] = function( id ) { + var attrId = id.replace( runescape, funescape ); + return function( elem ) { + return elem.getAttribute("id") === attrId; + }; + }; + } else { + // Support: IE6/7 + // getElementById is not reliable as a find shortcut + delete Expr.find["ID"]; + + Expr.filter["ID"] = function( id ) { + var attrId = id.replace( runescape, funescape ); + return function( elem ) { + var node = typeof elem.getAttributeNode !== "undefined" && + elem.getAttributeNode("id"); + return node && node.value === attrId; + }; + }; + } + + // Tag + Expr.find["TAG"] = support.getElementsByTagName ? + function( tag, context ) { + if ( typeof context.getElementsByTagName !== "undefined" ) { + return context.getElementsByTagName( tag ); + + // DocumentFragment nodes don't have gEBTN + } else if ( support.qsa ) { + return context.querySelectorAll( tag ); + } + } : + + function( tag, context ) { + var elem, + tmp = [], + i = 0, + // By happy coincidence, a (broken) gEBTN appears on DocumentFragment nodes too + results = context.getElementsByTagName( tag ); + + // Filter out possible comments + if ( tag === "*" ) { + while ( (elem = results[i++]) ) { + if ( elem.nodeType === 1 ) { + tmp.push( elem ); + } + } + + return tmp; + } + return results; + }; + + // Class + Expr.find["CLASS"] = support.getElementsByClassName && function( className, context ) { + if ( typeof context.getElementsByClassName !== "undefined" && documentIsHTML ) { + return context.getElementsByClassName( className ); + } + }; + + /* QSA/matchesSelector + ---------------------------------------------------------------------- */ + + // QSA and matchesSelector support + + // matchesSelector(:active) reports false when true (IE9/Opera 11.5) + rbuggyMatches = []; + + // qSa(:focus) reports false when true (Chrome 21) + // We allow this because of a bug in IE8/9 that throws an error + // whenever `document.activeElement` is accessed on an iframe + // So, we allow :focus to pass through QSA all the time to avoid the IE error + // See http://bugs.jquery.com/ticket/13378 + rbuggyQSA = []; + + if ( (support.qsa = rnative.test( document.querySelectorAll )) ) { + // Build QSA regex + // Regex strategy adopted from Diego Perini + assert(function( div ) { + // Select is set to empty string on purpose + // This is to test IE's treatment of not explicitly + // setting a boolean content attribute, + // since its presence should be enough + // http://bugs.jquery.com/ticket/12359 + docElem.appendChild( div ).innerHTML = "" + + "" + + ""; + + // Support: IE8, Opera 11-12.16 + // Nothing should be selected when empty strings follow ^= or $= or *= + // The test attribute must be unknown in Opera but "safe" for WinRT + // http://msdn.microsoft.com/en-us/library/ie/hh465388.aspx#attribute_section + if ( div.querySelectorAll("[msallowcapture^='']").length ) { + rbuggyQSA.push( "[*^$]=" + whitespace + "*(?:''|\"\")" ); + } + + // Support: IE8 + // Boolean attributes and "value" are not treated correctly + if ( !div.querySelectorAll("[selected]").length ) { + rbuggyQSA.push( "\\[" + whitespace + "*(?:value|" + booleans + ")" ); + } + + // Support: Chrome<29, Android<4.4, Safari<7.0+, iOS<7.0+, PhantomJS<1.9.8+ + if ( !div.querySelectorAll( "[id~=" + expando + "-]" ).length ) { + rbuggyQSA.push("~="); + } + + // Webkit/Opera - :checked should return selected option elements + // http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked + // IE8 throws error here and will not see later tests + if ( !div.querySelectorAll(":checked").length ) { + rbuggyQSA.push(":checked"); + } + + // Support: Safari 8+, iOS 8+ + // https://bugs.webkit.org/show_bug.cgi?id=136851 + // In-page `selector#id sibing-combinator selector` fails + if ( !div.querySelectorAll( "a#" + expando + "+*" ).length ) { + rbuggyQSA.push(".#.+[+~]"); + } + }); + + assert(function( div ) { + // Support: Windows 8 Native Apps + // The type and name attributes are restricted during .innerHTML assignment + var input = document.createElement("input"); + input.setAttribute( "type", "hidden" ); + div.appendChild( input ).setAttribute( "name", "D" ); + + // Support: IE8 + // Enforce case-sensitivity of name attribute + if ( div.querySelectorAll("[name=d]").length ) { + rbuggyQSA.push( "name" + whitespace + "*[*^$|!~]?=" ); + } + + // FF 3.5 - :enabled/:disabled and hidden elements (hidden elements are still enabled) + // IE8 throws error here and will not see later tests + if ( !div.querySelectorAll(":enabled").length ) { + rbuggyQSA.push( ":enabled", ":disabled" ); + } + + // Opera 10-11 does not throw on post-comma invalid pseudos + div.querySelectorAll("*,:x"); + rbuggyQSA.push(",.*:"); + }); + } + + if ( (support.matchesSelector = rnative.test( (matches = docElem.matches || + docElem.webkitMatchesSelector || + docElem.mozMatchesSelector || + docElem.oMatchesSelector || + docElem.msMatchesSelector) )) ) { + + assert(function( div ) { + // Check to see if it's possible to do matchesSelector + // on a disconnected node (IE 9) + support.disconnectedMatch = matches.call( div, "div" ); + + // This should fail with an exception + // Gecko does not error, returns false instead + matches.call( div, "[s!='']:x" ); + rbuggyMatches.push( "!=", pseudos ); + }); + } + + rbuggyQSA = rbuggyQSA.length && new RegExp( rbuggyQSA.join("|") ); + rbuggyMatches = rbuggyMatches.length && new RegExp( rbuggyMatches.join("|") ); + + /* Contains + ---------------------------------------------------------------------- */ + hasCompare = rnative.test( docElem.compareDocumentPosition ); + + // Element contains another + // Purposefully self-exclusive + // As in, an element does not contain itself + contains = hasCompare || rnative.test( docElem.contains ) ? + function( a, b ) { + var adown = a.nodeType === 9 ? a.documentElement : a, + bup = b && b.parentNode; + return a === bup || !!( bup && bup.nodeType === 1 && ( + adown.contains ? + adown.contains( bup ) : + a.compareDocumentPosition && a.compareDocumentPosition( bup ) & 16 + )); + } : + function( a, b ) { + if ( b ) { + while ( (b = b.parentNode) ) { + if ( b === a ) { + return true; + } + } + } + return false; + }; + + /* Sorting + ---------------------------------------------------------------------- */ + + // Document order sorting + sortOrder = hasCompare ? + function( a, b ) { + + // Flag for duplicate removal + if ( a === b ) { + hasDuplicate = true; + return 0; + } + + // Sort on method existence if only one input has compareDocumentPosition + var compare = !a.compareDocumentPosition - !b.compareDocumentPosition; + if ( compare ) { + return compare; + } + + // Calculate position if both inputs belong to the same document + compare = ( a.ownerDocument || a ) === ( b.ownerDocument || b ) ? + a.compareDocumentPosition( b ) : + + // Otherwise we know they are disconnected + 1; + + // Disconnected nodes + if ( compare & 1 || + (!support.sortDetached && b.compareDocumentPosition( a ) === compare) ) { + + // Choose the first element that is related to our preferred document + if ( a === document || a.ownerDocument === preferredDoc && contains(preferredDoc, a) ) { + return -1; + } + if ( b === document || b.ownerDocument === preferredDoc && contains(preferredDoc, b) ) { + return 1; + } + + // Maintain original order + return sortInput ? + ( indexOf( sortInput, a ) - indexOf( sortInput, b ) ) : + 0; + } + + return compare & 4 ? -1 : 1; + } : + function( a, b ) { + // Exit early if the nodes are identical + if ( a === b ) { + hasDuplicate = true; + return 0; + } + + var cur, + i = 0, + aup = a.parentNode, + bup = b.parentNode, + ap = [ a ], + bp = [ b ]; + + // Parentless nodes are either documents or disconnected + if ( !aup || !bup ) { + return a === document ? -1 : + b === document ? 1 : + aup ? -1 : + bup ? 1 : + sortInput ? + ( indexOf( sortInput, a ) - indexOf( sortInput, b ) ) : + 0; + + // If the nodes are siblings, we can do a quick check + } else if ( aup === bup ) { + return siblingCheck( a, b ); + } + + // Otherwise we need full lists of their ancestors for comparison + cur = a; + while ( (cur = cur.parentNode) ) { + ap.unshift( cur ); + } + cur = b; + while ( (cur = cur.parentNode) ) { + bp.unshift( cur ); + } + + // Walk down the tree looking for a discrepancy + while ( ap[i] === bp[i] ) { + i++; + } + + return i ? + // Do a sibling check if the nodes have a common ancestor + siblingCheck( ap[i], bp[i] ) : + + // Otherwise nodes in our document sort first + ap[i] === preferredDoc ? -1 : + bp[i] === preferredDoc ? 1 : + 0; + }; + + return document; +}; + +Sizzle.matches = function( expr, elements ) { + return Sizzle( expr, null, null, elements ); +}; + +Sizzle.matchesSelector = function( elem, expr ) { + // Set document vars if needed + if ( ( elem.ownerDocument || elem ) !== document ) { + setDocument( elem ); + } + + // Make sure that attribute selectors are quoted + expr = expr.replace( rattributeQuotes, "='$1']" ); + + if ( support.matchesSelector && documentIsHTML && + !compilerCache[ expr + " " ] && + ( !rbuggyMatches || !rbuggyMatches.test( expr ) ) && + ( !rbuggyQSA || !rbuggyQSA.test( expr ) ) ) { + + try { + var ret = matches.call( elem, expr ); + + // IE 9's matchesSelector returns false on disconnected nodes + if ( ret || support.disconnectedMatch || + // As well, disconnected nodes are said to be in a document + // fragment in IE 9 + elem.document && elem.document.nodeType !== 11 ) { + return ret; + } + } catch (e) {} + } + + return Sizzle( expr, document, null, [ elem ] ).length > 0; +}; + +Sizzle.contains = function( context, elem ) { + // Set document vars if needed + if ( ( context.ownerDocument || context ) !== document ) { + setDocument( context ); + } + return contains( context, elem ); +}; + +Sizzle.attr = function( elem, name ) { + // Set document vars if needed + if ( ( elem.ownerDocument || elem ) !== document ) { + setDocument( elem ); + } + + var fn = Expr.attrHandle[ name.toLowerCase() ], + // Don't get fooled by Object.prototype properties (jQuery #13807) + val = fn && hasOwn.call( Expr.attrHandle, name.toLowerCase() ) ? + fn( elem, name, !documentIsHTML ) : + undefined; + + return val !== undefined ? + val : + support.attributes || !documentIsHTML ? + elem.getAttribute( name ) : + (val = elem.getAttributeNode(name)) && val.specified ? + val.value : + null; +}; + +Sizzle.error = function( msg ) { + throw new Error( "Syntax error, unrecognized expression: " + msg ); +}; + +/** + * Document sorting and removing duplicates + * @param {ArrayLike} results + */ +Sizzle.uniqueSort = function( results ) { + var elem, + duplicates = [], + j = 0, + i = 0; + + // Unless we *know* we can detect duplicates, assume their presence + hasDuplicate = !support.detectDuplicates; + sortInput = !support.sortStable && results.slice( 0 ); + results.sort( sortOrder ); + + if ( hasDuplicate ) { + while ( (elem = results[i++]) ) { + if ( elem === results[ i ] ) { + j = duplicates.push( i ); + } + } + while ( j-- ) { + results.splice( duplicates[ j ], 1 ); + } + } + + // Clear input after sorting to release objects + // See https://github.com/jquery/sizzle/pull/225 + sortInput = null; + + return results; +}; + +/** + * Utility function for retrieving the text value of an array of DOM nodes + * @param {Array|Element} elem + */ +getText = Sizzle.getText = function( elem ) { + var node, + ret = "", + i = 0, + nodeType = elem.nodeType; + + if ( !nodeType ) { + // If no nodeType, this is expected to be an array + while ( (node = elem[i++]) ) { + // Do not traverse comment nodes + ret += getText( node ); + } + } else if ( nodeType === 1 || nodeType === 9 || nodeType === 11 ) { + // Use textContent for elements + // innerText usage removed for consistency of new lines (jQuery #11153) + if ( typeof elem.textContent === "string" ) { + return elem.textContent; + } else { + // Traverse its children + for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) { + ret += getText( elem ); + } + } + } else if ( nodeType === 3 || nodeType === 4 ) { + return elem.nodeValue; + } + // Do not include comment or processing instruction nodes + + return ret; +}; + +Expr = Sizzle.selectors = { + + // Can be adjusted by the user + cacheLength: 50, + + createPseudo: markFunction, + + match: matchExpr, + + attrHandle: {}, + + find: {}, + + relative: { + ">": { dir: "parentNode", first: true }, + " ": { dir: "parentNode" }, + "+": { dir: "previousSibling", first: true }, + "~": { dir: "previousSibling" } + }, + + preFilter: { + "ATTR": function( match ) { + match[1] = match[1].replace( runescape, funescape ); + + // Move the given value to match[3] whether quoted or unquoted + match[3] = ( match[3] || match[4] || match[5] || "" ).replace( runescape, funescape ); + + if ( match[2] === "~=" ) { + match[3] = " " + match[3] + " "; + } + + return match.slice( 0, 4 ); + }, + + "CHILD": function( match ) { + /* matches from matchExpr["CHILD"] + 1 type (only|nth|...) + 2 what (child|of-type) + 3 argument (even|odd|\d*|\d*n([+-]\d+)?|...) + 4 xn-component of xn+y argument ([+-]?\d*n|) + 5 sign of xn-component + 6 x of xn-component + 7 sign of y-component + 8 y of y-component + */ + match[1] = match[1].toLowerCase(); + + if ( match[1].slice( 0, 3 ) === "nth" ) { + // nth-* requires argument + if ( !match[3] ) { + Sizzle.error( match[0] ); + } + + // numeric x and y parameters for Expr.filter.CHILD + // remember that false/true cast respectively to 0/1 + match[4] = +( match[4] ? match[5] + (match[6] || 1) : 2 * ( match[3] === "even" || match[3] === "odd" ) ); + match[5] = +( ( match[7] + match[8] ) || match[3] === "odd" ); + + // other types prohibit arguments + } else if ( match[3] ) { + Sizzle.error( match[0] ); + } + + return match; + }, + + "PSEUDO": function( match ) { + var excess, + unquoted = !match[6] && match[2]; + + if ( matchExpr["CHILD"].test( match[0] ) ) { + return null; + } + + // Accept quoted arguments as-is + if ( match[3] ) { + match[2] = match[4] || match[5] || ""; + + // Strip excess characters from unquoted arguments + } else if ( unquoted && rpseudo.test( unquoted ) && + // Get excess from tokenize (recursively) + (excess = tokenize( unquoted, true )) && + // advance to the next closing parenthesis + (excess = unquoted.indexOf( ")", unquoted.length - excess ) - unquoted.length) ) { + + // excess is a negative index + match[0] = match[0].slice( 0, excess ); + match[2] = unquoted.slice( 0, excess ); + } + + // Return only captures needed by the pseudo filter method (type and argument) + return match.slice( 0, 3 ); + } + }, + + filter: { + + "TAG": function( nodeNameSelector ) { + var nodeName = nodeNameSelector.replace( runescape, funescape ).toLowerCase(); + return nodeNameSelector === "*" ? + function() { return true; } : + function( elem ) { + return elem.nodeName && elem.nodeName.toLowerCase() === nodeName; + }; + }, + + "CLASS": function( className ) { + var pattern = classCache[ className + " " ]; + + return pattern || + (pattern = new RegExp( "(^|" + whitespace + ")" + className + "(" + whitespace + "|$)" )) && + classCache( className, function( elem ) { + return pattern.test( typeof elem.className === "string" && elem.className || typeof elem.getAttribute !== "undefined" && elem.getAttribute("class") || "" ); + }); + }, + + "ATTR": function( name, operator, check ) { + return function( elem ) { + var result = Sizzle.attr( elem, name ); + + if ( result == null ) { + return operator === "!="; + } + if ( !operator ) { + return true; + } + + result += ""; + + return operator === "=" ? result === check : + operator === "!=" ? result !== check : + operator === "^=" ? check && result.indexOf( check ) === 0 : + operator === "*=" ? check && result.indexOf( check ) > -1 : + operator === "$=" ? check && result.slice( -check.length ) === check : + operator === "~=" ? ( " " + result.replace( rwhitespace, " " ) + " " ).indexOf( check ) > -1 : + operator === "|=" ? result === check || result.slice( 0, check.length + 1 ) === check + "-" : + false; + }; + }, + + "CHILD": function( type, what, argument, first, last ) { + var simple = type.slice( 0, 3 ) !== "nth", + forward = type.slice( -4 ) !== "last", + ofType = what === "of-type"; + + return first === 1 && last === 0 ? + + // Shortcut for :nth-*(n) + function( elem ) { + return !!elem.parentNode; + } : + + function( elem, context, xml ) { + var cache, uniqueCache, outerCache, node, nodeIndex, start, + dir = simple !== forward ? "nextSibling" : "previousSibling", + parent = elem.parentNode, + name = ofType && elem.nodeName.toLowerCase(), + useCache = !xml && !ofType, + diff = false; + + if ( parent ) { + + // :(first|last|only)-(child|of-type) + if ( simple ) { + while ( dir ) { + node = elem; + while ( (node = node[ dir ]) ) { + if ( ofType ? + node.nodeName.toLowerCase() === name : + node.nodeType === 1 ) { + + return false; + } + } + // Reverse direction for :only-* (if we haven't yet done so) + start = dir = type === "only" && !start && "nextSibling"; + } + return true; + } + + start = [ forward ? parent.firstChild : parent.lastChild ]; + + // non-xml :nth-child(...) stores cache data on `parent` + if ( forward && useCache ) { + + // Seek `elem` from a previously-cached index + + // ...in a gzip-friendly way + node = parent; + outerCache = node[ expando ] || (node[ expando ] = {}); + + // Support: IE <9 only + // Defend against cloned attroperties (jQuery gh-1709) + uniqueCache = outerCache[ node.uniqueID ] || + (outerCache[ node.uniqueID ] = {}); + + cache = uniqueCache[ type ] || []; + nodeIndex = cache[ 0 ] === dirruns && cache[ 1 ]; + diff = nodeIndex && cache[ 2 ]; + node = nodeIndex && parent.childNodes[ nodeIndex ]; + + while ( (node = ++nodeIndex && node && node[ dir ] || + + // Fallback to seeking `elem` from the start + (diff = nodeIndex = 0) || start.pop()) ) { + + // When found, cache indexes on `parent` and break + if ( node.nodeType === 1 && ++diff && node === elem ) { + uniqueCache[ type ] = [ dirruns, nodeIndex, diff ]; + break; + } + } + + } else { + // Use previously-cached element index if available + if ( useCache ) { + // ...in a gzip-friendly way + node = elem; + outerCache = node[ expando ] || (node[ expando ] = {}); + + // Support: IE <9 only + // Defend against cloned attroperties (jQuery gh-1709) + uniqueCache = outerCache[ node.uniqueID ] || + (outerCache[ node.uniqueID ] = {}); + + cache = uniqueCache[ type ] || []; + nodeIndex = cache[ 0 ] === dirruns && cache[ 1 ]; + diff = nodeIndex; + } + + // xml :nth-child(...) + // or :nth-last-child(...) or :nth(-last)?-of-type(...) + if ( diff === false ) { + // Use the same loop as above to seek `elem` from the start + while ( (node = ++nodeIndex && node && node[ dir ] || + (diff = nodeIndex = 0) || start.pop()) ) { + + if ( ( ofType ? + node.nodeName.toLowerCase() === name : + node.nodeType === 1 ) && + ++diff ) { + + // Cache the index of each encountered element + if ( useCache ) { + outerCache = node[ expando ] || (node[ expando ] = {}); + + // Support: IE <9 only + // Defend against cloned attroperties (jQuery gh-1709) + uniqueCache = outerCache[ node.uniqueID ] || + (outerCache[ node.uniqueID ] = {}); + + uniqueCache[ type ] = [ dirruns, diff ]; + } + + if ( node === elem ) { + break; + } + } + } + } + } + + // Incorporate the offset, then check against cycle size + diff -= last; + return diff === first || ( diff % first === 0 && diff / first >= 0 ); + } + }; + }, + + "PSEUDO": function( pseudo, argument ) { + // pseudo-class names are case-insensitive + // http://www.w3.org/TR/selectors/#pseudo-classes + // Prioritize by case sensitivity in case custom pseudos are added with uppercase letters + // Remember that setFilters inherits from pseudos + var args, + fn = Expr.pseudos[ pseudo ] || Expr.setFilters[ pseudo.toLowerCase() ] || + Sizzle.error( "unsupported pseudo: " + pseudo ); + + // The user may use createPseudo to indicate that + // arguments are needed to create the filter function + // just as Sizzle does + if ( fn[ expando ] ) { + return fn( argument ); + } + + // But maintain support for old signatures + if ( fn.length > 1 ) { + args = [ pseudo, pseudo, "", argument ]; + return Expr.setFilters.hasOwnProperty( pseudo.toLowerCase() ) ? + markFunction(function( seed, matches ) { + var idx, + matched = fn( seed, argument ), + i = matched.length; + while ( i-- ) { + idx = indexOf( seed, matched[i] ); + seed[ idx ] = !( matches[ idx ] = matched[i] ); + } + }) : + function( elem ) { + return fn( elem, 0, args ); + }; + } + + return fn; + } + }, + + pseudos: { + // Potentially complex pseudos + "not": markFunction(function( selector ) { + // Trim the selector passed to compile + // to avoid treating leading and trailing + // spaces as combinators + var input = [], + results = [], + matcher = compile( selector.replace( rtrim, "$1" ) ); + + return matcher[ expando ] ? + markFunction(function( seed, matches, context, xml ) { + var elem, + unmatched = matcher( seed, null, xml, [] ), + i = seed.length; + + // Match elements unmatched by `matcher` + while ( i-- ) { + if ( (elem = unmatched[i]) ) { + seed[i] = !(matches[i] = elem); + } + } + }) : + function( elem, context, xml ) { + input[0] = elem; + matcher( input, null, xml, results ); + // Don't keep the element (issue #299) + input[0] = null; + return !results.pop(); + }; + }), + + "has": markFunction(function( selector ) { + return function( elem ) { + return Sizzle( selector, elem ).length > 0; + }; + }), + + "contains": markFunction(function( text ) { + text = text.replace( runescape, funescape ); + return function( elem ) { + return ( elem.textContent || elem.innerText || getText( elem ) ).indexOf( text ) > -1; + }; + }), + + // "Whether an element is represented by a :lang() selector + // is based solely on the element's language value + // being equal to the identifier C, + // or beginning with the identifier C immediately followed by "-". + // The matching of C against the element's language value is performed case-insensitively. + // The identifier C does not have to be a valid language name." + // http://www.w3.org/TR/selectors/#lang-pseudo + "lang": markFunction( function( lang ) { + // lang value must be a valid identifier + if ( !ridentifier.test(lang || "") ) { + Sizzle.error( "unsupported lang: " + lang ); + } + lang = lang.replace( runescape, funescape ).toLowerCase(); + return function( elem ) { + var elemLang; + do { + if ( (elemLang = documentIsHTML ? + elem.lang : + elem.getAttribute("xml:lang") || elem.getAttribute("lang")) ) { + + elemLang = elemLang.toLowerCase(); + return elemLang === lang || elemLang.indexOf( lang + "-" ) === 0; + } + } while ( (elem = elem.parentNode) && elem.nodeType === 1 ); + return false; + }; + }), + + // Miscellaneous + "target": function( elem ) { + var hash = window.location && window.location.hash; + return hash && hash.slice( 1 ) === elem.id; + }, + + "root": function( elem ) { + return elem === docElem; + }, + + "focus": function( elem ) { + return elem === document.activeElement && (!document.hasFocus || document.hasFocus()) && !!(elem.type || elem.href || ~elem.tabIndex); + }, + + // Boolean properties + "enabled": function( elem ) { + return elem.disabled === false; + }, + + "disabled": function( elem ) { + return elem.disabled === true; + }, + + "checked": function( elem ) { + // In CSS3, :checked should return both checked and selected elements + // http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked + var nodeName = elem.nodeName.toLowerCase(); + return (nodeName === "input" && !!elem.checked) || (nodeName === "option" && !!elem.selected); + }, + + "selected": function( elem ) { + // Accessing this property makes selected-by-default + // options in Safari work properly + if ( elem.parentNode ) { + elem.parentNode.selectedIndex; + } + + return elem.selected === true; + }, + + // Contents + "empty": function( elem ) { + // http://www.w3.org/TR/selectors/#empty-pseudo + // :empty is negated by element (1) or content nodes (text: 3; cdata: 4; entity ref: 5), + // but not by others (comment: 8; processing instruction: 7; etc.) + // nodeType < 6 works because attributes (2) do not appear as children + for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) { + if ( elem.nodeType < 6 ) { + return false; + } + } + return true; + }, + + "parent": function( elem ) { + return !Expr.pseudos["empty"]( elem ); + }, + + // Element/input types + "header": function( elem ) { + return rheader.test( elem.nodeName ); + }, + + "input": function( elem ) { + return rinputs.test( elem.nodeName ); + }, + + "button": function( elem ) { + var name = elem.nodeName.toLowerCase(); + return name === "input" && elem.type === "button" || name === "button"; + }, + + "text": function( elem ) { + var attr; + return elem.nodeName.toLowerCase() === "input" && + elem.type === "text" && + + // Support: IE<8 + // New HTML5 attribute values (e.g., "search") appear with elem.type === "text" + ( (attr = elem.getAttribute("type")) == null || attr.toLowerCase() === "text" ); + }, + + // Position-in-collection + "first": createPositionalPseudo(function() { + return [ 0 ]; + }), + + "last": createPositionalPseudo(function( matchIndexes, length ) { + return [ length - 1 ]; + }), + + "eq": createPositionalPseudo(function( matchIndexes, length, argument ) { + return [ argument < 0 ? argument + length : argument ]; + }), + + "even": createPositionalPseudo(function( matchIndexes, length ) { + var i = 0; + for ( ; i < length; i += 2 ) { + matchIndexes.push( i ); + } + return matchIndexes; + }), + + "odd": createPositionalPseudo(function( matchIndexes, length ) { + var i = 1; + for ( ; i < length; i += 2 ) { + matchIndexes.push( i ); + } + return matchIndexes; + }), + + "lt": createPositionalPseudo(function( matchIndexes, length, argument ) { + var i = argument < 0 ? argument + length : argument; + for ( ; --i >= 0; ) { + matchIndexes.push( i ); + } + return matchIndexes; + }), + + "gt": createPositionalPseudo(function( matchIndexes, length, argument ) { + var i = argument < 0 ? argument + length : argument; + for ( ; ++i < length; ) { + matchIndexes.push( i ); + } + return matchIndexes; + }) + } +}; + +Expr.pseudos["nth"] = Expr.pseudos["eq"]; + +// Add button/input type pseudos +for ( i in { radio: true, checkbox: true, file: true, password: true, image: true } ) { + Expr.pseudos[ i ] = createInputPseudo( i ); +} +for ( i in { submit: true, reset: true } ) { + Expr.pseudos[ i ] = createButtonPseudo( i ); +} + +// Easy API for creating new setFilters +function setFilters() {} +setFilters.prototype = Expr.filters = Expr.pseudos; +Expr.setFilters = new setFilters(); + +tokenize = Sizzle.tokenize = function( selector, parseOnly ) { + var matched, match, tokens, type, + soFar, groups, preFilters, + cached = tokenCache[ selector + " " ]; + + if ( cached ) { + return parseOnly ? 0 : cached.slice( 0 ); + } + + soFar = selector; + groups = []; + preFilters = Expr.preFilter; + + while ( soFar ) { + + // Comma and first run + if ( !matched || (match = rcomma.exec( soFar )) ) { + if ( match ) { + // Don't consume trailing commas as valid + soFar = soFar.slice( match[0].length ) || soFar; + } + groups.push( (tokens = []) ); + } + + matched = false; + + // Combinators + if ( (match = rcombinators.exec( soFar )) ) { + matched = match.shift(); + tokens.push({ + value: matched, + // Cast descendant combinators to space + type: match[0].replace( rtrim, " " ) + }); + soFar = soFar.slice( matched.length ); + } + + // Filters + for ( type in Expr.filter ) { + if ( (match = matchExpr[ type ].exec( soFar )) && (!preFilters[ type ] || + (match = preFilters[ type ]( match ))) ) { + matched = match.shift(); + tokens.push({ + value: matched, + type: type, + matches: match + }); + soFar = soFar.slice( matched.length ); + } + } + + if ( !matched ) { + break; + } + } + + // Return the length of the invalid excess + // if we're just parsing + // Otherwise, throw an error or return tokens + return parseOnly ? + soFar.length : + soFar ? + Sizzle.error( selector ) : + // Cache the tokens + tokenCache( selector, groups ).slice( 0 ); +}; + +function toSelector( tokens ) { + var i = 0, + len = tokens.length, + selector = ""; + for ( ; i < len; i++ ) { + selector += tokens[i].value; + } + return selector; +} + +function addCombinator( matcher, combinator, base ) { + var dir = combinator.dir, + checkNonElements = base && dir === "parentNode", + doneName = done++; + + return combinator.first ? + // Check against closest ancestor/preceding element + function( elem, context, xml ) { + while ( (elem = elem[ dir ]) ) { + if ( elem.nodeType === 1 || checkNonElements ) { + return matcher( elem, context, xml ); + } + } + } : + + // Check against all ancestor/preceding elements + function( elem, context, xml ) { + var oldCache, uniqueCache, outerCache, + newCache = [ dirruns, doneName ]; + + // We can't set arbitrary data on XML nodes, so they don't benefit from combinator caching + if ( xml ) { + while ( (elem = elem[ dir ]) ) { + if ( elem.nodeType === 1 || checkNonElements ) { + if ( matcher( elem, context, xml ) ) { + return true; + } + } + } + } else { + while ( (elem = elem[ dir ]) ) { + if ( elem.nodeType === 1 || checkNonElements ) { + outerCache = elem[ expando ] || (elem[ expando ] = {}); + + // Support: IE <9 only + // Defend against cloned attroperties (jQuery gh-1709) + uniqueCache = outerCache[ elem.uniqueID ] || (outerCache[ elem.uniqueID ] = {}); + + if ( (oldCache = uniqueCache[ dir ]) && + oldCache[ 0 ] === dirruns && oldCache[ 1 ] === doneName ) { + + // Assign to newCache so results back-propagate to previous elements + return (newCache[ 2 ] = oldCache[ 2 ]); + } else { + // Reuse newcache so results back-propagate to previous elements + uniqueCache[ dir ] = newCache; + + // A match means we're done; a fail means we have to keep checking + if ( (newCache[ 2 ] = matcher( elem, context, xml )) ) { + return true; + } + } + } + } + } + }; +} + +function elementMatcher( matchers ) { + return matchers.length > 1 ? + function( elem, context, xml ) { + var i = matchers.length; + while ( i-- ) { + if ( !matchers[i]( elem, context, xml ) ) { + return false; + } + } + return true; + } : + matchers[0]; +} + +function multipleContexts( selector, contexts, results ) { + var i = 0, + len = contexts.length; + for ( ; i < len; i++ ) { + Sizzle( selector, contexts[i], results ); + } + return results; +} + +function condense( unmatched, map, filter, context, xml ) { + var elem, + newUnmatched = [], + i = 0, + len = unmatched.length, + mapped = map != null; + + for ( ; i < len; i++ ) { + if ( (elem = unmatched[i]) ) { + if ( !filter || filter( elem, context, xml ) ) { + newUnmatched.push( elem ); + if ( mapped ) { + map.push( i ); + } + } + } + } + + return newUnmatched; +} + +function setMatcher( preFilter, selector, matcher, postFilter, postFinder, postSelector ) { + if ( postFilter && !postFilter[ expando ] ) { + postFilter = setMatcher( postFilter ); + } + if ( postFinder && !postFinder[ expando ] ) { + postFinder = setMatcher( postFinder, postSelector ); + } + return markFunction(function( seed, results, context, xml ) { + var temp, i, elem, + preMap = [], + postMap = [], + preexisting = results.length, + + // Get initial elements from seed or context + elems = seed || multipleContexts( selector || "*", context.nodeType ? [ context ] : context, [] ), + + // Prefilter to get matcher input, preserving a map for seed-results synchronization + matcherIn = preFilter && ( seed || !selector ) ? + condense( elems, preMap, preFilter, context, xml ) : + elems, + + matcherOut = matcher ? + // If we have a postFinder, or filtered seed, or non-seed postFilter or preexisting results, + postFinder || ( seed ? preFilter : preexisting || postFilter ) ? + + // ...intermediate processing is necessary + [] : + + // ...otherwise use results directly + results : + matcherIn; + + // Find primary matches + if ( matcher ) { + matcher( matcherIn, matcherOut, context, xml ); + } + + // Apply postFilter + if ( postFilter ) { + temp = condense( matcherOut, postMap ); + postFilter( temp, [], context, xml ); + + // Un-match failing elements by moving them back to matcherIn + i = temp.length; + while ( i-- ) { + if ( (elem = temp[i]) ) { + matcherOut[ postMap[i] ] = !(matcherIn[ postMap[i] ] = elem); + } + } + } + + if ( seed ) { + if ( postFinder || preFilter ) { + if ( postFinder ) { + // Get the final matcherOut by condensing this intermediate into postFinder contexts + temp = []; + i = matcherOut.length; + while ( i-- ) { + if ( (elem = matcherOut[i]) ) { + // Restore matcherIn since elem is not yet a final match + temp.push( (matcherIn[i] = elem) ); + } + } + postFinder( null, (matcherOut = []), temp, xml ); + } + + // Move matched elements from seed to results to keep them synchronized + i = matcherOut.length; + while ( i-- ) { + if ( (elem = matcherOut[i]) && + (temp = postFinder ? indexOf( seed, elem ) : preMap[i]) > -1 ) { + + seed[temp] = !(results[temp] = elem); + } + } + } + + // Add elements to results, through postFinder if defined + } else { + matcherOut = condense( + matcherOut === results ? + matcherOut.splice( preexisting, matcherOut.length ) : + matcherOut + ); + if ( postFinder ) { + postFinder( null, results, matcherOut, xml ); + } else { + push.apply( results, matcherOut ); + } + } + }); +} + +function matcherFromTokens( tokens ) { + var checkContext, matcher, j, + len = tokens.length, + leadingRelative = Expr.relative[ tokens[0].type ], + implicitRelative = leadingRelative || Expr.relative[" "], + i = leadingRelative ? 1 : 0, + + // The foundational matcher ensures that elements are reachable from top-level context(s) + matchContext = addCombinator( function( elem ) { + return elem === checkContext; + }, implicitRelative, true ), + matchAnyContext = addCombinator( function( elem ) { + return indexOf( checkContext, elem ) > -1; + }, implicitRelative, true ), + matchers = [ function( elem, context, xml ) { + var ret = ( !leadingRelative && ( xml || context !== outermostContext ) ) || ( + (checkContext = context).nodeType ? + matchContext( elem, context, xml ) : + matchAnyContext( elem, context, xml ) ); + // Avoid hanging onto element (issue #299) + checkContext = null; + return ret; + } ]; + + for ( ; i < len; i++ ) { + if ( (matcher = Expr.relative[ tokens[i].type ]) ) { + matchers = [ addCombinator(elementMatcher( matchers ), matcher) ]; + } else { + matcher = Expr.filter[ tokens[i].type ].apply( null, tokens[i].matches ); + + // Return special upon seeing a positional matcher + if ( matcher[ expando ] ) { + // Find the next relative operator (if any) for proper handling + j = ++i; + for ( ; j < len; j++ ) { + if ( Expr.relative[ tokens[j].type ] ) { + break; + } + } + return setMatcher( + i > 1 && elementMatcher( matchers ), + i > 1 && toSelector( + // If the preceding token was a descendant combinator, insert an implicit any-element `*` + tokens.slice( 0, i - 1 ).concat({ value: tokens[ i - 2 ].type === " " ? "*" : "" }) + ).replace( rtrim, "$1" ), + matcher, + i < j && matcherFromTokens( tokens.slice( i, j ) ), + j < len && matcherFromTokens( (tokens = tokens.slice( j )) ), + j < len && toSelector( tokens ) + ); + } + matchers.push( matcher ); + } + } + + return elementMatcher( matchers ); +} + +function matcherFromGroupMatchers( elementMatchers, setMatchers ) { + var bySet = setMatchers.length > 0, + byElement = elementMatchers.length > 0, + superMatcher = function( seed, context, xml, results, outermost ) { + var elem, j, matcher, + matchedCount = 0, + i = "0", + unmatched = seed && [], + setMatched = [], + contextBackup = outermostContext, + // We must always have either seed elements or outermost context + elems = seed || byElement && Expr.find["TAG"]( "*", outermost ), + // Use integer dirruns iff this is the outermost matcher + dirrunsUnique = (dirruns += contextBackup == null ? 1 : Math.random() || 0.1), + len = elems.length; + + if ( outermost ) { + outermostContext = context === document || context || outermost; + } + + // Add elements passing elementMatchers directly to results + // Support: IE<9, Safari + // Tolerate NodeList properties (IE: "length"; Safari: ) matching elements by id + for ( ; i !== len && (elem = elems[i]) != null; i++ ) { + if ( byElement && elem ) { + j = 0; + if ( !context && elem.ownerDocument !== document ) { + setDocument( elem ); + xml = !documentIsHTML; + } + while ( (matcher = elementMatchers[j++]) ) { + if ( matcher( elem, context || document, xml) ) { + results.push( elem ); + break; + } + } + if ( outermost ) { + dirruns = dirrunsUnique; + } + } + + // Track unmatched elements for set filters + if ( bySet ) { + // They will have gone through all possible matchers + if ( (elem = !matcher && elem) ) { + matchedCount--; + } + + // Lengthen the array for every element, matched or not + if ( seed ) { + unmatched.push( elem ); + } + } + } + + // `i` is now the count of elements visited above, and adding it to `matchedCount` + // makes the latter nonnegative. + matchedCount += i; + + // Apply set filters to unmatched elements + // NOTE: This can be skipped if there are no unmatched elements (i.e., `matchedCount` + // equals `i`), unless we didn't visit _any_ elements in the above loop because we have + // no element matchers and no seed. + // Incrementing an initially-string "0" `i` allows `i` to remain a string only in that + // case, which will result in a "00" `matchedCount` that differs from `i` but is also + // numerically zero. + if ( bySet && i !== matchedCount ) { + j = 0; + while ( (matcher = setMatchers[j++]) ) { + matcher( unmatched, setMatched, context, xml ); + } + + if ( seed ) { + // Reintegrate element matches to eliminate the need for sorting + if ( matchedCount > 0 ) { + while ( i-- ) { + if ( !(unmatched[i] || setMatched[i]) ) { + setMatched[i] = pop.call( results ); + } + } + } + + // Discard index placeholder values to get only actual matches + setMatched = condense( setMatched ); + } + + // Add matches to results + push.apply( results, setMatched ); + + // Seedless set matches succeeding multiple successful matchers stipulate sorting + if ( outermost && !seed && setMatched.length > 0 && + ( matchedCount + setMatchers.length ) > 1 ) { + + Sizzle.uniqueSort( results ); + } + } + + // Override manipulation of globals by nested matchers + if ( outermost ) { + dirruns = dirrunsUnique; + outermostContext = contextBackup; + } + + return unmatched; + }; + + return bySet ? + markFunction( superMatcher ) : + superMatcher; +} + +compile = Sizzle.compile = function( selector, match /* Internal Use Only */ ) { + var i, + setMatchers = [], + elementMatchers = [], + cached = compilerCache[ selector + " " ]; + + if ( !cached ) { + // Generate a function of recursive functions that can be used to check each element + if ( !match ) { + match = tokenize( selector ); + } + i = match.length; + while ( i-- ) { + cached = matcherFromTokens( match[i] ); + if ( cached[ expando ] ) { + setMatchers.push( cached ); + } else { + elementMatchers.push( cached ); + } + } + + // Cache the compiled function + cached = compilerCache( selector, matcherFromGroupMatchers( elementMatchers, setMatchers ) ); + + // Save selector and tokenization + cached.selector = selector; + } + return cached; +}; + +/** + * A low-level selection function that works with Sizzle's compiled + * selector functions + * @param {String|Function} selector A selector or a pre-compiled + * selector function built with Sizzle.compile + * @param {Element} context + * @param {Array} [results] + * @param {Array} [seed] A set of elements to match against + */ +select = Sizzle.select = function( selector, context, results, seed ) { + var i, tokens, token, type, find, + compiled = typeof selector === "function" && selector, + match = !seed && tokenize( (selector = compiled.selector || selector) ); + + results = results || []; + + // Try to minimize operations if there is only one selector in the list and no seed + // (the latter of which guarantees us context) + if ( match.length === 1 ) { + + // Reduce context if the leading compound selector is an ID + tokens = match[0] = match[0].slice( 0 ); + if ( tokens.length > 2 && (token = tokens[0]).type === "ID" && + support.getById && context.nodeType === 9 && documentIsHTML && + Expr.relative[ tokens[1].type ] ) { + + context = ( Expr.find["ID"]( token.matches[0].replace(runescape, funescape), context ) || [] )[0]; + if ( !context ) { + return results; + + // Precompiled matchers will still verify ancestry, so step up a level + } else if ( compiled ) { + context = context.parentNode; + } + + selector = selector.slice( tokens.shift().value.length ); + } + + // Fetch a seed set for right-to-left matching + i = matchExpr["needsContext"].test( selector ) ? 0 : tokens.length; + while ( i-- ) { + token = tokens[i]; + + // Abort if we hit a combinator + if ( Expr.relative[ (type = token.type) ] ) { + break; + } + if ( (find = Expr.find[ type ]) ) { + // Search, expanding context for leading sibling combinators + if ( (seed = find( + token.matches[0].replace( runescape, funescape ), + rsibling.test( tokens[0].type ) && testContext( context.parentNode ) || context + )) ) { + + // If seed is empty or no tokens remain, we can return early + tokens.splice( i, 1 ); + selector = seed.length && toSelector( tokens ); + if ( !selector ) { + push.apply( results, seed ); + return results; + } + + break; + } + } + } + } + + // Compile and execute a filtering function if one is not provided + // Provide `match` to avoid retokenization if we modified the selector above + ( compiled || compile( selector, match ) )( + seed, + context, + !documentIsHTML, + results, + !context || rsibling.test( selector ) && testContext( context.parentNode ) || context + ); + return results; +}; + +// One-time assignments + +// Sort stability +support.sortStable = expando.split("").sort( sortOrder ).join("") === expando; + +// Support: Chrome 14-35+ +// Always assume duplicates if they aren't passed to the comparison function +support.detectDuplicates = !!hasDuplicate; + +// Initialize against the default document +setDocument(); + +// Support: Webkit<537.32 - Safari 6.0.3/Chrome 25 (fixed in Chrome 27) +// Detached nodes confoundingly follow *each other* +support.sortDetached = assert(function( div1 ) { + // Should return 1, but returns 4 (following) + return div1.compareDocumentPosition( document.createElement("div") ) & 1; +}); + +// Support: IE<8 +// Prevent attribute/property "interpolation" +// http://msdn.microsoft.com/en-us/library/ms536429%28VS.85%29.aspx +if ( !assert(function( div ) { + div.innerHTML = ""; + return div.firstChild.getAttribute("href") === "#" ; +}) ) { + addHandle( "type|href|height|width", function( elem, name, isXML ) { + if ( !isXML ) { + return elem.getAttribute( name, name.toLowerCase() === "type" ? 1 : 2 ); + } + }); +} + +// Support: IE<9 +// Use defaultValue in place of getAttribute("value") +if ( !support.attributes || !assert(function( div ) { + div.innerHTML = ""; + div.firstChild.setAttribute( "value", "" ); + return div.firstChild.getAttribute( "value" ) === ""; +}) ) { + addHandle( "value", function( elem, name, isXML ) { + if ( !isXML && elem.nodeName.toLowerCase() === "input" ) { + return elem.defaultValue; + } + }); +} + +// Support: IE<9 +// Use getAttributeNode to fetch booleans when getAttribute lies +if ( !assert(function( div ) { + return div.getAttribute("disabled") == null; +}) ) { + addHandle( booleans, function( elem, name, isXML ) { + var val; + if ( !isXML ) { + return elem[ name ] === true ? name.toLowerCase() : + (val = elem.getAttributeNode( name )) && val.specified ? + val.value : + null; + } + }); +} + +return Sizzle; + +})( window ); + + + +jQuery.find = Sizzle; +jQuery.expr = Sizzle.selectors; +jQuery.expr[ ":" ] = jQuery.expr.pseudos; +jQuery.uniqueSort = jQuery.unique = Sizzle.uniqueSort; +jQuery.text = Sizzle.getText; +jQuery.isXMLDoc = Sizzle.isXML; +jQuery.contains = Sizzle.contains; + + + +var dir = function( elem, dir, until ) { + var matched = [], + truncate = until !== undefined; + + while ( ( elem = elem[ dir ] ) && elem.nodeType !== 9 ) { + if ( elem.nodeType === 1 ) { + if ( truncate && jQuery( elem ).is( until ) ) { + break; + } + matched.push( elem ); + } + } + return matched; +}; + + +var siblings = function( n, elem ) { + var matched = []; + + for ( ; n; n = n.nextSibling ) { + if ( n.nodeType === 1 && n !== elem ) { + matched.push( n ); + } + } + + return matched; +}; + + +var rneedsContext = jQuery.expr.match.needsContext; + +var rsingleTag = ( /^<([\w-]+)\s*\/?>(?:<\/\1>|)$/ ); + + + +var risSimple = /^.[^:#\[\.,]*$/; + +// Implement the identical functionality for filter and not +function winnow( elements, qualifier, not ) { + if ( jQuery.isFunction( qualifier ) ) { + return jQuery.grep( elements, function( elem, i ) { + /* jshint -W018 */ + return !!qualifier.call( elem, i, elem ) !== not; + } ); + + } + + if ( qualifier.nodeType ) { + return jQuery.grep( elements, function( elem ) { + return ( elem === qualifier ) !== not; + } ); + + } + + if ( typeof qualifier === "string" ) { + if ( risSimple.test( qualifier ) ) { + return jQuery.filter( qualifier, elements, not ); + } + + qualifier = jQuery.filter( qualifier, elements ); + } + + return jQuery.grep( elements, function( elem ) { + return ( indexOf.call( qualifier, elem ) > -1 ) !== not; + } ); +} + +jQuery.filter = function( expr, elems, not ) { + var elem = elems[ 0 ]; + + if ( not ) { + expr = ":not(" + expr + ")"; + } + + return elems.length === 1 && elem.nodeType === 1 ? + jQuery.find.matchesSelector( elem, expr ) ? [ elem ] : [] : + jQuery.find.matches( expr, jQuery.grep( elems, function( elem ) { + return elem.nodeType === 1; + } ) ); +}; + +jQuery.fn.extend( { + find: function( selector ) { + var i, + len = this.length, + ret = [], + self = this; + + if ( typeof selector !== "string" ) { + return this.pushStack( jQuery( selector ).filter( function() { + for ( i = 0; i < len; i++ ) { + if ( jQuery.contains( self[ i ], this ) ) { + return true; + } + } + } ) ); + } + + for ( i = 0; i < len; i++ ) { + jQuery.find( selector, self[ i ], ret ); + } + + // Needed because $( selector, context ) becomes $( context ).find( selector ) + ret = this.pushStack( len > 1 ? jQuery.unique( ret ) : ret ); + ret.selector = this.selector ? this.selector + " " + selector : selector; + return ret; + }, + filter: function( selector ) { + return this.pushStack( winnow( this, selector || [], false ) ); + }, + not: function( selector ) { + return this.pushStack( winnow( this, selector || [], true ) ); + }, + is: function( selector ) { + return !!winnow( + this, + + // If this is a positional/relative selector, check membership in the returned set + // so $("p:first").is("p:last") won't return true for a doc with two "p". + typeof selector === "string" && rneedsContext.test( selector ) ? + jQuery( selector ) : + selector || [], + false + ).length; + } +} ); + + +// Initialize a jQuery object + + +// A central reference to the root jQuery(document) +var rootjQuery, + + // A simple way to check for HTML strings + // Prioritize #id over to avoid XSS via location.hash (#9521) + // Strict HTML recognition (#11290: must start with <) + rquickExpr = /^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/, + + init = jQuery.fn.init = function( selector, context, root ) { + var match, elem; + + // HANDLE: $(""), $(null), $(undefined), $(false) + if ( !selector ) { + return this; + } + + // Method init() accepts an alternate rootjQuery + // so migrate can support jQuery.sub (gh-2101) + root = root || rootjQuery; + + // Handle HTML strings + if ( typeof selector === "string" ) { + if ( selector[ 0 ] === "<" && + selector[ selector.length - 1 ] === ">" && + selector.length >= 3 ) { + + // Assume that strings that start and end with <> are HTML and skip the regex check + match = [ null, selector, null ]; + + } else { + match = rquickExpr.exec( selector ); + } + + // Match html or make sure no context is specified for #id + if ( match && ( match[ 1 ] || !context ) ) { + + // HANDLE: $(html) -> $(array) + if ( match[ 1 ] ) { + context = context instanceof jQuery ? context[ 0 ] : context; + + // Option to run scripts is true for back-compat + // Intentionally let the error be thrown if parseHTML is not present + jQuery.merge( this, jQuery.parseHTML( + match[ 1 ], + context && context.nodeType ? context.ownerDocument || context : document, + true + ) ); + + // HANDLE: $(html, props) + if ( rsingleTag.test( match[ 1 ] ) && jQuery.isPlainObject( context ) ) { + for ( match in context ) { + + // Properties of context are called as methods if possible + if ( jQuery.isFunction( this[ match ] ) ) { + this[ match ]( context[ match ] ); + + // ...and otherwise set as attributes + } else { + this.attr( match, context[ match ] ); + } + } + } + + return this; + + // HANDLE: $(#id) + } else { + elem = document.getElementById( match[ 2 ] ); + + // Support: Blackberry 4.6 + // gEBID returns nodes no longer in the document (#6963) + if ( elem && elem.parentNode ) { + + // Inject the element directly into the jQuery object + this.length = 1; + this[ 0 ] = elem; + } + + this.context = document; + this.selector = selector; + return this; + } + + // HANDLE: $(expr, $(...)) + } else if ( !context || context.jquery ) { + return ( context || root ).find( selector ); + + // HANDLE: $(expr, context) + // (which is just equivalent to: $(context).find(expr) + } else { + return this.constructor( context ).find( selector ); + } + + // HANDLE: $(DOMElement) + } else if ( selector.nodeType ) { + this.context = this[ 0 ] = selector; + this.length = 1; + return this; + + // HANDLE: $(function) + // Shortcut for document ready + } else if ( jQuery.isFunction( selector ) ) { + return root.ready !== undefined ? + root.ready( selector ) : + + // Execute immediately if ready is not present + selector( jQuery ); + } + + if ( selector.selector !== undefined ) { + this.selector = selector.selector; + this.context = selector.context; + } + + return jQuery.makeArray( selector, this ); + }; + +// Give the init function the jQuery prototype for later instantiation +init.prototype = jQuery.fn; + +// Initialize central reference +rootjQuery = jQuery( document ); + + +var rparentsprev = /^(?:parents|prev(?:Until|All))/, + + // Methods guaranteed to produce a unique set when starting from a unique set + guaranteedUnique = { + children: true, + contents: true, + next: true, + prev: true + }; + +jQuery.fn.extend( { + has: function( target ) { + var targets = jQuery( target, this ), + l = targets.length; + + return this.filter( function() { + var i = 0; + for ( ; i < l; i++ ) { + if ( jQuery.contains( this, targets[ i ] ) ) { + return true; + } + } + } ); + }, + + closest: function( selectors, context ) { + var cur, + i = 0, + l = this.length, + matched = [], + pos = rneedsContext.test( selectors ) || typeof selectors !== "string" ? + jQuery( selectors, context || this.context ) : + 0; + + for ( ; i < l; i++ ) { + for ( cur = this[ i ]; cur && cur !== context; cur = cur.parentNode ) { + + // Always skip document fragments + if ( cur.nodeType < 11 && ( pos ? + pos.index( cur ) > -1 : + + // Don't pass non-elements to Sizzle + cur.nodeType === 1 && + jQuery.find.matchesSelector( cur, selectors ) ) ) { + + matched.push( cur ); + break; + } + } + } + + return this.pushStack( matched.length > 1 ? jQuery.uniqueSort( matched ) : matched ); + }, + + // Determine the position of an element within the set + index: function( elem ) { + + // No argument, return index in parent + if ( !elem ) { + return ( this[ 0 ] && this[ 0 ].parentNode ) ? this.first().prevAll().length : -1; + } + + // Index in selector + if ( typeof elem === "string" ) { + return indexOf.call( jQuery( elem ), this[ 0 ] ); + } + + // Locate the position of the desired element + return indexOf.call( this, + + // If it receives a jQuery object, the first element is used + elem.jquery ? elem[ 0 ] : elem + ); + }, + + add: function( selector, context ) { + return this.pushStack( + jQuery.uniqueSort( + jQuery.merge( this.get(), jQuery( selector, context ) ) + ) + ); + }, + + addBack: function( selector ) { + return this.add( selector == null ? + this.prevObject : this.prevObject.filter( selector ) + ); + } +} ); + +function sibling( cur, dir ) { + while ( ( cur = cur[ dir ] ) && cur.nodeType !== 1 ) {} + return cur; +} + +jQuery.each( { + parent: function( elem ) { + var parent = elem.parentNode; + return parent && parent.nodeType !== 11 ? parent : null; + }, + parents: function( elem ) { + return dir( elem, "parentNode" ); + }, + parentsUntil: function( elem, i, until ) { + return dir( elem, "parentNode", until ); + }, + next: function( elem ) { + return sibling( elem, "nextSibling" ); + }, + prev: function( elem ) { + return sibling( elem, "previousSibling" ); + }, + nextAll: function( elem ) { + return dir( elem, "nextSibling" ); + }, + prevAll: function( elem ) { + return dir( elem, "previousSibling" ); + }, + nextUntil: function( elem, i, until ) { + return dir( elem, "nextSibling", until ); + }, + prevUntil: function( elem, i, until ) { + return dir( elem, "previousSibling", until ); + }, + siblings: function( elem ) { + return siblings( ( elem.parentNode || {} ).firstChild, elem ); + }, + children: function( elem ) { + return siblings( elem.firstChild ); + }, + contents: function( elem ) { + return elem.contentDocument || jQuery.merge( [], elem.childNodes ); + } +}, function( name, fn ) { + jQuery.fn[ name ] = function( until, selector ) { + var matched = jQuery.map( this, fn, until ); + + if ( name.slice( -5 ) !== "Until" ) { + selector = until; + } + + if ( selector && typeof selector === "string" ) { + matched = jQuery.filter( selector, matched ); + } + + if ( this.length > 1 ) { + + // Remove duplicates + if ( !guaranteedUnique[ name ] ) { + jQuery.uniqueSort( matched ); + } + + // Reverse order for parents* and prev-derivatives + if ( rparentsprev.test( name ) ) { + matched.reverse(); + } + } + + return this.pushStack( matched ); + }; +} ); +var rnotwhite = ( /\S+/g ); + + + +// Convert String-formatted options into Object-formatted ones +function createOptions( options ) { + var object = {}; + jQuery.each( options.match( rnotwhite ) || [], function( _, flag ) { + object[ flag ] = true; + } ); + return object; +} + +/* + * Create a callback list using the following parameters: + * + * options: an optional list of space-separated options that will change how + * the callback list behaves or a more traditional option object + * + * By default a callback list will act like an event callback list and can be + * "fired" multiple times. + * + * Possible options: + * + * once: will ensure the callback list can only be fired once (like a Deferred) + * + * memory: will keep track of previous values and will call any callback added + * after the list has been fired right away with the latest "memorized" + * values (like a Deferred) + * + * unique: will ensure a callback can only be added once (no duplicate in the list) + * + * stopOnFalse: interrupt callings when a callback returns false + * + */ +jQuery.Callbacks = function( options ) { + + // Convert options from String-formatted to Object-formatted if needed + // (we check in cache first) + options = typeof options === "string" ? + createOptions( options ) : + jQuery.extend( {}, options ); + + var // Flag to know if list is currently firing + firing, + + // Last fire value for non-forgettable lists + memory, + + // Flag to know if list was already fired + fired, + + // Flag to prevent firing + locked, + + // Actual callback list + list = [], + + // Queue of execution data for repeatable lists + queue = [], + + // Index of currently firing callback (modified by add/remove as needed) + firingIndex = -1, + + // Fire callbacks + fire = function() { + + // Enforce single-firing + locked = options.once; + + // Execute callbacks for all pending executions, + // respecting firingIndex overrides and runtime changes + fired = firing = true; + for ( ; queue.length; firingIndex = -1 ) { + memory = queue.shift(); + while ( ++firingIndex < list.length ) { + + // Run callback and check for early termination + if ( list[ firingIndex ].apply( memory[ 0 ], memory[ 1 ] ) === false && + options.stopOnFalse ) { + + // Jump to end and forget the data so .add doesn't re-fire + firingIndex = list.length; + memory = false; + } + } + } + + // Forget the data if we're done with it + if ( !options.memory ) { + memory = false; + } + + firing = false; + + // Clean up if we're done firing for good + if ( locked ) { + + // Keep an empty list if we have data for future add calls + if ( memory ) { + list = []; + + // Otherwise, this object is spent + } else { + list = ""; + } + } + }, + + // Actual Callbacks object + self = { + + // Add a callback or a collection of callbacks to the list + add: function() { + if ( list ) { + + // If we have memory from a past run, we should fire after adding + if ( memory && !firing ) { + firingIndex = list.length - 1; + queue.push( memory ); + } + + ( function add( args ) { + jQuery.each( args, function( _, arg ) { + if ( jQuery.isFunction( arg ) ) { + if ( !options.unique || !self.has( arg ) ) { + list.push( arg ); + } + } else if ( arg && arg.length && jQuery.type( arg ) !== "string" ) { + + // Inspect recursively + add( arg ); + } + } ); + } )( arguments ); + + if ( memory && !firing ) { + fire(); + } + } + return this; + }, + + // Remove a callback from the list + remove: function() { + jQuery.each( arguments, function( _, arg ) { + var index; + while ( ( index = jQuery.inArray( arg, list, index ) ) > -1 ) { + list.splice( index, 1 ); + + // Handle firing indexes + if ( index <= firingIndex ) { + firingIndex--; + } + } + } ); + return this; + }, + + // Check if a given callback is in the list. + // If no argument is given, return whether or not list has callbacks attached. + has: function( fn ) { + return fn ? + jQuery.inArray( fn, list ) > -1 : + list.length > 0; + }, + + // Remove all callbacks from the list + empty: function() { + if ( list ) { + list = []; + } + return this; + }, + + // Disable .fire and .add + // Abort any current/pending executions + // Clear all callbacks and values + disable: function() { + locked = queue = []; + list = memory = ""; + return this; + }, + disabled: function() { + return !list; + }, + + // Disable .fire + // Also disable .add unless we have memory (since it would have no effect) + // Abort any pending executions + lock: function() { + locked = queue = []; + if ( !memory ) { + list = memory = ""; + } + return this; + }, + locked: function() { + return !!locked; + }, + + // Call all callbacks with the given context and arguments + fireWith: function( context, args ) { + if ( !locked ) { + args = args || []; + args = [ context, args.slice ? args.slice() : args ]; + queue.push( args ); + if ( !firing ) { + fire(); + } + } + return this; + }, + + // Call all the callbacks with the given arguments + fire: function() { + self.fireWith( this, arguments ); + return this; + }, + + // To know if the callbacks have already been called at least once + fired: function() { + return !!fired; + } + }; + + return self; +}; + + +jQuery.extend( { + + Deferred: function( func ) { + var tuples = [ + + // action, add listener, listener list, final state + [ "resolve", "done", jQuery.Callbacks( "once memory" ), "resolved" ], + [ "reject", "fail", jQuery.Callbacks( "once memory" ), "rejected" ], + [ "notify", "progress", jQuery.Callbacks( "memory" ) ] + ], + state = "pending", + promise = { + state: function() { + return state; + }, + always: function() { + deferred.done( arguments ).fail( arguments ); + return this; + }, + then: function( /* fnDone, fnFail, fnProgress */ ) { + var fns = arguments; + return jQuery.Deferred( function( newDefer ) { + jQuery.each( tuples, function( i, tuple ) { + var fn = jQuery.isFunction( fns[ i ] ) && fns[ i ]; + + // deferred[ done | fail | progress ] for forwarding actions to newDefer + deferred[ tuple[ 1 ] ]( function() { + var returned = fn && fn.apply( this, arguments ); + if ( returned && jQuery.isFunction( returned.promise ) ) { + returned.promise() + .progress( newDefer.notify ) + .done( newDefer.resolve ) + .fail( newDefer.reject ); + } else { + newDefer[ tuple[ 0 ] + "With" ]( + this === promise ? newDefer.promise() : this, + fn ? [ returned ] : arguments + ); + } + } ); + } ); + fns = null; + } ).promise(); + }, + + // Get a promise for this deferred + // If obj is provided, the promise aspect is added to the object + promise: function( obj ) { + return obj != null ? jQuery.extend( obj, promise ) : promise; + } + }, + deferred = {}; + + // Keep pipe for back-compat + promise.pipe = promise.then; + + // Add list-specific methods + jQuery.each( tuples, function( i, tuple ) { + var list = tuple[ 2 ], + stateString = tuple[ 3 ]; + + // promise[ done | fail | progress ] = list.add + promise[ tuple[ 1 ] ] = list.add; + + // Handle state + if ( stateString ) { + list.add( function() { + + // state = [ resolved | rejected ] + state = stateString; + + // [ reject_list | resolve_list ].disable; progress_list.lock + }, tuples[ i ^ 1 ][ 2 ].disable, tuples[ 2 ][ 2 ].lock ); + } + + // deferred[ resolve | reject | notify ] + deferred[ tuple[ 0 ] ] = function() { + deferred[ tuple[ 0 ] + "With" ]( this === deferred ? promise : this, arguments ); + return this; + }; + deferred[ tuple[ 0 ] + "With" ] = list.fireWith; + } ); + + // Make the deferred a promise + promise.promise( deferred ); + + // Call given func if any + if ( func ) { + func.call( deferred, deferred ); + } + + // All done! + return deferred; + }, + + // Deferred helper + when: function( subordinate /* , ..., subordinateN */ ) { + var i = 0, + resolveValues = slice.call( arguments ), + length = resolveValues.length, + + // the count of uncompleted subordinates + remaining = length !== 1 || + ( subordinate && jQuery.isFunction( subordinate.promise ) ) ? length : 0, + + // the master Deferred. + // If resolveValues consist of only a single Deferred, just use that. + deferred = remaining === 1 ? subordinate : jQuery.Deferred(), + + // Update function for both resolve and progress values + updateFunc = function( i, contexts, values ) { + return function( value ) { + contexts[ i ] = this; + values[ i ] = arguments.length > 1 ? slice.call( arguments ) : value; + if ( values === progressValues ) { + deferred.notifyWith( contexts, values ); + } else if ( !( --remaining ) ) { + deferred.resolveWith( contexts, values ); + } + }; + }, + + progressValues, progressContexts, resolveContexts; + + // Add listeners to Deferred subordinates; treat others as resolved + if ( length > 1 ) { + progressValues = new Array( length ); + progressContexts = new Array( length ); + resolveContexts = new Array( length ); + for ( ; i < length; i++ ) { + if ( resolveValues[ i ] && jQuery.isFunction( resolveValues[ i ].promise ) ) { + resolveValues[ i ].promise() + .progress( updateFunc( i, progressContexts, progressValues ) ) + .done( updateFunc( i, resolveContexts, resolveValues ) ) + .fail( deferred.reject ); + } else { + --remaining; + } + } + } + + // If we're not waiting on anything, resolve the master + if ( !remaining ) { + deferred.resolveWith( resolveContexts, resolveValues ); + } + + return deferred.promise(); + } +} ); + + +// The deferred used on DOM ready +var readyList; + +jQuery.fn.ready = function( fn ) { + + // Add the callback + jQuery.ready.promise().done( fn ); + + return this; +}; + +jQuery.extend( { + + // Is the DOM ready to be used? Set to true once it occurs. + isReady: false, + + // A counter to track how many items to wait for before + // the ready event fires. See #6781 + readyWait: 1, + + // Hold (or release) the ready event + holdReady: function( hold ) { + if ( hold ) { + jQuery.readyWait++; + } else { + jQuery.ready( true ); + } + }, + + // Handle when the DOM is ready + ready: function( wait ) { + + // Abort if there are pending holds or we're already ready + if ( wait === true ? --jQuery.readyWait : jQuery.isReady ) { + return; + } + + // Remember that the DOM is ready + jQuery.isReady = true; + + // If a normal DOM Ready event fired, decrement, and wait if need be + if ( wait !== true && --jQuery.readyWait > 0 ) { + return; + } + + // If there are functions bound, to execute + readyList.resolveWith( document, [ jQuery ] ); + + // Trigger any bound ready events + if ( jQuery.fn.triggerHandler ) { + jQuery( document ).triggerHandler( "ready" ); + jQuery( document ).off( "ready" ); + } + } +} ); + +/** + * The ready event handler and self cleanup method + */ +function completed() { + document.removeEventListener( "DOMContentLoaded", completed ); + window.removeEventListener( "load", completed ); + jQuery.ready(); +} + +jQuery.ready.promise = function( obj ) { + if ( !readyList ) { + + readyList = jQuery.Deferred(); + + // Catch cases where $(document).ready() is called + // after the browser event has already occurred. + // Support: IE9-10 only + // Older IE sometimes signals "interactive" too soon + if ( document.readyState === "complete" || + ( document.readyState !== "loading" && !document.documentElement.doScroll ) ) { + + // Handle it asynchronously to allow scripts the opportunity to delay ready + window.setTimeout( jQuery.ready ); + + } else { + + // Use the handy event callback + document.addEventListener( "DOMContentLoaded", completed ); + + // A fallback to window.onload, that will always work + window.addEventListener( "load", completed ); + } + } + return readyList.promise( obj ); +}; + +// Kick off the DOM ready check even if the user does not +jQuery.ready.promise(); + + + + +// Multifunctional method to get and set values of a collection +// The value/s can optionally be executed if it's a function +var access = function( elems, fn, key, value, chainable, emptyGet, raw ) { + var i = 0, + len = elems.length, + bulk = key == null; + + // Sets many values + if ( jQuery.type( key ) === "object" ) { + chainable = true; + for ( i in key ) { + access( elems, fn, i, key[ i ], true, emptyGet, raw ); + } + + // Sets one value + } else if ( value !== undefined ) { + chainable = true; + + if ( !jQuery.isFunction( value ) ) { + raw = true; + } + + if ( bulk ) { + + // Bulk operations run against the entire set + if ( raw ) { + fn.call( elems, value ); + fn = null; + + // ...except when executing function values + } else { + bulk = fn; + fn = function( elem, key, value ) { + return bulk.call( jQuery( elem ), value ); + }; + } + } + + if ( fn ) { + for ( ; i < len; i++ ) { + fn( + elems[ i ], key, raw ? + value : + value.call( elems[ i ], i, fn( elems[ i ], key ) ) + ); + } + } + } + + return chainable ? + elems : + + // Gets + bulk ? + fn.call( elems ) : + len ? fn( elems[ 0 ], key ) : emptyGet; +}; +var acceptData = function( owner ) { + + // Accepts only: + // - Node + // - Node.ELEMENT_NODE + // - Node.DOCUMENT_NODE + // - Object + // - Any + /* jshint -W018 */ + return owner.nodeType === 1 || owner.nodeType === 9 || !( +owner.nodeType ); +}; + + + + +function Data() { + this.expando = jQuery.expando + Data.uid++; +} + +Data.uid = 1; + +Data.prototype = { + + register: function( owner, initial ) { + var value = initial || {}; + + // If it is a node unlikely to be stringify-ed or looped over + // use plain assignment + if ( owner.nodeType ) { + owner[ this.expando ] = value; + + // Otherwise secure it in a non-enumerable, non-writable property + // configurability must be true to allow the property to be + // deleted with the delete operator + } else { + Object.defineProperty( owner, this.expando, { + value: value, + writable: true, + configurable: true + } ); + } + return owner[ this.expando ]; + }, + cache: function( owner ) { + + // We can accept data for non-element nodes in modern browsers, + // but we should not, see #8335. + // Always return an empty object. + if ( !acceptData( owner ) ) { + return {}; + } + + // Check if the owner object already has a cache + var value = owner[ this.expando ]; + + // If not, create one + if ( !value ) { + value = {}; + + // We can accept data for non-element nodes in modern browsers, + // but we should not, see #8335. + // Always return an empty object. + if ( acceptData( owner ) ) { + + // If it is a node unlikely to be stringify-ed or looped over + // use plain assignment + if ( owner.nodeType ) { + owner[ this.expando ] = value; + + // Otherwise secure it in a non-enumerable property + // configurable must be true to allow the property to be + // deleted when data is removed + } else { + Object.defineProperty( owner, this.expando, { + value: value, + configurable: true + } ); + } + } + } + + return value; + }, + set: function( owner, data, value ) { + var prop, + cache = this.cache( owner ); + + // Handle: [ owner, key, value ] args + if ( typeof data === "string" ) { + cache[ data ] = value; + + // Handle: [ owner, { properties } ] args + } else { + + // Copy the properties one-by-one to the cache object + for ( prop in data ) { + cache[ prop ] = data[ prop ]; + } + } + return cache; + }, + get: function( owner, key ) { + return key === undefined ? + this.cache( owner ) : + owner[ this.expando ] && owner[ this.expando ][ key ]; + }, + access: function( owner, key, value ) { + var stored; + + // In cases where either: + // + // 1. No key was specified + // 2. A string key was specified, but no value provided + // + // Take the "read" path and allow the get method to determine + // which value to return, respectively either: + // + // 1. The entire cache object + // 2. The data stored at the key + // + if ( key === undefined || + ( ( key && typeof key === "string" ) && value === undefined ) ) { + + stored = this.get( owner, key ); + + return stored !== undefined ? + stored : this.get( owner, jQuery.camelCase( key ) ); + } + + // When the key is not a string, or both a key and value + // are specified, set or extend (existing objects) with either: + // + // 1. An object of properties + // 2. A key and value + // + this.set( owner, key, value ); + + // Since the "set" path can have two possible entry points + // return the expected data based on which path was taken[*] + return value !== undefined ? value : key; + }, + remove: function( owner, key ) { + var i, name, camel, + cache = owner[ this.expando ]; + + if ( cache === undefined ) { + return; + } + + if ( key === undefined ) { + this.register( owner ); + + } else { + + // Support array or space separated string of keys + if ( jQuery.isArray( key ) ) { + + // If "name" is an array of keys... + // When data is initially created, via ("key", "val") signature, + // keys will be converted to camelCase. + // Since there is no way to tell _how_ a key was added, remove + // both plain key and camelCase key. #12786 + // This will only penalize the array argument path. + name = key.concat( key.map( jQuery.camelCase ) ); + } else { + camel = jQuery.camelCase( key ); + + // Try the string as a key before any manipulation + if ( key in cache ) { + name = [ key, camel ]; + } else { + + // If a key with the spaces exists, use it. + // Otherwise, create an array by matching non-whitespace + name = camel; + name = name in cache ? + [ name ] : ( name.match( rnotwhite ) || [] ); + } + } + + i = name.length; + + while ( i-- ) { + delete cache[ name[ i ] ]; + } + } + + // Remove the expando if there's no more data + if ( key === undefined || jQuery.isEmptyObject( cache ) ) { + + // Support: Chrome <= 35-45+ + // Webkit & Blink performance suffers when deleting properties + // from DOM nodes, so set to undefined instead + // https://code.google.com/p/chromium/issues/detail?id=378607 + if ( owner.nodeType ) { + owner[ this.expando ] = undefined; + } else { + delete owner[ this.expando ]; + } + } + }, + hasData: function( owner ) { + var cache = owner[ this.expando ]; + return cache !== undefined && !jQuery.isEmptyObject( cache ); + } +}; +var dataPriv = new Data(); + +var dataUser = new Data(); + + + +// Implementation Summary +// +// 1. Enforce API surface and semantic compatibility with 1.9.x branch +// 2. Improve the module's maintainability by reducing the storage +// paths to a single mechanism. +// 3. Use the same single mechanism to support "private" and "user" data. +// 4. _Never_ expose "private" data to user code (TODO: Drop _data, _removeData) +// 5. Avoid exposing implementation details on user objects (eg. expando properties) +// 6. Provide a clear path for implementation upgrade to WeakMap in 2014 + +var rbrace = /^(?:\{[\w\W]*\}|\[[\w\W]*\])$/, + rmultiDash = /[A-Z]/g; + +function dataAttr( elem, key, data ) { + var name; + + // If nothing was found internally, try to fetch any + // data from the HTML5 data-* attribute + if ( data === undefined && elem.nodeType === 1 ) { + name = "data-" + key.replace( rmultiDash, "-$&" ).toLowerCase(); + data = elem.getAttribute( name ); + + if ( typeof data === "string" ) { + try { + data = data === "true" ? true : + data === "false" ? false : + data === "null" ? null : + + // Only convert to a number if it doesn't change the string + +data + "" === data ? +data : + rbrace.test( data ) ? jQuery.parseJSON( data ) : + data; + } catch ( e ) {} + + // Make sure we set the data so it isn't changed later + dataUser.set( elem, key, data ); + } else { + data = undefined; + } + } + return data; +} + +jQuery.extend( { + hasData: function( elem ) { + return dataUser.hasData( elem ) || dataPriv.hasData( elem ); + }, + + data: function( elem, name, data ) { + return dataUser.access( elem, name, data ); + }, + + removeData: function( elem, name ) { + dataUser.remove( elem, name ); + }, + + // TODO: Now that all calls to _data and _removeData have been replaced + // with direct calls to dataPriv methods, these can be deprecated. + _data: function( elem, name, data ) { + return dataPriv.access( elem, name, data ); + }, + + _removeData: function( elem, name ) { + dataPriv.remove( elem, name ); + } +} ); + +jQuery.fn.extend( { + data: function( key, value ) { + var i, name, data, + elem = this[ 0 ], + attrs = elem && elem.attributes; + + // Gets all values + if ( key === undefined ) { + if ( this.length ) { + data = dataUser.get( elem ); + + if ( elem.nodeType === 1 && !dataPriv.get( elem, "hasDataAttrs" ) ) { + i = attrs.length; + while ( i-- ) { + + // Support: IE11+ + // The attrs elements can be null (#14894) + if ( attrs[ i ] ) { + name = attrs[ i ].name; + if ( name.indexOf( "data-" ) === 0 ) { + name = jQuery.camelCase( name.slice( 5 ) ); + dataAttr( elem, name, data[ name ] ); + } + } + } + dataPriv.set( elem, "hasDataAttrs", true ); + } + } + + return data; + } + + // Sets multiple values + if ( typeof key === "object" ) { + return this.each( function() { + dataUser.set( this, key ); + } ); + } + + return access( this, function( value ) { + var data, camelKey; + + // The calling jQuery object (element matches) is not empty + // (and therefore has an element appears at this[ 0 ]) and the + // `value` parameter was not undefined. An empty jQuery object + // will result in `undefined` for elem = this[ 0 ] which will + // throw an exception if an attempt to read a data cache is made. + if ( elem && value === undefined ) { + + // Attempt to get data from the cache + // with the key as-is + data = dataUser.get( elem, key ) || + + // Try to find dashed key if it exists (gh-2779) + // This is for 2.2.x only + dataUser.get( elem, key.replace( rmultiDash, "-$&" ).toLowerCase() ); + + if ( data !== undefined ) { + return data; + } + + camelKey = jQuery.camelCase( key ); + + // Attempt to get data from the cache + // with the key camelized + data = dataUser.get( elem, camelKey ); + if ( data !== undefined ) { + return data; + } + + // Attempt to "discover" the data in + // HTML5 custom data-* attrs + data = dataAttr( elem, camelKey, undefined ); + if ( data !== undefined ) { + return data; + } + + // We tried really hard, but the data doesn't exist. + return; + } + + // Set the data... + camelKey = jQuery.camelCase( key ); + this.each( function() { + + // First, attempt to store a copy or reference of any + // data that might've been store with a camelCased key. + var data = dataUser.get( this, camelKey ); + + // For HTML5 data-* attribute interop, we have to + // store property names with dashes in a camelCase form. + // This might not apply to all properties...* + dataUser.set( this, camelKey, value ); + + // *... In the case of properties that might _actually_ + // have dashes, we need to also store a copy of that + // unchanged property. + if ( key.indexOf( "-" ) > -1 && data !== undefined ) { + dataUser.set( this, key, value ); + } + } ); + }, null, value, arguments.length > 1, null, true ); + }, + + removeData: function( key ) { + return this.each( function() { + dataUser.remove( this, key ); + } ); + } +} ); + + +jQuery.extend( { + queue: function( elem, type, data ) { + var queue; + + if ( elem ) { + type = ( type || "fx" ) + "queue"; + queue = dataPriv.get( elem, type ); + + // Speed up dequeue by getting out quickly if this is just a lookup + if ( data ) { + if ( !queue || jQuery.isArray( data ) ) { + queue = dataPriv.access( elem, type, jQuery.makeArray( data ) ); + } else { + queue.push( data ); + } + } + return queue || []; + } + }, + + dequeue: function( elem, type ) { + type = type || "fx"; + + var queue = jQuery.queue( elem, type ), + startLength = queue.length, + fn = queue.shift(), + hooks = jQuery._queueHooks( elem, type ), + next = function() { + jQuery.dequeue( elem, type ); + }; + + // If the fx queue is dequeued, always remove the progress sentinel + if ( fn === "inprogress" ) { + fn = queue.shift(); + startLength--; + } + + if ( fn ) { + + // Add a progress sentinel to prevent the fx queue from being + // automatically dequeued + if ( type === "fx" ) { + queue.unshift( "inprogress" ); + } + + // Clear up the last queue stop function + delete hooks.stop; + fn.call( elem, next, hooks ); + } + + if ( !startLength && hooks ) { + hooks.empty.fire(); + } + }, + + // Not public - generate a queueHooks object, or return the current one + _queueHooks: function( elem, type ) { + var key = type + "queueHooks"; + return dataPriv.get( elem, key ) || dataPriv.access( elem, key, { + empty: jQuery.Callbacks( "once memory" ).add( function() { + dataPriv.remove( elem, [ type + "queue", key ] ); + } ) + } ); + } +} ); + +jQuery.fn.extend( { + queue: function( type, data ) { + var setter = 2; + + if ( typeof type !== "string" ) { + data = type; + type = "fx"; + setter--; + } + + if ( arguments.length < setter ) { + return jQuery.queue( this[ 0 ], type ); + } + + return data === undefined ? + this : + this.each( function() { + var queue = jQuery.queue( this, type, data ); + + // Ensure a hooks for this queue + jQuery._queueHooks( this, type ); + + if ( type === "fx" && queue[ 0 ] !== "inprogress" ) { + jQuery.dequeue( this, type ); + } + } ); + }, + dequeue: function( type ) { + return this.each( function() { + jQuery.dequeue( this, type ); + } ); + }, + clearQueue: function( type ) { + return this.queue( type || "fx", [] ); + }, + + // Get a promise resolved when queues of a certain type + // are emptied (fx is the type by default) + promise: function( type, obj ) { + var tmp, + count = 1, + defer = jQuery.Deferred(), + elements = this, + i = this.length, + resolve = function() { + if ( !( --count ) ) { + defer.resolveWith( elements, [ elements ] ); + } + }; + + if ( typeof type !== "string" ) { + obj = type; + type = undefined; + } + type = type || "fx"; + + while ( i-- ) { + tmp = dataPriv.get( elements[ i ], type + "queueHooks" ); + if ( tmp && tmp.empty ) { + count++; + tmp.empty.add( resolve ); + } + } + resolve(); + return defer.promise( obj ); + } +} ); +var pnum = ( /[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/ ).source; + +var rcssNum = new RegExp( "^(?:([+-])=|)(" + pnum + ")([a-z%]*)$", "i" ); + + +var cssExpand = [ "Top", "Right", "Bottom", "Left" ]; + +var isHidden = function( elem, el ) { + + // isHidden might be called from jQuery#filter function; + // in that case, element will be second argument + elem = el || elem; + return jQuery.css( elem, "display" ) === "none" || + !jQuery.contains( elem.ownerDocument, elem ); + }; + + + +function adjustCSS( elem, prop, valueParts, tween ) { + var adjusted, + scale = 1, + maxIterations = 20, + currentValue = tween ? + function() { return tween.cur(); } : + function() { return jQuery.css( elem, prop, "" ); }, + initial = currentValue(), + unit = valueParts && valueParts[ 3 ] || ( jQuery.cssNumber[ prop ] ? "" : "px" ), + + // Starting value computation is required for potential unit mismatches + initialInUnit = ( jQuery.cssNumber[ prop ] || unit !== "px" && +initial ) && + rcssNum.exec( jQuery.css( elem, prop ) ); + + if ( initialInUnit && initialInUnit[ 3 ] !== unit ) { + + // Trust units reported by jQuery.css + unit = unit || initialInUnit[ 3 ]; + + // Make sure we update the tween properties later on + valueParts = valueParts || []; + + // Iteratively approximate from a nonzero starting point + initialInUnit = +initial || 1; + + do { + + // If previous iteration zeroed out, double until we get *something*. + // Use string for doubling so we don't accidentally see scale as unchanged below + scale = scale || ".5"; + + // Adjust and apply + initialInUnit = initialInUnit / scale; + jQuery.style( elem, prop, initialInUnit + unit ); + + // Update scale, tolerating zero or NaN from tween.cur() + // Break the loop if scale is unchanged or perfect, or if we've just had enough. + } while ( + scale !== ( scale = currentValue() / initial ) && scale !== 1 && --maxIterations + ); + } + + if ( valueParts ) { + initialInUnit = +initialInUnit || +initial || 0; + + // Apply relative offset (+=/-=) if specified + adjusted = valueParts[ 1 ] ? + initialInUnit + ( valueParts[ 1 ] + 1 ) * valueParts[ 2 ] : + +valueParts[ 2 ]; + if ( tween ) { + tween.unit = unit; + tween.start = initialInUnit; + tween.end = adjusted; + } + } + return adjusted; +} +var rcheckableType = ( /^(?:checkbox|radio)$/i ); + +var rtagName = ( /<([\w:-]+)/ ); + +var rscriptType = ( /^$|\/(?:java|ecma)script/i ); + + + +// We have to close these tags to support XHTML (#13200) +var wrapMap = { + + // Support: IE9 + option: [ 1, "", "" ], + + // XHTML parsers do not magically insert elements in the + // same way that tag soup parsers do. So we cannot shorten + // this by omitting or other required elements. + thead: [ 1, "", "" ], + col: [ 2, "", "" ], + tr: [ 2, "", "" ], + td: [ 3, "", "" ], + + _default: [ 0, "", "" ] +}; + +// Support: IE9 +wrapMap.optgroup = wrapMap.option; + +wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead; +wrapMap.th = wrapMap.td; + + +function getAll( context, tag ) { + + // Support: IE9-11+ + // Use typeof to avoid zero-argument method invocation on host objects (#15151) + var ret = typeof context.getElementsByTagName !== "undefined" ? + context.getElementsByTagName( tag || "*" ) : + typeof context.querySelectorAll !== "undefined" ? + context.querySelectorAll( tag || "*" ) : + []; + + return tag === undefined || tag && jQuery.nodeName( context, tag ) ? + jQuery.merge( [ context ], ret ) : + ret; +} + + +// Mark scripts as having already been evaluated +function setGlobalEval( elems, refElements ) { + var i = 0, + l = elems.length; + + for ( ; i < l; i++ ) { + dataPriv.set( + elems[ i ], + "globalEval", + !refElements || dataPriv.get( refElements[ i ], "globalEval" ) + ); + } +} + + +var rhtml = /<|?\w+;/; + +function buildFragment( elems, context, scripts, selection, ignored ) { + var elem, tmp, tag, wrap, contains, j, + fragment = context.createDocumentFragment(), + nodes = [], + i = 0, + l = elems.length; + + for ( ; i < l; i++ ) { + elem = elems[ i ]; + + if ( elem || elem === 0 ) { + + // Add nodes directly + if ( jQuery.type( elem ) === "object" ) { + + // Support: Android<4.1, PhantomJS<2 + // push.apply(_, arraylike) throws on ancient WebKit + jQuery.merge( nodes, elem.nodeType ? [ elem ] : elem ); + + // Convert non-html into a text node + } else if ( !rhtml.test( elem ) ) { + nodes.push( context.createTextNode( elem ) ); + + // Convert html into DOM nodes + } else { + tmp = tmp || fragment.appendChild( context.createElement( "div" ) ); + + // Deserialize a standard representation + tag = ( rtagName.exec( elem ) || [ "", "" ] )[ 1 ].toLowerCase(); + wrap = wrapMap[ tag ] || wrapMap._default; + tmp.innerHTML = wrap[ 1 ] + jQuery.htmlPrefilter( elem ) + wrap[ 2 ]; + + // Descend through wrappers to the right content + j = wrap[ 0 ]; + while ( j-- ) { + tmp = tmp.lastChild; + } + + // Support: Android<4.1, PhantomJS<2 + // push.apply(_, arraylike) throws on ancient WebKit + jQuery.merge( nodes, tmp.childNodes ); + + // Remember the top-level container + tmp = fragment.firstChild; + + // Ensure the created nodes are orphaned (#12392) + tmp.textContent = ""; + } + } + } + + // Remove wrapper from fragment + fragment.textContent = ""; + + i = 0; + while ( ( elem = nodes[ i++ ] ) ) { + + // Skip elements already in the context collection (trac-4087) + if ( selection && jQuery.inArray( elem, selection ) > -1 ) { + if ( ignored ) { + ignored.push( elem ); + } + continue; + } + + contains = jQuery.contains( elem.ownerDocument, elem ); + + // Append to fragment + tmp = getAll( fragment.appendChild( elem ), "script" ); + + // Preserve script evaluation history + if ( contains ) { + setGlobalEval( tmp ); + } + + // Capture executables + if ( scripts ) { + j = 0; + while ( ( elem = tmp[ j++ ] ) ) { + if ( rscriptType.test( elem.type || "" ) ) { + scripts.push( elem ); + } + } + } + } + + return fragment; +} + + +( function() { + var fragment = document.createDocumentFragment(), + div = fragment.appendChild( document.createElement( "div" ) ), + input = document.createElement( "input" ); + + // Support: Android 4.0-4.3, Safari<=5.1 + // Check state lost if the name is set (#11217) + // Support: Windows Web Apps (WWA) + // `name` and `type` must use .setAttribute for WWA (#14901) + input.setAttribute( "type", "radio" ); + input.setAttribute( "checked", "checked" ); + input.setAttribute( "name", "t" ); + + div.appendChild( input ); + + // Support: Safari<=5.1, Android<4.2 + // Older WebKit doesn't clone checked state correctly in fragments + support.checkClone = div.cloneNode( true ).cloneNode( true ).lastChild.checked; + + // Support: IE<=11+ + // Make sure textarea (and checkbox) defaultValue is properly cloned + div.innerHTML = "x"; + support.noCloneChecked = !!div.cloneNode( true ).lastChild.defaultValue; +} )(); + + +var + rkeyEvent = /^key/, + rmouseEvent = /^(?:mouse|pointer|contextmenu|drag|drop)|click/, + rtypenamespace = /^([^.]*)(?:\.(.+)|)/; + +function returnTrue() { + return true; +} + +function returnFalse() { + return false; +} + +// Support: IE9 +// See #13393 for more info +function safeActiveElement() { + try { + return document.activeElement; + } catch ( err ) { } +} + +function on( elem, types, selector, data, fn, one ) { + var origFn, type; + + // Types can be a map of types/handlers + if ( typeof types === "object" ) { + + // ( types-Object, selector, data ) + if ( typeof selector !== "string" ) { + + // ( types-Object, data ) + data = data || selector; + selector = undefined; + } + for ( type in types ) { + on( elem, type, selector, data, types[ type ], one ); + } + return elem; + } + + if ( data == null && fn == null ) { + + // ( types, fn ) + fn = selector; + data = selector = undefined; + } else if ( fn == null ) { + if ( typeof selector === "string" ) { + + // ( types, selector, fn ) + fn = data; + data = undefined; + } else { + + // ( types, data, fn ) + fn = data; + data = selector; + selector = undefined; + } + } + if ( fn === false ) { + fn = returnFalse; + } else if ( !fn ) { + return elem; + } + + if ( one === 1 ) { + origFn = fn; + fn = function( event ) { + + // Can use an empty set, since event contains the info + jQuery().off( event ); + return origFn.apply( this, arguments ); + }; + + // Use same guid so caller can remove using origFn + fn.guid = origFn.guid || ( origFn.guid = jQuery.guid++ ); + } + return elem.each( function() { + jQuery.event.add( this, types, fn, data, selector ); + } ); +} + +/* + * Helper functions for managing events -- not part of the public interface. + * Props to Dean Edwards' addEvent library for many of the ideas. + */ +jQuery.event = { + + global: {}, + + add: function( elem, types, handler, data, selector ) { + + var handleObjIn, eventHandle, tmp, + events, t, handleObj, + special, handlers, type, namespaces, origType, + elemData = dataPriv.get( elem ); + + // Don't attach events to noData or text/comment nodes (but allow plain objects) + if ( !elemData ) { + return; + } + + // Caller can pass in an object of custom data in lieu of the handler + if ( handler.handler ) { + handleObjIn = handler; + handler = handleObjIn.handler; + selector = handleObjIn.selector; + } + + // Make sure that the handler has a unique ID, used to find/remove it later + if ( !handler.guid ) { + handler.guid = jQuery.guid++; + } + + // Init the element's event structure and main handler, if this is the first + if ( !( events = elemData.events ) ) { + events = elemData.events = {}; + } + if ( !( eventHandle = elemData.handle ) ) { + eventHandle = elemData.handle = function( e ) { + + // Discard the second event of a jQuery.event.trigger() and + // when an event is called after a page has unloaded + return typeof jQuery !== "undefined" && jQuery.event.triggered !== e.type ? + jQuery.event.dispatch.apply( elem, arguments ) : undefined; + }; + } + + // Handle multiple events separated by a space + types = ( types || "" ).match( rnotwhite ) || [ "" ]; + t = types.length; + while ( t-- ) { + tmp = rtypenamespace.exec( types[ t ] ) || []; + type = origType = tmp[ 1 ]; + namespaces = ( tmp[ 2 ] || "" ).split( "." ).sort(); + + // There *must* be a type, no attaching namespace-only handlers + if ( !type ) { + continue; + } + + // If event changes its type, use the special event handlers for the changed type + special = jQuery.event.special[ type ] || {}; + + // If selector defined, determine special event api type, otherwise given type + type = ( selector ? special.delegateType : special.bindType ) || type; + + // Update special based on newly reset type + special = jQuery.event.special[ type ] || {}; + + // handleObj is passed to all event handlers + handleObj = jQuery.extend( { + type: type, + origType: origType, + data: data, + handler: handler, + guid: handler.guid, + selector: selector, + needsContext: selector && jQuery.expr.match.needsContext.test( selector ), + namespace: namespaces.join( "." ) + }, handleObjIn ); + + // Init the event handler queue if we're the first + if ( !( handlers = events[ type ] ) ) { + handlers = events[ type ] = []; + handlers.delegateCount = 0; + + // Only use addEventListener if the special events handler returns false + if ( !special.setup || + special.setup.call( elem, data, namespaces, eventHandle ) === false ) { + + if ( elem.addEventListener ) { + elem.addEventListener( type, eventHandle ); + } + } + } + + if ( special.add ) { + special.add.call( elem, handleObj ); + + if ( !handleObj.handler.guid ) { + handleObj.handler.guid = handler.guid; + } + } + + // Add to the element's handler list, delegates in front + if ( selector ) { + handlers.splice( handlers.delegateCount++, 0, handleObj ); + } else { + handlers.push( handleObj ); + } + + // Keep track of which events have ever been used, for event optimization + jQuery.event.global[ type ] = true; + } + + }, + + // Detach an event or set of events from an element + remove: function( elem, types, handler, selector, mappedTypes ) { + + var j, origCount, tmp, + events, t, handleObj, + special, handlers, type, namespaces, origType, + elemData = dataPriv.hasData( elem ) && dataPriv.get( elem ); + + if ( !elemData || !( events = elemData.events ) ) { + return; + } + + // Once for each type.namespace in types; type may be omitted + types = ( types || "" ).match( rnotwhite ) || [ "" ]; + t = types.length; + while ( t-- ) { + tmp = rtypenamespace.exec( types[ t ] ) || []; + type = origType = tmp[ 1 ]; + namespaces = ( tmp[ 2 ] || "" ).split( "." ).sort(); + + // Unbind all events (on this namespace, if provided) for the element + if ( !type ) { + for ( type in events ) { + jQuery.event.remove( elem, type + types[ t ], handler, selector, true ); + } + continue; + } + + special = jQuery.event.special[ type ] || {}; + type = ( selector ? special.delegateType : special.bindType ) || type; + handlers = events[ type ] || []; + tmp = tmp[ 2 ] && + new RegExp( "(^|\\.)" + namespaces.join( "\\.(?:.*\\.|)" ) + "(\\.|$)" ); + + // Remove matching events + origCount = j = handlers.length; + while ( j-- ) { + handleObj = handlers[ j ]; + + if ( ( mappedTypes || origType === handleObj.origType ) && + ( !handler || handler.guid === handleObj.guid ) && + ( !tmp || tmp.test( handleObj.namespace ) ) && + ( !selector || selector === handleObj.selector || + selector === "**" && handleObj.selector ) ) { + handlers.splice( j, 1 ); + + if ( handleObj.selector ) { + handlers.delegateCount--; + } + if ( special.remove ) { + special.remove.call( elem, handleObj ); + } + } + } + + // Remove generic event handler if we removed something and no more handlers exist + // (avoids potential for endless recursion during removal of special event handlers) + if ( origCount && !handlers.length ) { + if ( !special.teardown || + special.teardown.call( elem, namespaces, elemData.handle ) === false ) { + + jQuery.removeEvent( elem, type, elemData.handle ); + } + + delete events[ type ]; + } + } + + // Remove data and the expando if it's no longer used + if ( jQuery.isEmptyObject( events ) ) { + dataPriv.remove( elem, "handle events" ); + } + }, + + dispatch: function( event ) { + + // Make a writable jQuery.Event from the native event object + event = jQuery.event.fix( event ); + + var i, j, ret, matched, handleObj, + handlerQueue = [], + args = slice.call( arguments ), + handlers = ( dataPriv.get( this, "events" ) || {} )[ event.type ] || [], + special = jQuery.event.special[ event.type ] || {}; + + // Use the fix-ed jQuery.Event rather than the (read-only) native event + args[ 0 ] = event; + event.delegateTarget = this; + + // Call the preDispatch hook for the mapped type, and let it bail if desired + if ( special.preDispatch && special.preDispatch.call( this, event ) === false ) { + return; + } + + // Determine handlers + handlerQueue = jQuery.event.handlers.call( this, event, handlers ); + + // Run delegates first; they may want to stop propagation beneath us + i = 0; + while ( ( matched = handlerQueue[ i++ ] ) && !event.isPropagationStopped() ) { + event.currentTarget = matched.elem; + + j = 0; + while ( ( handleObj = matched.handlers[ j++ ] ) && + !event.isImmediatePropagationStopped() ) { + + // Triggered event must either 1) have no namespace, or 2) have namespace(s) + // a subset or equal to those in the bound event (both can have no namespace). + if ( !event.rnamespace || event.rnamespace.test( handleObj.namespace ) ) { + + event.handleObj = handleObj; + event.data = handleObj.data; + + ret = ( ( jQuery.event.special[ handleObj.origType ] || {} ).handle || + handleObj.handler ).apply( matched.elem, args ); + + if ( ret !== undefined ) { + if ( ( event.result = ret ) === false ) { + event.preventDefault(); + event.stopPropagation(); + } + } + } + } + } + + // Call the postDispatch hook for the mapped type + if ( special.postDispatch ) { + special.postDispatch.call( this, event ); + } + + return event.result; + }, + + handlers: function( event, handlers ) { + var i, matches, sel, handleObj, + handlerQueue = [], + delegateCount = handlers.delegateCount, + cur = event.target; + + // Support (at least): Chrome, IE9 + // Find delegate handlers + // Black-hole SVG instance trees (#13180) + // + // Support: Firefox<=42+ + // Avoid non-left-click in FF but don't block IE radio events (#3861, gh-2343) + if ( delegateCount && cur.nodeType && + ( event.type !== "click" || isNaN( event.button ) || event.button < 1 ) ) { + + for ( ; cur !== this; cur = cur.parentNode || this ) { + + // Don't check non-elements (#13208) + // Don't process clicks on disabled elements (#6911, #8165, #11382, #11764) + if ( cur.nodeType === 1 && ( cur.disabled !== true || event.type !== "click" ) ) { + matches = []; + for ( i = 0; i < delegateCount; i++ ) { + handleObj = handlers[ i ]; + + // Don't conflict with Object.prototype properties (#13203) + sel = handleObj.selector + " "; + + if ( matches[ sel ] === undefined ) { + matches[ sel ] = handleObj.needsContext ? + jQuery( sel, this ).index( cur ) > -1 : + jQuery.find( sel, this, null, [ cur ] ).length; + } + if ( matches[ sel ] ) { + matches.push( handleObj ); + } + } + if ( matches.length ) { + handlerQueue.push( { elem: cur, handlers: matches } ); + } + } + } + } + + // Add the remaining (directly-bound) handlers + if ( delegateCount < handlers.length ) { + handlerQueue.push( { elem: this, handlers: handlers.slice( delegateCount ) } ); + } + + return handlerQueue; + }, + + // Includes some event props shared by KeyEvent and MouseEvent + props: ( "altKey bubbles cancelable ctrlKey currentTarget detail eventPhase " + + "metaKey relatedTarget shiftKey target timeStamp view which" ).split( " " ), + + fixHooks: {}, + + keyHooks: { + props: "char charCode key keyCode".split( " " ), + filter: function( event, original ) { + + // Add which for key events + if ( event.which == null ) { + event.which = original.charCode != null ? original.charCode : original.keyCode; + } + + return event; + } + }, + + mouseHooks: { + props: ( "button buttons clientX clientY offsetX offsetY pageX pageY " + + "screenX screenY toElement" ).split( " " ), + filter: function( event, original ) { + var eventDoc, doc, body, + button = original.button; + + // Calculate pageX/Y if missing and clientX/Y available + if ( event.pageX == null && original.clientX != null ) { + eventDoc = event.target.ownerDocument || document; + doc = eventDoc.documentElement; + body = eventDoc.body; + + event.pageX = original.clientX + + ( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) - + ( doc && doc.clientLeft || body && body.clientLeft || 0 ); + event.pageY = original.clientY + + ( doc && doc.scrollTop || body && body.scrollTop || 0 ) - + ( doc && doc.clientTop || body && body.clientTop || 0 ); + } + + // Add which for click: 1 === left; 2 === middle; 3 === right + // Note: button is not normalized, so don't use it + if ( !event.which && button !== undefined ) { + event.which = ( button & 1 ? 1 : ( button & 2 ? 3 : ( button & 4 ? 2 : 0 ) ) ); + } + + return event; + } + }, + + fix: function( event ) { + if ( event[ jQuery.expando ] ) { + return event; + } + + // Create a writable copy of the event object and normalize some properties + var i, prop, copy, + type = event.type, + originalEvent = event, + fixHook = this.fixHooks[ type ]; + + if ( !fixHook ) { + this.fixHooks[ type ] = fixHook = + rmouseEvent.test( type ) ? this.mouseHooks : + rkeyEvent.test( type ) ? this.keyHooks : + {}; + } + copy = fixHook.props ? this.props.concat( fixHook.props ) : this.props; + + event = new jQuery.Event( originalEvent ); + + i = copy.length; + while ( i-- ) { + prop = copy[ i ]; + event[ prop ] = originalEvent[ prop ]; + } + + // Support: Cordova 2.5 (WebKit) (#13255) + // All events should have a target; Cordova deviceready doesn't + if ( !event.target ) { + event.target = document; + } + + // Support: Safari 6.0+, Chrome<28 + // Target should not be a text node (#504, #13143) + if ( event.target.nodeType === 3 ) { + event.target = event.target.parentNode; + } + + return fixHook.filter ? fixHook.filter( event, originalEvent ) : event; + }, + + special: { + load: { + + // Prevent triggered image.load events from bubbling to window.load + noBubble: true + }, + focus: { + + // Fire native event if possible so blur/focus sequence is correct + trigger: function() { + if ( this !== safeActiveElement() && this.focus ) { + this.focus(); + return false; + } + }, + delegateType: "focusin" + }, + blur: { + trigger: function() { + if ( this === safeActiveElement() && this.blur ) { + this.blur(); + return false; + } + }, + delegateType: "focusout" + }, + click: { + + // For checkbox, fire native event so checked state will be right + trigger: function() { + if ( this.type === "checkbox" && this.click && jQuery.nodeName( this, "input" ) ) { + this.click(); + return false; + } + }, + + // For cross-browser consistency, don't fire native .click() on links + _default: function( event ) { + return jQuery.nodeName( event.target, "a" ); + } + }, + + beforeunload: { + postDispatch: function( event ) { + + // Support: Firefox 20+ + // Firefox doesn't alert if the returnValue field is not set. + if ( event.result !== undefined && event.originalEvent ) { + event.originalEvent.returnValue = event.result; + } + } + } + } +}; + +jQuery.removeEvent = function( elem, type, handle ) { + + // This "if" is needed for plain objects + if ( elem.removeEventListener ) { + elem.removeEventListener( type, handle ); + } +}; + +jQuery.Event = function( src, props ) { + + // Allow instantiation without the 'new' keyword + if ( !( this instanceof jQuery.Event ) ) { + return new jQuery.Event( src, props ); + } + + // Event object + if ( src && src.type ) { + this.originalEvent = src; + this.type = src.type; + + // Events bubbling up the document may have been marked as prevented + // by a handler lower down the tree; reflect the correct value. + this.isDefaultPrevented = src.defaultPrevented || + src.defaultPrevented === undefined && + + // Support: Android<4.0 + src.returnValue === false ? + returnTrue : + returnFalse; + + // Event type + } else { + this.type = src; + } + + // Put explicitly provided properties onto the event object + if ( props ) { + jQuery.extend( this, props ); + } + + // Create a timestamp if incoming event doesn't have one + this.timeStamp = src && src.timeStamp || jQuery.now(); + + // Mark it as fixed + this[ jQuery.expando ] = true; +}; + +// jQuery.Event is based on DOM3 Events as specified by the ECMAScript Language Binding +// http://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html +jQuery.Event.prototype = { + constructor: jQuery.Event, + isDefaultPrevented: returnFalse, + isPropagationStopped: returnFalse, + isImmediatePropagationStopped: returnFalse, + isSimulated: false, + + preventDefault: function() { + var e = this.originalEvent; + + this.isDefaultPrevented = returnTrue; + + if ( e && !this.isSimulated ) { + e.preventDefault(); + } + }, + stopPropagation: function() { + var e = this.originalEvent; + + this.isPropagationStopped = returnTrue; + + if ( e && !this.isSimulated ) { + e.stopPropagation(); + } + }, + stopImmediatePropagation: function() { + var e = this.originalEvent; + + this.isImmediatePropagationStopped = returnTrue; + + if ( e && !this.isSimulated ) { + e.stopImmediatePropagation(); + } + + this.stopPropagation(); + } +}; + +// Create mouseenter/leave events using mouseover/out and event-time checks +// so that event delegation works in jQuery. +// Do the same for pointerenter/pointerleave and pointerover/pointerout +// +// Support: Safari 7 only +// Safari sends mouseenter too often; see: +// https://code.google.com/p/chromium/issues/detail?id=470258 +// for the description of the bug (it existed in older Chrome versions as well). +jQuery.each( { + mouseenter: "mouseover", + mouseleave: "mouseout", + pointerenter: "pointerover", + pointerleave: "pointerout" +}, function( orig, fix ) { + jQuery.event.special[ orig ] = { + delegateType: fix, + bindType: fix, + + handle: function( event ) { + var ret, + target = this, + related = event.relatedTarget, + handleObj = event.handleObj; + + // For mouseenter/leave call the handler if related is outside the target. + // NB: No relatedTarget if the mouse left/entered the browser window + if ( !related || ( related !== target && !jQuery.contains( target, related ) ) ) { + event.type = handleObj.origType; + ret = handleObj.handler.apply( this, arguments ); + event.type = fix; + } + return ret; + } + }; +} ); + +jQuery.fn.extend( { + on: function( types, selector, data, fn ) { + return on( this, types, selector, data, fn ); + }, + one: function( types, selector, data, fn ) { + return on( this, types, selector, data, fn, 1 ); + }, + off: function( types, selector, fn ) { + var handleObj, type; + if ( types && types.preventDefault && types.handleObj ) { + + // ( event ) dispatched jQuery.Event + handleObj = types.handleObj; + jQuery( types.delegateTarget ).off( + handleObj.namespace ? + handleObj.origType + "." + handleObj.namespace : + handleObj.origType, + handleObj.selector, + handleObj.handler + ); + return this; + } + if ( typeof types === "object" ) { + + // ( types-object [, selector] ) + for ( type in types ) { + this.off( type, selector, types[ type ] ); + } + return this; + } + if ( selector === false || typeof selector === "function" ) { + + // ( types [, fn] ) + fn = selector; + selector = undefined; + } + if ( fn === false ) { + fn = returnFalse; + } + return this.each( function() { + jQuery.event.remove( this, types, fn, selector ); + } ); + } +} ); + + +var + rxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:-]+)[^>]*)\/>/gi, + + // Support: IE 10-11, Edge 10240+ + // In IE/Edge using regex groups here causes severe slowdowns. + // See https://connect.microsoft.com/IE/feedback/details/1736512/ + rnoInnerhtml = / - + + + + + + Index: lams_tool_mindmap/web/pages/authoring/basic.jsp =================================================================== diff -u -r4866a98b42db4c24eaf70a450bac79e7e9838e65 -r9cda52bbbc8ef19343bd53f563c0734560af1a3a --- lams_tool_mindmap/web/pages/authoring/basic.jsp (.../basic.jsp) (revision 4866a98b42db4c24eaf70a450bac79e7e9838e65) +++ lams_tool_mindmap/web/pages/authoring/basic.jsp (.../basic.jsp) (revision 9cda52bbbc8ef19343bd53f563c0734560af1a3a) @@ -21,10 +21,11 @@ - - - - message.enableFlashAuthorMonitor - - - +<%-- Page is reloaded after save and then disappears. Do not want to try to render the mindmap. --%> + + false + true + author + ${mindmapId} + <%@ include file="/common/mapjs.jsp"%> + Index: lams_tool_mindmap/web/pages/learning/mindmap.jsp =================================================================== diff -u -r7a5f32bc6599e75f9619b17bcc37b316a443cef2 -r9cda52bbbc8ef19343bd53f563c0734560af1a3a --- lams_tool_mindmap/web/pages/learning/mindmap.jsp (.../mindmap.jsp) (revision 7a5f32bc6599e75f9619b17bcc37b316a443cef2) +++ lams_tool_mindmap/web/pages/learning/mindmap.jsp (.../mindmap.jsp) (revision 9cda52bbbc8ef19343bd53f563c0734560af1a3a) @@ -1,63 +1,37 @@ <%@ include file="/common/taglibs.jsp"%> - - - + + + + + + - $(window).resize(makeNice); - - embedFlashObject(540, 405); - - - + @@ -104,16 +78,10 @@ - <%--MindMap Flash-----------------------------------%> - - - - - - - - - + <%--MindMup -----------------------------------%> + <%@ include file="/common/mapjs.jsp"%> + <%-- End MindMup -----------------------------------%> + Index: lams_tool_mindmap/web/pages/monitoring/mindmapDisplay.jsp =================================================================== diff -u -r282164973f73a12f93366c1fe97b2b46e4e31bd7 -r9cda52bbbc8ef19343bd53f563c0734560af1a3a --- lams_tool_mindmap/web/pages/monitoring/mindmapDisplay.jsp (.../mindmapDisplay.jsp) (revision 282164973f73a12f93366c1fe97b2b46e4e31bd7) +++ lams_tool_mindmap/web/pages/monitoring/mindmapDisplay.jsp (.../mindmapDisplay.jsp) (revision 9cda52bbbc8ef19343bd53f563c0734560af1a3a) @@ -1,88 +1,28 @@ <%@ include file="/common/taglibs.jsp"%> + + + - - - + - embedFlashObject(700, 525); - $(window).resize(makeNice); - - function embedFlashObject(x, y) { - swfobject.embedSWF("${mindmapType}", "flashContent", x, y, "9.0.0", false, flashvars); - } - - function setToolContentID() { - var toolContentID = document.getElementById("toolContentID"); - toolContentID.value = "${toolContentID}"; - } - - function setUserId() { - var userId = document.getElementById("userId"); - userId.value = "${userDTO.uid}"; - } - - function setMindmapContent() { - var mindmapContent = document.getElementById("mindmapContent"); - if(mindmapContent != null) { - mindmapContent.value = document['flashContent'].getMindmap(); - } + + + + + + + + - setUserId(); - setToolContentID(); - } +<%-- Monitor cannot edit mindmaps. mapjs.jsp also uses sessionId and mindmapId --%> +${isMultiUserMode} +false + +<%@ include file="/common/mapjs.jsp"%> - function updateContent() { - $.post("${get}", { dispatch: "${dispatch}", mindmapId: "${mindmapId}", userId: "${userId}", - content: document['flashContent'].getMindmap() } ); - } -//]]> - - - - - - - - - - - - - - - - - - - - - - message.enableFlashAuthorMonitor - - - - - - - - button.save - - - - - - button.back - - - - - + + button.back + Index: lams_tool_mindmap/web/pages/monitoring/summary.jsp =================================================================== diff -u -r8e090b3ddf269cdffececa4bc55a9333da5b0858 -r9cda52bbbc8ef19343bd53f563c0734560af1a3a --- lams_tool_mindmap/web/pages/monitoring/summary.jsp (.../summary.jsp) (revision 8e090b3ddf269cdffececa4bc55a9333da5b0858) +++ lams_tool_mindmap/web/pages/monitoring/summary.jsp (.../summary.jsp) (revision 9cda52bbbc8ef19343bd53f563c0734560af1a3a) @@ -82,18 +82,20 @@ ${session.numberOfLearners} - + - + - + + + @@ -108,13 +110,13 @@ - + - + - + @@ -129,14 +131,10 @@ - + - + - - - - @@ -157,25 +155,18 @@ - + + - - - - - - - - - - + +