diff --git a/app/actors/PLMActor.scala b/app/actors/PLMActor.scala index ee748f21..b8c7efb3 100644 --- a/app/actors/PLMActor.scala +++ b/app/actors/PLMActor.scala @@ -4,6 +4,7 @@ import akka.actor._ import play.api.libs.json._ import play.api.mvc.RequestHeader import json._ +import json.world._ import models.PLM import models.User import log.PLMLogger @@ -26,6 +27,21 @@ import play.api.Logger import java.util.UUID import models.daos.UserDAOMongoImpl +import java.util.HashMap +import java.nio.file.Files +import java.nio.file.FileSystems +import java.nio.file.FileVisitResult +import java.nio.file.Path +import java.nio.file.Paths +import java.nio.file.SimpleFileVisitor +import java.nio.file.attribute.BasicFileAttributes +import java.io.File +import java.io.BufferedWriter +import java.io.BufferedReader +import java.io.FileWriter +import java.io.FileReader +import java.io.IOException + object PLMActor { def props(actorUUID: String, gitID: String, newUser: Boolean, preferredLang: Option[Lang], lastProgLang: Option[String])(out: ActorRef) = Props(new PLMActor(actorUUID, gitID, newUser, preferredLang, lastProgLang, out)) def propsWithUser(actorUUID: String, user: User)(out: ActorRef) = Props(new PLMActor(actorUUID, user, out)) @@ -102,6 +118,145 @@ class PLMActor(actorUUID: String, gitID: String, newUser: Boolean, preferredLang case _ => Logger.debug("getExercise: non-correct JSON") } + case "filterMission" => + var optMissionText: Option[String] = (msg \ "args" \ "missionText").asOpt[String] + var optAll: Option[Boolean] = (msg \ "args" \ "all").asOpt[Boolean] + var optLangs: Option[Array[String]] = (msg \ "args" \ "languages").asOpt[Array[String]] + + (optMissionText.getOrElse(None), optAll.getOrElse(None), optLangs.getOrElse(None)) match { + case (missionText: String, all: Boolean, langs: Array[String]) => { + if(all) { + sendMessage("missionFiltered", Json.obj("filteredMission" -> plm.filterMission(missionText, true, false, null))) + } + else { + sendMessage("missionFiltered", Json.obj("filteredMission" -> plm.filterMission(missionText, false, true, langs))) + } + } + case (_, _, _) => Logger.debug("filterMission: non-correct JSON") + } + case "editorRunSolution" => + var optLessonID: Option[String] = (msg \ "args" \ "lessonID").asOpt[String] + var optExerciseID: Option[String] = (msg \ "args" \ "exerciseID").asOpt[String] + var optCode: Option[String] = (msg \ "args" \ "code").asOpt[String] + var buggleWorld = GridWorldToJson.JsonToBuggleWorld(plm.game, (msg \ "args" \ "world")) + + (optLessonID.getOrElse(None), optExerciseID.getOrElse(None), optCode.getOrElse(None)) match { + case (lessonID:String, exerciseID: String, code: String) => + plm.currentExercise.setupWorlds(Array(buggleWorld)) + var executionSpy: ExecutionSpy = new ExecutionSpy(this, "operations") + var lect: Lecture = plm.game.getCurrentLesson.getCurrentExercise + var exo: Exercise = lect.asInstanceOf[Exercise] + plm.addExecutionSpy(exo, executionSpy, Exercise.WorldKind.CURRENT) + plm.runExercise(lessonID, exerciseID, code) + case (_, _, _) => + Logger.debug("editorRunSolution: non-correct JSON") + } + + case "editorSaveExercise" => + var optName: Option[String] = (msg \ "args" \ "name").asOpt[String] + var optMission: Option[String] = (msg \ "args" \ "mission").asOpt[String] + var optWorlds: Option[Array[JsObject]] = (msg \ "args" \ "worlds").asOpt[Array[JsObject]] + + (optName.getOrElse(None), optMission.getOrElse(None), optWorlds.getOrElse(None)) match { + case (name: String, mission: String, jsWorlds: Array[JsObject]) => { + + var worlds: Array[BuggleWorld] = new Array[BuggleWorld](0) + for(jsWorld <- jsWorlds) { + worlds = GridWorldToJson.JsonToBuggleWorld(plm.game, jsWorld) +: worlds + } + + /* + Delete old exercise files + */ + class DeleteFiles extends SimpleFileVisitor[Path] { + + override def visitFile(file: Path, attrs: BasicFileAttributes): FileVisitResult = { + Files.delete(file); + return FileVisitResult.CONTINUE; + } + + override def postVisitDirectory(dir: Path, e: IOException): FileVisitResult = { + if (e == null) { + Files.delete(dir); + return FileVisitResult.CONTINUE; + } + else { + throw e; + } + } + } + + var exercisePath = Paths.get("./editor/exercises", name) + + if(Files.exists(exercisePath)) { + Files.walkFileTree(exercisePath, new DeleteFiles()) + } + + Files.createDirectory(exercisePath) + + /* + Try writing exercise + */ + try { + var buffer: BufferedWriter = null + + /* Writing main class... */ + var bufferRead: BufferedReader = new BufferedReader(new FileReader("./editor/classTemplate.java")); + var newLine: String = null + var mainClassContent: String = "" + do { + newLine = bufferRead.readLine() + mainClassContent = mainClassContent + newLine + "\n" + } while(newLine != null) + mainClassContent = mainClassContent.replace("$className", name) + var loadWorldsContent: String = "" + for(world <- worlds) { + loadWorldsContent = loadWorldsContent + "BuggleWorld.newFromFile(\"" + world.getName + "\")," + "\n" + } + mainClassContent = mainClassContent.replace("$loadWorlds", loadWorldsContent) + buffer = new BufferedWriter(new FileWriter("./editor/exercises/" + name + "/" + name + ".java")) + buffer.write(mainClassContent) + buffer.close + bufferRead.close + + /* Writing worlds... */ + for(world <- worlds) { + buffer = new BufferedWriter(new FileWriter("./editor/exercises/" + name + "/" + + world.getName + ".map")) + world.writeToFile(buffer) + buffer.close + } + + /* Writing missions... */ + buffer = new BufferedWriter(new FileWriter("./editor/exercises/" + name + "/" + name + ".html")) + buffer.write(mission) + buffer.close + + /* Writing solutions... */ + var progLangs = plm._currentExercise.getProgLanguages.toArray(Array[ProgrammingLanguage]()) + + for(pl <- progLangs) { + var className = name.capitalize + "Entity" + var replace = new HashMap[String, String]() + + replace.put("class Editor", "class " + className) + + buffer = new BufferedWriter(new FileWriter("./editor/exercises/" + name + "/" + + className + "." + pl.getExt)); + buffer.write(plm.getCompilableContent(replace, pl)); + buffer.close + } + + } + catch { + case e: IOException => + Logger.debug("Error while writing world file") + } + } + case (_) => + Logger.debug("editorSaveExercise: non-correct JSON") + } + case "getExercise" => var optLessonID: Option[String] = (msg \ "args" \ "lessonID").asOpt[String] var optExerciseID: Option[String] = (msg \ "args" \ "exerciseID").asOpt[String] @@ -121,6 +276,34 @@ class PLMActor(actorUUID: String, gitID: String, newUser: Boolean, preferredLang "exercise" -> LectureToJson.lectureWrites(lecture, plm.programmingLanguage, plm.getStudentCode, plm.getInitialWorlds, plm.getSelectedWorldID) )) } + case "getExerciseToEdit" => + var optLessonID: Option[String] = (msg \ "args" \ "lessonID").asOpt[String] + var optExerciseID: Option[String] = (msg \ "args" \ "exerciseID").asOpt[String] + var lecture: Lecture = null; + var executionSpy: ExecutionSpy = new ExecutionSpy(this, "operations") + var demoExecutionSpy: ExecutionSpy = new ExecutionSpy(this, "demoOperations") + (optLessonID.getOrElse(None), optExerciseID.getOrElse(None)) match { + case (lessonID:String, exerciseID: String) => + lecture = plm.switchExercise(lessonID, exerciseID, executionSpy, demoExecutionSpy) + case (lessonID:String, _) => + lecture = plm.switchLesson(lessonID, executionSpy, demoExecutionSpy) + case (_, _) => + Logger.debug("getExerciseToEdit: non-correct JSON") + } + if(lecture != null) { + var progLangs = lecture.asInstanceOf[Exercise].getProgLanguages.toArray(Array[ProgrammingLanguage]()) + var solutionCodes: Array[(String,String)] = new Array[(String,String)](progLangs.length) + var i = 0 + for(progLang <- progLangs) { + solutionCodes(i) = (progLang.getLang, plm.getSolutionCode(progLang)) + i = i + 1 + } + + sendMessage("exerciseToEdit", Json.obj( + "exercise" -> LectureToJson.lectureWritesForEditor(lecture, plm.programmingLanguage, plm.getStudentCode, plm.getInitialWorlds, plm.getSelectedWorldID, solutionCodes) + )) + } + case "runExercise" => var optLessonID: Option[String] = (msg \ "args" \ "lessonID").asOpt[String] var optExerciseID: Option[String] = (msg \ "args" \ "exerciseID").asOpt[String] diff --git a/app/controllers/Application.scala b/app/controllers/Application.scala index ee936b15..c62924ba 100644 --- a/app/controllers/Application.scala +++ b/app/controllers/Application.scala @@ -35,6 +35,18 @@ object Application extends Controller { def exercise(lessonID: String, exerciseID: String) = Action { implicit request => Ok(views.html.index("Accueil")) } + + def editor = Action { implicit request => + Ok(views.html.index("Accueil")) + } + + def editorLoadLesson(lessonID: String) = Action { implicit request => + Ok(views.html.index("Accueil")) + } + + def editorLoadExercise(lessonID: String, exerciseID: String) = Action { implicit request => + Ok(views.html.index("Accueil")) + } def specRunner() = Action { implicit request => Ok(views.html.specRunner()) diff --git a/app/json/LectureToJson.scala b/app/json/LectureToJson.scala index 38712a44..820d9f05 100644 --- a/app/json/LectureToJson.scala +++ b/app/json/LectureToJson.scala @@ -38,6 +38,26 @@ object LectureToJson { return json } + + def lectureWritesForEditor (lecture: Lecture, progLang: ProgrammingLanguage, code: String, + initialWorlds: Array[World], selectedWorldID: String, + solutionCodes: Array[(String,String)]): JsValue = { + + var jsonSolutions = Json.obj() + for(solution <- solutionCodes) { + jsonSolutions = jsonSolutions.as[JsObject] ++ Json.obj( + solution._1.toLowerCase -> solution._2 + ) + } + + var json = lectureWrites(lecture, progLang, code, initialWorlds, selectedWorldID) + json = json.as[JsObject] ++ Json.obj( + "missionHTML" -> lecture.getRawMission(), + "solutionCodes" -> jsonSolutions + ) + + return json + } def instructionsWrite(lecture: Lecture, progLang: ProgrammingLanguage, initialWorlds: Array[World]): JsValue = { return Json.obj( diff --git a/app/json/entity/AbstractBuggleToJson.scala b/app/json/entity/AbstractBuggleToJson.scala index 79e63c0f..ec767111 100644 --- a/app/json/entity/AbstractBuggleToJson.scala +++ b/app/json/entity/AbstractBuggleToJson.scala @@ -1,7 +1,12 @@ package json.entity +import java.awt.Color + import play.api.libs.json._ import plm.universe.bugglequest.AbstractBuggle +import plm.universe.bugglequest.SimpleBuggle +import plm.universe.bugglequest.BuggleWorld +import plm.universe.Direction import play.api.libs.json.Json.toJsFieldJsValueWrapper object AbstractBuggleToJson { @@ -15,4 +20,42 @@ object AbstractBuggleToJson { "carryBaggle" -> abstractBuggle.isCarryingBaggle() ) } + + def JsonSimpleBugglesWrite(buggleWorld: BuggleWorld, jsSimpleBuggles: JsObject): Array[SimpleBuggle] = { + var bugglesID = jsSimpleBuggles.keys + var newBuggles: Array[SimpleBuggle] = new Array[SimpleBuggle](bugglesID.size) + var i = 0; + for(buggleID <- bugglesID) { + var optBuggle: Option[JsObject] = (jsSimpleBuggles \ buggleID).asOpt[JsObject] + optBuggle.getOrElse(None) match { + case buggle: JsObject => + newBuggles(i) = JsonSimpleBuggleWrite(buggleWorld, buggleID, buggle) + i += 1 + } + } + return newBuggles + } + + def JsonSimpleBuggleWrite(buggleWorld: BuggleWorld, name: String, jsSimpleBuggle: JsObject): SimpleBuggle = { + var optX: Option[Int] = (jsSimpleBuggle \ "x").asOpt[Int] + var optY: Option[Int] = (jsSimpleBuggle \ "y").asOpt[Int] + var optColor: Option[Array[Int]] = (jsSimpleBuggle \ "color").asOpt[Array[Int]] + var optDirection: Option[Int] = (jsSimpleBuggle \ "direction").asOpt[Int] + var optcarryBaggle: Option[Boolean] = (jsSimpleBuggle \ "carryBaggle").asOpt[Boolean] + + (optX.getOrElse(None), optY.getOrElse(None), optColor.getOrElse(None), + optDirection.getOrElse(None), optcarryBaggle.getOrElse(None)) match { + case (x: Int, y: Int, color: Array[Int], directionNb: Int, carryBaggle: Boolean) => { + var direction = directionNb match { + case Direction.NORTH_VALUE => Direction.NORTH + case Direction.EAST_VALUE => Direction.EAST + case Direction.WEST_VALUE => Direction.WEST + case Direction.SOUTH_VALUE => Direction.SOUTH + } + return new SimpleBuggle(buggleWorld, name, x, y, direction, + new Color(color(0), color(1), color(2), color(3)), + new Color(42, 42, 42, 255)) + } + } + } } \ No newline at end of file diff --git a/app/json/world/BuggleWorldCellToJson.scala b/app/json/world/BuggleWorldCellToJson.scala index 46e75ecc..808300f6 100644 --- a/app/json/world/BuggleWorldCellToJson.scala +++ b/app/json/world/BuggleWorldCellToJson.scala @@ -1,6 +1,10 @@ package json.world +import java.awt.Color + import play.api.libs.json._ +import plm.core.model.Game +import plm.universe.bugglequest.BuggleWorld import plm.universe.bugglequest.BuggleWorldCell import play.api.libs.json.Json.toJsFieldJsValueWrapper @@ -17,4 +21,81 @@ object BuggleWorldCellToJson { "hasTopWall" -> buggleWorldCell.hasTopWall ) } + + def JsonToBuggleWorldCells(buggleWorld: BuggleWorld, width: Int, height: Int, cells: JsValue): Array[Array[BuggleWorldCell]] = { + var optColumns: Option[Array[JsArray]] = cells.asOpt[Array[JsArray]] + var newCells: Array[Array[BuggleWorldCell]] = Array.ofDim[BuggleWorldCell](width, height) + + optColumns.getOrElse(None) match { + case columns: Array[JsArray] => + var colX = 0 + for(column <- columns) { + var optCells: Option[Array[JsObject]] = column.asOpt[Array[JsObject]] + + optCells.getOrElse(None) match { + case cells: Array[JsObject] => + var i = 0 + var j = 0 + for(i <- 0 to height - 1) { + if(j < cells.length) { + var newCell = JsonToBuggleWorldCell(buggleWorld, cells(j)) + if(newCell.getY == i) { + newCells(colX)(i) = newCell + j += 1 + } + else { + newCells(colX)(i) = new BuggleWorldCell(buggleWorld , colX, i) + } + } + else { + newCells(colX)(i) = new BuggleWorldCell(buggleWorld , colX, i) + } + } + } + colX += 1 + } + } + return newCells + } + + def JsonToBuggleWorldCell(buggleWorld: BuggleWorld, cell: JsValue): BuggleWorldCell = { + var newCell: BuggleWorldCell = null + var optX: Option[Int] = (cell \ "x").asOpt[Int] + var optY: Option[Int] = (cell \ "y").asOpt[Int] + var optColor: Option[Array[Int]] = (cell \ "color").asOpt[Array[Int]] + var optHasBaggle: Option[Boolean] = (cell \ "hasBaggle").asOpt[Boolean] + var optHasContent: Option[Boolean] = (cell \ "hasContent").asOpt[Boolean] + var optContent: Option[String] = (cell \ "content").asOpt[String] + var optHasLeftWall: Option[Boolean] = (cell \ "hasLeftWall").asOpt[Boolean] + var optHasTopWall: Option[Boolean] = (cell \ "hasTopWall").asOpt[Boolean] + + (optX.getOrElse(None), optY.getOrElse(None)) match { + case(x: Int, y: Int) => { + newCell = new BuggleWorldCell(buggleWorld , x, y) + } + } + + var color = optColor.getOrElse(null) + if(color != null) { + newCell.setColor(new Color(color(0), color(1), color(2), color(3))) + } + else { + newCell.setColor(new Color(255, 255, 255, 255)) + } + + if(optHasBaggle.getOrElse(false)) { + newCell.baggleAdd() + } + if(optHasContent.getOrElse(false)) { + newCell.setContent(optContent.getOrElse("")) + } + if(optHasLeftWall.getOrElse(false)) { + newCell.putLeftWall() + } + if(optHasTopWall.getOrElse(false)) { + newCell.putTopWall() + } + + return newCell; + } } \ No newline at end of file diff --git a/app/json/world/GridWorldToJson.scala b/app/json/world/GridWorldToJson.scala index 60c4e4e1..0a617d8a 100644 --- a/app/json/world/GridWorldToJson.scala +++ b/app/json/world/GridWorldToJson.scala @@ -1,8 +1,13 @@ package json.world +import collection.JavaConversions._ + import exceptions.NonImplementedWorldException +import json.entity.AbstractBuggleToJson + import play.api.libs.json._ +import plm.core.model.Game import plm.universe.GridWorld import plm.universe.bugglequest.BuggleWorld import play.api.libs.json.Json.toJsFieldJsValueWrapper @@ -26,4 +31,28 @@ object GridWorldToJson { ) return json } + + def JsonToBuggleWorld(game: Game, JsBuggleWorld: JsValue): BuggleWorld = { + var optName: Option[String] = (JsBuggleWorld \ "name").asOpt[String] + var optWidth: Option[Int] = (JsBuggleWorld \ "width").asOpt[Int] + var optHeight: Option[Int] = (JsBuggleWorld \ "height").asOpt[Int] + var optBuggles: Option[JsObject] = (JsBuggleWorld \ "entities").asOpt[JsObject] + var newWorld: BuggleWorld = null; + + (optName.getOrElse(None), optWidth.getOrElse(None), optHeight.getOrElse(None), + optBuggles.getOrElse(None)) match { + case (name: String, width: Int, height: Int, buggles: JsObject) => { + newWorld = new BuggleWorld(game, name, width, height) + var newCells = BuggleWorldCellToJson.JsonToBuggleWorldCells(newWorld, width, height, JsBuggleWorld \ "cells") + for(column <- newCells) { + for(cell <- column) { + newWorld.setCell(cell, cell.getX, cell.getY) + } + } + AbstractBuggleToJson.JsonSimpleBugglesWrite(newWorld, buggles) + } + } + + return newWorld + } } \ No newline at end of file diff --git a/app/models/PLM.scala b/app/models/PLM.scala index e7e39454..3f5e750c 100644 --- a/app/models/PLM.scala +++ b/app/models/PLM.scala @@ -21,6 +21,13 @@ import play.api.i18n.Lang import log.PLMLogger import java.util.Locale +import plm.core.model.lesson.ExerciseTemplated +import plm.core.ui.PlmHtmlEditorKit +import plm.universe.bugglequest._ +import plm.universe.Direction +import java.awt.Color +import java.util.Map + class PLM(userUUID: String, plmLogger: PLMLogger, locale: Locale, lastProgLang: Option[String]) { var _currentExercise: Exercise = _ @@ -113,6 +120,18 @@ class PLM(userUUID: String, plmLogger: PLMLogger, locale: Locale, lastProgLang: if(_currentExercise != null && _currentExercise.getSourceFile(programmingLanguage, 0) != null) _currentExercise.getSourceFile(programmingLanguage, 0).getBody else "" } + def getCompilableContent: String = { + if(_currentExercise != null && _currentExercise.getSourceFile(programmingLanguage, 0) != null) _currentExercise.getSourceFile(programmingLanguage, 0).getCompilableContent(StudentOrCorrection.STUDENT) else "" + } + + def getCompilableContent(patterns: Map[String, String], pl: ProgrammingLanguage): String = { + if(_currentExercise != null && _currentExercise.getSourceFile(pl, 0) != null) _currentExercise.getSourceFile(pl, 0).getCompilableContent(patterns, StudentOrCorrection.STUDENT) else "" + } + + def getSolutionCode(progLang: ProgrammingLanguage): String = { + return _currentExercise.getSourceFile(progLang, 0).getCorrection + } + def addProgressSpyListener(progressSpyListener: ProgressSpyListener) { game.addProgressSpyListener(progressSpyListener) } @@ -134,6 +153,19 @@ class PLM(userUUID: String, plmLogger: PLMLogger, locale: Locale, lastProgLang: if(_currentExercise != null) _currentExercise.getMission(progLang) else "" } + def filterMission(missionText: String, all: Boolean, showMulti: Boolean, languages: Array[String]): String = { + var progLangs: Array[ProgrammingLanguage] = null + if(!all) { + progLangs = languages.map(l => l match { + case "c" => Game.C + case "java" => Game.JAVA + case "scala" => Game.SCALA + case "python" => Game.PYTHON + }) + } + return PlmHtmlEditorKit.filterHTMLBis(missionText, all, showMulti, progLangs, null) + } + def setUserUUID(userUUID: String) { _currentExercise = null game.setUserUUID(userUUID) diff --git a/app/views/index.scala.html b/app/views/index.scala.html index 6c453dfc..5c417c58 100644 --- a/app/views/index.scala.html +++ b/app/views/index.scala.html @@ -22,6 +22,7 @@ + @@ -65,6 +66,9 @@ + + + @@ -83,8 +87,15 @@ + + + + + + + @@ -128,6 +139,7 @@ + diff --git a/conf/routes b/conf/routes index 233ce203..7cb4b41a 100644 --- a/conf/routes +++ b/conf/routes @@ -8,6 +8,9 @@ GET /ui controllers.Applicat GET /ui/lessons controllers.Application.indexLessons GET /ui/lessons/:lessonID controllers.Application.lesson(lessonID: String) GET /ui/lessons/:lessonID/*exerciseID controllers.Application.exercise(lessonID: String, exerciseID: String) +GET /ui/editor controllers.Application.editor +GET /ui/editor/:lessonID controllers.Application.editorLoadLesson(lessonID: String) +GET /ui/editor/:lessonID/*exerciseID controllers.Application.editorLoadExercise(lessonID: String, exerciseID: String) GET /specRunner controllers.Application.specRunner() GET /assets/*file controllers.Assets.at(path="/public", file) diff --git a/editor/classTemplate.java b/editor/classTemplate.java new file mode 100644 index 00000000..7c071c3d --- /dev/null +++ b/editor/classTemplate.java @@ -0,0 +1,24 @@ + + +import java.io.IOException; + +import plm.core.model.lesson.ExerciseTemplated; +import plm.core.model.lesson.Lesson; +import plm.universe.BrokenWorldFileException; +import plm.universe.World; +import plm.universe.bugglequest.BuggleWorld; + +public class $className extends ExerciseTemplated { + + public $className(Lesson lesson) throws IOException, BrokenWorldFileException { + super(lesson); + tabName = "$className"; + + /* Create initial situation */ + World[] myWorlds = new World[] { + $loadWorlds + }; + + setup(myWorlds); + } +} diff --git a/lib/cachedir/packages/packages.idx b/lib/cachedir/packages/packages.idx index 9028e340..74693854 100644 Binary files a/lib/cachedir/packages/packages.idx and b/lib/cachedir/packages/packages.idx differ diff --git a/lib/plm-2.6-pre-20150202.jar b/lib/plm-2.6-pre-20150202.jar index ca0487a2..94d388a8 100644 Binary files a/lib/plm-2.6-pre-20150202.jar and b/lib/plm-2.6-pre-20150202.jar differ diff --git a/public/app/app.js b/public/app/app.js index 72c28040..65a5e281 100644 --- a/public/app/app.js +++ b/public/app/app.js @@ -2,5 +2,5 @@ 'use strict'; angular - .module('PLMApp', ['ngAnimate', 'gettext', 'ngCookies', 'ui.router', 'ui.codemirror', 'angular-locker', 'satellizer']); + .module('PLMApp', ['ngAnimate', 'ngSanitize', 'gettext', 'ngCookies', 'ui.router', 'ui.codemirror', 'angular-locker', 'satellizer']); })(); \ No newline at end of file diff --git a/public/app/app.routes.js b/public/app/app.routes.js index 17e45aa2..99038ee9 100644 --- a/public/app/app.routes.js +++ b/public/app/app.routes.js @@ -44,6 +44,22 @@ }, controller: 'Exercise', controllerAs: 'exercise' - }); + }) + .state('editor', { + url: '/ui/editor', + templateUrl: 'assets/app/editor/editor.html', + controller: 'Editor', + controllerAs: 'editor' + }) + .state('editorLoadExercise', { + url: '/ui/editor/:lessonID/:exerciseID', + templateUrl: 'assets/app/editor/editor.html', + params: { + lessonID: {value: null, squash: false}, + exerciseID: {value: '', squash: false} + }, + controller: 'Editor', + controllerAs: 'editor' + }); } })(); \ No newline at end of file diff --git a/public/app/components/animation-player.directive.html b/public/app/components/animation-player.directive.html index 2fc2df90..84f29f86 100644 --- a/public/app/components/animation-player.directive.html +++ b/public/app/components/animation-player.directive.html @@ -1,16 +1,16 @@ - - - - - - \ No newline at end of file diff --git a/public/app/components/animation-player.directive.js b/public/app/components/animation-player.directive.js index 2d01d087..921279bf 100644 --- a/public/app/components/animation-player.directive.js +++ b/public/app/components/animation-player.directive.js @@ -8,6 +8,9 @@ function animationPlayer() { return { restrict: 'E', + scope: { + ctrl: '=ctrl' + }, templateUrl: '/assets/app/components/animation-player.directive.html' }; } diff --git a/public/app/components/application-header.directive.html b/public/app/components/application-header.directive.html index 02daf52c..2a554cbf 100644 --- a/public/app/components/application-header.directive.html +++ b/public/app/components/application-header.directive.html @@ -25,6 +25,7 @@

PLM

diff --git a/public/app/components/convert-to-number.directive.js b/public/app/components/convert-to-number.directive.js new file mode 100644 index 00000000..4849ea38 --- /dev/null +++ b/public/app/components/convert-to-number.directive.js @@ -0,0 +1,21 @@ +(function(){ + 'use strict'; + + angular + .module('PLMApp') + .directive('convertToNumber', convertToNumber); + + function convertToNumber() { + return { + require: 'ngModel', + link: function(scope, element, attrs, ngModel) { + ngModel.$parsers.push(function(val) { + return parseInt(val, 10); + }); + ngModel.$formatters.push(function(val) { + return '' + val; + }); + } + }; + } +})(); \ No newline at end of file diff --git a/public/app/components/ide.directive.html b/public/app/components/ide.directive.html index d288f087..108f6ff7 100644 --- a/public/app/components/ide.directive.html +++ b/public/app/components/ide.directive.html @@ -3,18 +3,18 @@       - + - +


- +

\ No newline at end of file diff --git a/public/app/components/instructions.directive.html b/public/app/components/instructions.directive.html index bd6fc74e..4b00d8ba 100644 --- a/public/app/components/instructions.directive.html +++ b/public/app/components/instructions.directive.html @@ -18,7 +18,7 @@

Player's options

- +
\ No newline at end of file diff --git a/public/app/components/mission-edition.directive.html b/public/app/components/mission-edition.directive.html new file mode 100644 index 00000000..355b0048 --- /dev/null +++ b/public/app/components/mission-edition.directive.html @@ -0,0 +1,25 @@ +
+
+
+
+ All + C + java + Scala + Python +
+
+ +
+ +
+
+

+
+
\ No newline at end of file diff --git a/public/app/components/mission-edition.directive.js b/public/app/components/mission-edition.directive.js new file mode 100644 index 00000000..ba62b3d6 --- /dev/null +++ b/public/app/components/mission-edition.directive.js @@ -0,0 +1,14 @@ +(function(){ + 'use strict'; + + angular + .module('PLMApp') + .directive('missionEdition', missionEdition); + + function missionEdition() { + return { + restrict: 'E', + templateUrl: '/assets/app/components/mission-edition.directive.html' + }; + } +})(); \ No newline at end of file diff --git a/public/app/components/select-programming-language.directive.html b/public/app/components/select-programming-language.directive.html index ab837ba3..a1586525 100644 --- a/public/app/components/select-programming-language.directive.html +++ b/public/app/components/select-programming-language.directive.html @@ -1,6 +1,6 @@ - {{exercise.currentProgrammingLanguage.lang}} + {{ide.programmingLanguage().lang}} \ No newline at end of file diff --git a/public/app/components/select-programming-language.directive.js b/public/app/components/select-programming-language.directive.js index aafba9f4..c927dfe2 100644 --- a/public/app/components/select-programming-language.directive.js +++ b/public/app/components/select-programming-language.directive.js @@ -5,11 +5,17 @@ .module('PLMApp') .directive('selectProgrammingLanguage', selectProgrammingLanguage); - function selectProgrammingLanguage() { + selectProgrammingLanguage.$inject = ['ide']; + + function selectProgrammingLanguage(ide) { return { restrict: 'E', + scope: { + ctrl: '=ctrl' + }, templateUrl: '/assets/app/components/select-programming-language.directive.html', - link: function (scope, element, attrs) { + link: function (scope, element, attrs) { + scope.ide = ide; $(document).foundation('dropdown', 'reflow'); } }; diff --git a/public/app/components/solution-edition.directive.html b/public/app/components/solution-edition.directive.html new file mode 100644 index 00000000..8a7ba8cd --- /dev/null +++ b/public/app/components/solution-edition.directive.html @@ -0,0 +1,46 @@ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+ + +
+
+
+
+
+
+
+
+ +
+ +
+
\ No newline at end of file diff --git a/public/app/components/solution-edition.directive.js b/public/app/components/solution-edition.directive.js new file mode 100644 index 00000000..59e8311c --- /dev/null +++ b/public/app/components/solution-edition.directive.js @@ -0,0 +1,14 @@ +(function(){ + 'use strict'; + + angular + .module('PLMApp') + .directive('solutionEdition', solutionEdition); + + function solutionEdition() { + return { + restrict: 'E', + templateUrl: '/assets/app/components/solution-edition.directive.html' + }; + } +})(); \ No newline at end of file diff --git a/public/app/components/world-edition-commands.directive.html b/public/app/components/world-edition-commands.directive.html new file mode 100644 index 00000000..25a609b5 --- /dev/null +++ b/public/app/components/world-edition-commands.directive.html @@ -0,0 +1,126 @@ + + +
+

Custom color

+

What color you want (r/g/b)

+
+ + +
+ × +
+ + +
+

Change message

+

Choose a new message

+
+ + +
+
+ +
+

Add line

+

Add a line below or above the line {{editor.selectedCell.y}} ?

+ + + × +
+ +
+

Add Column

+

Add a column left or right the column {{editor.selectedCell.x}} ?

+ + + × +
\ No newline at end of file diff --git a/public/app/components/world-edition-commands.directive.js b/public/app/components/world-edition-commands.directive.js new file mode 100644 index 00000000..dd4f1bb3 --- /dev/null +++ b/public/app/components/world-edition-commands.directive.js @@ -0,0 +1,17 @@ +(function(){ + 'use strict'; + + angular + .module('PLMApp') + .directive('worldEditionCommands', worldEditionCommands); + + function worldEditionCommands() { + return { + restrict: 'E', + templateUrl: '/assets/app/components/world-edition-commands.directive.html', + link: function (scope, element, attrs) { + $(document).foundation('reveal', 'reflow'); + } + }; + } +})(); \ No newline at end of file diff --git a/public/app/components/world-edition-properties.directive.html b/public/app/components/world-edition-properties.directive.html new file mode 100644 index 00000000..e17db5f1 --- /dev/null +++ b/public/app/components/world-edition-properties.directive.html @@ -0,0 +1,103 @@ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PropertyValue
World name
World width
World height
Cell X{{editor.selectedCell.x}}
Cell Y{{editor.selectedCell.y}}
Message
Color +
Top wall?
Left wall?
Baggle?
+
+
+ + + + + + + + + + + + + + + + + + + + + +
PropertyValue
Buggle name
Buggle direction +
Buggle color +
+ + + +
+
\ No newline at end of file diff --git a/public/app/components/world-edition-properties.directive.js b/public/app/components/world-edition-properties.directive.js new file mode 100644 index 00000000..59e3e0d8 --- /dev/null +++ b/public/app/components/world-edition-properties.directive.js @@ -0,0 +1,14 @@ +(function(){ + 'use strict'; + + angular + .module('PLMApp') + .directive('worldEditionProperties', worldEditionProperties); + + function worldEditionProperties() { + return { + restrict: 'E', + templateUrl: '/assets/app/components/world-edition-properties.directive.html' + }; + } +})(); \ No newline at end of file diff --git a/public/app/components/world-edition-worlds.directive.html b/public/app/components/world-edition-worlds.directive.html new file mode 100644 index 00000000..f334aa15 --- /dev/null +++ b/public/app/components/world-edition-worlds.directive.html @@ -0,0 +1,35 @@ +
+
+ +
+
+ + +
+ +
+

Delete {{editor.world.name}}?

+

+ Are you sure you want to delete world {{editor.world.name}}? +

+ + + × +
+
\ No newline at end of file diff --git a/public/app/components/world-edition-worlds.directive.js b/public/app/components/world-edition-worlds.directive.js new file mode 100644 index 00000000..6057ebf5 --- /dev/null +++ b/public/app/components/world-edition-worlds.directive.js @@ -0,0 +1,14 @@ +(function(){ + 'use strict'; + + angular + .module('PLMApp') + .directive('worldEditionWorlds', worldEditionWorlds); + + function worldEditionWorlds() { + return { + restrict: 'E', + templateUrl: '/assets/app/components/world-edition-worlds.directive.html' + }; + } +})(); \ No newline at end of file diff --git a/public/app/components/world-edition.directive.html b/public/app/components/world-edition.directive.html new file mode 100644 index 00000000..93f74ff0 --- /dev/null +++ b/public/app/components/world-edition.directive.html @@ -0,0 +1,10 @@ +
+
+
+ +
+
+
\ No newline at end of file diff --git a/public/app/components/world-edition.directive.js b/public/app/components/world-edition.directive.js new file mode 100644 index 00000000..c0f14b00 --- /dev/null +++ b/public/app/components/world-edition.directive.js @@ -0,0 +1,14 @@ +(function(){ + 'use strict'; + + angular + .module('PLMApp') + .directive('worldEdition', worldEdition); + + function worldEdition() { + return { + restrict: 'E', + templateUrl: '/assets/app/components/world-edition.directive.html' + }; + } +})(); \ No newline at end of file diff --git a/public/app/components/worlds-view.directive.html b/public/app/components/worlds-view.directive.html index 18ae06c2..7c26dbb0 100644 --- a/public/app/components/worlds-view.directive.html +++ b/public/app/components/worlds-view.directive.html @@ -5,7 +5,7 @@
-
@@ -13,14 +13,14 @@
- +



- +
- +
diff --git a/public/app/editor/editor.controller.js b/public/app/editor/editor.controller.js new file mode 100644 index 00000000..61250a3a --- /dev/null +++ b/public/app/editor/editor.controller.js @@ -0,0 +1,787 @@ +(function(){ + 'use strict'; + + angular + .module('PLMApp') + .controller('Editor', Editor); + + Editor.$inject = [ + '$window', '$http', '$scope', '$sce', '$stateParams', + 'connection', 'listenersHandler', 'langs', 'exercisesList', + 'canvas', 'color', 'ide', 'worlds', + '$timeout', '$interval', + 'locker', + 'BuggleWorld', 'BuggleWorldView' + ]; + + function Editor($window, $http, $scope, $sce, $stateParams, + connection, listenersHandler, langs, exercisesList, + canvas, color, ide, worlds, + $timeout, $interval, + locker, + BuggleWorld, BuggleWorldView) { + + var panelID = 'panel'; + var canvasID = 'canvas'; + + var editorExerciseLessonID = 'editor'; + var editorExerciseID = 'editor.lessons.editor.editor.Editor'; + var lessonToLoadID = $stateParams.lessonID; + var exerciseToLoadID = $stateParams.exerciseID; + + var editor = this; + + editor.editorMode = 'world'; + + editor.colorService = color; + editor.drawService = null; + editor.drawingArea = 'drawingArea'; + + editor.errorMessage = null; + + editor.exerciseName = ''; + + /* + World editor + */ + var topWallCmd = {name: 'Top wall', ID: 'topwall'}; + var leftWallCmd = {name: 'Left wall', ID: 'leftwall'}; + var buggleCmd = {name: 'Buggle', ID: 'buggle'}; + var deleteBuggleCmd = {name: 'Delete Buggle', ID: 'deletebuggle'}; + var addLineCmd = {name: 'Add line', ID: 'addline'}; + var removeLineCmd = {name: 'Remove line', ID: 'deleteline'}; + var addColumnCmd = {name: 'Add column', ID: 'addcolumn'}; + var removeColumnCmd = {name: 'Remove column', ID: 'deletecolumn'}; + + editor.showConfirmDelete = false; + + editor.initialWorlds = []; + editor.world = null; + editor.currentWorldName = ''; + editor.nbWorldsAdded = 0; + editor.selectedCell = null; + editor.command = ''; + editor.commandType = ''; + editor.lastWallCommand = topWallCmd; + editor.lastBuggleCommand = buggleCmd; + editor.lastGridCommand = addLineCmd; + editor.customColorInput = '42/42/42'; + editor.color = [42, 42, 42, 255]; + editor.colors = color.getColorsNames(); + + editor.directions = [{value: 0, name: 'North'}, + {value: 1, name: 'East'}, + {value: 2, name: 'South'}, + {value: 3, name: 'West'}]; + + editor._selectedBuggleID = null; + editor.selectedBuggle = null; + editor.nbBugglesCreated = 0; + + + editor.selectCell = selectCell; + editor.initEditor = initEditor; + editor.setCommand = setCommand; + editor.setRGBColor = setRGBColor; + editor.addLineAbove = addLineAbove; + editor.addLineBelow = addLineBelow; + editor.addColumnRight = addColumnRight; + editor.addColumnLeft = addColumnLeft; + editor.editorColor = editorColor; + editor.deleteWorldCommand = deleteWorldCommand; + editor.selectedBuggleColor = selectedBuggleColor; + editor.selectedBuggleID = selectedBuggleID; + editor.selectedCellColor = selectedCellColor; + editor.selectedCellContent = selectedCellContent; + editor.height = height; + editor.width = width; + + editor.addWorld = addWorld; + editor.deleteWorld = deleteWorld; + editor.switchWorld = switchWorld; + editor.deselectAll = deselectAll; + editor.getWorldIndex = getWorldIndex; + editor.worldName = worldName; + + /* + Mission editor + */ + editor.missionProgrammingLanguages = {all: true, c: false, java: false, scala: false, python: false}; + editor.missionText = ''; + editor.filteredMission = editor.missionText; + + + editor.filterMission = filterMission; + + /* + Solution editor + */ + editor.ideService = ide; + editor.worlds = worlds; + editor.api = ''; + editor.solutionDisplayPanel = 'solution'; + locker.bind($scope, 'timer', 1000); + editor.solutionCodes = {java: '', scala: '', c: '', python: ''}; + + + editor.initSolutionEditor = initSolutionEditor; + editor.runSolution = runSolution; + + /* + Save + */ + editor.saveExercise = saveExercise; + + + /* + Editor + */ + function getExercise(lessonID, exerciseID, mode) { + var args = { + lessonID: lessonID, + }; + if(exerciseID !== '') { + args.exerciseID = exerciseID; + } + connection.sendMessage(mode, args); + } + + var offDisplayMessage = listenersHandler.register('onmessage', handleMessage); + + function handleMessage(data) { + console.log('message received: ', data); + var cmd = data.cmd; + var args = data.args; + switch(cmd) { + case 'exercise': + setEditorExercise(args.exercise); + break; + case 'exerciseToEdit': + initFromExercise(args.exercise); + break; + case 'missionFiltered': + editor.filteredMission = args.filteredMission; + break; + case 'operations': + worlds.handleOperations(args.worldID, 'current', args.operations); + break; + case 'executionResult': + worlds.isRunning(false); + break; + } + } + + function displayError(errorMessage) { + editor.errorMessage = errorMessage; + $('#errorModal').foundation('reveal', 'open'); + } + + $scope.codemirrorLoaded = function(_solutionEditor){ + ide.init(_solutionEditor); + }; + + + /* + World editor + */ + function initEditor() { + canvasID = 'canvas'; + editor.drawingArea = 'drawingArea'; + initCanvas(BuggleWorldView.draw); + + if(editor.world === null) { + initWorlds(); + } + else { + editor.drawService.setWorld(editor.world); + } + } + + function initFromExercise(exercise) { + var lastNewWorldName; + for(var worldID in exercise.initialWorlds) { + var buggleWorld = new BuggleWorld(exercise.initialWorlds[worldID]); + buggleWorld.name = worldID; + editor.initialWorlds.push(buggleWorld); + lastNewWorldName = worldID; + } + switchWorld(lastNewWorldName); + + editor.missionText = exercise.missionHTML; + editor.filterMission(); + + + var templateRegex = /^[\s\S]*((?:(?:\/\*)|#) BEGIN TEMPLATE(?: \*\/)?)([\s\S]+)((?:(?:\/\*)|#) BEGIN SOLUTION(?: \*\/)?)([\s\S]+)((?:(?:\/\*)|#) END SOLUTION(?: \*\/)?)([\s\S]+)((?:(?:\/\*)|#) END TEMPLATE(?: \*\/)?)[\s\S]*$/; + + var noTemplateRegex = /^[\s\S]*((?:(?:\/\*)|#) BEGIN SOLUTION(?: \*\/)?)([\s\S]+)((?:(?:\/\*)|#) END SOLUTION(?: \*\/)?)[\s\S]*$/; + + for(var solution in exercise.solutionCodes) { + var beginTemplate = ''; + var endTemplate = ''; + var beginSolution = ''; + var endSolution = ''; + var templateCode1 = '\n'; + var templateCode2 = '\n'; + var solutionCode = ''; + + if(exercise.solutionCodes[solution].match(templateRegex)) { + beginTemplate = exercise.solutionCodes[solution].replace(templateRegex, '$1'); + endTemplate = exercise.solutionCodes[solution].replace(templateRegex, '$7'); + beginSolution = exercise.solutionCodes[solution].replace(templateRegex, '$3'); + endSolution = exercise.solutionCodes[solution].replace(templateRegex, '$5'); + templateCode1 = exercise.solutionCodes[solution].replace(templateRegex, '$2'); + templateCode2 = exercise.solutionCodes[solution].replace(templateRegex, '$6'); + solutionCode = exercise.solutionCodes[solution].replace(templateRegex, '$4'); + } + else { + beginSolution = exercise.solutionCodes[solution].replace(noTemplateRegex, '$1'); + endSolution = exercise.solutionCodes[solution].replace(noTemplateRegex, '$3'); + solutionCode = exercise.solutionCodes[solution].replace(noTemplateRegex, '$2'); + } + + editor.solutionCodes[solution] = beginTemplate + templateCode1 + beginSolution + solutionCode + + endSolution + templateCode2 + endTemplate; + } + } + + function initWorlds() { + if(angular.isDefined(lessonToLoadID)) { + getExercise(lessonToLoadID, exerciseToLoadID, 'getExerciseToEdit'); + } + else { + editor.addWorld('New world'); + } + } + + function initCanvas(draw) { + var canvasElt; + var canvasWidth; + var canvasHeight; + + editor.drawService = canvas; + + canvasElt = document.getElementById(canvasID); + canvasWidth = $('#'+editor.drawingArea).parent().width(); + canvasHeight = canvasWidth; + + canvas.init(canvasElt, canvasWidth, canvasHeight, draw); + + window.addEventListener('resize', resizeCanvas, false); + } + + function resizeCanvas() { + var canvasWidth = $('#'+editor.drawingArea).parent().width(); + var canvasHeight = canvasWidth; + editor.drawService.resize(canvasWidth, canvasHeight); + $(document).foundation('equalizer', 'reflow'); + } + + function selectCell(event) { + var offset = $('#' + canvasID).offset(); + var x = Math.floor((event.pageX - offset.left) / BuggleWorldView.getCellWidth()); + var y = Math.floor((event.pageY - offset.top) / BuggleWorldView.getCellHeight()); + + editor.world.selectCell(x, y); + editor.selectedCell = editor.world.selectedCell; + + switch(editor.command) { + case 'topwall': + editor.selectedCell.hasTopWall = (editor.selectedCell.hasTopWall === true) ? false : true; + break; + case 'leftwall': + editor.selectedCell.hasLeftWall = (editor.selectedCell.hasLeftWall === true) ? false : true; + break; + case 'addbaggle': + editor.selectedCell.hasBaggle = (editor.selectedCell.hasBaggle === true) ? false : true; + break; + case 'buggle': + buggleCommand(x, y); + break; + case 'deletebuggle': + deleteBuggleCommand(x, y); + break; + case 'color': + colorCommand(); + break; + case 'pickcolor': + pickColorCommand(); + break; + case 'text': + textCommand(); + break; + case 'addline': + addLineCommand(); + break; + case 'addcolumn': + addColumnCommand(); + break; + case 'deleteline': + deleteLineCommand(y); + break; + case 'deletecolumn': + deleteColumnCommand(x); + break; + } + + if(editor.command != 'buggle') { + editor.selectedBuggle = null; + } + + editor.drawService.update(); + } + + + function addWorld(name) { + var newWorld = new BuggleWorld(); + + if(angular.isDefined(name)) { + newWorld.name = name; + } + else { + do { + editor.nbWorldsAdded++; + newWorld.name = 'New world ' + editor.nbWorldsAdded; + } while(editor.getWorldIndex(newWorld.name) !== -1); + } + + editor.initialWorlds.push(newWorld); + editor.deselectAll(); + editor.world = newWorld; + editor.currentWorldName = editor.world.name; + editor.drawService.setWorld(newWorld); + } + + function deleteWorld(name) { + var nbWorlds = editor.initialWorlds.length; + var index; + + if(nbWorlds !== 1) { + index = editor.getWorldIndex(name); + } + + if(index > -1) { + editor.deselectAll(); + if(index === nbWorlds - 1) { + editor.world = editor.initialWorlds[index - 1]; + } + else { + editor.world = editor.initialWorlds[index + 1]; + } + editor.currentWorldName = editor.world.name; + editor.drawService.setWorld(editor.world); + editor.initialWorlds.splice(index, 1); + } + } + + function switchWorld(name) { + for(var i = 0 ; i < editor.initialWorlds.length ; i++) { + var world = editor.initialWorlds[i]; + if(world.name === name) { + editor.deselectAll(); + editor.world = world; + editor.currentWorldName = editor.world.name; + editor.drawService.setWorld(world); + } + } + } + + function deselectAll() { + if(editor.world !== null) { + editor.world.deselectCell(); + editor.selectedCell = null; + editor._selectedBuggleID = null; + editor.selectedBuggle = null; + } + } + + function getWorldIndex(name) { + var index = -1; + var i = 0; + + while(i < editor.initialWorlds.length && index === -1) { + if(editor.initialWorlds[i].name === name) { + index = i; + } + i++; + } + + return index; + } + + function worldName(newName) { + if(editor.world === null) { + return null; + } + + if(angular.isDefined(newName) && editor.getWorldIndex(newName) === -1) { + editor.world.name = newName; + editor.currentWorldName = newName; + } + return editor.world.name; + } + + function buggleCommand(x, y) { + var buggle; + var bugglesID; + var buggleID; + var i = 0; + var buggleFound = false; + + bugglesID = Object.keys(editor.world.entities); + while(i < bugglesID.length && !buggleFound) { + buggleID = bugglesID[i]; + buggle = editor.world.entities[buggleID]; + if(buggle.x == x && buggle.y == y) { + buggleFound = true; + } + i++; + } + if(!buggleFound) { + do { + editor.nbBugglesCreated++; + buggleID = 'buggle' + editor.nbBugglesCreated; + } while(editor.world.entities.hasOwnProperty(buggleID)) + + buggle = { + x: x, + y: y, + color: [0, 0, 0, 255], + direction: 0, + carryBaggle: false, + brushDown: false + }; + editor.world.entities[buggleID] = buggle; + } + + editor._selectedBuggleID = buggleID; + editor.selectedBuggle = buggle; + } + + function deleteBuggleCommand(x, y) { + var deleted = false; + var bugglesID = Object.keys(editor.world.entities); + var buggle; + var buggleID; + var i = 0; + + while(i < bugglesID.length && !deleted) { + buggleID = bugglesID[i]; + buggle = editor.world.entities[buggleID]; + if(buggle.x == x && buggle.y == y) { + delete editor.world.entities[buggleID]; + } + i++; + } + } + + function colorCommand() { + var sameColor = true; + var i = 0; + while(i < editor.selectedCell.color.length && sameColor) { + if(editor.selectedCell.color[i] !== editor.color[i]) { + sameColor = false; + } + i++; + } + editor.selectedCell.color = (sameColor) ? [255,255,255,255] : editor.color; + } + + function pickColorCommand() { + editor.color = editor.selectedCell.color.slice(); + editor.setCommand('color'); + } + + function textCommand() { + $scope.contentForm.cellContent.$rollbackViewValue(); + $('#setTextModal').foundation('reveal', 'open'); + } + + function addLineCommand() { + $('#addLineModal').foundation('reveal', 'open'); + } + + function addColumnCommand() { + $('#addColumnModal').foundation('reveal', 'open'); + } + + function deleteWorldCommand() { + if(editor.initialWorlds.length > 1) { + $('#deleteWorldModal').foundation('reveal', 'open'); + } + } + + function deleteLineCommand(y) { + if(editor.world.height > 1) { + editor.world.addLine(y, -1); + } + else { + displayError('You can\'t delete more lines'); + } + } + + function deleteColumnCommand(x) { + if(editor.world.width > 1) { + editor.world.addColumn(x, -1); + } + else { + displayError('You can\'t delete more columns'); + } + } + + function setRGBColor() { + var newColor = color.strToRGB(editor.customColorInput); + if(newColor !== null) { + editor.color = newColor; + editor.color.push(255); + } + editor.customColorInput = color.RGBtoStr(editor.color); + } + + function addLineAbove() { + editor.world.addLine(editor.selectedCell.y); + editor.drawService.update(); + } + + function addLineBelow() { + editor.world.addLine(editor.selectedCell.y + 1); + editor.drawService.update(); + } + + function addColumnLeft() { + editor.world.addColumn(editor.selectedCell.x); + editor.drawService.update(); + } + + function addColumnRight() { + editor.world.addColumn(editor.selectedCell.x + 1); + editor.drawService.update(); + } + + function setCommand(command) { + switch(command) { + case 'topwall': + editor.lastWallCommand = topWallCmd; + editor.commandType = 'wall'; + break; + case 'leftwall': + editor.lastWallCommand = leftWallCmd; + editor.commandType = 'wall'; + break; + case 'buggle': + editor.lastBuggleCommand = buggleCmd; + editor.commandType = 'buggle'; + break; + case 'deletebuggle': + editor.lastBuggleCommand = deleteBuggleCmd; + editor.commandType = 'buggle'; + break; + case 'addline': + editor.lastGridCommand = addLineCmd; + editor.commandType = 'grid'; + break; + case 'deleteline': + editor.lastGridCommand = removeLineCmd; + editor.commandType = 'grid'; + break; + case 'addcolumn': + editor.lastGridCommand = addColumnCmd; + editor.commandType = 'grid'; + break; + case 'deletecolumn': + editor.lastGridCommand = removeColumnCmd; + editor.commandType = 'grid'; + break; + default: + editor.commandType = ''; + } + editor.command = command; + } + + function editorColor(newColor) { + if(angular.isDefined(newColor)) { + newColor = color.nameToRGB(newColor) || color.strToRGB(newColor); + if(newColor) { + newColor.push(255); + editor.color = newColor; + } + } + return editor.color; + } + + function selectedBuggleColor(newColor) { + if(editor.selectedBuggle === null) { + return null; + } + if(angular.isDefined(newColor)) { + newColor = color.strToRGB(newColor) || color.nameToRGB(newColor); + if(newColor) { + newColor.push(255); + editor.selectedBuggle.color = newColor; + editor.drawService.update(); + } + } + return color.RGBtoName(editor.selectedBuggle.color) || color.RGBtoStr(editor.selectedBuggle.color); + } + + function selectedBuggleID(newID) { + if(editor.world === null) { + return null; + } + if(angular.isDefined(newID)) { + editor._selectedBuggleID = editor.world.changeBuggleID(editor._selectedBuggleID, newID); + } + return editor._selectedBuggleID; + } + + function selectedCellColor(newColor) { + if(editor.selectedCell === null) { + return null; + } + if(angular.isDefined(newColor)) { + newColor = color.strToRGB(newColor) || color.nameToRGB(newColor); + if(newColor) { + newColor.push(255); + editor.selectedCell.color = newColor; + editor.drawService.update(); + } + } + return color.RGBtoName(editor.selectedCell.color) || color.RGBtoStr(editor.selectedCell.color); + } + + function selectedCellContent(newContent) { + if(editor.selectedCell === null) { + return null; + } + if(angular.isDefined(newContent)) { + editor.selectedCell.content = newContent; + editor.selectedCell.hasContent = newContent !== ''; + } + return editor.selectedCell.content; + } + + function height(newHeight) { + if(editor.world === null) { + return null; + } + if(angular.isDefined(newHeight)) { + newHeight = parseInt(newHeight); + if(!isNaN(newHeight) && newHeight > 0) { + editor.world.setHeight(newHeight); + editor.drawService.update(); + } + } + return editor.world.height; + } + + function width(newWidth) { + if(editor.world === null) { + return null; + } + if(angular.isDefined(newWidth)) { + newWidth = parseInt(newWidth); + if(!isNaN(newWidth) && newWidth > 0) { + editor.world.setWidth(newWidth); + editor.drawService.update(); + } + } + return editor.world.width; + } + + + /* + Mission editor + */ + + function filterMission() { + var listLang = []; + for(var lang in editor.missionProgrammingLanguages) { + if(editor.missionProgrammingLanguages.hasOwnProperty(lang) && lang !== 'all') { + if(editor.missionProgrammingLanguages[lang]) { + listLang.push(lang); + } + } + } + var args = { + missionText: editor.missionText, + all: editor.missionProgrammingLanguages.all, + languages: listLang + }; + connection.sendMessage('filterMission', args); + } + + + /* + Solution editor + */ + + function initSolutionEditor() { + getExercise(editorExerciseLessonID, editorExerciseID, 'getExercise'); + + canvasID = 'solutionCanvas'; + editor.drawingArea = 'solutionDrawingArea'; + initCanvas(BuggleWorldView.draw); + + editor.world.deselectCell(); + + worlds.init(); + worlds.setDrawService(editor.drawService); + var initialWorlds = {}; + for(var i = 0 ; i < editor.initialWorlds.length ; i++) { + var world = editor.initialWorlds[i]; + initialWorlds[world.name] = world; + } + worlds.setWorlds(editor.world.name, initialWorlds); + } + + function runSolution() { + worlds.setUpdateViewLoop(null); + worlds.isPlaying(true); + worlds.resetAll('current', false); + worlds.setCurrentWorld('current'); + + var args = { + lessonID: editorExerciseLessonID, + exerciseID: editorExerciseID, + lang: ide.programmingLanguage().lang, + code: editor.solutionCodes[ide.programmingLanguage().lang.toLowerCase()], + world: worlds.currentWorld() + }; + connection.sendMessage('editorRunSolution', args); + worlds.isRunning(true); + } + + function setEditorExercise(data) { + editor.api = $sce.trustAsHtml(data.api); + ide.programmingLanguages(data.programmingLanguages, data.currentProgrammingLanguage); + $(document).foundation('dropdown', 'reflow'); + } + + + /* + Save + */ + + function saveExercise() { + var lightInitialWorlds = []; + + for(var i = 0 ; i < editor.initialWorlds.length ; i++) { + lightInitialWorlds[i] = editor.initialWorlds[i].toLightJSON() + } + + var args = { + name: editor.exerciseName, + worlds: lightInitialWorlds, + mission: editor.missionText + }; + + connection.sendMessage('editorSaveExercise', args); + } + + $scope.$on('$destroy',function() { + offDisplayMessage(); + worlds.init(); + editor.drawService.setWorld(null); + editor.api = null; + }); + } +})(); \ No newline at end of file diff --git a/public/app/editor/editor.html b/public/app/editor/editor.html new file mode 100644 index 00000000..120883ec --- /dev/null +++ b/public/app/editor/editor.html @@ -0,0 +1,56 @@ +
+
+ +
+
+ +
+
+ +
+ + + +
+
+ +
+
+ +
+
+
+ +
+

Error

+
+ {{editor.errorMessage}} +
+ × +
+
+ +
+ +
+ +
+ +
+ +
+
+
+
+ +
+
+ Save +
+
+
+
\ No newline at end of file diff --git a/public/app/exercise/exercise.controller.js b/public/app/exercise/exercise.controller.js index 241d11f8..16dd9886 100644 --- a/public/app/exercise/exercise.controller.js +++ b/public/app/exercise/exercise.controller.js @@ -8,7 +8,7 @@ Exercise.$inject = [ '$window', '$http', '$scope', '$sce', '$stateParams', 'connection', 'listenersHandler', 'langs', 'exercisesList', - 'canvas', 'drawWithDOM', + 'canvas', 'drawWithDOM', 'ide', 'worlds', '$timeout', '$interval', 'locker', 'BuggleWorld', 'BuggleWorldView', @@ -21,7 +21,7 @@ function Exercise($window, $http, $scope, $sce, $stateParams, connection, listenersHandler, langs, exercisesList, - canvas, drawWithDOM, + canvas, drawWithDOM, ide, worlds, $timeout, $interval, locker, BuggleWorld, BuggleWorldView, @@ -39,7 +39,7 @@ exercise.currentTab = 0; exercise.drawFnct = null; - + exercise.worlds = worlds; exercise.lessonID = $stateParams.lessonID; exercise.id = $stateParams.exerciseID; @@ -47,9 +47,6 @@ exercise.displayInstructions = 'instructions'; exercise.displayResults = 'result'; - exercise.isRunning = false; - exercise.isPlaying = false; - exercise.isChangingProgLang = false; exercise.playedDemo = false; exercise.instructions = null; @@ -59,28 +56,10 @@ exercise.logs = ''; exercise.nonImplementedWorldException = false; - - exercise.initialWorlds = {}; - exercise.answerWorlds = {}; - exercise.currentWorlds = {}; - exercise.currentWorld = null; - exercise.currentWorldID = null; - exercise.worldKind = 'current'; - exercise.worldIDs = []; // Mandatory to generate dynamically the select - exercise.updateModelLoop = null; - exercise.updateViewLoop = null; locker.bind($scope, 'timer', 1000); - exercise.timer = locker.get('timer'); - - exercise.currentState = -1; - exercise.lastStateDrawn = -1; - exercise.preventLoop = false; - - exercise.currentProgrammingLanguage = null; - exercise.programmingLanguages = []; - exercise.editor = null; + exercise.ideService = ide; exercise.exercisesAsList = null; exercise.exercisesAsTree = null; @@ -103,14 +82,9 @@ exercise.runDemo = runDemo; exercise.runCode = runCode; - exercise.reset = reset; - exercise.replay = replay; exercise.stopExecution = stopExecution; - exercise.setWorldState = setWorldState; - exercise.setCurrentWorld = setCurrentWorld; exercise.switchToTab = switchToTab; - exercise.setProgrammingLanguage = setProgrammingLanguage; exercise.setSelectedRootLecture = setSelectedRootLecture; exercise.setSelectedNextExercise = setSelectedNextExercise; exercise.updateSpeed = updateSpeed; @@ -118,7 +92,7 @@ exercise.resizeInstructions = resizeInstructions; $scope.codemirrorLoaded = function(_editor){ - exercise.editor = _editor; + ide.init(_editor); resizeCodeMirror(); }; @@ -136,7 +110,17 @@ $scope.$on('exercisesListReady', initExerciseSelector); var offDisplayMessage = listenersHandler.register('onmessage', handleMessage); - getExercise(); + + function waitCanvas() { + var c = document.getElementById(canvasID); + if(c === null) { + $timeout(waitCanvas, 0); + } + else { + getExercise(); + } + } + waitCanvas(); function handleMessage(data) { console.log('message received: ', data); @@ -151,20 +135,19 @@ break; case 'demoEnded': console.log('The demo ended!'); - exercise.isRunning = false; + worlds.isRunning(false); break; case 'operations': - handleOperations(args.worldID, 'current', args.operations); + worlds.handleOperations(args.worldID, 'current', args.operations); break; case 'demoOperations': - handleOperations(args.worldID, 'answer', args.operations); + worlds.handleOperations(args.worldID, 'answer', args.operations); break; case 'log': exercise.logs += args.msg; break; case 'newProgLang': - updateUI(args.newProgLang, args.instructions, null, args.code); - exercise.isChangingProgLang = false; + updateUI(args.instructions, null); break; case 'newHumanLang': updateUI(exercise.currentProgrammingLanguage, args.instructions, args.api, null); @@ -176,9 +159,10 @@ exercise.id = data.id; exercise.instructions = $sce.trustAsHtml(data.instructions); exercise.api = $sce.trustAsHtml(data.api); - exercise.code = data.code.trim(); - exercise.currentWorldID = data.selectedWorldID; - + ide.code(data.code.trim()); + + worlds.init(); + if(data.exception === 'nonImplementedWorldException') { exercise.nonImplementedWorldException = true; } @@ -186,10 +170,8 @@ if(!exercise.nonImplementedWorldException) { for(var worldID in data.initialWorlds) { if(data.initialWorlds.hasOwnProperty(worldID)) { - exercise.initialWorlds[worldID] = {}; var initialWorld = data.initialWorlds[worldID]; var world; - exercise.nameWorld = initialWorld.type; switch(initialWorld.type) { case 'BuggleWorld': exercise.tabs = [ @@ -208,7 +190,6 @@ ]; exercise.objectiveViewNeeded = true; exercise.animationPlayerNeeded = true; - world = new BuggleWorld(initialWorld); initCanvas(BuggleWorldView.draw); exercise.drawFnct = BuggleWorldView.draw; break; @@ -222,7 +203,6 @@ } ]; exercise.drawFnct = BatWorldView.draw; - world = new BatWorld(initialWorld); BatWorldView.setScope($scope); initDrawWithDOM(BatWorldView.draw); break; @@ -257,7 +237,6 @@ exercise.objectiveViewNeeded = true; exercise.animationPlayerNeeded = true; exercise.secondViewNeeded = true; - world = new SortingWorld(initialWorld); initCanvas(SortingWorldView.draw); break; case 'DutchFlagWorld': @@ -278,7 +257,6 @@ exercise.drawFnct = DutchFlagView.draw; exercise.objectiveViewNeeded = true; exercise.animationPlayerNeeded = true; - world = new DutchFlagWorld(initialWorld); initCanvas(DutchFlagView.draw); break; case 'PancakeWorld' : @@ -299,32 +277,19 @@ exercise.drawFnct = PancakeView.draw; exercise.objectiveViewNeeded = true; exercise.animationPlayerNeeded = true; - world = new PancakeWorld(initialWorld); initCanvas(PancakeView.draw); break; } - exercise.initialWorlds[worldID] = world; - exercise.answerWorlds[worldID] = world.clone(); - exercise.currentWorlds[worldID] = world.clone(); } } - exercise.worldIDs = Object.keys(exercise.currentWorlds); - - setCurrentWorld('current'); - + worlds.setWorlds(data.selectedWorldID, data.initialWorlds); + window.addEventListener('resize', resizeCodeMirror, false); } - - exercise.programmingLanguages = data.programmingLanguages; - for(var i=0; i= c; x--) { + for(y = 0; y < this.height; y++) { + this.cells[x][y].x = x; + } + } + + for(var buggleID in this.entities) { + if(this.entities.hasOwnProperty(buggleID)) { + if(nb < 0 && this.entities[buggleID].x >= columnX && this.entities[buggleID].x <= columnX - nb - 1) { + bugglesToRemove.push(buggleID); + } + else if((nb < 0 && this.entities[buggleID].x > columnX - nb - 1) + || (nb > 0 && this.entities[buggleID].x >= columnX)) { + this.entities[buggleID].x += nb; + } + } + } + + for(i = 0; i < bugglesToRemove.length; i++) { + delete this.entities[bugglesToRemove[i]]; + } + }; + + BuggleWorld.prototype.addLine = function(lineY, nb) { + nb = typeof nb !== 'undefined' ? nb : 1; + var x, y, i, c; + var bugglesToRemove = []; + + for(x = 0; x < this.width; x++) { + c = (nb < 0) ? lineY - nb : lineY; + + for(y = this.height - 1; y >= c; y--) { + this.cells[x][y].y += nb; + } + for(y = 0; y < nb; y++) { + this.cells[x].splice(lineY + y, 0, new BuggleWorldCell({ + color: [255, 255, 255, 255], + content: '', + hasBaggle: false, + hasContent: false, + hasLeftWall: false, + hasTopWall: false, + x: x, + y: lineY + y + })); + } + if(nb < 0) { + this.cells[x].splice(lineY, nb * -1); + } + } + + this.height += nb; + + for(var buggleID in this.entities) { + if(this.entities.hasOwnProperty(buggleID)) { + if(nb < 0 && this.entities[buggleID].y >= lineY && this.entities[buggleID].y <= lineY - nb - 1) { + bugglesToRemove.push(buggleID); + } + else if((nb < 0 && this.entities[buggleID].y > lineY - nb - 1) + || (nb > 0 && this.entities[buggleID].y >= lineY)) { + this.entities[buggleID].y += nb; + } + } + } + + for(i = 0; i < bugglesToRemove.length; i++) { + delete this.entities[bugglesToRemove[i]]; + } + }; + + BuggleWorld.prototype.changeBuggleID= function(buggleID, newID) { + if(this.entities.hasOwnProperty(buggleID) && !this.entities.hasOwnProperty(newID)) { + this.entities[newID] = this.entities[buggleID]; + delete this.entities[buggleID]; + buggleID = newID; + } + return buggleID; + }; + + BuggleWorld.prototype.setHeight = function(newHeight) { + var nbLines = newHeight - this.height; + var position = (nbLines < 0) ? this.height + nbLines : this.height; + this.addLine(position, nbLines); + }; + + BuggleWorld.prototype.setWidth = function(newWidth) { + var nbColumns = newWidth - this.width; + var position = (nbColumns < 0) ? this.width + nbColumns : this.width; + this.addColumn(position, nbColumns); + }; + + BuggleWorld.prototype.newEmptyWorld = function () { + var i, j; + var world = { + entities: {}, + cells: [], + height: 7, + type: 'BuggleWorld', + width: 7 + }; + + for(i = 0; i < world.width; i++) { + world.cells.push([]); + for(j = 0; j < world.height; j++) { + world.cells[i].push({ + color: [255, 255, 255, 255], + content: '', + hasBaggle: false, + hasContent: false, + hasLeftWall: false, + hasTopWall: false, + x: i, + y: j + }); + } + } + return world; + }; + + BuggleWorld.prototype.selectCell = function (x, y) { + if(this.selectedCell !== null) { + this.selectedCell.isSelected = false; + } + this.selectedCell = this.cells[x][y]; + this.cells[x][y].isSelected = true; + }; + + BuggleWorld.prototype.deselectCell = function () { + if(this.selectedCell) { + this.selectedCell.isSelected = false; + } + this.selectedCell = null; + }; + + BuggleWorld.prototype.toLightJSON = function () { + var world = new BuggleWorld(this); + world.cells = []; + + for(var i = 0 ; i < this.cells.length ; i++) { + world.cells[i] = []; + for(var j = 0 ; j < this.cells[i].length ; j++) { + var currentCell = this.cells[i][j]; + if(!currentCell.isEmpty()) { + world.cells[i].push(currentCell.toLightJSON()); + } + } + } + + return world; + }; + return BuggleWorld; } })(); \ No newline at end of file diff --git a/public/app/models/universe/buggle/buggleworldcell.factory.js b/public/app/models/universe/buggle/buggleworldcell.factory.js index 42d7a359..33979546 100644 --- a/public/app/models/universe/buggle/buggleworldcell.factory.js +++ b/public/app/models/universe/buggle/buggleworldcell.factory.js @@ -16,8 +16,39 @@ this.content = cell.content; this.hasLeftWall = cell.hasLeftWall; this.hasTopWall = cell.hasTopWall; + this.isSelected = false; }; - + + BuggleWorldCell.prototype.isColored = function() { + var isColored = false; + var i = 0; + while(!isColored && i < this.color.length) { + isColored = (this.color[i] !== 255); + i++; + } + return isColored; + }; + + BuggleWorldCell.prototype.isEmpty = function() { + return !(this.isColored() || this.hasBaggle || this.hasContent || this.hasLeftWall || this.hasTopWall); + }; + + BuggleWorldCell.prototype.toLightJSON = function() { + var cell = {} + cell.x = this.x; + cell.y = this.y; + if(this.isColored()) cell.color = this.color; + if(this.hasBaggle) cell.hasBaggle = this.hasBaggle; + if(this.hasLeftWall) cell.hasLeftWall = this.hasLeftWall; + if(this.hasTopWall) cell.hasTopWall = this.hasTopWall; + if(this.hasContent) { + cell.hasContent = this.hasContent; + cell.content = this.content; + } + + return cell; + }; + return BuggleWorldCell; } })(); \ No newline at end of file diff --git a/public/app/models/universe/buggle/buggleworldview.factory.js b/public/app/models/universe/buggle/buggleworldview.factory.js index 73d21ffa..8ffd3f85 100644 --- a/public/app/models/universe/buggle/buggleworldview.factory.js +++ b/public/app/models/universe/buggle/buggleworldview.factory.js @@ -74,10 +74,20 @@ var service = { draw: draw, + getCellWidth: getCellWidth, + getCellHeight: getCellHeight }; return service; - + + function getCellWidth() { + return cellWidth; + } + + function getCellHeight() { + return cellHeight; + } + function initUtils(canvas, buggleWorld) { ctx = canvas.getContext('2d'); canvasWidth = canvas.width; @@ -131,6 +141,9 @@ ctx.fillStyle = 'rgb(255, 255, 255)'; } } + if(cell.isSelected) { + ctx.rect(xLeft + 2, yTop + 2, cellWidth - 4, cellHeight - 4); + } ctx.fillRect(xLeft, yTop, xRight, yBottom); if(cell.hasLeftWall) { diff --git a/public/app/services/color.service.js b/public/app/services/color.service.js new file mode 100644 index 00000000..180cd47e --- /dev/null +++ b/public/app/services/color.service.js @@ -0,0 +1,83 @@ +(function(){ + 'use strict'; + + angular + .module('PLMApp') + .factory('color', color); + + function color() { + var colorsNames = ['white', 'black', 'blue', 'cyan', 'darkGray', 'gray', 'green', 'lightGray', 'magenta', 'orange', 'pink', 'red', 'yellow']; + var colorsRGB = [[255, 255, 255], [0, 0, 0], [0, 0, 255], [0, 255, 255], [64, 64, 64], [128, 128, 128], [0, 255, 0], [192, 192, 192], [255, 0, 255], [255, 200, 0], [255, 175, 175], [255, 0, 0], [255, 255, 0]]; + var regexRGB = /^(\d+)\/(\d+)\/(\d+)$/; + + var service = { + getColorsNames: getColorsNames, + nameToHex: nameToHex, + nameToRGB: nameToRGB, + RGBtoHex: RGBtoHex, + RGBtoName: RGBtoName, + RGBtoStr: RGBtoStr, + strToRGB: strToRGB + } + return service; + + function getColorsNames() { + return colorsNames.slice(0); + } + + function nameToHex(name) { + return RGBtoHex(nameToRGB(name)); + } + + function nameToRGB(name) { + var index = colorsNames.indexOf(name); + return (index !== -1) ? colorsRGB[index].slice(0) : null; + } + + function RGBtoHex(rgbValue) { + var red = rgbValue[0].toString(16); + var green = rgbValue[1].toString(16); + var blue = rgbValue[2].toString(16); + red = (red.length === 1) ? 0 + red : red; + green = (green.length === 1) ? 0 + green : green; + blue = (blue.length === 1) ? 0 + blue : blue; + + return red + green + blue; + } + + function RGBtoName(rgbValue) { + var i = 0; + var index = -1; + while(i < colorsRGB.length && index === -1) { + if(colorsRGB[i][0] === rgbValue[0] && colorsRGB[i][1] === rgbValue[1] && colorsRGB[i][2] === rgbValue[2]) { + index = i; + } + i++; + } + return (index !== -1) ? colorsNames[index] : null; + } + + function RGBtoStr(rgbValue) { + var strValue = null; + if(Array.isArray(rgbValue) && rgbValue.length >= 3) { + strValue = rgbValue[0] + '/' + rgbValue[1] + '/' + rgbValue[2]; + } + return strValue; + } + + function strToRGB(strValue) { + var rgbValue = null; + if(regexRGB.test(strValue)) { + rgbValue = []; + rgbValue.push(parseInt(strValue.replace(regexRGB, '$1'))); + rgbValue.push(parseInt(strValue.replace(regexRGB, '$2'))); + rgbValue.push(parseInt(strValue.replace(regexRGB, '$3'))); + rgbValue.forEach(function(num, pos, tab) { + if(num > 255) tab[pos] = 255; + else if(num < 0) tab[pos] = 0; + }); + } + return rgbValue; + } + } +})(); \ No newline at end of file diff --git a/public/app/services/ide.service.js b/public/app/services/ide.service.js new file mode 100644 index 00000000..c816e49a --- /dev/null +++ b/public/app/services/ide.service.js @@ -0,0 +1,122 @@ +(function(){ + 'use strict'; + + angular + .module('PLMApp') + .factory('ide', ide); + + ide.$inject = ['listenersHandler', 'connection']; + + function ide(listenersHandler, connection) { + + var _code; + var _codeEditor; + var _currentProgrammingLanguage; + var _isChangingProgLang; + var _programmingLanguages; + + listenersHandler.register('onmessage', handleMessage); + + var service = { + init: init, + code: code, + codeEditor: codeEditor, + isChangingProgLang: isChangingProgLang, + programmingLanguage: programmingLanguage, + programmingLanguages: programmingLanguages, + setIDEMode: setIDEMode, + update: update + }; + + return service; + + function handleMessage(data) { + var cmd = data.cmd; + var args = data.args; + switch(cmd) { + case 'newProgLang': + update(args.newProgLang, args.code); + _isChangingProgLang = false; + break; + } + } + + function init(editorElt) { + _code = ''; + _codeEditor = editorElt; + _currentProgrammingLanguage = null; + _isChangingProgLang = false; + _programmingLanguages = []; + } + + function code(newCode) { + if(angular.isDefined(newCode)) { + _code = newCode; + } + return _code; + } + + function codeEditor() { + return _codeEditor; + } + + function isChangingProgLang(isChanging) { + if(angular.isDefined(isChanging)) { + _isChangingProgLang = isChanging; + } + return _isChangingProgLang; + } + + function programmingLanguage(newLang) { + if(angular.isDefined(newLang)) { + _isChangingProgLang = true; + connection.sendMessage('setProgrammingLanguage', { programmingLanguage: newLang.lang }); + } + else { + return _currentProgrammingLanguage; + } + } + + function programmingLanguages(progLangs, currentProgLang) { + if(angular.isDefined(progLangs) && angular.isDefined(currentProgLang)) { + _programmingLanguages = progLangs; + + for(var i = 0; i < _programmingLanguages.length; i++) { + var pl = _programmingLanguages[i]; + if(pl.lang === currentProgLang) { + _currentProgrammingLanguage = pl; + setIDEMode(pl); + } + } + } + return _programmingLanguages; + } + + function setIDEMode(pl) { + switch(pl.lang.toLowerCase()) { + case 'java': + _codeEditor.setOption('mode', 'text/x-java'); + break; + case 'scala': + _codeEditor.setOption('mode', 'text/x-scala'); + break; + case 'c': + _codeEditor.setOption('mode', 'text/x-csrc'); + break; + case 'python': + _codeEditor.setOption('mode', 'text/x-python'); + break; + } + } + + function update(pl, code) { + if(pl !== null) { + _currentProgrammingLanguage = pl; + setIDEMode(pl); + } + if(code !== null) { + _code = code; + } + } + } +})(); diff --git a/public/app/services/worlds.service.js b/public/app/services/worlds.service.js new file mode 100644 index 00000000..5e17b7cc --- /dev/null +++ b/public/app/services/worlds.service.js @@ -0,0 +1,292 @@ +( function () { + 'use strict'; + + angular + .module('PLMApp') + .factory('worlds', worlds); + + worlds.$inject = ['$timeout', '$interval', + 'locker', + 'BuggleWorld', 'BatWorld', 'SortingWorld', 'DutchFlagWorld', 'PancakeWorld']; + + function worlds($timeout, $interval, + locker, + BuggleWorld, BatWorld, SortingWorld, DutchFlagWorld, PancakeWorld) { + + var initialWorlds; + var answerWorlds; + var currentWorlds; + var _currentWorld; + var _currentWorldID; + var _currentState; + var _drawService; + var _drawServiceType; + var _isPlaying; + var _isRunning; + var _worldIDs; + var _worldKind; + var preventLoop; + var lastStateDrawn; + var updateModelLoop; + var updateViewLoop; + var _timer; + + var service = { + init: init, + currentWorld: currentWorld, + setCurrentWorld: setCurrentWorld, + currentWorldID: currentWorldID, + currentState: currentState, + drawServiceType: drawServiceType, + setDrawService: setDrawService, + isPlaying: isPlaying, + isRunning: isRunning, + timer: timer, + worldIDs: worldIDs, + worldKind: worldKind, + getWorldsByKind: getWorldsByKind, + getWorld: getWorld, + setWorlds: setWorlds, + startUpdateModelLoop: startUpdateModelLoop, + startUpdateViewLoop: startUpdateViewLoop, + updateModel: updateModel, + updateView: updateView, + handleOperations: handleOperations, + replay: replay, + setUpdateViewLoop: setUpdateViewLoop, + reset: reset, + resetAll: resetAll + }; + + return service; + + function init() { + initialWorlds = {}; + answerWorlds = {}; + currentWorlds = {}; + _currentWorld = null; + _currentState = -1; + _drawService = null; + _drawServiceType = ''; + _isPlaying = false; + _isRunning = false; + _currentWorldID = null; + _worldKind = 'current'; + _worldIDs = []; + preventLoop = false; + lastStateDrawn = -1; + updateModelLoop = null; + updateViewLoop = null; + _timer = locker.get('timer'); + } + + function currentWorld() { + return _currentWorld; + } + + function currentWorldID(id) { + return arguments.length ? (_currentWorldID = id) : _currentWorldID; + } + + function setCurrentWorld(worldKind) { + $timeout.cancel(updateModelLoop); + $interval.cancel(updateViewLoop); + _worldKind = worldKind; + _currentWorld = getWorldsByKind(_worldKind)[_currentWorldID]; + $timeout(function () { + _currentState = _currentWorld.currentState; + }, 0); + _drawService.setWorld(_currentWorld); + } + + function currentState(state) { + if(arguments.length) { + $timeout.cancel(updateModelLoop); + $interval.cancel(updateViewLoop); + _isPlaying = false; + state = parseInt(state); + _currentWorld.setState(state); + _currentState = state; + _drawService.update(); + } + return _currentState; + } + + function drawServiceType() { + return _drawServiceType; + } + + function setDrawService(ds, dst) { + _drawService = ds; + _drawServiceType = dst; + } + + function isPlaying(playing) { + return arguments.length ? (_isPlaying = playing) : _isPlaying; + } + + function isRunning(running) { + return arguments.length ? (_isRunning = running) : _isRunning; + } + + function timer(time) { + return arguments.length ? (_timer = time) : _timer; + } + + function worldIDs() { + return _worldIDs; + } + + function worldKind() { + return _worldKind; + } + + function setUpdateViewLoop(uvl) { + updateViewLoop = uvl; + } + + function getWorldsByKind(wk) { + var worlds; + switch(wk) { + case 'initial': + worlds = initialWorlds; + break; + case 'current': + worlds = currentWorlds; + break; + case 'answer': + worlds = answerWorlds; + break; + } + return worlds; + } + + function getWorld(wid, wk) { + return getWorldsByKind(wk)[wid]; + } + + function setWorlds(newSelectedWorldID, newInitalWorlds) { + _currentWorldID = newSelectedWorldID; + + for(var worldID in newInitalWorlds) { + if(newInitalWorlds.hasOwnProperty(worldID)) { + initialWorlds[worldID] = {}; + var initialWorld = newInitalWorlds[worldID]; + var world; + switch(initialWorld.type) { + case 'BuggleWorld': + world = new BuggleWorld(initialWorld); + break; + case 'BatWorld': + world = new BatWorld(initialWorld); + break; + case 'SortingWorld': + world = new SortingWorld(initialWorld); + break; + case 'DutchFlagWorld': + world = new DutchFlagWorld(initialWorld); + break; + case 'PancakeWorld': + world = new PancakeWorld(initialWorld); + break; + } + + initialWorlds[worldID] = world; + answerWorlds[worldID] = world.clone(); + currentWorlds[worldID] = world.clone(); + } + } + + _worldIDs = Object.keys(currentWorlds); + + setCurrentWorld('current'); + } + + function handleOperations(wID, wk, op) { + var world = getWorldsByKind(wk)[wID]; + world.addOperations(op); + if(world === _currentWorld && updateViewLoop === null) { + _isPlaying = true; + startUpdateModelLoop(); + startUpdateViewLoop(); + } + } + + function replay() { + reset(_currentWorldID, _worldKind, true); + _isPlaying = true; + startUpdateModelLoop(); + startUpdateViewLoop(); + } + + function reset(wID, wk, keepOp) { + // We may want to keep the operations in order to replay the execution + var operations = []; + var steps = []; + var worlds = getWorldsByKind(wk); + + if(keepOp === true) { + operations = worlds[wID].operations; + steps = worlds[wID].steps; + } + + var initialWorld = initialWorlds[wID]; + worlds[wID] = initialWorld.clone(); + worlds[wID].operations = operations; + worlds[wID].steps = steps; + + if(wID === _currentWorldID) { + _currentState = -1; + _currentWorld = worlds[wID]; + _drawService.setWorld(_currentWorld); + } + + lastStateDrawn = -1; + + $timeout.cancel(updateViewLoop); + _isPlaying = false; + } + + function resetAll(wk, keepOp) { + _worldIDs.map(function(key) { + reset(key, wk, keepOp); + }); + } + + function startUpdateModelLoop() { + updateModelLoop = $timeout(updateModel, _timer); + } + + function updateModel() { + var currentState = _currentWorld.currentState; + var nbStates = _currentWorld.operations.length-1; + if(currentState !== nbStates) { + _currentWorld.setState(++currentState); + _currentState = currentState; + } + + if(!_isRunning && currentState === nbStates){ + updateModelLoop = null; + _isPlaying = false; + } + else { + updateModelLoop = $timeout(updateModel, _timer); + } + } + + function startUpdateViewLoop() { + updateViewLoop = $interval(updateView, 1/10); + } + + function updateView() { + if(lastStateDrawn !== _currentWorld.currentState) { + _drawService.update(); + lastStateDrawn = _currentWorld.currentState; + } + + if(!_isPlaying){ + $interval.cancel(updateViewLoop); + } + } + } +})(); diff --git a/public/images/edit_baggle.png b/public/images/edit_baggle.png new file mode 100644 index 00000000..d345be47 Binary files /dev/null and b/public/images/edit_baggle.png differ diff --git a/public/images/paint-cursor.png b/public/images/paint-cursor.png new file mode 100644 index 00000000..e1697d9a Binary files /dev/null and b/public/images/paint-cursor.png differ diff --git a/public/images/pipette-cursor.gif b/public/images/pipette-cursor.gif new file mode 100644 index 00000000..e1ece433 Binary files /dev/null and b/public/images/pipette-cursor.gif differ diff --git a/public/javascripts/angular-sanitize.js b/public/javascripts/angular-sanitize.js new file mode 100644 index 00000000..61c50899 --- /dev/null +++ b/public/javascripts/angular-sanitize.js @@ -0,0 +1,679 @@ +/** + * @license AngularJS v1.3.15 + * (c) 2010-2014 Google, Inc. http://angularjs.org + * License: MIT + */ +(function(window, angular, undefined) {'use strict'; + +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * Any commits to this file should be reviewed with security in mind. * + * Changes to this file can potentially create security vulnerabilities. * + * An approval from 2 Core members with history of modifying * + * this file is required. * + * * + * Does the change somehow allow for arbitrary javascript to be executed? * + * Or allows for someone to change the prototype of built-in objects? * + * Or gives undesired access to variables likes document or window? * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +var $sanitizeMinErr = angular.$$minErr('$sanitize'); + +/** + * @ngdoc module + * @name ngSanitize + * @description + * + * # ngSanitize + * + * The `ngSanitize` module provides functionality to sanitize HTML. + * + * + *
+ * + * See {@link ngSanitize.$sanitize `$sanitize`} for usage. + */ + +/* + * HTML Parser By Misko Hevery (misko@hevery.com) + * based on: HTML Parser By John Resig (ejohn.org) + * Original code by Erik Arvidsson, Mozilla Public License + * http://erik.eae.net/simplehtmlparser/simplehtmlparser.js + * + * // Use like so: + * htmlParser(htmlString, { + * start: function(tag, attrs, unary) {}, + * end: function(tag) {}, + * chars: function(text) {}, + * comment: function(text) {} + * }); + * + */ + + +/** + * @ngdoc service + * @name $sanitize + * @kind function + * + * @description + * The input is sanitized by parsing the HTML into tokens. All safe tokens (from a whitelist) are + * then serialized back to properly escaped html string. This means that no unsafe input can make + * it into the returned string, however, since our parser is more strict than a typical browser + * parser, it's possible that some obscure input, which would be recognized as valid HTML by a + * browser, won't make it through the sanitizer. The input may also contain SVG markup. + * The whitelist is configured using the functions `aHrefSanitizationWhitelist` and + * `imgSrcSanitizationWhitelist` of {@link ng.$compileProvider `$compileProvider`}. + * + * @param {string} html HTML input. + * @returns {string} Sanitized HTML. + * + * @example + + + +
+ Snippet: + + + + + + + + + + + + + + + + + + + + + + + + + +
DirectiveHowSourceRendered
ng-bind-htmlAutomatically uses $sanitize
<div ng-bind-html="snippet">
</div>
ng-bind-htmlBypass $sanitize by explicitly trusting the dangerous value +
<div ng-bind-html="deliberatelyTrustDangerousSnippet()">
+</div>
+
ng-bindAutomatically escapes
<div ng-bind="snippet">
</div>
+
+
+ + it('should sanitize the html snippet by default', function() { + expect(element(by.css('#bind-html-with-sanitize div')).getInnerHtml()). + toBe('

an html\nclick here\nsnippet

'); + }); + + it('should inline raw snippet if bound to a trusted value', function() { + expect(element(by.css('#bind-html-with-trust div')).getInnerHtml()). + toBe("

an html\n" + + "click here\n" + + "snippet

"); + }); + + it('should escape snippet without any filter', function() { + expect(element(by.css('#bind-default div')).getInnerHtml()). + toBe("<p style=\"color:blue\">an html\n" + + "<em onmouseover=\"this.textContent='PWN3D!'\">click here</em>\n" + + "snippet</p>"); + }); + + it('should update', function() { + element(by.model('snippet')).clear(); + element(by.model('snippet')).sendKeys('new text'); + expect(element(by.css('#bind-html-with-sanitize div')).getInnerHtml()). + toBe('new text'); + expect(element(by.css('#bind-html-with-trust div')).getInnerHtml()).toBe( + 'new text'); + expect(element(by.css('#bind-default div')).getInnerHtml()).toBe( + "new <b onclick=\"alert(1)\">text</b>"); + }); +
+
+ */ +function $SanitizeProvider() { + this.$get = ['$$sanitizeUri', function($$sanitizeUri) { + return function(html) { + var buf = []; + htmlParser(html, htmlSanitizeWriter(buf, function(uri, isImage) { + return !/^unsafe/.test($$sanitizeUri(uri, isImage)); + })); + return buf.join(''); + }; + }]; +} + +function sanitizeText(chars) { + var buf = []; + var writer = htmlSanitizeWriter(buf, angular.noop); + writer.chars(chars); + return buf.join(''); +} + + +// Regular Expressions for parsing tags and attributes +var START_TAG_REGEXP = + /^<((?:[a-zA-Z])[\w:-]*)((?:\s+[\w:-]+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)\s*(>?)/, + END_TAG_REGEXP = /^<\/\s*([\w:-]+)[^>]*>/, + ATTR_REGEXP = /([\w:-]+)(?:\s*=\s*(?:(?:"((?:[^"])*)")|(?:'((?:[^'])*)')|([^>\s]+)))?/g, + BEGIN_TAG_REGEXP = /^/g, + DOCTYPE_REGEXP = /]*?)>/i, + CDATA_REGEXP = //g, + SURROGATE_PAIR_REGEXP = /[\uD800-\uDBFF][\uDC00-\uDFFF]/g, + // Match everything outside of normal chars and " (quote character) + NON_ALPHANUMERIC_REGEXP = /([^\#-~| |!])/g; + + +// Good source of info about elements and attributes +// http://dev.w3.org/html5/spec/Overview.html#semantics +// http://simon.html5.org/html-elements + +// Safe Void Elements - HTML5 +// http://dev.w3.org/html5/spec/Overview.html#void-elements +var voidElements = makeMap("area,br,col,hr,img,wbr"); + +// Elements that you can, intentionally, leave open (and which close themselves) +// http://dev.w3.org/html5/spec/Overview.html#optional-tags +var optionalEndTagBlockElements = makeMap("colgroup,dd,dt,li,p,tbody,td,tfoot,th,thead,tr"), + optionalEndTagInlineElements = makeMap("rp,rt"), + optionalEndTagElements = angular.extend({}, + optionalEndTagInlineElements, + optionalEndTagBlockElements); + +// Safe Block Elements - HTML5 +var blockElements = angular.extend({}, optionalEndTagBlockElements, makeMap("address,article," + + "aside,blockquote,caption,center,del,dir,div,dl,figure,figcaption,footer,h1,h2,h3,h4,h5," + + "h6,header,hgroup,hr,ins,map,menu,nav,ol,pre,script,section,table,ul")); + +// Inline Elements - HTML5 +var inlineElements = angular.extend({}, optionalEndTagInlineElements, makeMap("a,abbr,acronym,b," + + "bdi,bdo,big,br,cite,code,del,dfn,em,font,i,img,ins,kbd,label,map,mark,q,ruby,rp,rt,s," + + "samp,small,span,strike,strong,sub,sup,time,tt,u,var")); + +// SVG Elements +// https://wiki.whatwg.org/wiki/Sanitization_rules#svg_Elements +var svgElements = makeMap("animate,animateColor,animateMotion,animateTransform,circle,defs," + + "desc,ellipse,font-face,font-face-name,font-face-src,g,glyph,hkern,image,linearGradient," + + "line,marker,metadata,missing-glyph,mpath,path,polygon,polyline,radialGradient,rect,set," + + "stop,svg,switch,text,title,tspan,use"); + +// Special Elements (can contain anything) +var specialElements = makeMap("script,style"); + +var validElements = angular.extend({}, + voidElements, + blockElements, + inlineElements, + optionalEndTagElements, + svgElements); + +//Attributes that have href and hence need to be sanitized +var uriAttrs = makeMap("background,cite,href,longdesc,src,usemap,xlink:href"); + +var htmlAttrs = makeMap('abbr,align,alt,axis,bgcolor,border,cellpadding,cellspacing,class,clear,' + + 'color,cols,colspan,compact,coords,dir,face,headers,height,hreflang,hspace,' + + 'ismap,lang,language,nohref,nowrap,rel,rev,rows,rowspan,rules,' + + 'scope,scrolling,shape,size,span,start,summary,target,title,type,' + + 'valign,value,vspace,width'); + +// SVG attributes (without "id" and "name" attributes) +// https://wiki.whatwg.org/wiki/Sanitization_rules#svg_Attributes +var svgAttrs = makeMap('accent-height,accumulate,additive,alphabetic,arabic-form,ascent,' + + 'attributeName,attributeType,baseProfile,bbox,begin,by,calcMode,cap-height,class,color,' + + 'color-rendering,content,cx,cy,d,dx,dy,descent,display,dur,end,fill,fill-rule,font-family,' + + 'font-size,font-stretch,font-style,font-variant,font-weight,from,fx,fy,g1,g2,glyph-name,' + + 'gradientUnits,hanging,height,horiz-adv-x,horiz-origin-x,ideographic,k,keyPoints,' + + 'keySplines,keyTimes,lang,marker-end,marker-mid,marker-start,markerHeight,markerUnits,' + + 'markerWidth,mathematical,max,min,offset,opacity,orient,origin,overline-position,' + + 'overline-thickness,panose-1,path,pathLength,points,preserveAspectRatio,r,refX,refY,' + + 'repeatCount,repeatDur,requiredExtensions,requiredFeatures,restart,rotate,rx,ry,slope,stemh,' + + 'stemv,stop-color,stop-opacity,strikethrough-position,strikethrough-thickness,stroke,' + + 'stroke-dasharray,stroke-dashoffset,stroke-linecap,stroke-linejoin,stroke-miterlimit,' + + 'stroke-opacity,stroke-width,systemLanguage,target,text-anchor,to,transform,type,u1,u2,' + + 'underline-position,underline-thickness,unicode,unicode-range,units-per-em,values,version,' + + 'viewBox,visibility,width,widths,x,x-height,x1,x2,xlink:actuate,xlink:arcrole,xlink:role,' + + 'xlink:show,xlink:title,xlink:type,xml:base,xml:lang,xml:space,xmlns,xmlns:xlink,y,y1,y2,' + + 'zoomAndPan'); + +var validAttrs = angular.extend({}, + uriAttrs, + svgAttrs, + htmlAttrs); + +function makeMap(str) { + var obj = {}, items = str.split(','), i; + for (i = 0; i < items.length; i++) obj[items[i]] = true; + return obj; +} + + +/** + * @example + * htmlParser(htmlString, { + * start: function(tag, attrs, unary) {}, + * end: function(tag) {}, + * chars: function(text) {}, + * comment: function(text) {} + * }); + * + * @param {string} html string + * @param {object} handler + */ +function htmlParser(html, handler) { + if (typeof html !== 'string') { + if (html === null || typeof html === 'undefined') { + html = ''; + } else { + html = '' + html; + } + } + var index, chars, match, stack = [], last = html, text; + stack.last = function() { return stack[stack.length - 1]; }; + + while (html) { + text = ''; + chars = true; + + // Make sure we're not in a script or style element + if (!stack.last() || !specialElements[stack.last()]) { + + // Comment + if (html.indexOf("", index) === index) { + if (handler.comment) handler.comment(html.substring(4, index)); + html = html.substring(index + 3); + chars = false; + } + // DOCTYPE + } else if (DOCTYPE_REGEXP.test(html)) { + match = html.match(DOCTYPE_REGEXP); + + if (match) { + html = html.replace(match[0], ''); + chars = false; + } + // end tag + } else if (BEGING_END_TAGE_REGEXP.test(html)) { + match = html.match(END_TAG_REGEXP); + + if (match) { + html = html.substring(match[0].length); + match[0].replace(END_TAG_REGEXP, parseEndTag); + chars = false; + } + + // start tag + } else if (BEGIN_TAG_REGEXP.test(html)) { + match = html.match(START_TAG_REGEXP); + + if (match) { + // We only have a valid start-tag if there is a '>'. + if (match[4]) { + html = html.substring(match[0].length); + match[0].replace(START_TAG_REGEXP, parseStartTag); + } + chars = false; + } else { + // no ending tag found --- this piece should be encoded as an entity. + text += '<'; + html = html.substring(1); + } + } + + if (chars) { + index = html.indexOf("<"); + + text += index < 0 ? html : html.substring(0, index); + html = index < 0 ? "" : html.substring(index); + + if (handler.chars) handler.chars(decodeEntities(text)); + } + + } else { + // IE versions 9 and 10 do not understand the regex '[^]', so using a workaround with [\W\w]. + html = html.replace(new RegExp("([\\W\\w]*)<\\s*\\/\\s*" + stack.last() + "[^>]*>", 'i'), + function(all, text) { + text = text.replace(COMMENT_REGEXP, "$1").replace(CDATA_REGEXP, "$1"); + + if (handler.chars) handler.chars(decodeEntities(text)); + + return ""; + }); + + parseEndTag("", stack.last()); + } + + if (html == last) { + throw $sanitizeMinErr('badparse', "The sanitizer was unable to parse the following block " + + "of html: {0}", html); + } + last = html; + } + + // Clean up any remaining tags + parseEndTag(); + + function parseStartTag(tag, tagName, rest, unary) { + tagName = angular.lowercase(tagName); + if (blockElements[tagName]) { + while (stack.last() && inlineElements[stack.last()]) { + parseEndTag("", stack.last()); + } + } + + if (optionalEndTagElements[tagName] && stack.last() == tagName) { + parseEndTag("", tagName); + } + + unary = voidElements[tagName] || !!unary; + + if (!unary) + stack.push(tagName); + + var attrs = {}; + + rest.replace(ATTR_REGEXP, + function(match, name, doubleQuotedValue, singleQuotedValue, unquotedValue) { + var value = doubleQuotedValue + || singleQuotedValue + || unquotedValue + || ''; + + attrs[name] = decodeEntities(value); + }); + if (handler.start) handler.start(tagName, attrs, unary); + } + + function parseEndTag(tag, tagName) { + var pos = 0, i; + tagName = angular.lowercase(tagName); + if (tagName) + // Find the closest opened tag of the same type + for (pos = stack.length - 1; pos >= 0; pos--) + if (stack[pos] == tagName) + break; + + if (pos >= 0) { + // Close all the open elements, up the stack + for (i = stack.length - 1; i >= pos; i--) + if (handler.end) handler.end(stack[i]); + + // Remove the open elements from the stack + stack.length = pos; + } + } +} + +var hiddenPre=document.createElement("pre"); +/** + * decodes all entities into regular string + * @param value + * @returns {string} A string with decoded entities. + */ +function decodeEntities(value) { + if (!value) { return ''; } + + hiddenPre.innerHTML = value.replace(//g, '>'); +} + +/** + * create an HTML/XML writer which writes to buffer + * @param {Array} buf use buf.jain('') to get out sanitized html string + * @returns {object} in the form of { + * start: function(tag, attrs, unary) {}, + * end: function(tag) {}, + * chars: function(text) {}, + * comment: function(text) {} + * } + */ +function htmlSanitizeWriter(buf, uriValidator) { + var ignore = false; + var out = angular.bind(buf, buf.push); + return { + start: function(tag, attrs, unary) { + tag = angular.lowercase(tag); + if (!ignore && specialElements[tag]) { + ignore = tag; + } + if (!ignore && validElements[tag] === true) { + out('<'); + out(tag); + angular.forEach(attrs, function(value, key) { + var lkey=angular.lowercase(key); + var isImage = (tag === 'img' && lkey === 'src') || (lkey === 'background'); + if (validAttrs[lkey] === true && + (uriAttrs[lkey] !== true || uriValidator(value, isImage))) { + out(' '); + out(key); + out('="'); + out(encodeEntities(value)); + out('"'); + } + }); + out(unary ? '/>' : '>'); + } + }, + end: function(tag) { + tag = angular.lowercase(tag); + if (!ignore && validElements[tag] === true) { + out(''); + } + if (tag == ignore) { + ignore = false; + } + }, + chars: function(chars) { + if (!ignore) { + out(encodeEntities(chars)); + } + } + }; +} + + +// define ngSanitize module and register $sanitize service +angular.module('ngSanitize', []).provider('$sanitize', $SanitizeProvider); + +/* global sanitizeText: false */ + +/** + * @ngdoc filter + * @name linky + * @kind function + * + * @description + * Finds links in text input and turns them into html links. Supports http/https/ftp/mailto and + * plain email address links. + * + * Requires the {@link ngSanitize `ngSanitize`} module to be installed. + * + * @param {string} text Input text. + * @param {string} target Window (_blank|_self|_parent|_top) or named frame to open links in. + * @returns {string} Html-linkified text. + * + * @usage + + * + * @example + + + +
+ Snippet: + + + + + + + + + + + + + + + + + + + + + +
FilterSourceRendered
linky filter +
<div ng-bind-html="snippet | linky">
</div>
+
+
+
linky target +
<div ng-bind-html="snippetWithTarget | linky:'_blank'">
</div>
+
+
+
no filter
<div ng-bind="snippet">
</div>
+ + + it('should linkify the snippet with urls', function() { + expect(element(by.id('linky-filter')).element(by.binding('snippet | linky')).getText()). + toBe('Pretty text with some links: http://angularjs.org/, us@somewhere.org, ' + + 'another@somewhere.org, and one more: ftp://127.0.0.1/.'); + expect(element.all(by.css('#linky-filter a')).count()).toEqual(4); + }); + + it('should not linkify snippet without the linky filter', function() { + expect(element(by.id('escaped-html')).element(by.binding('snippet')).getText()). + toBe('Pretty text with some links: http://angularjs.org/, mailto:us@somewhere.org, ' + + 'another@somewhere.org, and one more: ftp://127.0.0.1/.'); + expect(element.all(by.css('#escaped-html a')).count()).toEqual(0); + }); + + it('should update', function() { + element(by.model('snippet')).clear(); + element(by.model('snippet')).sendKeys('new http://link.'); + expect(element(by.id('linky-filter')).element(by.binding('snippet | linky')).getText()). + toBe('new http://link.'); + expect(element.all(by.css('#linky-filter a')).count()).toEqual(1); + expect(element(by.id('escaped-html')).element(by.binding('snippet')).getText()) + .toBe('new http://link.'); + }); + + it('should work with the target property', function() { + expect(element(by.id('linky-target')). + element(by.binding("snippetWithTarget | linky:'_blank'")).getText()). + toBe('http://angularjs.org/'); + expect(element(by.css('#linky-target a')).getAttribute('target')).toEqual('_blank'); + }); + + + */ +angular.module('ngSanitize').filter('linky', ['$sanitize', function($sanitize) { + var LINKY_URL_REGEXP = + /((ftp|https?):\/\/|(www\.)|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s.;,(){}<>"”’]/, + MAILTO_REGEXP = /^mailto:/; + + return function(text, target) { + if (!text) return text; + var match; + var raw = text; + var html = []; + var url; + var i; + while ((match = raw.match(LINKY_URL_REGEXP))) { + // We can not end in these as they are sometimes found at the end of the sentence + url = match[0]; + // if we did not match ftp/http/www/mailto then assume mailto + if (!match[2] && !match[4]) { + url = (match[3] ? 'http://' : 'mailto:') + url; + } + i = match.index; + addText(raw.substr(0, i)); + addLink(url, match[0].replace(MAILTO_REGEXP, '')); + raw = raw.substring(i + match[0].length); + } + addText(raw); + return $sanitize(html.join('')); + + function addText(text) { + if (!text) { + return; + } + html.push(sanitizeText(text)); + } + + function addLink(url, text) { + html.push(''); + addText(text); + html.push(''); + } + }; +}]); + + +})(window, window.angular); diff --git a/public/stylesheets/main.css b/public/stylesheets/main.css index eb02f31c..daa86044 100644 --- a/public/stylesheets/main.css +++ b/public/stylesheets/main.css @@ -190,4 +190,110 @@ div[ui-view] { .contain-to-grid .top-bar { max-width: 90%; } +} + +#mission-input { + margin-top: 0; + height: 512px; +} + +#mission-preview { + overflow: scroll; + margin-top: 0; + height: 512px; +} + +#drop-color a { + display: inline-block; +} + +.color-preview { + display: inline-block; + width: 5%; + padding-top: 5%; + margin-left: 4%; + margin-right: 4%; + border-radius: 50%; + vertical-align: middle; + box-shadow: 0px 1px 1px rgba(0, 0, 0, 0.3); +} + +.color-preview-custom { + background: red; /* not working, let's see some red */ + background: -moz-linear-gradient( top , + rgba(255, 0, 0, 1) 0%, + rgba(255, 255, 0, 1) 15%, + rgba(0, 255, 0, 1) 30%, + rgba(0, 255, 255, 1) 50%, + rgba(0, 0, 255, 1) 65%, + rgba(255, 0, 255, 1) 80%, + rgba(255, 0, 0, 1) 100%); + background: -webkit-gradient(linear, left top, left bottom, + color-stop(0%, rgba(255, 0, 0, 1)), + color-stop(15%, rgba(255, 255, 0, 1)), + color-stop(30%, rgba(0, 255, 0, 1)), + color-stop(50%, rgba(0, 255, 255, 1)), + color-stop(65%, rgba(0, 0, 255, 1)), + color-stop(80%, rgba(255, 0, 255, 1)), + color-stop(100%, rgba(255, 0, 0, 1))); +} + +.cursor-pipette { + cursor: url('../images/pipette-cursor.gif') 0 14, auto; +} + +.cursor-paint { + cursor: url('../images/paint-cursor.png') 0 18, auto; +} + +.color-indic-li { + position: relative; +} + +.color-indic { + position: absolute; + top: -14px; + left: 8px; + width: 25px; + height: 25px; + border-radius: 50%; + border: 3px solid #fff; + box-shadow: 0px 0px 8px rgba(0, 0, 0, 0.5); + z-index: 10; +} + +.color-indic.ng-hide-add, +.color-indic.ng-hide-remove, +.color-indic.ng-show-add, +.color-indic.ng-show-remove { + -webkit-transition: all linear 0.2s; + -moz-transition: all linear 0.2s; + -o-transition: all linear 0.2s; + transition: all linear 0.2s; +} + +.color-indic.ng-hide-add.ng-hide-add-active, +.color-indic.ng-hide-remove, +.color-indic.ng-show-add.ng-show-add-active, +.color-indic.ng-show-remove +{ + opacity: 0; +} + +.color-indic.ng-hide-add, +.color-indic.ng-hide-remove.ng-hide-remove-active, +.color-indic.ng-show-add, +.color-indic.ng-show-remove.ng-show-remove-active +{ + opacity: 1; +} + +button.selected-btn, button.selected-btn:focus, button.selected-btn:active, button.selected-btn:hover { + background-color: #006586; + box-shadow: 0px 2px 3px #00516B inset; +} + +#select-world-row .columns { + padding-top: 0; + padding-bottom: 0; } \ No newline at end of file