소스 검색

Squashed merge of all 91 commits on 'refactor-and-test-editor' branch

Benoît Hubert 8 년 전
부모
커밋
31bd0388a3
100개의 변경된 파일2972개의 추가작업 그리고 517개의 파일을 삭제
  1. 1 1
      .gitignore
  2. BIN
      css/fonts/icomoon.eot
  3. 2 0
      css/fonts/icomoon.svg
  4. BIN
      css/fonts/icomoon.ttf
  5. BIN
      css/fonts/icomoon.woff
  6. 252 123
      css/styles.css
  7. 0 0
      css/vendor/bootstrap.min.css
  8. 7 0
      css/vendor/pure-grids-min.css
  9. 7 0
      css/vendor/pure-grids-responsive-min.css
  10. 133 0
      css/vendor/styles.css
  11. 0 0
      exemples/.gitkeep
  12. 0 3
      exemples/ajax-exemple-simple/script.js
  13. 0 10
      exemples/ajax-requete-themoviedborg/contenu.html
  14. 10 0
      exemples/javascript/repo-config.json
  15. 9 0
      exemples/javascript/tableaux-boucles/config.json
  16. 0 0
      exemples/javascript/tableaux-boucles/example.html
  17. 2 0
      exemples/javascript/tableaux-boucles/script.js
  18. 9 0
      exemples/jquery/ajax-donnees-page-produit/config.json
  19. 0 0
      exemples/jquery/ajax-donnees-page-produit/example.html
  20. 1 1
      exemples/ajax-donnees-page-produit/script.js
  21. 9 0
      exemples/jquery/ajax-envoi-formulaire-par-get/config.json
  22. 1 1
      exemples/ajax-envoi-formulaire-par-get/contenu.html
  23. 7 0
      exemples/ajax-envoi-formulaire-par-get/script.js
  24. 9 0
      exemples/jquery/ajax-envoi-formulaire-par-post/config.json
  25. 1 1
      exemples/ajax-envoi-formulaire-par-post/contenu.html
  26. 0 0
      exemples/jquery/ajax-envoi-formulaire-par-post/script.js
  27. 9 0
      exemples/jquery/ajax-exemple-simple/config.json
  28. 0 0
      exemples/jquery/ajax-exemple-simple/example.html
  29. 3 0
      exemples/jquery/ajax-exemple-simple/script.js
  30. 9 0
      exemples/jquery/ajax-requete-randomuserme/config.json
  31. 0 0
      exemples/jquery/ajax-requete-randomuserme/example.html
  32. 0 0
      exemples/jquery/ajax-requete-randomuserme/script.js
  33. 9 0
      exemples/jquery/ajax-requete-themoviedborg/config.json
  34. 25 0
      exemples/jquery/ajax-requete-themoviedborg/example.html
  35. 1 1
      exemples/ajax-requete-themoviedborg/script.js
  36. 8 0
      exemples/jquery/ajax-requete-themoviedborg/styles.css
  37. 9 0
      exemples/jquery/evenements-1-click-submit/config.json
  38. 0 0
      exemples/jquery/evenements-1-click-submit/example.html
  39. 0 0
      exemples/jquery/evenements-1-click-submit/script.js
  40. 9 0
      exemples/jquery/evenements-2-change/config.json
  41. 0 0
      exemples/jquery/evenements-2-change/example.html
  42. 0 0
      exemples/jquery/evenements-2-change/script.js
  43. 9 0
      exemples/jquery/evenements-3-keyup/config.json
  44. 0 0
      exemples/jquery/evenements-3-keyup/example.html
  45. 0 0
      exemples/jquery/evenements-3-keyup/script.js
  46. 9 0
      exemples/jquery/exercice-1-verifier-un-formulaire/config.json
  47. 2 1
      exemples/exercice-1-verifier-un-formulaire/contenu.html
  48. 2 0
      exemples/exercice-1-verifier-un-formulaire/script.js
  49. 0 0
      exemples/jquery/exercice-1-verifier-un-formulaire/test.js
  50. 9 0
      exemples/jquery/onglets/config.json
  51. 0 0
      exemples/jquery/onglets/example.html
  52. 0 0
      exemples/jquery/onglets/script.js
  53. 22 0
      exemples/jquery/repo-config.json
  54. 9 0
      exemples/jquery/selecteurs-basiques/config.json
  55. 0 0
      exemples/jquery/selecteurs-basiques/example.html
  56. 0 0
      exemples/jquery/selecteurs-basiques/script.js
  57. 9 0
      exemples/jquery/selecteurs-cibler-un-element-enfant/config.json
  58. 14 0
      exemples/jquery/selecteurs-cibler-un-element-enfant/example.html
  59. 3 0
      exemples/jquery/selecteurs-cibler-un-element-enfant/script.js
  60. 1 0
      exemples/jquery/selecteurs-cibler-un-element-enfant/styles.css
  61. 9 0
      exemples/jquery/selecteurs-filtres-2/config.json
  62. 0 0
      exemples/jquery/selecteurs-filtres-2/example.html
  63. 0 0
      exemples/jquery/selecteurs-filtres-2/script.js
  64. 9 0
      exemples/jquery/selecteurs-filtres/config.json
  65. 0 0
      exemples/jquery/selecteurs-filtres/example.html
  66. 0 0
      exemples/jquery/selecteurs-filtres/script.js
  67. 9 0
      exemples/jquery/selecteurs-multiples/config.json
  68. 1 1
      exemples/selecteurs-multiples/contenu.html
  69. 0 0
      exemples/jquery/selecteurs-multiples/script.js
  70. 9 0
      exemples/jquery/validation-de-formulaires-bootstrap/config.json
  71. 0 0
      exemples/jquery/validation-de-formulaires-bootstrap/example.html
  72. 3 3
      exemples/validation-de-formulaires-bootstrap/script.js
  73. 0 25
      exemples/liste.json
  74. BIN
      favicon.ico
  75. 0 0
      html/empty.html
  76. 233 0
      html/index.mustache.html
  77. 26 0
      html/start-iframe.html
  78. 41 0
      html/template.mustache.html
  79. 0 71
      index.html
  80. 0 271
      js/editor.js
  81. 59 0
      js/req-promise.js
  82. 162 0
      js/test/ws.test.js
  83. 5 4
      js/vendor/ace/ace.js
  84. 14 0
      js/vendor/ace/ace.min.js
  85. 69 0
      js/vendor/eventemitter.js
  86. 654 0
      js/vendor/jquery.color.js
  87. 1 0
      js/vendor/mustache.min.js
  88. 251 0
      js/ws-editor.js
  89. 6 0
      js/ws-events.js
  90. 182 0
      js/ws-forms.js
  91. 205 0
      js/ws-menu.js
  92. 22 0
      js/ws-notify.js
  93. 138 0
      js/ws-ui-parts.js
  94. 14 0
      languages/en-US.json
  95. 14 0
      languages/fr-FR.json
  96. 121 0
      lib/ExampleStore.js
  97. 7 0
      lib/exampleTmpl.json
  98. 14 0
      lib/execAsync.js
  99. 63 0
      lib/fsio.js
  100. 0 0
      lib/indexHandlers.js

+ 1 - 1
.gitignore

@@ -4,5 +4,5 @@
 doc/
 node_modules/
 package-lock.json
-exemples/ajax-requete-themoviedborg/tmdb-key.json
+exemples/jquery/ajax-requete-themoviedborg/tmdb-key.json
 _stuff/

BIN
css/fonts/icomoon.eot


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 2 - 0
css/fonts/icomoon.svg


BIN
css/fonts/icomoon.ttf


BIN
css/fonts/icomoon.woff


+ 252 - 123
css/styles.css

@@ -3,11 +3,11 @@
  */
 @font-face {
   font-family: 'icomoon';
-  src:  url('fonts/icomoon.eot?xjk54g');
-  src:  url('fonts/icomoon.eot?xjk54g#iefix') format('embedded-opentype'),
-    url('fonts/icomoon.ttf?xjk54g') format('truetype'),
-    url('fonts/icomoon.woff?xjk54g') format('woff'),
-    url('fonts/icomoon.svg?xjk54g#icomoon') format('svg');
+  src:  url('fonts/icomoon.eot?q7b5dg');
+  src:  url('fonts/icomoon.eot?q7b5dg#iefix') format('embedded-opentype'),
+    url('fonts/icomoon.ttf?q7b5dg') format('truetype'),
+    url('fonts/icomoon.woff?q7b5dg') format('woff'),
+    url('fonts/icomoon.svg?q7b5dg#icomoon') format('svg');
   font-weight: normal;
   font-style: normal;
 }
@@ -22,11 +22,17 @@
   text-transform: none;
   line-height: 1;
 
+  /* Customized by Bibi */
+  font-size: 80%;
+
   /* Better Font Rendering =========== */
   -webkit-font-smoothing: antialiased;
   -moz-osx-font-smoothing: grayscale;
 }
 
+.icon-menu:before {
+  content: "\e9bd";
+}
 .icon-cloud-upload:before {
   content: "\e9c3";
 }
@@ -39,6 +45,9 @@
 .icon-checkmark:before {
   content: "\ea10";
 }
+.icon-loop2:before {
+  content: "\ea2e";
+}
 
 
 /**
@@ -49,7 +58,8 @@
  * Grosse font pour le vidéo-proj
  */
 body {
-  font-size: 130%;
+  font-size: 100%;
+  font-family: Arial, Helvetica;
 }
 
 /**
@@ -76,7 +86,9 @@ body {
 
 /* jQuery resizable: https://codepen.io/rstrahl/pen/eJZQej */
 /* horizontal panel*/
-
+html, body {
+  height: 100%;
+}
 .panel-container {
   display: flex;
   flex-direction: row;
@@ -90,16 +102,17 @@ body {
 }
 
 .panel-left {
+  position: relative;
   flex: 0 0 auto;
   /* only manually resize */
-  padding: 10px;
+  /*padding: 10px;*/
   width: 50%;
   /*min-height: 200px;*/
   height: 100%;
   min-width: 150px;
   white-space: nowrap;
   background: #fff;
-  color: white;
+  color: #555;
 }
 
 .splitter {
@@ -117,16 +130,23 @@ body {
   width: 100%;
   height: 100%;
   overflow-y: scroll;
-
   min-width: 200px;
   background: #eee;
 }
-
-
+#sandbox-iframe {
+  border: none;
+  width: 100%;
+  height: 100%;
+}
+#sandbox-iframe.test-mode {
+  max-height: 300px;
+  margin-bottom: 10px;
+}
 .panel-inner {
   padding: 10px;
 }
-.panel-left nav,
+#site-menu{ position: fixed;top:0;right:0; }
+/*.panel-left nav,
 #add-example-form {
   display: inline-block;
 }
@@ -136,24 +156,27 @@ body {
 }
 .panel-left nav ul li {
   display: inline-block;
-}
+}*/
 #notification {
-  color: black;
+  color: #333;
   position: absolute;
   top: -100px;
   right: 10px;
   transition: top 1s;
+}
+.alert-box {
+  text-align: center;
   border-radius: 3px;
-  padding: 3px;
+  padding: 5px 10px;
 }
 #notification.active {
   top: 5px;
 }
-#notification.error {
+.alert-box.error {
   border: 1px solid #d42;
   background: #ffe8e0;
 }
-#notification.success {
+.alert-box.success {
   border: 1px solid #2d4;
   background: #e0ffe8;
 }
@@ -169,161 +192,267 @@ a:hover {
   text-decoration: none;
   color: #37b;
 }
+button {
+  border: none;
+  border-radius: 3px;
+  background: #fff;
+  color: #555;
+}
+.input,
+.btn {
+  padding: 10px;
+}
+.input-sm,
+.btn-sm {
+  padding: 3px;
+  font-size: 90%;
+}
+.input-xs {
+  padding: 1px;
+  font-size: 80%;
+}
+.input-light-border {
+  border: 1px solid #f8f8f8;
+}
+.input-warning {
+  border: 1px solid #cc5;
+}
+.input-error {
+  border: 1px solid #e65;
+}
+.input-success {
+  border: 1px solid #4c2;
+}
+[class^="icon-"].rounded,
+[class*=" icon-"].rounded {
+  border-radius: 10px;
+  padding: 3px;
+}
+[class^="icon-"].rounded:hover,
+[class*=" icon-"].rounded:hover {
+  background: #ddd;
+  color: #222;
+}
 button a {
   color: #fff;
 }
 button.active {
   background: #9ad;
 }
-button:hover {
+/*button:hover {
   opacity: 0.6;
+}*/
+button.btn-lg {
+  font-size: 120%;
+}
+textarea,
+select,
+input {
+  border: 1px solid #ddd;
+  border-radius: 3px;
 }
-#editor-js,#editor-html {
+.light-gray {
+  background: #f4f4f4;
+  color: #555;
+}
+.light-gray:hover {
+  background: #ddd;
+  color: #222;
+}
+.blue {
+  background: #24d;
+  color: #fff;
+}
+.red {
+  background: #d42;
+  color: #fff;
+}
+.green {
+  background: #4b2;
+  color: #fff;
+}
+#editor-js,
+#editor-html,
+#editor-css {
   /*display: none;*/
   position: absolute;
   top: 510px;
-
 }
+
 /**
  * Styles pour l'éditeur Ace
  */
+#editor-wrapper,
+#editor {
+  position: absolute;
+  bottom: 0;
+  width: 100%;
+}
+#editor-wrapper {
+  top: 145px;
+}
+
 #editor {
   font-size: 90%;
+  border-top: 3px solid #ddd;
+  top: 22px;
+  left: 0;
+}
+
+#navbar {
+  height: 49px;
+  border-bottom: 1px solid #ddd;
+  font-size: 26px;
+}
+#navbar a {
+  color: #555;
+}
+#navbar a:hover,
+#navbar button:hover {
+  color: #777;
+}
+
+#nav-menus {
   position: absolute;
-  top: 90px;
-  right: 500px;
+  top: 50px;
   bottom: 0;
   left: 0;
-  border-top: 1px solid #aaa;
-  border-bottom: 1px solid #aaa;
+  width: 0;
+  overflow: hidden;
+  transition: all 0.2s;
+  z-index: 100;
+  color: #000;
+  background: rgba(248, 248, 248, 1);
+  padding: 10px 0;
 }
 
-.container {
-  margin: 20px auto;
-	background: #fff;
-	border: 1px solid #ddd;
-	padding: 30px;
-  width: 96%;
-  border-radius: 4px;
-  box-sizing: border-box;
-	/*max-width: 600px;*/
+#menu-example .cat-title {
+  text-transform: uppercase;
 }
-input[type="submit"],
-button {
+
+#nav-menus.in {
+  width: 540px;
   padding: 10px;
-  color: white;
-  border: none;
-  border-radius: 3px;
 }
-.blue {
-  background: #24d;
+#nav-menus .pure-u-1 {
+  padding: 10px;
+  box-sizing: border-box;
 }
-.red {
-  background: #d42;
+
+#nav-menus ul {
+  padding-left: 0;
+  margin-top: 5px;
 }
-.green {
-  background: #4b2;
+#menu-btn:focus {
+  outline: none;
 }
-textarea,
-select,
-input {
-  border: 1px solid #ddd;  
-  padding: 12px;
-  border-radius: 3px;
+/* Menus de nav */
+#nav-menus {
 }
-input.input-sm {
-  padding: 6px;
-  font-size: 70%;
+#nav-menus li {
+ list-style-type: none;
 }
-select {
-  background: #fcfcfc;
+#menu-example {
+  overflow: 
 }
-a,
-button {
-  margin: 5px;
-  box-sizing: border-box;
+#menu-example > li {
+  display: block;
+  float:left;
+  vertical-align: top;
+  width: 50%;
 }
-.bordure-flashy {
-  border: 2px solid red;
+.nav-menu a {
+  color: #58c;
+}
+.nav-menu a:hover {
+  color: #7af;
 }
 
-#html-content h5 {
-  margin: 20px 0 5px 0;
+#tabs, #add-file {
+  display: inline-block;
+  /*font-size: 16px;*/
+  position: relative;
+  vertical-align: bottom;
+  margin: 0;
 }
-#html-content p {
-  margin: 10px 0;
+#tabs {
+  padding-left: 10px;
 }
-button.d {
-  padding: 0;
-  width: 32px;
-  height: 32px;
-  border-radius: 16px;
+#add-file {
+  padding-left: 5px;
 }
-
-/**
- * Styles communs pour les différents exemples
- */
-a.active {
-  font-weight: bold;
-  color: #48c;
+#tabs li,
+#add-file li {
+  border-top: 1px solid #ddd;
+  border-left: 1px solid #ddd;
+  border-right: 1px solid #ddd;
+  display: inline;
+  box-sizing: border-box;
+  padding: 4px 8px;
 }
-.cyan {
-  background: #6ff;
+.tab-html {
+  background: #cef;
 }
-.magenta {
-  background: #f6f;
+.tab-js {
+  background: #ffb;
 }
-.yellow {
-  background: #ff6;
-  color: #444;
+.tab-css {
+  background: #fce;
 }
-.orange {
-  background: #eb6;
+.inline-block {
+  display: inline-block;
+  vertical-align: top;
 }
-.bold-text {
-  font-weight: bold;
+.hidden {
+  display: none;
+}
+.h-collapsed {
+  box-sizing: border-box;
+  max-width: 0;
+  padding-left: 0;
+  padding-right: 0;
+  transition: 0.3s;
+  overflow: hidden;
 }
-.underlined-text {
-  text-decoration: underline;
+.h-collapsed.fast {
+  transition: 0.2s;
 }
-.text-green {
-  color: #4b2;
+.h-collapsed.in {
+  padding-left: 15px;
+  padding-right: 15px;
+  max-width: 350px;
 }
-.text-red {
-  color: #b42;
+.h-collapsed input[type="text"] {
+  margin: 2px;
+  padding-left: 10px;
 }
-.table {
-  border-collapse: collapse;
+.details-div section {
+  vertical-align: middle;
 }
-.table tr td {
-  border: 1px solid #aaa;
-  padding: 2px;
+.details-div button {
+  margin-top: 3px;
 }
-ul.tab-nav {
-   padding-left: 5px;
-   padding-bottom: 4px;
-   border-bottom: 1px solid #eee;
+#details-repo,
+#details-example,
+.details-div div.bold {
+  margin: 10px 0;
 }
-ul.tab-nav li {
-  display:inline;
-  padding: 6px 10px;
-  margin-right: 5px;
-  border-top:1px solid #ccc;
-  border-left:1px solid #ccc;
-  border-right:1px solid #ccc;
+.details-div .left {
+  width: 310px;
 }
-
-#qunit-header,
-#qunit-testrunner-toolbar {
+.bold {
+  font-weight: bold;
+}
+.dirty {
+  color: #339;
+}
+#tabs .icon-cloud-upload {
   display: none;
 }
-#revert-editor {
-  background: none;
-  border: none;
-  position: absolute;
-  padding: 0 3px;
-  top: 0;
-  left: 0;
-  color: red;
-  font-size: 11px;
+#tabs .dirty .icon-cloud-upload {
+  display: inline;
+  color: #33c;
+}
+.details-div .btn-form {
+  margin: 8px 0 10px 20px;
 }

css/bootstrap.min.css → css/vendor/bootstrap.min.css


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 7 - 0
css/vendor/pure-grids-min.css


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 7 - 0
css/vendor/pure-grids-responsive-min.css


+ 133 - 0
css/vendor/styles.css

@@ -0,0 +1,133 @@
+
+.container {
+  margin: 20px auto;
+  background: #fff;
+  border: 1px solid #ddd;
+  padding: 30px;
+  width: 96%;
+  border-radius: 4px;
+  box-sizing: border-box;
+  /*max-width: 600px;*/
+}
+input[type="submit"],
+button {
+  padding: 10px;
+  color: white;
+  border: none;
+  border-radius: 3px;
+}
+.blue {
+  background: #24d;
+}
+.red {
+  background: #d42;
+}
+.green {
+  background: #4b2;
+}
+.gray {
+  background: #888;
+}
+textarea,
+select,
+input {
+  border: 1px solid #ddd;  
+  padding: 12px;
+  border-radius: 3px;
+}
+input.input-sm {
+  padding: 6px;
+  font-size: 70%;
+}
+select {
+  background: #fcfcfc;
+}
+a,
+button {
+  margin: 5px;
+  box-sizing: border-box;
+}
+.bordure-flashy {
+  border: 2px solid red;
+}
+
+#html-content h5 {
+  margin: 20px 0 5px 0;
+}
+#html-content p {
+  margin: 10px 0;
+}
+button.d {
+  padding: 0;
+  width: 32px;
+  height: 32px;
+  border-radius: 16px;
+}
+
+/**
+ * Styles communs pour les différents exemples
+ */
+a.active {
+  font-weight: bold;
+  color: #48c;
+}
+.cyan {
+  background: #6ff;
+}
+.magenta {
+  background: #f6f;
+}
+.yellow {
+  background: #ff6;
+  color: #444;
+}
+.orange {
+  background: #eb6;
+}
+.bold-text {
+  font-weight: bold;
+}
+.underlined-text {
+  text-decoration: underline;
+}
+.text-green {
+  color: #4b2;
+}
+.text-red {
+  color: #b42;
+}
+.table {
+  border-collapse: collapse;
+}
+.table tr td {
+  border: 1px solid #aaa;
+  padding: 2px;
+}
+ul.tab-nav {
+   padding-left: 5px;
+   padding-bottom: 4px;
+   border-bottom: 1px solid #eee;
+}
+ul.tab-nav li {
+  display:inline;
+  padding: 6px 10px;
+  margin-right: 5px;
+  border-top:1px solid #ccc;
+  border-left:1px solid #ccc;
+  border-right:1px solid #ccc;
+}
+
+#qunit-header,
+#qunit-testrunner-toolbar {
+  display: none;
+}
+#revert-editor {
+  background: none;
+  border: none;
+  position: absolute;
+  padding: 0 3px;
+  top: 0;
+  left: 0;
+  color: red;
+  font-size: 11px;
+}

js/main.js → exemples/.gitkeep


+ 0 - 3
exemples/ajax-exemple-simple/script.js

@@ -1,3 +0,0 @@
-$.get('/ajax-example', function(data) {
-   $('#ajax-html').html(data); 
-});

+ 0 - 10
exemples/ajax-requete-themoviedborg/contenu.html

@@ -1,10 +0,0 @@
-<h5>API themoviedb.org</h5>
-
-<form id="movie-title">
-    <p>Entrez un titre de film:</p>
-    <input type="text" name="title" placeholder="Titre" />
-    <button type="submit" class="blue" disabled="disabled">Go</button>
-</form>
-
-<div style="border: 1px solid #888; padding: 10px; margin: 10px;" id="tmdb-results">
-</div>

+ 10 - 0
exemples/javascript/repo-config.json

@@ -0,0 +1,10 @@
+{
+  "title": "JavaScript",
+  "defaultCategory": "array",
+  "categories": [
+    {
+      "slug": "array",
+      "title": "Tableaux"
+    }
+  ]
+}

+ 9 - 0
exemples/javascript/tableaux-boucles/config.json

@@ -0,0 +1,9 @@
+{
+  "title": "Tableaux - Boucles",
+  "category": "array",
+  "html": [ "example.html" ],
+  "js": [ "script.js" ],
+  "css": [],
+  "libsJs": [],
+  "libsCss": [ "styles.css" ]
+}

+ 0 - 0
exemples/javascript/tableaux-boucles/example.html


+ 2 - 0
exemples/javascript/tableaux-boucles/script.js

@@ -0,0 +1,2 @@
+var suiteFibonacci = [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89];
+console.log(suiteFibonacci);

+ 9 - 0
exemples/jquery/ajax-donnees-page-produit/config.json

@@ -0,0 +1,9 @@
+{
+  "title": "AJAX - Données - page produit",
+  "category": "misc",
+  "html": [ "example.html" ],
+  "js": [ "script.js" ],
+  "css": [],
+  "libsJs": [ "jquery-3.2.1.min.js" ],
+  "libsCss": [ "styles.css", "bootstrap.min.css" ]
+}

exemples/ajax-donnees-page-produit/contenu.html → exemples/jquery/ajax-donnees-page-produit/example.html


+ 1 - 1
exemples/ajax-donnees-page-produit/script.js

@@ -2,7 +2,7 @@ $.get('/produits.json', function(produits) {
    var divProduits = $('#produits');
    
     produits.forEach(function(produit) {
-        divProduits.append('<div class="col-md-6 col-sm-12">' +
+        divProduits.append('<div class="col-sm-12 col-md-6">' +
             '<img class="img-thumbnail" src="' +
                 produit.image + '" />' +
             '<div>' + produit.titre + '</div>' +

+ 9 - 0
exemples/jquery/ajax-envoi-formulaire-par-get/config.json

@@ -0,0 +1,9 @@
+{
+  "title": "AJAX - Envoi formulaire par GET",
+  "category": "ajax",
+  "html": [ "example.html" ],
+  "js": [ "script.js" ],
+  "css": [],
+  "libsJs": [ "jquery-3.2.1.min.js" ],
+  "libsCss": [ "styles.css" ]
+}

+ 1 - 1
exemples/ajax-envoi-formulaire-par-get/contenu.html

@@ -1,6 +1,6 @@
 <h1>AJAX - Envoi formulaire par GET</h1>
 
-<form id="ajax-form-get" method="GET" action="/ajax-form-get">
+<form id="ajax-form-get" method="GET" action="/jquery/ajax/form-get">
     <input type="text" name="name" placeholder="Nom ?" />
     <input type="text" name="birthdate" placeholder="Date de naissance ?" />
     <input type="submit" class="btn btn-primary" value="Envoyer" />

+ 7 - 0
exemples/ajax-envoi-formulaire-par-get/script.js

@@ -5,12 +5,19 @@ $('#ajax-form-get').submit(function(e) {
     var birthdate = form.find('input[name="birthdate"]').val();
     $.ajax({
         method: 'GET',
+        // Notez l'utilisation de .prop() pour récupérer l'attribut action,
+        // contenant l'URL vers laquelle le formulaire est sensé soumettre,
+        // vers laquelle il serait envoyé sans e.preventDefault() ci-dessus.
         url: form.prop('action'),
         data: {
             name: name, birthdate: birthdate
         },
         success: function(data) {
+            var message = "<p>Regardez l'onglet Network/Réseau "+
+              "de la console de développement...</p>";
             $('#ajax-html').html(data);
+            $( message ).appendTo( $('#ajax-html') )
+            .css('color', '#b33');
         },
         dataType: 'html'
     });

+ 9 - 0
exemples/jquery/ajax-envoi-formulaire-par-post/config.json

@@ -0,0 +1,9 @@
+{
+  "title": "AJAX - Envoi formulaire par POST",
+  "category": "ajax",
+  "html": [ "example.html" ],
+  "js": [ "script.js" ],
+  "css": [],
+  "libsJs": [ "jquery-3.2.1.min.js" ],
+  "libsCss": [ "styles.css" ]
+}

+ 1 - 1
exemples/ajax-envoi-formulaire-par-post/contenu.html

@@ -1,7 +1,7 @@
 <h1>AJAX - Envoi formulaire par POST</h1>
 
 <p>Presque la même chose que l'exemple précédent !</p>
-<form id="ajax-form-post" method="POST" action="/ajax-form-post">
+<form id="ajax-form-post" method="POST" action="/jquery/ajax/form-post">
     <input type="text" name="title" placeholder="Titre" /><br>
     <textarea name="text" rows="10" placeholder="Collez du texte"></textarea><br>
     <input type="submit" class="btn btn-primary" value="Envoyer" />

exemples/ajax-envoi-formulaire-par-post/script.js → exemples/jquery/ajax-envoi-formulaire-par-post/script.js


+ 9 - 0
exemples/jquery/ajax-exemple-simple/config.json

@@ -0,0 +1,9 @@
+{
+  "title": "AJAX - Exemple simple",
+  "category": "ajax",
+  "html": [ "example.html" ],
+  "js": [ "script.js" ],
+  "css": [],
+  "libsJs": [ "jquery-3.2.1.min.js" ],
+  "libsCss": [ "styles.css" ]
+}

exemples/ajax-exemple-simple/contenu.html → exemples/jquery/ajax-exemple-simple/example.html


+ 3 - 0
exemples/jquery/ajax-exemple-simple/script.js

@@ -0,0 +1,3 @@
+$.get('/jquery/ajax/example-simple', function(data) {
+   $('#ajax-html').html(data); 
+});

+ 9 - 0
exemples/jquery/ajax-requete-randomuserme/config.json

@@ -0,0 +1,9 @@
+{
+  "title": "AJAX - Requête randomuser.me",
+  "category": "ajax",
+  "html": [ "example.html" ],
+  "js": [ "script.js" ],
+  "css": [],
+  "libsJs": [ "jquery-3.2.1.min.js" ],
+  "libsCss": [ "styles.css" ]
+}

exemples/ajax-requete-randomuserme/contenu.html → exemples/jquery/ajax-requete-randomuserme/example.html


exemples/ajax-requete-randomuserme/script.js → exemples/jquery/ajax-requete-randomuserme/script.js


+ 9 - 0
exemples/jquery/ajax-requete-themoviedborg/config.json

@@ -0,0 +1,9 @@
+{
+  "title": "AJAX - Requête themoviedb.org",
+  "category": "ajax",
+  "html": [ "example.html" ],
+  "js": [ "script.js" ],
+  "css": [ "styles.css" ],
+  "libsJs": [ "jquery-3.2.1.min.js" ],
+  "libsCss": [ "styles.css" ]
+}

+ 25 - 0
exemples/jquery/ajax-requete-themoviedborg/example.html

@@ -0,0 +1,25 @@
+<h5>API themoviedb.org</h5>
+
+<p class="notice">
+  Cet exemple fait appel à une API protégée par une clé d'accès.
+  Pour récupérer cette clé :
+</p>
+<ol>
+<li>Cliquez sur
+    <a target="_blank" href="http://bhubr.eu/tmdb.php?code=CGI-JQUERY-2017">ce lien</a> puis <strong>copiez</strong> ce qui s'affiche.
+</li>
+<li>
+    <strong>Collez</strong> ce que vous voyez dans le fichier <span class="code">
+        exemples/jquery/ajax-requete-themoviedborg/tmdb-key.json
+    </span> (en partant de la racine de ce dossier).
+</li>
+</ol>
+
+<form id="movie-title">
+    <p>Entrez un titre de film:</p>
+    <input type="text" name="title" placeholder="Titre" />
+    <button type="submit" class="blue" disabled="disabled">Go</button>
+</form>
+
+<div style="border: 1px solid #888; padding: 10px; margin: 10px;" id="tmdb-results">
+</div>

+ 1 - 1
exemples/ajax-requete-themoviedborg/script.js

@@ -25,7 +25,7 @@ form.submit(function(evt) {
 
 // Charge la clé d'API
 // (ne PAS la sauver dans Git)
-$.get('/exemples/ajax-requete-themoviedborg/tmdb-key.json', function(data) {
+$.get('/exemples/jquery/ajax-requete-themoviedborg/tmdb-key.json', function(data) {
     apiKey = data.key;
     tmdbUrl = 'http://api.themoviedb.org/3/search/movie?api_key=' + apiKey + '&include_adult=false&page=1&language=en-US&query=';
     form.find('button')

+ 8 - 0
exemples/jquery/ajax-requete-themoviedborg/styles.css

@@ -0,0 +1,8 @@
+/* styles.css */
+.notice {
+  color: #b33;
+  font-size: 90%;
+}
+.code {
+  font-family: "Courier New", Courier;
+}

+ 9 - 0
exemples/jquery/evenements-1-click-submit/config.json

@@ -0,0 +1,9 @@
+{
+  "title": "Evènements 1 : click et submit",
+  "category": "evenements",
+  "html": [ "example.html" ],
+  "js": [ "script.js" ],
+  "css": [],
+  "libsJs": [ "jquery-3.2.1.min.js" ],
+  "libsCss": [ "styles.css" ]
+}

exemples/evenements-1-click-submit/contenu.html → exemples/jquery/evenements-1-click-submit/example.html


exemples/evenements-1-click-submit/script.js → exemples/jquery/evenements-1-click-submit/script.js


+ 9 - 0
exemples/jquery/evenements-2-change/config.json

@@ -0,0 +1,9 @@
+{
+  "title": "Evènements 2 : change",
+  "category": "evenements",
+  "html": [ "example.html" ],
+  "js": [ "script.js" ],
+  "css": [],
+  "libsJs": [ "jquery-3.2.1.min.js" ],
+  "libsCss": [ "styles.css" ]
+}

exemples/evenements-2-change/contenu.html → exemples/jquery/evenements-2-change/example.html


exemples/evenements-2-change/script.js → exemples/jquery/evenements-2-change/script.js


+ 9 - 0
exemples/jquery/evenements-3-keyup/config.json

@@ -0,0 +1,9 @@
+{
+  "title": "Evènements 3 : keyup",
+  "category": "evenements",
+  "html": [ "example.html" ],
+  "js": [ "script.js" ],
+  "css": [],
+  "libsJs": [ "jquery-3.2.1.min.js" ],
+  "libsCss": [ "styles.css" ]
+}

exemples/evenements-3-keyup/contenu.html → exemples/jquery/evenements-3-keyup/example.html


exemples/evenements-3-keyup/script.js → exemples/jquery/evenements-3-keyup/script.js


+ 9 - 0
exemples/jquery/exercice-1-verifier-un-formulaire/config.json

@@ -0,0 +1,9 @@
+{
+  "title": "Exercice 1 : vérifier un formulaire",
+  "category": "misc",
+  "html": [ "example.html" ],
+  "js": [ "script.js" ],
+  "css": [],
+  "libsJs": [ "jquery-3.2.1.min.js" ],
+  "libsCss": [ "styles.css" ]
+}

+ 2 - 1
exemples/exercice-1-verifier-un-formulaire/contenu.html

@@ -9,4 +9,5 @@
     <input type="password" name="mdp" placeholder="mot de passe" />
     <div id="mdp-statut" style="display:none"></div>
     <button type="submit">Go</button>
-</form>
+</form>
+<div id="result"></div>

+ 2 - 0
exemples/exercice-1-verifier-un-formulaire/script.js

@@ -17,4 +17,6 @@ $('#exo1-form').submit(function(e) {
         .addClass('text-red')
         .html('Le nom doit être rempli et comporter un espace');
     }
+    $('#result').html("Bienvenue " + inputName.val() +
+    ", votre mot de passe contient " + mdp.length + " caractères");
 });

exemples/exercice-1-verifier-un-formulaire/test.js → exemples/jquery/exercice-1-verifier-un-formulaire/test.js


+ 9 - 0
exemples/jquery/onglets/config.json

@@ -0,0 +1,9 @@
+{
+  "title": "Onglets",
+  "category": "misc",
+  "html": [ "example.html" ],
+  "js": [ "script.js" ],
+  "css": [],
+  "libsJs": [ "jquery-3.2.1.min.js" ],
+  "libsCss": [ "styles.css" ]
+}

exemples/onglets/contenu.html → exemples/jquery/onglets/example.html


exemples/onglets/script.js → exemples/jquery/onglets/script.js


+ 22 - 0
exemples/jquery/repo-config.json

@@ -0,0 +1,22 @@
+{
+  "title": "jQuery",
+  "defaultCategory": "misc",
+  "categories": [
+    {
+      "slug": "selecteurs",
+      "title": "Sélecteurs"
+    },
+    {
+      "slug": "evenements",
+      "title": "Évènements"
+    },
+    {
+      "slug": "ajax",
+      "title": "AJAX"
+    },
+    {
+      "slug": "misc",
+      "title": "Non-classé"
+    }
+  ]
+}

+ 9 - 0
exemples/jquery/selecteurs-basiques/config.json

@@ -0,0 +1,9 @@
+{
+  "title": "Sélecteurs basiques",
+  "category": "selecteurs",
+  "html": [ "example.html" ],
+  "js": [ "script.js" ],
+  "css": [],
+  "libsJs": [ "jquery-3.2.1.min.js" ],
+  "libsCss": [ "styles.css" ]
+}

exemples/selecteurs-basiques/contenu.html → exemples/jquery/selecteurs-basiques/example.html


exemples/selecteurs-basiques/script.js → exemples/jquery/selecteurs-basiques/script.js


+ 9 - 0
exemples/jquery/selecteurs-cibler-un-element-enfant/config.json

@@ -0,0 +1,9 @@
+{
+  "title": "Cibler un élément enfant",
+  "category": "selecteurs",
+  "html": [ "example.html" ],
+  "js": [ "script.js" ],
+  "css": [],
+  "libsJs": [ "jquery-3.2.1.min.js" ],
+  "libsCss": [ "styles.css" ]
+}

+ 14 - 0
exemples/jquery/selecteurs-cibler-un-element-enfant/example.html

@@ -0,0 +1,14 @@
+<div id="main">
+  <p>Paragraphe 1</p>
+  <div class="section">
+    <p>Paragraphe 2</p>
+    <div><p>Paragraphe 3</p></div>
+    <div><div>Div très imbriquée</div></div>
+  </div>
+  <div class="section">
+    <p>Paragraphe 4</p>
+    <div><p>Paragraphe 5</p></div>
+  </div>
+  <div class="pas-une-section">
+  </div>
+</div>

+ 3 - 0
exemples/jquery/selecteurs-cibler-un-element-enfant/script.js

@@ -0,0 +1,3 @@
+$('#main > div').addClass('gray')    // div DIRECTEMENT sous #main
+$('.section > p').addClass('blue')   // paragraphes 2 et 4 seulement
+$('.section > div').addClass('red')  // div contenant les paragraphes 2 et 4

+ 1 - 0
exemples/jquery/selecteurs-cibler-un-element-enfant/styles.css

@@ -0,0 +1 @@
+div {}

+ 9 - 0
exemples/jquery/selecteurs-filtres-2/config.json

@@ -0,0 +1,9 @@
+{
+  "title": "Filtres et pseudo-sélecteurs - 2",
+  "category": "selecteurs",
+  "html": [ "example.html" ],
+  "js": [ "script.js" ],
+  "css": [],
+  "libsJs": [ "jquery-3.2.1.min.js" ],
+  "libsCss": [ "styles.css" ]
+}

exemples/selecteurs-filtres-2/contenu.html → exemples/jquery/selecteurs-filtres-2/example.html


exemples/selecteurs-filtres-2/script.js → exemples/jquery/selecteurs-filtres-2/script.js


+ 9 - 0
exemples/jquery/selecteurs-filtres/config.json

@@ -0,0 +1,9 @@
+{
+  "title": "Filtres et pseudo-sélecteurs",
+  "category": "selecteurs",
+  "html": [ "example.html" ],
+  "js": [ "script.js" ],
+  "css": [],
+  "libsJs": [ "jquery-3.2.1.min.js" ],
+  "libsCss": [ "styles.css" ]
+}

exemples/selecteurs-filtres/contenu.html → exemples/jquery/selecteurs-filtres/example.html


exemples/selecteurs-filtres/script.js → exemples/jquery/selecteurs-filtres/script.js


+ 9 - 0
exemples/jquery/selecteurs-multiples/config.json

@@ -0,0 +1,9 @@
+{
+  "title": "Sélecteurs multiples",
+  "category": "selecteurs",
+  "html": [ "example.html" ],
+  "js": [ "script.js" ],
+  "css": [],
+  "libsJs": [ "jquery-3.2.1.min.js" ],
+  "libsCss": [ "styles.css" ]
+}

+ 1 - 1
exemples/selecteurs-multiples/contenu.html

@@ -1,5 +1,5 @@
 <h5>Sélection de boutons </h5>
-<p>Boutons cyan et magenta&nbsp;: enlève toutes les classes sur event clic</p>
+<p>Boutons cyan et magenta&nbsp;: enlève toutes les classes sur event click</p>
 <div>
   <button class="cyan bold-text">Cyan 1</button>
   <button class="cyan">Cyan 2</button>

exemples/selecteurs-multiples/script.js → exemples/jquery/selecteurs-multiples/script.js


+ 9 - 0
exemples/jquery/validation-de-formulaires-bootstrap/config.json

@@ -0,0 +1,9 @@
+{
+  "title": "Validation de formulaires Bootstrap",
+  "category": "misc",
+  "html": [ "example.html" ],
+  "js": [ "script.js" ],
+  "css": [],
+  "libsJs": [ "jquery-3.2.1.min.js" ],
+  "libsCss": [ "styles.css", "bootstrap.min.css" ]
+}

exemples/validation-de-formulaires-bootstrap/contenu.html → exemples/jquery/validation-de-formulaires-bootstrap/example.html


+ 3 - 3
exemples/validation-de-formulaires-bootstrap/script.js

@@ -30,7 +30,7 @@ $('#register-username')
     }
 
     $.get(
-        'http://localhost:3000/username-check?username=' + username,
+        'http://localhost:3000/jquery/ajax/username-check?username=' + username,
         function(response) {
             console.log(response.success)
             if(response.success) {
@@ -63,7 +63,7 @@ $('#form-register').submit(function(e) {
     console.log(userJSON);
     e.preventDefault();
     $(this).find('input').val('');
-    $.post('/register', userJSON, 'json');
+    $.post('/jquery/ajax/register', userJSON, 'json');
 })
 
 
@@ -78,7 +78,7 @@ $('#form-login').submit(function(e) {
     e.preventDefault();
     $(this).find('input').val('');
     $.post(
-      '/login',
+      '/jquery/ajax/login',
       userJSON,
       function(data) {
         $('#alert-box')

+ 0 - 25
exemples/liste.json

@@ -1,25 +0,0 @@
-[
-  { "slug": "selecteurs-basiques", "title": "Sélecteurs basiques" },
-  { "slug": "selecteurs-multiples", "title": "Sélecteurs multiples" },
-  { "slug": "selecteurs-filtres", "title": "Sélecteurs : filtres" },
-  { "slug": "selecteurs-filtres-2", "title": "Sélecteurs : filtres 2" },
-  { "slug": "evenements-1-click-submit", "title": "Evènements 1 : click et submit" },
-  { "slug": "evenements-2-change", "title": "Evènements 2 : change" },
-  { "slug": "evenements-3-keyup", "title": "Evènements 3 : keyup" },
-  { "slug": "ajax-requete-randomuserme", "title": "AJAX - Requête randomuser.me" },
-  { "slug": "ajax-requete-themoviedborg", "title": "AJAX - Requête themoviedb.org" },
-  {
-    "slug": "exercice-1-verifier-un-formulaire",
-    "title": "Exercice 1 : vérifier un formulaire",
-    "test": true
-  },
-  { "slug": "onglets", "title": "Onglets" },
-  {
-    "slug": "validation-de-formulaires-bootstrap",
-    "title": "Validation de formulaires Bootstrap"
-  },
-  { "slug": "ajax-exemple-simple", "title": "AJAX - Exemple simple" },
-  { "slug": "ajax-envoi-formulaire-par-get", "title": "AJAX - Envoi formulaire par GET" },
-  { "slug": "ajax-envoi-formulaire-par-post", "title": "AJAX - Envoi formulaire par POST" },
-  { "slug": "ajax-donnees-page-produit", "title": "AJAX - Données - page produit" }
-]

BIN
favicon.ico


+ 0 - 0
html/empty.html


+ 233 - 0
html/index.mustache.html

@@ -0,0 +1,233 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<title>JS Sandbox</title>
+    <link rel="stylesheet" href="/css/normalize.css">
+    <link rel="stylesheet" href="/css/main.css">
+    {{#testMode}}<link rel="stylesheet" href="/css/qunit-2.4.1.css">{{/testMode}}
+    <link rel="stylesheet" href="/css/vendor/pure-grids-min.css">
+    <link rel="stylesheet" href="/css/vendor/pure-grids-responsive-min.css">
+    <link rel="stylesheet" href="/css/styles.css">
+<style type="text/css" media="screen">
+
+</style>
+</head>
+<body>
+
+<div class="panel-container">
+
+    <div class="panel-left">
+
+        <div id="navbar">
+            <button id="menu-btn" class="btn icon-menu"></button>
+            <a id="nav-back-home" href="/">JS Sandbox</a>
+            {{#testMode}}<a id="test-reload-btn" href="/?testing=1" style="float:right;"><button class="btn icon-loop2"></button></a>{{/testMode}}
+        </div>
+
+        <div id="nav-menus">
+            <div id="menu-repo" class="pure-g">
+              <div class="pure-u-1">
+                <span class="cat-title">{{_.exampleCollections}}</span>
+                <ul class="nav-menu">{{#menuRepo}}
+                  <li><a href="/{{path}}">{{title}}</a></li>{{/menuRepo}}
+                </ul>
+              </div>
+            </div>
+
+            <div id="menu-example" class="pure-g nav-menu">{{#menuExample}}
+              <div class="pure-u-1 pure-u-md-1-2">
+              <span class="cat-title">{{category.title}}</span>
+              <ul data-id="{{category.slug}}">{{#examples}}
+              <li><a href="/{{{slug}}}">{{title}}</a></li>{{/examples}}</ul>
+            </div>{{/menuExample}}</div>
+        </div>
+
+        <div id="notification" class="alert-box"></div>
+        <div class="panel-inner">
+
+        {{#errorMessage}}
+            <div id="server-alert" class="alert-box error">{{errorMessage}}</div>
+        {{/errorMessage}}
+
+            <div class="details-div">
+              <div class="inline-block left">
+                <section id="shortcut-repo"{{#repo}} class="hidden"{{/repo}}>
+                  <div class="bold">{{_.exampleCollections}}</div><ul>{{#menuRepo}}
+                    <li><a href="/{{path}}">{{title}}</a></li>{{/menuRepo}}
+                  </ul>
+                </section>
+                <section id="details-repo" class="inline-block">
+                {{#repo}}
+                    <span class="bold">{{_.collection}} </span><span>{{title}}</span>
+                {{/repo}}
+                </section>
+              </div>
+
+              <div id="add-repo" class="btn-form inline-block">
+                  <button class="add-btn inline-block h-collapsed light-gray fast in"><span class="icon-plus"></span> {{_.addCollection}}</button>
+                  <form class="inline-block h-collapsed">
+                      <input type="text" name="title" class="input-sm" value="" placeholder="{{_.collectionName}}" required />
+                      <button type="button" class="icon-cross rounded"></button><!--
+                      --><button type="submit" class="icon-checkmark rounded"></button>
+                  </form>
+              </div>
+            </div>
+
+            <div class="details-div">
+              <div class="inline-block left">
+                <section id="shortcut-example"{{^showShortcutExample}} class="hidden"{{/showShortcutExample}}>
+                  <div class="bold">{{_.examplesInCollection}}</div>{{#menuExample}}
+                  <span class="cat-title">{{category.title}}</span>
+                  <ul data-id="{{category.slug}}">{{#examples}}
+                  <li><a href="/{{{slug}}}">{{title}}</a></li>{{/examples}}</ul>
+                {{/menuExample}}
+                </section>
+                <section id="details-example" class="inline-block">
+                {{#example}}
+                    <strong>{{_.example}} </strong><span>{{title}}</span>
+                {{/example}}
+                </section>
+              </div>
+              <div id="add-example" class="btn-form inline-block">
+                <button class="add-btn inline-block h-collapsed light-gray fast{{#repo}} in{{/repo}}"><span class="icon-plus"></span> {{_.addExample}}</button>
+                <form class="inline-block h-collapsed">
+                    <input type="text" name="title" class="input-sm" value="" placeholder="{{_.exampleName}}" required />
+                    <button type="button" id="add-example-cancel" class="icon-cross rounded"></button><!--
+                    --><button type="submit" id="add-example-save" class="icon-checkmark rounded"></button>
+                </form>
+              </div>
+            </div>
+
+
+            <!-- <button id="save-changes" class="icon-cloud-upload green"></button> -->
+
+        </div>
+
+        
+        <div id="editor-wrapper"{{^showEditor}} style="display: none";{{/showEditor}}>
+            <ul id="tabs">{{#files}}
+                <li class="tab-{{type}}" data-type="{{type}}" data-name="{{name}}">{{name}} <span class="icon-cloud-upload"></span></li>
+            {{/files}}
+            </ul><ul id="add-file">
+            <li><button class="add-btn inline-block h-collapsed fast in">+</button>
+                <form class="inline-block h-collapsed">
+                    <input type="text" name="title" class="input-xs input-light-border" value="" placeholder="{{_.fileNameWithExt}}" required />
+                    <button type="button" id="add-example-cancel" class="icon-cross"></button><!--
+                    --><button type="submit" id="add-example-save" class="icon-checkmark"></button>
+                </form></li>
+            </ul>
+            <div id="editor"></div>
+        </div>
+
+        <!-- <script type="text/html" id="editor-javascript"></script>
+        <script type="text/html" id="editor-html"></script>
+        <script type="text/html" id="editor-css"></script> -->
+        <!-- <button id="revert-editor">!</button> -->
+    </div>
+
+    <div class="splitter">
+    </div>
+
+    <div class="panel-right">
+      <iframe id="sandbox-iframe"{{#testMode}} class="test-mode"{{/testMode}} {{#example}}src="/examples/{{repo.path}}/{{example.slug}}"{{/example}}{{^example}}src="/html/start-iframe.html"{{/example}}></iframe>
+      {{#testMode}}
+      <div id="qunit"></div>
+      <div id="qunit-fixture"></div>
+      {{/testMode}}
+    </div>
+
+</div>
+
+{{=<% %>=}}
+<script data-tmpl-for="menu-example" id="menu-example-tmpl" type="text/x-mustache-tmpl">
+  {{#menuExample}}
+  <div class="pure-u-1 pure-u-md-1-2">
+    <span class="cat-title">{{category.title}}</span>
+    <ul data-id="{{category.slug}}">{{#examples}}
+      <li><a href="/{{{slug}}}">{{title}}</a></li>{{/examples}}
+    </ul>
+  </div>{{/menuExample}}
+</script>
+
+<script data-tmpl-for="details-example" id="details-example-tmpl" type="text/x-mustache-tmpl">
+  {{#example}}
+    <strong>{{_.example}} </strong><span>{{title}}</span>
+  {{/example}}
+</script>
+
+<script data-tmpl-for="tabs" id="tabs-tmpl" type="text/x-mustache-tmpl">
+{{#files}}
+    <li class="tab-{{type}}" data-type="{{type}}" data-name="{{name}}">{{name}} <span class="icon-cloud-upload"></span></li>
+{{/files}}
+</script>
+
+<script data-tmpl-for="details-repo" id="details-example-repo" type="text/x-mustache-tmpl">
+{{#repo}}
+  <strong>{{_.collection}} </strong><span>{{title}}</span>
+{{/repo}}
+</script>
+
+<script data-tmpl-for="shortcut-repo" type="text/x-mustache-tmpl">
+  <div class="bold">{{_.exampleCollections}}</div><ul>{{#repos}}
+    <li><a href="/{{path}}">{{title}}</a></li>{{/repos}}
+  </ul>
+</script>
+
+<script data-tmpl-for="menu-repo" type="text/x-mustache-tmpl">
+  <div class="pure-u-1">
+    <span class="cat-title">{{_.exampleCollections}}</span>
+    <ul class="nav-menu">{{#repos}}
+      <li><a href="/{{path}}">{{title}}</a></li>{{/repos}}
+    </ul>
+  </div>
+</script>
+
+<script data-tmpl-for="shortcut-example" type="text/x-mustache-tmpl">
+  <div class="bold">{{_.examplesInCollection}}</div>{{#menuExample}}
+  <span class="cat-title">{{category.title}}</span>
+  <ul data-id="{{category.slug}}">{{#examples}}
+  <li><a href="/{{{slug}}}">{{title}}</a></li>{{/examples}}</ul>
+  {{/menuExample}}
+</script>
+<%={{ }}=%>
+
+<!-- Vendor scripts -->
+<script src="/js/vendor/eventemitter.js"></script>
+<script src="/js/vendor/modernizr-3.5.0.min.js"></script>
+<script src="/js/plugins.js"></script>
+<script src="/js/vendor/jquery-3.2.1.min.js" ></script>
+<script src="/js/vendor/jquery-resizable.min.js" ></script>
+<script src="/js/vendor/jquery.color.js" ></script>
+<script src="/js/vendor/mustache.min.js" ></script>
+<script src="/js/vendor/lodash.min.js" ></script>
+<script src="/js/vendor/loadJS.js" ></script>
+<script src="/js/vendor/ace/ace.js" type="text/javascript" charset="utf-8"></script>
+
+<!-- WS scripts -->
+<script id="inline-js-data">
+window._ws = {
+  files: {{{filesJSON}}},
+  repo: {{{repoJSON}}},
+  repos: {{{reposJSON}}},
+  _: {{{_JSON}}},
+  example: {{{exampleJSON}}}
+};
+</script>
+<script src="/js/req-promise.js"></script>
+<script src="/js/ws-ui-parts.js"></script>
+<script src="/js/ws-notify.js"></script>
+<script src="/js/ws-events.js"></script>
+<script src="/js/ws-menu.js"></script>
+<script src="/js/ws-forms.js"></script>
+<script src="/js/editor-local-storage.js"></script>
+<script src="/js/ws-editor.js"></script>
+{{#testRun}}
+<script>
+window._ws._path = "{{{appPath}}}";
+</script>
+<script src="/js/vendor/qunit-2.4.1.js"></script>
+<script src="/js/test/ws.test.js"></script>
+{{/testRun}}
+
+</body>
+</html>

+ 26 - 0
html/start-iframe.html

@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<title>Sandbox iframe</title>
+    <link rel="stylesheet" href="../css/normalize.css">
+    <link rel="stylesheet" href="../css/main.css">
+    <link rel="stylesheet" href="../css/styles.css">
+<!--     <link rel="stylesheet" href="css/bootstrap.min.css"> -->
+<style type="text/css" media="screen">
+body {
+    font-family: Arial, Helvetica;
+}
+</style>
+</head>
+<body>
+
+<div class="container">
+    <h2>Choisissez un exemple avec le sélecteur pour commencer...</h2>
+</div>
+
+
+<script src="../js/vendor/modernizr-3.5.0.min.js"></script>
+<script src="../js/vendor/jquery-3.2.1.min.js" ></script>
+<script src="../js/plugins.js"></script>
+</body>
+</html>

+ 41 - 0
html/template.mustache.html

@@ -0,0 +1,41 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+  <title>{{ title }} - Sandbox iframe</title>
+    <!-- HTML5 Boilerplate CSS -->
+    <link rel="stylesheet" href="/css/normalize.css">
+    <link rel="stylesheet" href="/css/main.css">
+    <!-- Vendor CSS -->
+    {{#libsCss}}
+    <link rel="stylesheet" href="/css/vendor/{{ . }}">
+    {{/libsCss}}
+    <!-- Example's CSS -->
+    {{#css}}
+    <link rel="stylesheet" href="/{{examplesDir}}/{{ repoSlug }}/{{ slug }}/{{ . }}">
+    {{/css}}
+    <style type="text/css" media="screen">
+    body {
+        font-family: Arial, Helvetica;
+    }
+    </style>
+  </head>
+  <body>
+
+    <div class="container">
+        {{{ body }}}
+    </div>
+
+    <!-- HTML5 Boilerplate JS -->
+    <script src="/js/vendor/modernizr-3.5.0.min.js"></script>
+    <script src="/js/plugins.js"></script>
+    <!-- Vendor JS -->
+    {{#libsJs}}
+    <script src="/js/vendor/{{ . }}" ></script>
+    {{/libsJs}}
+    <!-- Example's JS -->
+    {{#js}}
+    <script src="/{{examplesDir}}/{{ repoSlug }}/{{ slug }}/{{ . }}"></script>
+    {{/js}}
+
+  </body>
+</html>

+ 0 - 71
index.html

@@ -1,71 +0,0 @@
-<!DOCTYPE html>
-<html lang="en">
-<head>
-<title>Formation jQuery</title>
-    <link rel="stylesheet" href="css/normalize.css">
-    <link rel="stylesheet" href="css/main.css">
-<!--     <link rel="stylesheet" href="css/qunit-2.4.1.css"> -->
-    <link rel="stylesheet" href="css/bootstrap.min.css">
-    <link rel="stylesheet" href="css/styles.css">
-    <link rel="stylesheet" href="css/bootstrap.min.css">
-<style type="text/css" media="screen">
-
-</style>
-</head>
-<body>
-
-<div class="panel-container">
-
-    <div class="panel-left">
-        <div id="notification"></div>
-        <div class="panel-inner">
-            <button id="add-example-btn" class="icon-plus rounded blue"></button>
-            <form id="add-example-form" style="display: none;">
-                <input type="text" name="title" class="input-sm" value="" />
-                <button type="button" id="add-example-cancel" class="icon-cross rounded red"></button><!--
-                --><button type="submit" id="add-example-save" class="icon-checkmark rounded green"></button>
-            </form>
-            <nav id="selector">
-                <select id="file-select">
-                    <option value="-">&mdash;</option>
-                </select>
-            </nav>
-            <nav id="tabs">
-                <ul>
-                    <li><button id="show-html">HTML</button></li>
-                    <li><button id="show-javascript">JS</button></li>
-                </ul>
-            </nav>
-            <button id="save-changes" class="icon-cloud-upload green"></button>
-        </div>
-        <div id="editor"></div>
-        <script type="text/html" id="editor-javascript"></script>
-        <script type="text/html" id="editor-html"></script>
-        <button id="revert-editor">!</button>
-    </div>
-
-    <div class="splitter">
-    </div>
-
-    <div class="panel-right">
-        <div id="html-content" class="container"></div>
-        <div id="tests" style="display:none">
-            <div id="qunit"></div>
-            <div id="qunit-fixture"></div>
-        </div>
-    </div>
-</div>
-
-<script src="js/vendor/modernizr-3.5.0.min.js"></script>
-<script src="js/vendor/jquery-3.2.1.min.js" ></script>
-<script src="js/vendor/jquery-resizable.min.js" ></script>
-<!-- <script src="js/vendor/qunit-2.4.1.js" ></script> -->
-<script src="js/vendor/lodash.min.js" ></script>
-<script src="js/vendor/loadJS.js" ></script>
-<script src="js/plugins.js"></script>
-<script src="js/main.js"></script>
-<script src="js/vendor/ace/ace.js" type="text/javascript" charset="utf-8"></script>
-<script src="js/editor-local-storage.js"></script>
-<script src="js/editor.js"></script>
-</body>
-</html>

+ 0 - 271
js/editor.js

@@ -1,271 +0,0 @@
-$(document).ready(function() {
-
-  var $editor        = $('#editor');
-  var $editorJs      = $('#editor-javascript');
-  var $editorHtml    = $('#editor-html');
-  var $htmlContent   = $('#html-content');
-  var $selectorNav   = $('#selector');
-  var $fileSelect    = $('#file-select');
-  var $addExampleBtn = $('#add-example-btn');
-  var $exampleForm   = $('#add-example-form');
-  var $exampleSave   = $('#add-example-save');
-  var $exampleCancel = $('#add-example-cancel');
-  var $saveChanges   = $('#save-changes');
-  var $notification  = $('#notification');
-  var $revertEditor  = $('#revert-editor');
-  var $panelLeft     = $('.panel-left');
-  var $panelRight    = $('.panel-right');
-  var $window        = $(window);
-  var activeMode     = 'html';
-  var currentHash; 
-  var editor;
-  var editorStorage = new LocalStorageDraft();
-  var saveTimeout1;
-  var saveTimeout2;
-  var exampleList;
-
-  $panelLeft.resizable({
-    handleSelector: ".splitter",
-    resizeHeight: false,
-    onDrag: function(e) {
-      $editor.width($panelLeft.width());
-    }
-  });
-  $window.resize(function() {
-    $editor.width($panelLeft.width());
-    $(".panel-container").height($(window).height());
-  });
-  $(".panel-container").height($(window).height());
-  $editor.width($panelLeft.width());
-
-
-  editor = ace.edit("editor");
-  editor.setTheme("ace/theme/eclipse");
-  editor.$blockScrolling = Infinity;
-  editor.getSession().setUseWrapMode(true);
-
-  function setCurrentHash(slug) {
-    // console.log('setCurrentHash', slug)
-    if(slug) {
-      // console.log('save current hash', slug);
-      window.location.hash = currentHash = slug;
-    }
-    else {
-      currentHash = window.location.hash ?
-          window.location.hash.substr(1) : undefined;
-      // if(currentHash) console.log('restored current hash', currentHash);
-    }
-  }
-
-  function setEditorMode(mode) {
-    editor.getSession().setMode("ace/mode/" + mode);
-  }
-
-  function saveToLocalStorage() {
-    var editorContent =  editor.getSession().getValue();
-    // console.log('saveToLocalStorage', activeMode, editorContent.substr(0, 10) + '[...]');
-    editorStorage.saveSource(activeMode, editorContent);
-    saveTimeout = undefined;
-  }
-
-  function editorContentChanged() {
-    // console.log('editorContent changed')
-    if(saveTimeout1 || saveTimeout2) {
-      clearTimeout(saveTimeout1);
-      clearTimeout(saveTimeout2);
-    }
-    saveTimeout1 = setTimeout(saveToLocalStorage, 500);
-    // saveTimeout2 = setTimeout(saveChanges, 1000);
-  }
-
-  editor.getSession().on('change', editorContentChanged);
-
-  function setActiveTab(mode) {
-    // console.log('setting mode', mode);
-    var elementId = 'show-' + mode;
-    $('#show-' + activeMode).removeClass('active');
-    activeMode = mode;
-    $('#' + elementId).addClass('active');
-    var ed = $('#editor-' + mode);
-    setEditorMode(mode);
-    editor.getSession().off('change');
-    editor.getSession().setValue(ed[0].innerHTML);
-    editor.getSession().on('change', editorContentChanged);
-  }
-
-  $('#tabs button').click(function() {
-    saveToLocalStorage();
-    var mode = $(this).prop('id').substr(5);
-    setActiveTab(mode);
-  })
-
-  function loadAsync(url, dataType) {
-    return new Promise(function(resolve, reject) {
-      $.ajax({
-        type: 'GET',
-        url: url,
-        success: function(data) {
-          resolve(data);
-        },
-        error: function(jqXHR) {
-          reject(new Error(jqXHR.responseText));
-        }
-      }, dataType);
-    });
-  }
-
-  function loadExample(exampleSlug) {
-    // console.log('loadExample', exampleSlug);
-    var serverPath = 'exemples/' + exampleSlug + '/';
-    loadAsync(serverPath + 'script.js', 'text')
-    .then(javascript => $editorJs.html(javascript))
-    .then(() => loadAsync(serverPath + '/contenu.html', 'text'))
-    .then(html => {
-      $editorHtml.html(html);
-      setHtmlContent(html);
-      setActiveTab('html');
-      setCurrentHash(exampleSlug);
-      var sources = {
-        html: $editorHtml.html(),
-        javascript: $editorJs.html()
-      };
-      editorStorage.init(exampleSlug, sources);
-      loadJS(serverPath + 'script.js');
-    })
-    .then(() => {
-      var item = _.find(exampleList, { slug: exampleSlug });
-      // console.log(item.test ? 'test' : 'no test');
-      // loadJS('exemples/' + item.slug + '/test.js', function() {
-      //   $('#tests').show();
-      // });
-    });
-
-  }
-
-  function addFileSelectItem(item) {
-    $fileSelect.append(
-      '<option value="' + item.slug + '">' +
-        item.title +
-      '</option>'
-    );
-  }
-
-  function loadExampleList() {
-    $.get('exemples/liste.json', function(_exampleList) {
-      exampleList = _exampleList;
-      var restoredDraft;
-      exampleList.forEach(addFileSelectItem);
-      if(currentHash) {
-        $fileSelect.val(currentHash);
-        var item = _.find(exampleList, { slug: currentHash });
-        if( ! item) {
-          return;
-        }
-        restoredDraft = editorStorage.restore(item.slug);
-        if(! restoredDraft) {
-          loadExample(item.slug);
-        }
-        else {
-          $editorHtml.html(restoredDraft.sources.html);
-          setHtmlContent(restoredDraft.sources.html);
-          $editorJs.html(restoredDraft.sources.javascript);
-          loadJS('exemples/' + item.slug + '/script.js');
-          // if(item.test) {
-          //   loadJS('exemples/' + item.slug + '/test.js', function() {
-          //     $('#tests').show();
-          //   });
-          // }
-          setActiveTab('html');
-        }
-      }
-
-    }, 'json');
-  }
-
-  function setHtmlContent(html) {
-    $htmlContent.empty();
-    $htmlContent.html(html);
-  }
-
-  function notify(type, text) {
-    $notification
-    .addClass(type)
-    .addClass('active');
-    $notification.html(text);
-    setTimeout(function() {
-      $notification.removeClass('active');
-    }, 2000);
-    setTimeout(function() {
-      $notification.removeClass(type);
-    }, 3000);
-  }
-
-  function toggleEditor() {
-    $addExampleBtn.toggle();
-    $selectorNav.toggle();
-    $exampleForm.toggle();
-  }
-
-  function saveExample(e) {
-    e.preventDefault();
-    var title = $(this).find('input[name="title"]').val();
-    $.ajax({
-      type: 'POST',
-      url: '/examples',
-      data: JSON.stringify({ title }),
-      success: function(newExample) {
-        clearAndCloseEditor();
-        addFileSelectItem(newExample);
-        $fileSelect.val(newExample.slug);
-        $fileSelect.trigger('change');
-        notify('success', "Exemple créé !");
-      },
-      error: function(jqXHR, textStatus, errorThrown ) {
-        notify('error', 'Erreur: ' + jqXHR.responseText);
-      },
-      contentType: 'application/json',
-      dataType: 'json'
-    });
-  }
-
-  function clearAndCloseEditor() {
-    $exampleForm.find('input').val('');
-    toggleEditor();
-  }
-
-  function revertEditor() {
-    editorStorage.reset();
-    location.reload();
-  }
-
-  function saveChanges() {
-    var payload = editorStorage.getSources();
-
-    $.ajax({
-      type: 'PUT',
-      url: '/examples/' + currentHash,
-      data: JSON.stringify(payload),
-      success: function(newExample) {
-        notify('success', "Exemple sauvegardé !");
-        loadExample(currentHash);
-      },
-      error: function(jqXHR, textStatus, errorThrown ) {
-        notify('error', 'Erreur: ' + jqXHR.responseText);
-      },
-      contentType: 'application/json',
-      dataType: 'json'
-    });
-  }
-
-  setCurrentHash();
-
-  $fileSelect.change(function() {
-    loadExample($(this).val());
-  });
-  $addExampleBtn.click(toggleEditor);
-  $exampleCancel.click(clearAndCloseEditor);
-  $saveChanges.click(saveChanges);
-  $exampleForm.submit(saveExample);
-  $revertEditor.click(revertEditor);
-  loadExampleList();
-});

+ 59 - 0
js/req-promise.js

@@ -0,0 +1,59 @@
+(function($) {
+  if(! $) {
+    throw new Error('neither jQuery nor Zepto is loaded');
+  }
+
+  function getAsync(url, dataType) {
+    return new Promise(function(resolve, reject) {
+      $.ajax({
+        type: 'GET',
+        url: url,
+        success: function(data) {
+          resolve(data);
+        },
+        error: function(jqXHR) {
+          reject(new Error(jqXHR.responseText));
+        }
+      }, dataType);
+    });
+  }
+
+  function sendAsync(url, data, options) {
+    if(['POST', 'PUT', 'PATCH'].indexOf(options.type) === -1) {
+      throw new Error('sendAsync must be called with options.type set to POST or PUT');
+    }
+    if(typeof data === 'object') {
+      data = JSON.stringify(data);
+    }
+    return new Promise(function(resolve, reject) {
+      options = _.extend(options, {
+        url: url,
+        data: data,
+        contentType: 'application/json',
+        dataType: 'json',
+        success: function(data) {
+          resolve(data);
+        },
+        error: function(jqXHR, textStatus, errorThrown) {
+          console.log('this should throw a bloody error', jqXHR.responseText);
+          reject(new Error(errorThrown));
+        }
+      });
+      $.ajax(options);
+    });
+  }
+
+  function postAsync(url, data, options) {
+    return sendAsync(url, data, _.extend(options, { type: 'POST' }));
+  }
+
+  function putAsync(url, data, options) {
+    return sendAsync(url, data, _.extend(options, { type: 'PUT' }));
+  }
+
+  window.rp = {
+    get: getAsync,
+    post: postAsync,
+    put: putAsync
+  };
+})(jQuery || Zepto);

+ 162 - 0
js/test/ws.test.js

@@ -0,0 +1,162 @@
+function timeoutAsync(cb, delay) {
+  return new Promise((resolve, reject) => {
+    setTimeout(function() {
+      cb();
+      resolve(true);
+    }, delay);
+  });
+}
+
+function delayAsync(delay) {
+  return new Promise((resolve, reject) => {
+    setTimeout(function() {
+      resolve(true);
+    }, delay);
+  });
+}
+
+
+function getId() {
+  return Date.now().toString(36);
+}
+
+(function($) {
+  $(document).ready(function() {
+
+
+    var $mainMenu    = $('#nav-menus');
+    var $menuRepo    = $('#menu-repo');
+    var $linkToRepo1 = $menuRepo.find('a:first');
+    var $menuExample = $('#menu-example');
+    var $menuBtn     = $('#menu-btn');
+    var $homeLink    = $('#nav-back-home');
+    var $editor      = $('#editor');
+    var $editorTabs  = $('#tabs');
+
+
+    QUnit.test( "hello test", function( assert ) {
+      assert.ok( 1 == "1", "Passed!" );
+    });
+
+
+    QUnit.test( "initial state", function( assert ) {
+      assert.equal( _ws.files.length, 0, "_ws.files should hold 0 file when app started from root" );
+      assert.ok( $menuRepo.is(':visible'), "menu repo visible (though menu is hidden)" );
+      assert.equal( ($menuRepo.find('a')).length, 2, "menu repo should contain 2 links" );
+      assert.ok( $menuExample.is(':visible'), "menu example visible (though menu is hidden AND menu empty)" );
+      assert.equal( ($menuExample.find('a')).length, 0, "menu example should contain 0 links" );
+      assert.ok( ! $editor.is(':visible'), "editor invisible" );
+      assert.ok( ! $editorTabs.is(':visible'), "editor tabs invisible" );
+    });
+
+
+    QUnit.test( "test menu toggle", function( assert ) {
+      var done = assert.async();
+      assert.equal( $mainMenu.width(), 0, "0. INIT width should be 0" );
+      assert.ok( $mainMenu.is(':visible'), "0. INIT menu should NOT have class 'in'" );
+      $menuBtn.trigger('click');
+      assert.ok( $mainMenu.hasClass('in'), "1.AFTER CLICK menu should have class 'in'" );
+      assert.ok( $mainMenu.is(':visible'), "1.AFTER CLICK menu should be visible" );
+      timeoutAsync(() => {
+        assert.notEqual( $mainMenu.width(), 0, "1.AFTER CLICK menu should have width > 0" );
+        $menuBtn.trigger('click');
+        assert.ok( ! $mainMenu.hasClass('in'), "2.AFTER 2ND CLICK menu should NOT have class 'in'" );
+      }, 120)
+      .then(() => timeoutAsync(() => {
+        assert.equal( $mainMenu.width(), 0, "2.AFTER 2ND CLICK width should be 0" );
+        done();
+      }, 180));
+    });
+
+
+    QUnit.test( "test nav to repo", function( assert ) {
+      var done = assert.async();
+      assert.equal( window.location.pathname, '/', "location should be / at first" );
+      $linkToRepo1.trigger('click');
+      assert.equal( window.location.pathname, '/example-repo-1', "location should be /example-repo-1" );
+      timeoutAsync(() => {
+        assert.equal( ($menuExample.find('a')).length, 1, "menu example should contain 1 entry" );
+        assert.equal( $menuExample.find('a:first').html(), "Test Example", "link to 1st example should be labelled 'Test Example'" );
+        assert.ok( !! _ws.repo, "_ws.repo should not be undefined" );
+        assert.equal( _ws.repo.title, "Example Repo 1", '_ws.repo.title is "Example Repo 1"' );
+        done();
+      }, 100);
+    });
+
+
+    QUnit.test( "test nav to example", function( assert ) {
+      var done = assert.async();
+      $linkToRepo1.trigger('click');
+      assert.equal( window.location.pathname, '/example-repo-1', "location should be /example-repo-1" );
+      assert.equal( ($menuExample.find('a')).length, 1, "menu example should contain 1 entry" );
+      var $linkToExmp1 = $menuExample.find('a:first');
+      $linkToExmp1.trigger('click');
+      assert.equal( window.location.pathname, '/example-repo-1/repo1-example1', "location should be /example-repo-1/repo1-example1" );
+      timeoutAsync(() => {
+        assert.ok( $editor.is(':visible'), "editor visible" );
+        assert.equal( _ws.files.length, 2, "_ws.files should hold 2 files" );
+        done();
+      }, 50);
+    });
+
+
+    QUnit.test( "test adding a collection", function( assert ) {
+      var done = assert.async();
+      var $addRepo = $('#add-repo');
+      var $addRepoAddBtn = $addRepo.find('.add-btn');
+      var $addRepoForm = $addRepo.find('form');
+      assert.equal( $addRepo.length, 1, "there should be one #add-repo element" );
+      assert.equal( $addRepoForm.length, 1, "#add-repo should contain one form element" );
+      assert.ok( $addRepoForm.is(':visible'), "the form element shouldn't be visible before clicking the + btn" );
+      $addRepoAddBtn.trigger( 'click' );
+      assert.ok( $addRepoForm.is(':visible'), "clicking on the + btn should have made the form appear" );
+
+      done();
+    });
+
+    QUnit.test( "test adding a file", function( assert ) {
+      var done = assert.async();
+      var $addFile = $('#add-file');
+      var $addFileInput = $('#add-file').find('input[type="text"]');
+      var $addFileAddBtn = $addFile.find('.add-btn');
+      var $addFileForm = $addFile.find('form');
+      $linkToRepo1.trigger('click');
+      timeoutAsync(() => {
+        var $linkToExmp1 = $menuExample.find('a:first');
+        console.log('##### clicked on first example', $linkToExmp1.prop('href'));
+        $linkToExmp1.trigger( 'click' );
+      }, 50)
+      .then(() => delayAsync(400))
+      .then(() => {
+        assert.equal( $addFile.length, 1, "there should be one #add-file element" );
+        assert.equal( $addFileForm.length, 1, "#add-file should contain one form element" );
+        // assert.ok( ! $addFileForm.is(':visible'), "the form element shouldn't be visible before clicking the + btn" );
+        assert.ok( $addFileForm.width() === 0, "the form element shouldn't be visible before clicking the + btn" );
+        $addFileAddBtn.trigger( 'click' );
+      })
+      .then(() => timeoutAsync(() => {
+        console.log( $addFileForm );
+        // assert.ok( $addFileForm.is(':visible'), "clicking on the + btn should have made the form appear" );
+        assert.ok( $addFileForm.width() > 0, "clicking on the + btn should have made the form appear" );
+        $addFileInput.val('script-' + getId() + '.js');
+        $addFileForm.find('button[type="submit"]')
+        .trigger( 'click' );
+      }, 100))
+      .then(() => delayAsync(400))
+      .then(done);
+    });
+
+
+    QUnit.test( "test nav back to home", function( assert ) {
+      var done = assert.async();
+      $homeLink.trigger('click');
+      assert.ok( window.location.pathname, '/', "location should be / when clicking app title" );
+      timeoutAsync(() => {
+        assert.equal( ($menuExample.find('a')).length, 0, "menu example should contain 0 entry" );
+        done();
+      }, 50);
+    });
+
+
+  });
+})(jQuery);

+ 5 - 4
js/vendor/ace/ace.js

@@ -14371,9 +14371,10 @@ Editor.$uid = 0;
         if (this.$tryReplace(range, replacement)) {
             replaced = 1;
         }
-
-        this.selection.setSelectionRange(range);
-        this.renderer.scrollSelectionIntoView(range.start, range.end);
+        if (range !== null) {
+            this.selection.setSelectionRange(range);
+            this.renderer.scrollSelectionIntoView(range.start, range.end);
+        }
 
         return replaced;
     };
@@ -19300,7 +19301,7 @@ var Editor = require("./editor").Editor;
         }
         function alignRight(m) {
             return !m[2] ? m[0] : spaces(startW + textW - m[2].length) + m[2]
-                + spaces(endW)
+                + spaces(endW, " ")
                 + m[4].replace(/^([=:])\s+/, "$1 ");
         }
         function unAlign(m) {

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 14 - 0
js/vendor/ace/ace.min.js


+ 69 - 0
js/vendor/eventemitter.js

@@ -0,0 +1,69 @@
+/* Polyfill indexOf. */
+var indexOf;
+
+if (typeof Array.prototype.indexOf === 'function') {
+    indexOf = function (haystack, needle) {
+        return haystack.indexOf(needle);
+    };
+} else {
+    indexOf = function (haystack, needle) {
+        var i = 0, length = haystack.length, idx = -1, found = false;
+
+        while (i < length && !found) {
+            if (haystack[i] === needle) {
+                idx = i;
+                found = true;
+            }
+
+            i++;
+        }
+
+        return idx;
+    };
+};
+
+
+/* Polyfill EventEmitter. */
+var EventEmitter = function () {
+    this.events = {};
+};
+
+EventEmitter.prototype.on = function (event, listener) {
+    if (typeof this.events[event] !== 'object') {
+        this.events[event] = [];
+    }
+
+    this.events[event].push(listener);
+};
+
+EventEmitter.prototype.removeListener = function (event, listener) {
+    var idx;
+
+    if (typeof this.events[event] === 'object') {
+        idx = indexOf(this.events[event], listener);
+
+        if (idx > -1) {
+            this.events[event].splice(idx, 1);
+        }
+    }
+};
+
+EventEmitter.prototype.emit = function (event) {
+    var i, listeners, length, args = [].slice.call(arguments, 1);
+
+    if (typeof this.events[event] === 'object') {
+        listeners = this.events[event].slice();
+        length = listeners.length;
+
+        for (i = 0; i < length; i++) {
+            listeners[i].apply(this, args);
+        }
+    }
+};
+
+EventEmitter.prototype.once = function (event, listener) {
+    this.on(event, function g () {
+        this.removeListener(event, g);
+        listener.apply(this, arguments);
+    });
+};

+ 654 - 0
js/vendor/jquery.color.js

@@ -0,0 +1,654 @@
+/*!
+ * jQuery Color Animations v@VERSION
+ * https://github.com/jquery/jquery-color
+ *
+ * Copyright jQuery Foundation and other contributors
+ * Released under the MIT license.
+ * http://jquery.org/license
+ *
+ * Date: @DATE
+ */
+
+( function( root, factory ) {
+	if ( typeof define === "function" && define.amd ) {
+
+		// AMD. Register as an anonymous module.
+		define( [ "jquery" ], factory );
+	} else if ( typeof exports === "object" ) {
+		module.exports = factory( require( "jquery" ) );
+	} else {
+		factory( root.jQuery );
+	}
+} )( this, function( jQuery, undefined ) {
+
+	var stepHooks = "backgroundColor borderBottomColor borderLeftColor borderRightColor " +
+		"borderTopColor color columnRuleColor outlineColor textDecorationColor textEmphasisColor",
+
+	// plusequals test for += 100 -= 100
+	rplusequals = /^([\-+])=\s*(\d+\.?\d*)/,
+
+	// a set of RE's that can match strings and generate color tuples.
+	stringParsers = [ {
+			re: /rgba?\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*(?:,\s*(\d?(?:\.\d+)?)\s*)?\)/,
+			parse: function( execResult ) {
+				return [
+					execResult[ 1 ],
+					execResult[ 2 ],
+					execResult[ 3 ],
+					execResult[ 4 ]
+				];
+			}
+		}, {
+			re: /rgba?\(\s*(\d+(?:\.\d+)?)\%\s*,\s*(\d+(?:\.\d+)?)\%\s*,\s*(\d+(?:\.\d+)?)\%\s*(?:,\s*(\d?(?:\.\d+)?)\s*)?\)/,
+			parse: function( execResult ) {
+				return [
+					execResult[ 1 ] * 2.55,
+					execResult[ 2 ] * 2.55,
+					execResult[ 3 ] * 2.55,
+					execResult[ 4 ]
+				];
+			}
+		}, {
+
+			// this regex ignores A-F because it's compared against an already lowercased string
+			re: /#([a-f0-9]{2})([a-f0-9]{2})([a-f0-9]{2})/,
+			parse: function( execResult ) {
+				return [
+					parseInt( execResult[ 1 ], 16 ),
+					parseInt( execResult[ 2 ], 16 ),
+					parseInt( execResult[ 3 ], 16 )
+				];
+			}
+		}, {
+
+			// this regex ignores A-F because it's compared against an already lowercased string
+			re: /#([a-f0-9])([a-f0-9])([a-f0-9])/,
+			parse: function( execResult ) {
+				return [
+					parseInt( execResult[ 1 ] + execResult[ 1 ], 16 ),
+					parseInt( execResult[ 2 ] + execResult[ 2 ], 16 ),
+					parseInt( execResult[ 3 ] + execResult[ 3 ], 16 )
+				];
+			}
+		}, {
+			re: /hsla?\(\s*(\d+(?:\.\d+)?)\s*,\s*(\d+(?:\.\d+)?)\%\s*,\s*(\d+(?:\.\d+)?)\%\s*(?:,\s*(\d?(?:\.\d+)?)\s*)?\)/,
+			space: "hsla",
+			parse: function( execResult ) {
+				return [
+					execResult[ 1 ],
+					execResult[ 2 ] / 100,
+					execResult[ 3 ] / 100,
+					execResult[ 4 ]
+				];
+			}
+		} ],
+
+	// jQuery.Color( )
+	color = jQuery.Color = function( color, green, blue, alpha ) {
+		return new jQuery.Color.fn.parse( color, green, blue, alpha );
+	},
+	spaces = {
+		rgba: {
+			props: {
+				red: {
+					idx: 0,
+					type: "byte"
+				},
+				green: {
+					idx: 1,
+					type: "byte"
+				},
+				blue: {
+					idx: 2,
+					type: "byte"
+				}
+			}
+		},
+
+		hsla: {
+			props: {
+				hue: {
+					idx: 0,
+					type: "degrees"
+				},
+				saturation: {
+					idx: 1,
+					type: "percent"
+				},
+				lightness: {
+					idx: 2,
+					type: "percent"
+				}
+			}
+		}
+	},
+	propTypes = {
+		"byte": {
+			floor: true,
+			max: 255
+		},
+		"percent": {
+			max: 1
+		},
+		"degrees": {
+			mod: 360,
+			floor: true
+		}
+	},
+
+	// colors = jQuery.Color.names
+	colors,
+
+	// local aliases of functions called often
+	each = jQuery.each;
+
+// define cache name and alpha properties
+// for rgba and hsla spaces
+each( spaces, function( spaceName, space ) {
+	space.cache = "_" + spaceName;
+	space.props.alpha = {
+		idx: 3,
+		type: "percent",
+		def: 1
+	};
+} );
+
+function clamp( value, prop, allowEmpty ) {
+	var type = propTypes[ prop.type ] || {};
+
+	if ( value == null ) {
+		return ( allowEmpty || !prop.def ) ? null : prop.def;
+	}
+
+	// ~~ is an short way of doing floor for positive numbers
+	value = type.floor ? ~~value : parseFloat( value );
+
+	if ( type.mod ) {
+
+		// we add mod before modding to make sure that negatives values
+		// get converted properly: -10 -> 350
+		return ( value + type.mod ) % type.mod;
+	}
+
+	// for now all property types without mod have min and max
+	return Math.min( type.max, Math.max( 0, value ) );
+}
+
+function stringParse( string ) {
+	var inst = color(),
+		rgba = inst._rgba = [];
+
+	string = string.toLowerCase();
+
+	each( stringParsers, function( i, parser ) {
+		var parsed,
+			match = parser.re.exec( string ),
+			values = match && parser.parse( match ),
+			spaceName = parser.space || "rgba";
+
+		if ( values ) {
+			parsed = inst[ spaceName ]( values );
+
+			// if this was an rgba parse the assignment might happen twice
+			// oh well....
+			inst[ spaces[ spaceName ].cache ] = parsed[ spaces[ spaceName ].cache ];
+			rgba = inst._rgba = parsed._rgba;
+
+			// exit each( stringParsers ) here because we matched
+			return false;
+		}
+	} );
+
+	// Found a stringParser that handled it
+	if ( rgba.length ) {
+
+		// if this came from a parsed string, force "transparent" when alpha is 0
+		// chrome, (and maybe others) return "transparent" as rgba(0,0,0,0)
+		if ( rgba.join() === "0,0,0,0" ) {
+			jQuery.extend( rgba, colors.transparent );
+		}
+		return inst;
+	}
+
+	// named colors
+	return colors[ string ];
+}
+
+color.fn = jQuery.extend( color.prototype, {
+	parse: function( red, green, blue, alpha ) {
+		if ( red === undefined ) {
+			this._rgba = [ null, null, null, null ];
+			return this;
+		}
+		if ( red.jquery || red.nodeType ) {
+			red = jQuery( red ).css( green );
+			green = undefined;
+		}
+
+		var inst = this,
+			type = jQuery.type( red ),
+			rgba = this._rgba = [];
+
+		// more than 1 argument specified - assume ( red, green, blue, alpha )
+		if ( green !== undefined ) {
+			red = [ red, green, blue, alpha ];
+			type = "array";
+		}
+
+		if ( type === "string" ) {
+			return this.parse( stringParse( red ) || colors._default );
+		}
+
+		if ( type === "array" ) {
+			each( spaces.rgba.props, function( key, prop ) {
+				rgba[ prop.idx ] = clamp( red[ prop.idx ], prop );
+			} );
+			return this;
+		}
+
+		if ( type === "object" ) {
+			if ( red instanceof color ) {
+				each( spaces, function( spaceName, space ) {
+					if ( red[ space.cache ] ) {
+						inst[ space.cache ] = red[ space.cache ].slice();
+					}
+				} );
+			} else {
+				each( spaces, function( spaceName, space ) {
+					var cache = space.cache;
+					each( space.props, function( key, prop ) {
+
+						// if the cache doesn't exist, and we know how to convert
+						if ( !inst[ cache ] && space.to ) {
+
+							// if the value was null, we don't need to copy it
+							// if the key was alpha, we don't need to copy it either
+							if ( key === "alpha" || red[ key ] == null ) {
+								return;
+							}
+							inst[ cache ] = space.to( inst._rgba );
+						}
+
+						// this is the only case where we allow nulls for ALL properties.
+						// call clamp with alwaysAllowEmpty
+						inst[ cache ][ prop.idx ] = clamp( red[ key ], prop, true );
+					} );
+
+					// everything defined but alpha?
+					if ( inst[ cache ] && jQuery.inArray( null, inst[ cache ].slice( 0, 3 ) ) < 0 ) {
+
+						// use the default of 1
+						inst[ cache ][ 3 ] = 1;
+						if ( space.from ) {
+							inst._rgba = space.from( inst[ cache ] );
+						}
+					}
+				} );
+			}
+			return this;
+		}
+	},
+	is: function( compare ) {
+		var is = color( compare ),
+			same = true,
+			inst = this;
+
+		each( spaces, function( _, space ) {
+			var localCache,
+				isCache = is[ space.cache ];
+			if ( isCache ) {
+				localCache = inst[ space.cache ] || space.to && space.to( inst._rgba ) || [];
+				each( space.props, function( _, prop ) {
+					if ( isCache[ prop.idx ] != null ) {
+						same = ( isCache[ prop.idx ] === localCache[ prop.idx ] );
+						return same;
+					}
+				} );
+			}
+			return same;
+		} );
+		return same;
+	},
+	_space: function() {
+		var used = [],
+			inst = this;
+		each( spaces, function( spaceName, space ) {
+			if ( inst[ space.cache ] ) {
+				used.push( spaceName );
+			}
+		} );
+		return used.pop();
+	},
+	transition: function( other, distance ) {
+		var end = color( other ),
+			spaceName = end._space(),
+			space = spaces[ spaceName ],
+			startColor = this.alpha() === 0 ? color( "transparent" ) : this,
+			start = startColor[ space.cache ] || space.to( startColor._rgba ),
+			result = start.slice();
+
+		end = end[ space.cache ];
+		each( space.props, function( key, prop ) {
+			var index = prop.idx,
+				startValue = start[ index ],
+				endValue = end[ index ],
+				type = propTypes[ prop.type ] || {};
+
+			// if null, don't override start value
+			if ( endValue === null ) {
+				return;
+			}
+
+			// if null - use end
+			if ( startValue === null ) {
+				result[ index ] = endValue;
+			} else {
+				if ( type.mod ) {
+					if ( endValue - startValue > type.mod / 2 ) {
+						startValue += type.mod;
+					} else if ( startValue - endValue > type.mod / 2 ) {
+						startValue -= type.mod;
+					}
+				}
+				result[ index ] = clamp( ( endValue - startValue ) * distance + startValue, prop );
+			}
+		} );
+		return this[ spaceName ]( result );
+	},
+	blend: function( opaque ) {
+
+		// if we are already opaque - return ourself
+		if ( this._rgba[ 3 ] === 1 ) {
+			return this;
+		}
+
+		var rgb = this._rgba.slice(),
+			a = rgb.pop(),
+			blend = color( opaque )._rgba;
+
+		return color( jQuery.map( rgb, function( v, i ) {
+			return ( 1 - a ) * blend[ i ] + a * v;
+		} ) );
+	},
+	toRgbaString: function() {
+		var prefix = "rgba(",
+			rgba = jQuery.map( this._rgba, function( v, i ) {
+                if ( v != null ) {
+                    return v;
+                }
+				return i > 2 ? 1 : 0;
+			} );
+
+		if ( rgba[ 3 ] === 1 ) {
+			rgba.pop();
+			prefix = "rgb(";
+		}
+
+		return prefix + rgba.join() + ")";
+	},
+	toHslaString: function() {
+		var prefix = "hsla(",
+			hsla = jQuery.map( this.hsla(), function( v, i ) {
+				if ( v == null ) {
+					v = i > 2 ? 1 : 0;
+				}
+
+				// catch 1 and 2
+				if ( i && i < 3 ) {
+					v = Math.round( v * 100 ) + "%";
+				}
+				return v;
+			} );
+
+		if ( hsla[ 3 ] === 1 ) {
+			hsla.pop();
+			prefix = "hsl(";
+		}
+		return prefix + hsla.join() + ")";
+	},
+	toHexString: function( includeAlpha ) {
+		var rgba = this._rgba.slice(),
+			alpha = rgba.pop();
+
+		if ( includeAlpha ) {
+			rgba.push( ~~( alpha * 255 ) );
+		}
+
+		return "#" + jQuery.map( rgba, function( v ) {
+
+			// default to 0 when nulls exist
+			return ( "0" + ( v || 0 ).toString( 16 ) ).substr( -2 );
+		} ).join( "" );
+	},
+	toString: function() {
+		return this._rgba[ 3 ] === 0 ? "transparent" : this.toRgbaString();
+	}
+} );
+color.fn.parse.prototype = color.fn;
+
+// hsla conversions adapted from:
+// https://code.google.com/p/maashaack/source/browse/packages/graphics/trunk/src/graphics/colors/HUE2RGB.as?r=5021
+
+function hue2rgb( p, q, h ) {
+	h = ( h + 1 ) % 1;
+	if ( h * 6 < 1 ) {
+		return p + ( q - p ) * h * 6;
+	}
+	if ( h * 2 < 1 ) {
+		return q;
+	}
+	if ( h * 3 < 2 ) {
+		return p + ( q - p ) * ( ( 2 / 3 ) - h ) * 6;
+	}
+	return p;
+}
+
+spaces.hsla.to = function( rgba ) {
+	if ( rgba[ 0 ] == null || rgba[ 1 ] == null || rgba[ 2 ] == null ) {
+		return [ null, null, null, rgba[ 3 ] ];
+	}
+	var r = rgba[ 0 ] / 255,
+		g = rgba[ 1 ] / 255,
+		b = rgba[ 2 ] / 255,
+		a = rgba[ 3 ],
+		max = Math.max( r, g, b ),
+		min = Math.min( r, g, b ),
+		diff = max - min,
+		add = max + min,
+		l = add * 0.5,
+		h, s;
+
+	if ( min === max ) {
+		h = 0;
+	} else if ( r === max ) {
+		h = ( 60 * ( g - b ) / diff ) + 360;
+	} else if ( g === max ) {
+		h = ( 60 * ( b - r ) / diff ) + 120;
+	} else {
+		h = ( 60 * ( r - g ) / diff ) + 240;
+	}
+
+	// chroma (diff) == 0 means greyscale which, by definition, saturation = 0%
+	// otherwise, saturation is based on the ratio of chroma (diff) to lightness (add)
+	if ( diff === 0 ) {
+		s = 0;
+	} else if ( l <= 0.5 ) {
+		s = diff / add;
+	} else {
+		s = diff / ( 2 - add );
+	}
+	return [ Math.round( h ) % 360, s, l, a == null ? 1 : a ];
+};
+
+spaces.hsla.from = function( hsla ) {
+	if ( hsla[ 0 ] == null || hsla[ 1 ] == null || hsla[ 2 ] == null ) {
+		return [ null, null, null, hsla[ 3 ] ];
+	}
+	var h = hsla[ 0 ] / 360,
+		s = hsla[ 1 ],
+		l = hsla[ 2 ],
+		a = hsla[ 3 ],
+		q = l <= 0.5 ? l * ( 1 + s ) : l + s - l * s,
+		p = 2 * l - q;
+
+	return [
+		Math.round( hue2rgb( p, q, h + ( 1 / 3 ) ) * 255 ),
+		Math.round( hue2rgb( p, q, h ) * 255 ),
+		Math.round( hue2rgb( p, q, h - ( 1 / 3 ) ) * 255 ),
+		a
+	];
+};
+
+
+each( spaces, function( spaceName, space ) {
+	var props = space.props,
+		cache = space.cache,
+		to = space.to,
+		from = space.from;
+
+	// makes rgba() and hsla()
+	color.fn[ spaceName ] = function( value ) {
+
+		// generate a cache for this space if it doesn't exist
+		if ( to && !this[ cache ] ) {
+			this[ cache ] = to( this._rgba );
+		}
+		if ( value === undefined ) {
+			return this[ cache ].slice();
+		}
+
+		var ret,
+			type = jQuery.type( value ),
+			arr = ( type === "array" || type === "object" ) ? value : arguments,
+			local = this[ cache ].slice();
+
+		each( props, function( key, prop ) {
+			var val = arr[ type === "object" ? key : prop.idx ];
+			if ( val == null ) {
+				val = local[ prop.idx ];
+			}
+			local[ prop.idx ] = clamp( val, prop );
+		} );
+
+		if ( from ) {
+			ret = color( from( local ) );
+			ret[ cache ] = local;
+			return ret;
+		} else {
+			return color( local );
+		}
+	};
+
+	// makes red() green() blue() alpha() hue() saturation() lightness()
+	each( props, function( key, prop ) {
+
+		// alpha is included in more than one space
+		if ( color.fn[ key ] ) {
+			return;
+		}
+		color.fn[ key ] = function( value ) {
+			var local, cur, match, fn,
+				vtype = jQuery.type( value );
+
+			if ( key === "alpha" ) {
+				fn = this._hsla ? "hsla" : "rgba";
+			} else {
+				fn = spaceName;
+			}
+			local = this[ fn ]();
+			cur = local[ prop.idx ];
+
+			if ( vtype === "undefined" ) {
+				return cur;
+			}
+
+			if ( vtype === "function" ) {
+				value = value.call( this, cur );
+				vtype = jQuery.type( value );
+			}
+			if ( value == null && prop.empty ) {
+				return this;
+			}
+			if ( vtype === "string" ) {
+				match = rplusequals.exec( value );
+				if ( match ) {
+					value = cur + parseFloat( match[ 2 ] ) * ( match[ 1 ] === "+" ? 1 : -1 );
+				}
+			}
+			local[ prop.idx ] = value;
+			return this[ fn ]( local );
+		};
+	} );
+} );
+
+// add cssHook and .fx.step function for each named hook.
+// accept a space separated string of properties
+color.hook = function( hook ) {
+	var hooks = hook.split( " " );
+	each( hooks, function( i, hook ) {
+		jQuery.cssHooks[ hook ] = {
+			set: function( elem, value ) {
+				var parsed;
+
+				if ( value !== "transparent" && ( jQuery.type( value ) !== "string" || ( parsed = stringParse( value ) ) ) ) {
+					value = color( parsed || value );
+					value = value.toRgbaString();
+				}
+				elem.style[ hook ] = value;
+			}
+		};
+		jQuery.fx.step[ hook ] = function( fx ) {
+			if ( !fx.colorInit ) {
+				fx.start = color( fx.elem, hook );
+				fx.end = color( fx.end );
+				fx.colorInit = true;
+			}
+			jQuery.cssHooks[ hook ].set( fx.elem, fx.start.transition( fx.end, fx.pos ) );
+		};
+	} );
+
+};
+
+color.hook( stepHooks );
+
+jQuery.cssHooks.borderColor = {
+	expand: function( value ) {
+		var expanded = {};
+
+		each( [ "Top", "Right", "Bottom", "Left" ], function( i, part ) {
+			expanded[ "border" + part + "Color" ] = value;
+		} );
+		return expanded;
+	}
+};
+
+// Basic color names only.
+// Usage of any of the other color names requires adding yourself or including
+// jquery.color.svg-names.js.
+colors = jQuery.Color.names = {
+
+	// 4.1. Basic color keywords
+	aqua: "#00ffff",
+	black: "#000000",
+	blue: "#0000ff",
+	fuchsia: "#ff00ff",
+	gray: "#808080",
+	green: "#008000",
+	lime: "#00ff00",
+	maroon: "#800000",
+	navy: "#000080",
+	olive: "#808000",
+	purple: "#800080",
+	red: "#ff0000",
+	silver: "#c0c0c0",
+	teal: "#008080",
+	white: "#ffffff",
+	yellow: "#ffff00",
+
+	// 4.2.3. "transparent" color keyword
+	transparent: [ null, null, null, 0 ],
+
+	_default: "#ffffff"
+};
+
+} );

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 1 - 0
js/vendor/mustache.min.js


+ 251 - 0
js/ws-editor.js

@@ -0,0 +1,251 @@
+/* global window,$,ace,setTimeout,rp,_ws */
+"use strict";
+$(document).ready(function() {
+  // console.log('ws: init editor');
+
+  var $editorWrapper = $('#editor-wrapper');
+  var $editor        = $('#editor');
+
+
+  var $saveChanges   = $('#save-changes');
+
+  var $revertEditor  = $('#revert-editor');
+  var $panelLeft     = $('.panel-left');
+  // var $panelRight    = $('.panel-right');
+  // var $panelWrap     = $('.panel-container');
+  var $tabItems      = $('#tabs li');
+  var $detailsRepo   = $('#details-repo');
+  var $detailsExmp   = $('#details-example');
+  var $window        = $(window);
+  var activeMode     = 'html';
+  var currentFileIdx;
+
+  var editor;
+  var editorSession;
+  var editorStorage = new LocalStorageDraft();
+  // var saveTimeout1;
+  // var saveTimeout2;
+  var mapTypes = {
+    html: 'html', js: 'javascript', css: 'css'
+  }
+
+  _ws.events.on('navToRoot', function() {
+    $editorWrapper.hide();
+    $detailsRepo.hide();
+    $detailsExmp.hide();
+  });
+
+  _ws.events.on('navToRepo', function(repoSlug) {
+    $editorWrapper.hide();
+    $detailsRepo.show();
+    $detailsExmp.hide();
+
+  });
+
+
+
+  // /**
+  //  * Make the left panel resizable
+  //  */
+  $panelLeft.resizable({
+    handleSelector: ".splitter",
+    resizeHeight: false,
+    onDrag: function(e) {
+      $editor.width($panelLeft.width());
+    }
+  });
+
+
+  function setDefaultEditorContent() {
+    if(_ws.files.length > 0) {
+      editorSession.setMode("ace/mode/html");
+      var firstHtml = _ws.files.find(f => (f.type === 'html'));
+      _ws.ui.tabs.setActiveTab(firstHtml);
+      editorSession.setValue(firstHtml.content);
+    }
+  }
+
+
+  function initEditor() {
+    editor = ace.edit("editor");
+    editorSession = editor.getSession();
+    editor.setTheme("ace/theme/eclipse");
+    editor.$blockScrolling = Infinity;
+    editorSession.setUseWrapMode(true);
+    setDefaultEditorContent();
+  }
+
+
+  function onTabClicked(e) {
+    var clickedItem = $(e.target);
+    var activeTab = _ws.ui.tabs.getActiveTab();
+    if(activeTab[0] === clickedItem[0] || clickedItem[0].tagName === 'SPAN') {
+      return true;
+    }
+    if(activeTab.hasClass('dirty')) {
+      var proceed = window.confirm("Vos changements sur " + activeTab.data('name') +
+        " seront perdus si vous changez de fichier. Continuer tout de même ?");
+      if(! proceed) {
+        return;
+      }
+      activeTab.removeClass('dirty');
+    }
+    var type = clickedItem.data('type');
+    editorSession.setMode("ace/mode/" + mapTypes[type]);
+    var name = clickedItem.data('name');
+    var file = _ws.files.find(f => (f.name === name));
+    _ws.ui.tabs.setActiveTab(file);
+
+    editor.getSession().off('change', _ws.ui.editor.contentChanged);
+    editorSession.setValue(file.content);
+    editor.getSession().on('change', _ws.ui.editor.contentChanged);
+
+
+    // saveToLocalStorage();
+    // var mode = $(this).prop('id').substr(5);
+    // console.log( .html() );
+    // setActiveTab(mode);
+  }
+
+  function setFirstActiveTab() {
+    var firstHtml = _ws.files.find(f => (f.type === 'html'));
+    this.setActiveTab(firstHtml);
+  }
+
+
+  _ws.makeView('tabs', {
+    setActiveTab: function(file) {
+      this.file = _ws.files.find(f => (f.name === file.name));
+      if(this.activeTab) {
+        this.activeTab.removeClass('bold');
+      }
+      var idx = this.fileIdx = _ws.files.indexOf(this.file);
+      this.activeTab = this.$elem.find('li:eq(' + idx + ')');
+      this.activeTab.addClass('bold');
+    },
+    setFirstActiveTab: setFirstActiveTab,
+    afterRender: setFirstActiveTab,
+    getActiveTab: function() {
+      return this.activeTab;
+    },
+    events: {
+      'click li[data-type]': onTabClicked,
+      'click li[data-type] span': onSaveChanges
+    }
+  })
+
+  initEditor();
+
+
+  _ws.makeView('editor', {
+    aceEditor: editor,
+    aceSession: editor.getSession(),
+
+    init: function() {
+      this.aceSession.on('change', this.contentChanged);
+    },
+
+    /**
+     * React to changes in editor by saving a copy
+     */
+    contentChanged: function() {
+      // console.log('content changed handler', editorSession.getValue());
+      _ws.ui.tabs.getActiveTab()
+      .addClass('dirty');
+      // if(saveTimeout1 || saveTimeout2) {
+      //   clearTimeout(saveTimeout1);
+      //   clearTimeout(saveTimeout2);
+      // }
+      // saveTimeout1 = setTimeout(saveToLocalStorage, 500);
+      // // saveTimeout2 = setTimeout(saveChanges, 1000);
+    },
+
+
+    render: function() {
+      // console.log('render editor', this);
+      setDefaultEditorContent();
+      $editorWrapper.show();
+      this.$elem.removeClass('hidden');
+    }
+  });
+
+  _ws.makeView('sandbox-iframe', {
+    reload: function() {
+      var src = this.$elem.prop('src');
+      this.$elem.prop('src', '/empty.html');
+      this.$elem.prop('src', src);
+    },
+    render: function(src) {
+      this.$elem.prop('src', src);
+    }
+  });
+
+
+
+  // /**
+  //  * Set editor mode (html, javascript, css)
+  //  */
+  // function setEditorMode(mode) {
+  //   editor.getSession().setMode("ace/mode/" + mode);
+  // }
+
+
+  // /**
+  //  * Save a copy in localStorage
+  //  */
+  // function saveToLocalStorage() {
+  //   var editorContent =  editor.getSession().getValue();
+  //   editorStorage.saveSource(activeMode, editorContent);
+  //   saveTimeout1 = undefined;
+  // }
+
+
+
+
+
+  // function revertEditor() {
+  //   editorStorage.reset();
+  //   location.reload();
+  // }
+
+  function onSaveChanges() {
+    // console.log('Saving', this.file, editorSession.getValue())
+    var filename = this.file.name;
+    // var payload = editorStorage.getSources();
+    var payload = {
+      content: editorSession.getValue()
+    };
+
+    // Submit the new file content to server
+    rp.put('/' + _ws.repo.path + '/examples/' + _ws.example.slug + '/files/' + filename, payload)
+    .then(function(updatedFile) {
+
+      // notify
+      _ws.notify('success', "Fichier " + filename + " sauvegardé !");
+
+      // remove the dirty state on current editor tab
+      _ws.ui.tabs.getActiveTab().removeClass('dirty');
+
+      // replace content with the updated one in _ws.files
+      var originalFile = _ws.files.find(f => (f.name === updatedFile.name));
+      var index = _ws.files.indexOf(originalFile);
+      _ws.files[index].content = updatedFile.content;
+
+      // reload sandbox iframe
+      _ws.ui.sandboxIframe.reload();
+    })
+    .catch(function(err) {
+      // console.error(err);
+      _ws.notify('error', 'Erreur: ' + err.message);
+    });
+  }
+
+  // $fileSelect.change(function() {
+  //   loadExample($(this).val());
+  // });
+  // $saveChanges.click(saveChanges);
+  // $revertEditor.click(revertEditor);
+  // $.get('/menu', menu => $('#site-menu').html(menu), 'html');
+  // loadExampleList();
+
+});

+ 6 - 0
js/ws-events.js

@@ -0,0 +1,6 @@
+"use strict";
+(function($) {
+  $(document).ready(function() {
+    _ws.events = new EventEmitter();
+  });
+})(jQuery);

+ 182 - 0
js/ws-forms.js

@@ -0,0 +1,182 @@
+"use strict";
+(function($) {
+  $(document).ready(function() {
+
+  function checkPropsExist(obj, props) {
+    if(typeof obj !== 'object' || ! props) {
+      throw new Error('checkPropsExist was called with wrong arguments');
+    }
+    props = typeof props === 'string' ? [props] : props;
+    for(let p = 0 ; p < props.length ; p++) {
+      const prop = props[p];
+      if(! obj[prop]) {
+        throw new Error('obj does not have a `' + prop + '` parameter: please provide it.');
+      }
+    }
+  }
+
+  function makeFormView(elemId, options) {
+    var callbacks = ['onSubmitAddPromise', 'onAddSuccess', 'onAddError'];
+    // Fallback options & options.events
+    checkPropsExist(options, callbacks)
+    options.events = options.events || {};
+
+    // Define events and optionally extend them with those
+    // provided in options.events
+
+    _ws.makeView(elemId, {
+
+      init: function() {
+        const self = this;
+        this.$btn   = this.$elem.find('.add-btn');
+        this.$form  = this.$elem.find('form');
+        this.$input = this.$form.find('input[name="title"]');
+        callbacks.forEach(function(cbName) {
+          self[cbName] = options[cbName].bind(self);
+        });
+        if(options.postInit) {
+          (options.postInit.bind(this))();
+        }
+      },
+
+      reset: function() {
+        this.$btn.addClass('in');
+        this.$form.removeClass('in');
+        this.$input.val('');
+      },
+
+      render: function() {
+        this.$btn.removeClass('in');
+        this.$form.addClass('in');
+        this.$input.focus();
+      },
+
+      events: {
+        'click .add-btn': function() {
+          this.render();
+        },
+
+        'click .icon-cross': function() {
+          this.$form.removeClass('in');
+          this.$btn.addClass('in');
+        },
+
+        'submit form': function(e) {
+          const self = this;
+          e.preventDefault();
+          if(this.cantSubmit) {
+            this.$input
+            .removeClass('input-success input-warning')
+            .addClass('input-error');
+            _ws.notify('error', this.cantSubmit);
+            return;
+          }
+          var title = this.$input.val();
+          // rp.post('/collections', { title: title })
+          self.onSubmitAddPromise(title)
+          .then(function(data) {
+            self.onAddSuccess(data);
+            self.reset();
+          })
+          .catch(function(err) {
+            console.error(err);
+            self.onAddError(err);
+            self.reset();
+          });
+        }
+      }
+
+    });
+  }
+
+  makeFormView('add-repo', {
+    onSubmitAddPromise: function(title) {
+      return rp.post('/repos', { title: title });
+    },
+    onAddSuccess: function(repo) {
+      _ws.repos.push(repo);
+      _ws.ui.menuRepo.render({ repos: _ws.repos });
+      _ws.notify('success', 'Collection créée: ' + repo.title);
+      _ws.navigateTo('/' + repo.path);
+    },
+    onAddError: function(err) {
+     _ws.notify('error', 'Impossible de créer la collection: ' + err.message); 
+    }
+  });
+
+  makeFormView('add-example', {
+    onSubmitAddPromise: function(title) {
+      return rp.post('/' + _ws.repo.path + '/examples', { title: title });
+    },
+    onAddSuccess: function(example) {
+      _ws.notify('success', 'Exemple crée: ' + example.title);
+      _ws.navigateTo('/' + _ws.repo.path + '/' + example.slug);
+
+      // console.log('##### addExample success', _ws, _ws.ui.menuExample.$elem.find('ul[data-id="' + example.category + '"]') )
+      _ws.ui.menuExample.addExampleLink(example, '/' + _ws.repo.path + '/' + example.slug);
+      _ws.ui.shortcutExample.addExampleLink(example, '/' + _ws.repo.path + '/' + example.slug);
+    },
+    onAddError: function(err) {
+     _ws.notify('error', "Impossible de créer l'exemple: " + err.message); 
+    }
+  });
+
+
+  makeFormView(
+    'add-file',
+    {
+      onSubmitAddPromise: function(name) {
+        return rp.post('/' + _ws.repo.path + '/examples/' + _ws.example.slug + '/files', { name: name });
+      },
+
+      onAddSuccess: function(file) {
+        _ws.notify('success', 'Fichier crée: ' + file.name);
+        _ws.files.push(file);
+        _ws.ui.tabs.render({ files: _ws.files });
+      },
+      onAddError: function(err) {
+       _ws.notify('error', "Impossible de créer le fichier: " + err.message); 
+      },
+
+      postInit: function() {
+        function onKeyup(e) {
+          var filename = this.$input.val();
+          var bits = filename.split('.');
+          var lastIdx = bits.length - 1;
+          this.cantSubmit = '';
+
+          // show warning if no extension has been provided
+          if(bits.length < 2) {
+            this.$input
+            .removeClass('input-success input-error')
+            .addClass('input-warning');
+            this.cantSubmit = 'Impossible de valider: extension (.html/.js/.css) manquante';
+            return;
+          }
+          // show error if provided extension is invalid
+          else {
+            var ext = (bits[lastIdx]).toLowerCase();
+
+            if( ['html', 'js', 'css'].indexOf( ext ) === -1 ) {
+              this.cantSubmit = 'Impossible de valider: extension ' + ext + ' invalide (autorisées: .html/.js/.css)';
+            }
+            else if(bits[0] === '') {
+              this.cantSubmit = "Le premier caractère du nom de fichier doit être autre chose qu'un point";
+            }
+            if( this.cantSubmit ) {
+              this.$input
+              .removeClass('input-success input-warning')
+              .addClass('input-error');
+              return;
+            }
+          }
+          this.$input.addClass('input-success');
+          this.cantSubmit = undefined;
+        }
+        this.$input.on('keyup', onKeyup.bind(this));
+      }
+    }
+  );
+
+  });
+})(jQuery);

+ 205 - 0
js/ws-menu.js

@@ -0,0 +1,205 @@
+"use strict";
+(function($) {
+  $(document).ready(function() {
+    // console.log('ws: init menu');
+
+    /**
+     * Main menu
+     */
+    var $mainMenu = $('#nav-menus');
+
+
+    /**
+     * Main menu toggle button
+     */
+    var $menuBtn = $('#menu-btn');
+    $menuBtn.click(toggleMainMenu);
+
+    /**
+     * Navigation state
+     */
+    var navState = parsePath();
+
+
+    /**
+     * Previous navigation state
+     */
+    var prevNavState;
+
+
+    /**
+     * Toggle main menu
+     */
+    function toggleMainMenu() {
+      $mainMenu.toggleClass('in');
+    };
+
+
+    /**
+     * Extract bits (repo&example slugs) from requested url path
+     */
+    function parsePath() {
+      const path = window.location.pathname.substr(1);
+      const bits = path.split('/');
+      return {
+        repo: bits[0],
+        example: bits.length === 1 ? '' : bits[1]
+      };
+    }
+
+    /**
+     * Handle navigation
+     */
+    _ws.navigateTo = function(path) {
+      $('#server-alert').remove();
+      history.pushState({}, 'New path', path);
+      prevNavState = navState;
+      navState = parsePath();
+
+      if(navState.repo !== prevNavState.repo) {
+        if(navState.repo === '') {
+          _ws.events.emit('navToRoot');
+          _ws.ui.addExample.$btn.removeClass('in');
+          _ws.ui.addExample.hide();
+          _ws.ui.menuExample.render({ examples: [] });
+          _ws.ui.shortcutRepo.render({ repos: _ws.repos, _: _ws._ });
+          _ws.ui.shortcutExample.hide();
+        }
+        else {
+          rp.get('/parts/' + navState.repo, 'json')
+          .then(function(parts) {
+            _ws.repo = parts.repo;
+            _ws.ui.addExample.$btn.addClass('in');
+            _ws.ui.addExample.show();
+            _ws.ui.detailsRepo.render(parts);
+            _ws.ui.menuExample.render(parts);
+            _ws.ui.shortcutExample.render(parts);
+            _ws.ui.shortcutRepo.hide();
+            _ws.events.emit('navToRepo', navState.repo);
+          });
+        }
+      }
+      else if(navState.example !== prevNavState.example) {
+        if(navState.example !== '') {
+          rp.get('/parts/' + navState.repo + '/' + navState.example, 'json')
+          .then(function(parts) {
+            _ws.example = parts.example;
+            _ws.files = parts.files;
+            $('#editor-wrapper').show();
+            _ws.ui.shortcutExample.hide();
+            _ws.ui.detailsExample.render(parts);
+            _ws.ui.editor.render(parts);
+            _ws.ui.tabs.render(parts);
+            _ws.ui.addFile.show();
+            _ws.ui.sandboxIframe.render('/examples/' + navState.repo + '/' + navState.example);
+          });
+        }
+        else {
+          $('#editor-wrapper').hide();
+          _ws.ui.shortcutExample.show();
+          _ws.ui.editor.hide();
+          _ws.ui.tabs.hide();
+          _ws.ui.addFile.hide();
+          _ws.ui.sandboxIframe.render('/html/start-iframe.html');
+        }
+      }
+    }
+
+
+    function menuItemClicked(e) {
+      e.preventDefault();
+      var $link = $(e.target);
+      var originalColor = $link.css('backgroundColor');
+      toggleMainMenu();
+      $link.animate({
+        backgroundColor: '#aaa',
+      }, 70);
+      $link.animate({
+        backgroundColor: originalColor,
+      }, 70);
+      _ws.navigateTo($link.prop('href'));
+    }
+
+    function shortcutItemClicked(e) {
+      e.preventDefault();
+      var $link = $(e.target);
+      _ws.navigateTo($link.prop('href'));
+    }
+
+    function addExampleLink(cb) {
+      return function(example, href) {
+        var $targetList = this.$elem.find('ul[data-id="' + example.category + '"]');
+        var $newItem = $('<li><a href="' + href + '">' + example.title + '</a></li>').appendTo($targetList);
+        $newItem.find('a').on('click', cb);
+      }
+    }
+
+    /**
+     * Initialize menu-example view
+     */
+    _ws.makeView('menu-example', {
+      addExampleLink: addExampleLink(menuItemClicked),
+
+      events: {
+        'click a': menuItemClicked
+      }
+    });
+
+
+    /**
+     * Initialize menu-repo view
+     */
+    _ws.makeView('menu-repo', {
+      events: {
+        'click a': menuItemClicked
+      }
+    });
+
+
+    /**
+     * Initialize shortcut-repo view
+     */
+    _ws.makeView('shortcut-repo', {
+      events: {
+        'click a': shortcutItemClicked
+      }
+    });
+
+
+    /**
+     * Initialize shortcut-example view
+     */
+    _ws.makeView('shortcut-example', {
+      addExampleLink: addExampleLink(shortcutItemClicked),
+
+      events: {
+        'click a': shortcutItemClicked
+      }
+    });
+
+
+    /**
+     * Handle click on links outside #menu-example
+     */
+    // $('#menu-repo a').click(menuItemClicked);
+    $('#nav-back-home').click(function(e) {
+      e.preventDefault();
+      $mainMenu.removeClass('in');
+      _ws.navigateTo('/');
+    });
+
+
+    /**
+     * Initialize details-example view
+     */
+    _ws.makeView('details-example');
+
+
+    /**
+     * Initialize details-repo view
+     */
+    _ws.makeView('details-repo');
+
+
+  });
+})(jQuery);

+ 22 - 0
js/ws-notify.js

@@ -0,0 +1,22 @@
+"use strict";
+(function($) {
+  $(document).ready(function() {
+
+    var $notification = $('#notification');
+
+    _ws.notify = function(type, text) {
+      $notification
+      .removeClass('success error')
+      .addClass(type)
+      .addClass('active');
+      $notification.html(text);
+      setTimeout(function() {
+        $notification.removeClass('active');
+      }, 2000);
+      setTimeout(function() {
+        $notification.removeClass(type);
+      }, 3000);
+    }
+
+  });
+})(jQuery);

+ 138 - 0
js/ws-ui-parts.js

@@ -0,0 +1,138 @@
+"use strict";
+(function($) {
+  $(document).ready(function() {
+    // console.log('ws: init UI parts');
+
+
+    /**
+     * Initialize an empty object to be populated as follows:
+     *   - key is an UI element's id
+     *   - value associated is an obj containing: tmpl (the template) and $elem (jQueried target element)
+     */
+    _ws.ui = {};
+
+
+    /**
+     * Generic template-based rendering function
+     */
+    function viewDefaultRender(elemId) {
+      return function(data) {
+        if(! data) {
+          throw new Error('WARNING! You should provide data for #' + elemId + "'s render()");
+        }
+        this.$elem.empty();
+        this.$elem.html( Mustache.render( this.tmpl, data ) );
+        this.$elem.removeClass('hidden');
+        this.$elem.show();
+        this.bindEvents();
+        // call if an afterRender function was provided
+        if(typeof this.afterRender === 'function') {
+          this.afterRender();
+        }
+      }
+    }
+
+    /**
+     * Wrapper around provided render function (show elem after render)
+     */
+    function viewRenderWrapper(data) {
+      this._render(data);
+      this.$elem.show();
+    }
+
+
+
+    /**
+     * Bind events
+     */
+    function viewBindEvents() {
+      var events = this.props.events;
+      for(var descriptor in events) {
+        var handler = events[descriptor];
+        var bits = descriptor.split(' ');
+        var evtName = bits.shift();
+        var selector = bits.join(' ');
+        var target = selector === '' ? this.$elem : this.$elem.find(selector);
+        target.on(evtName, handler.bind(this));
+      }
+    }
+
+
+    /**
+     * Bind events: nop
+     */
+    function viewBindEventsNop() {}
+
+
+    /**
+     * Show view element
+     */
+    function viewElemShow() {
+      this.$elem.removeClass('hidden');
+      this.$elem.show();
+    }
+
+
+    /**
+     * Hide view element
+     */
+    function hideElemShow() {
+      this.$elem.addClass('hidden');
+      this.$elem.hide();
+    }
+
+
+    /**
+     * Build a view
+     */
+    _ws.makeView = function(elemId, props) {
+      var v = {};
+      v.props = props || {};
+
+      v.$elem = $('#' + elemId);
+      v.partName = _.camelCase(elemId);
+      _ws.ui[v.partName] = v;
+
+      // Bind the provided render function
+      if(typeof v.props.render === 'function') {
+        v._render = props.render.bind(v);
+        v.render = viewRenderWrapper.bind(v);
+      }
+      // Otherwise assign the default render function
+      else {
+        var $tmplEl = $('script[data-tmpl-for="' + elemId + '"]');
+        v.tmpl = $tmplEl.html();
+        v.render = (viewDefaultRender(elemId)).bind(v);
+      }
+
+      // Set event handlers
+      if(v.props.events) {
+        v.bindEvents = viewBindEvents.bind(v);
+        v.bindEvents();
+      }
+      else {
+        v.bindEvents = viewBindEventsNop.bind(v);
+      }
+
+      // Pass arbitrary properties
+      for(var p in props) {
+        if(['render', 'events', 'init'].indexOf(p) !== -1) {
+          continue;
+        }
+        var prop = props[p];
+        v[p] = typeof prop !== 'function' ? prop : prop.bind(v);
+      }
+
+      v.show = viewElemShow.bind(v);
+      v.hide = hideElemShow.bind(v);
+
+      // Init if an init function was provided
+      if(typeof v.props.init === 'function') {
+        (v.props.init.bind(v))();
+      }
+
+    };
+
+
+  });
+})(jQuery);

+ 14 - 0
languages/en-US.json

@@ -0,0 +1,14 @@
+{
+  "home": "Home",
+  "repoNotFound": "Example collection `%s` not found",
+  "exampleNotFound": "Example `%s/%s` not found",
+  "addCollection": "Add collection",
+  "addExample": "Add example",
+  "exampleName": "Example name",
+  "collectionName": "Collection name",
+  "collection": "Collection:",
+  "example": "Example:",
+  "fileNameWithExt": "File name",
+  "exampleCollections": "Example collections",
+  "examplesInCollection": "Examples in this collection"
+}

+ 14 - 0
languages/fr-FR.json

@@ -0,0 +1,14 @@
+{
+  "home": "Accueil",
+  "repoNotFound": "Collection d'exemples `%s` introuvable",
+  "exampleNotFound": "Exemple `%s/%s` introuvable",
+  "addCollection": "Ajouter collection",
+  "addExample": "Ajouter exemple",
+  "exampleName": "Nom de l'exemple",
+  "collectionName": "Nom de la collection",
+  "collection": "Collection :",
+  "example": "Exemple :",
+  "fileNameWithExt": "Nom de fichier",
+  "exampleCollections": "Collections d'exemples",
+  "examplesInCollection": "Exemples dans cette collection"
+}

+ 121 - 0
lib/ExampleStore.js

@@ -0,0 +1,121 @@
+const {
+  scandirAsync
+}               = require('./fsio');
+const Promise   = require('bluebird');
+const _         = require('lodash');
+const beautify  = require('json-beautify');
+const fs        = require('fs');
+const path      = require('path');
+
+function ExampleStore(path) {
+  this.rootPath = path;
+  this.repos = [];
+}
+
+ExampleStore.prototype.init = function() {
+  const loadRepository = this.loadRepository.bind(this);
+  return scandirAsync(this.rootPath, ['.gitkeep'])
+  .then(repositories => Promise.map(
+    repositories, loadRepository
+  ));
+};
+
+ExampleStore.prototype.loadRepository = function(repoPath) {
+  const loadExample    = this.loadExample.bind(this);
+  const fullPath       = this.rootPath + '/' + repoPath;
+  const repoConfig     = require(fullPath + '/repo-config.json');
+  const repoDescriptor = Object.assign(repoConfig, {
+    path: repoPath,
+    fullPath,
+    examples: []
+  });
+  this.repos.push(repoDescriptor);
+  return scandirAsync(fullPath, ['.gitkeep', 'repo-config.json'])
+  .then(examples => Promise.map(
+    examples, example => loadExample(repoDescriptor, example)
+  ));
+  // .then(() => console.log(this.repos[0]))
+};
+
+ExampleStore.prototype.addRepository = function(repoPath, repoDescriptor) {
+
+  // Prepare files to write
+  const self = this;
+  const fullPath = this.rootPath + '/' + repoPath;
+  const repoConfigFile = fullPath + '/repo-config.json';
+  const repoConfig = beautify(repoDescriptor, null, 2, 100);
+
+  // Add repo path to descriptor
+  repoDescriptor.path = repoPath;
+  repoDescriptor.fullPath = this.rootPath + '/' + repoPath;
+  
+  return fs.mkdirAsync(fullPath)
+  .then(() => fs.writeFileAsync(repoConfigFile, repoConfig))
+  .then(() => self.repos.push(repoDescriptor));
+}
+
+ExampleStore.prototype.loadExample = function(repo, slug) {
+  const exampleConfig = require(repo.fullPath + '/' + slug + '/config.json');
+  repo.examples.push(Object.assign({ slug }, exampleConfig));
+  return exampleConfig;
+};
+
+ExampleStore.prototype.addExampleFile = function(repoSlug, slug, filename) {
+  const repo    = this.getRepo(repoSlug);
+  const example = _.find(repo.examples, { slug });
+  let ext       = path.extname(filename).substr(1);
+  const target  = repo.fullPath + '/' + slug + '/config.json';
+  example[ext].push(filename);
+  console.log('after adding file', _.find(repo.examples, { slug }));
+  const config = _.clone(example);
+  delete config.slug;
+  configJSON = beautify(config, null, 2, 100);
+  return fs.writeFileAsync(target, configJSON);
+};
+
+ExampleStore.prototype.getList = function(path) {
+  return _.find(this.repos, { path });
+}
+
+ExampleStore.prototype.getRepoMenu = function(path) {
+  return this.repos.map(
+    ({ title, path }) => ({ title, path })
+  )
+};
+
+ExampleStore.prototype.getExampleMenu = function(path) {
+  const repo = _.find(this.repos, { path });
+  if(! repo.categories) {
+    console.error('Repo ' + repo.title + 'has no categories key');
+    return [];
+  }
+  console.log('getExampleMenu repo examples', repo.examples)
+  const menu = repo.categories.map(category => {
+    const examplesInCat = repo.examples.filter(ex => (ex.category === category.slug));
+    // console.log('examples in cat', category, examplesInCat);
+    const examples = examplesInCat.map(
+    ({ title, slug }) => ({ title, slug: repo.path + '/' + slug }) 
+  );
+    return { category, examples };
+  });
+  // console.log(menu);
+  return menu;
+  // return repo.examples.map(
+  //   ({ title, slug }) => ({ title, slug: repo.path + '/' + slug }) 
+  // );
+};
+
+ExampleStore.prototype.getMenu = function(path) {
+  const self = this;
+  return '<ul>' + self.repos.reduce((menu, repo) => {
+    return menu + '<li>'+ repo.title + '<ul>' + repo.examples.reduce((submenu, example) =>
+      (submenu + '<li><a href="#' + repo.path + '/' + example.slug  + '">' + example.title + '</a></li>'),
+    '') + '</ul></li>';
+  }, '') + '</ul>';
+}
+
+ExampleStore.prototype.getRepo = function(path) {
+  return _.find(this.repos, { path });
+}
+
+module.exports = ExampleStore;

+ 7 - 0
lib/exampleTmpl.json

@@ -0,0 +1,7 @@
+{
+  "html": [ "example.html" ],
+  "js": [ "script.js" ],
+  "css": [],
+  "libsJs": [ "jquery-3.2.1.min.js" ],
+  "libsCss": [ "styles.css" ]
+}

+ 14 - 0
lib/execAsync.js

@@ -0,0 +1,14 @@
+const Promise  = require('bluebird');
+const { exec } = require('child_process');
+
+// Promisified exec
+module.exports = function execAsync(cmd) {
+  return new Promise((resolve, reject) => {
+    exec(cmd, (error, stdout, stderr) => {
+      if (error) {
+        return reject(error);
+      }
+      resolve({ stdout, stderr });
+    });
+  });
+}

+ 63 - 0
lib/fsio.js

@@ -0,0 +1,63 @@
+/**
+ * This has to be required *after* using Bluebird's promisifyAll() on fs
+ */
+var fs      = require('fs');
+var path    = require('path');
+var Promise = require('bluebird');
+
+if(typeof fs.readdirAsync !== 'function') {
+  // console.error("scandirAsync module requires promisifying fs with Bluebird's Promise.promisifyAll()");
+  Promise.promisifyAll(fs);
+}
+
+function scandirAsync(path, excludes) {
+  excludes = excludes || [];
+  return fs.readdirAsync(path)
+  .then(dirContent => {
+    excludes.forEach(file => {
+      var idxInContent = dirContent.indexOf(file);
+      if(idxInContent !== -1) {
+        dirContent.splice(idxInContent, 1);
+      }
+    });
+    return dirContent;
+  });
+}
+
+function readFileAsync(file) {
+  console.log('readFileAsync', file);
+  // var fs = {
+  //   readFileAsync: function() {
+  //     return new Promise((resolve, reject) => {
+  //       // resolve('pouet');
+  //       reject(new Error('pouet error'))
+  //     });
+  //   }
+  // }
+  return fs.readFileAsync(file)
+  .then(buf => (buf.toString()));
+  // .then(str => {
+  //   console.log('read file: ', str); return str;
+  // });
+}
+
+function readFilesAsync(fullPath, files) {
+  // console.log('reading files', files, 'from path', path);
+  return Promise.reduce(files,
+    (carry, f) => readFileAsync(fullPath + '/' + f)
+      .then(content => 
+        (carry.concat([{
+          type: path.extname(f).substr(1),
+          name: f,
+          content
+        }]))
+      ),
+    []
+  );
+}
+
+module.exports = {
+  scandirAsync,
+  readFileAsync,
+  readFilesAsync
+};

+ 0 - 0
lib/indexHandlers.js


이 변경점에서 너무 많은 파일들이 변경되어 몇몇 파일들은 표시되지 않았습니다.