--- app/assets/stylesheets/less/_markdown.less
+++ app/assets/stylesheets/less/_markdown.less
... | ... | @@ -155,10 +155,9 @@ |
155 | 155 |
} |
156 | 156 |
|
157 | 157 |
li > input[type='checkbox'] { |
158 |
- vertical-align: baseline; |
|
159 |
- position: relative; |
|
158 |
+ vertical-align: text-top; |
|
160 | 159 |
} |
161 |
- |
|
160 |
+ |
|
162 | 161 |
img{ |
163 | 162 |
max-width:100%; |
164 | 163 |
margin:10px 0; |
--- app/assets/stylesheets/less/_page.less
+++ app/assets/stylesheets/less/_page.less
... | ... | @@ -7321,3 +7321,49 @@ |
7321 | 7321 |
color: #e91e63 !important; |
7322 | 7322 |
} |
7323 | 7323 |
} |
7324 |
+ |
|
7325 |
+@keyframes showNav { |
|
7326 |
+ from {opacity: 0;} |
|
7327 |
+ to {opacity: 1;} |
|
7328 |
+} |
|
7329 |
+ |
|
7330 |
+.task-list-button { |
|
7331 |
+ margin-left: 20px; |
|
7332 |
+ margin-top: 2px; |
|
7333 |
+} |
|
7334 |
+ |
|
7335 |
+.tasklist { |
|
7336 |
+ padding: 10px 20px 0 20px; |
|
7337 |
+ box-shadow: none; |
|
7338 |
+ filter: none; |
|
7339 |
+ display: none; |
|
7340 |
+ |
|
7341 |
+ &.task-show { |
|
7342 |
+ display: block; |
|
7343 |
+ animation: showNav 250ms ease-in-out both; |
|
7344 |
+ } |
|
7345 |
+ |
|
7346 |
+ .task-title { |
|
7347 |
+ font-weight: 500; |
|
7348 |
+ |
|
7349 |
+ .done-counter { |
|
7350 |
+ margin-left: 5px; |
|
7351 |
+ } |
|
7352 |
+ } |
|
7353 |
+ |
|
7354 |
+ .task-progress{ |
|
7355 |
+ background-color: #D4D4D4; |
|
7356 |
+ |
|
7357 |
+ .bar { |
|
7358 |
+ transition: .2s; |
|
7359 |
+ height: 2px; |
|
7360 |
+ width: 100%; |
|
7361 |
+ } |
|
7362 |
+ .red { |
|
7363 |
+ background-color: red; |
|
7364 |
+ } |
|
7365 |
+ .green { |
|
7366 |
+ background-color: #8bc34a; |
|
7367 |
+ } |
|
7368 |
+ } |
|
7369 |
+} |
--- app/controllers/IssueApp.java
+++ app/controllers/IssueApp.java
... | ... | @@ -910,6 +910,11 @@ |
910 | 910 |
return redirect(RouteUtil.getUrl(savedComment)); |
911 | 911 |
} |
912 | 912 |
|
913 |
+ // Just made for compatibility. No meanings. |
|
914 |
+ public static Result updateComment(String ownerName, String projectName, Long number, Long commentId) throws IOException { |
|
915 |
+ return newComment(ownerName, projectName, number); |
|
916 |
+ } |
|
917 |
+ |
|
913 | 918 |
private static Comment saveComment(Project project, Issue issue, IssueComment comment) { |
914 | 919 |
Comment savedComment; |
915 | 920 |
IssueComment existingComment = IssueComment.find.where().eq("id", comment.id).findUnique(); |
--- app/controllers/api/IssueApi.java
+++ app/controllers/api/IssueApi.java
... | ... | @@ -19,7 +19,9 @@ |
19 | 19 |
import controllers.routes; |
20 | 20 |
import models.*; |
21 | 21 |
import models.enumeration.*; |
22 |
+import org.apache.commons.codec.digest.DigestUtils; |
|
22 | 23 |
import org.apache.commons.lang3.StringUtils; |
24 |
+import play.api.mvc.Codec; |
|
23 | 25 |
import play.db.ebean.Transactional; |
24 | 26 |
import play.i18n.Messages; |
25 | 27 |
import play.libs.F; |
... | ... | @@ -186,12 +188,46 @@ |
186 | 188 |
if (!newIssueState.equals(issue.state)) { |
187 | 189 |
addNewIssueEvent(issue, user, EventType.ISSUE_STATE_CHANGED, issue.state.state(), newIssueState.state()); |
188 | 190 |
} |
189 |
- play.Logger.debug("newIssueState: " + newIssueState); |
|
190 | 191 |
issue.state = newIssueState; |
191 | 192 |
issue.save(); |
192 | 193 |
|
193 | 194 |
result = ProjectApi.getResult(issue); |
194 | 195 |
return ok(Json.newObject().set("result", toJson(addIssueEvents(issue, result)))); |
196 |
+ } |
|
197 |
+ |
|
198 |
+ @Transactional |
|
199 |
+ @IsAllowed(Operation.UPDATE) |
|
200 |
+ public static Result updateIssueContent(String owner, String projectName, Long number) { |
|
201 |
+ ObjectNode result = Json.newObject(); |
|
202 |
+ |
|
203 |
+ User user = UserApp.currentUser(); |
|
204 |
+ if (user.isAnonymous()) { |
|
205 |
+ return unauthorized(result.put("message", "unauthorized request")); |
|
206 |
+ } |
|
207 |
+ |
|
208 |
+ JsonNode json = request().body().asJson(); |
|
209 |
+ if(json == null) { |
|
210 |
+ return badRequest(result.put("message", "Expecting Json data")); |
|
211 |
+ } |
|
212 |
+ |
|
213 |
+ Project project = Project.findByOwnerAndProjectName(owner, projectName); |
|
214 |
+ final Issue issue = Issue.findByNumber(project, number); |
|
215 |
+ |
|
216 |
+ String content = json.findValue("content").asText(); |
|
217 |
+ String sha1checksum = json.findValue("sha1").asText(); |
|
218 |
+ |
|
219 |
+ String originalSha1 = DigestUtils.sha1Hex(issue.body.trim()); |
|
220 |
+ |
|
221 |
+ if (!originalSha1.equals(sha1checksum)) { |
|
222 |
+ result.put("message", "Already modified by someone."); |
|
223 |
+ return new Status(play.core.j.JavaResults.Conflict(), result, Codec.javaSupported("utf-8")); |
|
224 |
+ } |
|
225 |
+ |
|
226 |
+ issue.body = content; |
|
227 |
+ issue.update(); |
|
228 |
+ |
|
229 |
+ result = ProjectApi.getResult(issue); |
|
230 |
+ return ok(result); |
|
195 | 231 |
} |
196 | 232 |
|
197 | 233 |
private static Result updateIssueNode(JsonNode json, Project project, Issue issue, User user) { |
... | ... | @@ -347,7 +383,8 @@ |
347 | 383 |
public static Result updateIssueComment(String ownerName, String projectName, Long number, Long commentId) { |
348 | 384 |
ObjectNode result = Json.newObject(); |
349 | 385 |
|
350 |
- if (!isAuthored(request())) { |
|
386 |
+ User user = UserApp.currentUser(); |
|
387 |
+ if (user.isAnonymous()) { |
|
351 | 388 |
return unauthorized(result.put("message", "unauthorized request")); |
352 | 389 |
} |
353 | 390 |
|
... | ... | @@ -356,12 +393,20 @@ |
356 | 393 |
return badRequest(result.put("message", "Expecting Json data")); |
357 | 394 |
} |
358 | 395 |
|
359 |
- User user = getAuthorizedUser(getAuthorizationToken(request())); |
|
360 |
- String comment = json.findValue("comment").asText(); |
|
396 |
+ String comment = json.findValue("content").asText(); |
|
397 |
+ String sha1checksum = json.findValue("sha1").asText(); |
|
361 | 398 |
|
362 | 399 |
Project project = Project.findByOwnerAndProjectName(ownerName, projectName); |
363 | 400 |
final Issue issue = Issue.findByNumber(project, number); |
364 | 401 |
IssueComment issueComment = issue.findCommentByCommentId(commentId); |
402 |
+ |
|
403 |
+ String originalSha1 = DigestUtils.sha1Hex(issueComment.contents.trim()); |
|
404 |
+ |
|
405 |
+ if (!originalSha1.equals(sha1checksum)) { |
|
406 |
+ result.put("message", "Already modified by someone."); |
|
407 |
+ result.put("text", issueComment.contents); |
|
408 |
+ return new Status(play.core.j.JavaResults.Conflict(), result, Codec.javaSupported("utf-8")); |
|
409 |
+ } |
|
365 | 410 |
|
366 | 411 |
issueComment.contents = comment; |
367 | 412 |
issueComment.save(); |
... | ... | @@ -372,7 +417,7 @@ |
372 | 417 |
commentNode.set("author", toJson(authorNode)); |
373 | 418 |
result.set("result", commentNode); |
374 | 419 |
|
375 |
- return created(result); |
|
420 |
+ return ok(result); |
|
376 | 421 |
} |
377 | 422 |
|
378 | 423 |
private static Result createCommentByUser(Project project, Issue issue, JsonNode json) { |
--- app/utils/Markdown.java
+++ app/utils/Markdown.java
... | ... | @@ -42,6 +42,10 @@ |
42 | 42 |
.allowUrlProtocols("http", "https", "mailto", "file").allowElements("a") |
43 | 43 |
.allowAttributes("href", "name", "target").onElements("a") |
44 | 44 |
.toFactory()) |
45 |
+ .and(new HtmlPolicyBuilder() |
|
46 |
+ .allowElements("input") |
|
47 |
+ .allowAttributes("type", "disabled", "checked").onElements("input") |
|
48 |
+ .toFactory()) |
|
45 | 49 |
.and(new HtmlPolicyBuilder().allowElements("pre").toFactory()) |
46 | 50 |
.and(new HtmlPolicyBuilder() |
47 | 51 |
.allowAttributes("class", "id", "style", "width", "height").globally().toFactory()); |
--- app/views/board/partial_comments.scala.html
+++ app/views/board/partial_comments.scala.html
... | ... | @@ -25,6 +25,7 @@ |
25 | 25 |
|
26 | 26 |
<ul class="comments"> |
27 | 27 |
@for(comment <-post.comments){ |
28 |
+ @defining(isAllowed(UserApp.currentUser(), comment.asResource(), Operation.UPDATE)) { isAllowedUpdate => |
|
28 | 29 |
<li class="comment @isAuthorComment(comment.authorLoginId)" id="comment-@comment.id"> |
29 | 30 |
<div class="comment-avatar"> |
30 | 31 |
<a href="@routes.UserApp.userInfo(comment.authorLoginId)" class="avatar-wrap" data-toggle="tooltip" data-placement="top" title="@comment.authorName"> |
... | ... | @@ -58,7 +59,7 @@ |
58 | 59 |
</span> |
59 | 60 |
</div> |
60 | 61 |
|
61 |
- @common.commentUpdateForm(comment, routes.BoardApp.newComment(project.owner, project.name, post.getNumber).toString(), comment.contents) |
|
62 |
+ @common.commentUpdateForm(comment, routes.BoardApp.newComment(project.owner, project.name, post.getNumber).toString(), comment.contents, isAllowedUpdate) |
|
62 | 63 |
|
63 | 64 |
<div id="comment-body-@comment.id"> |
64 | 65 |
<div class="comment-body markdown-wrap" data-via-email="@OriginalEmail.exists(comment.asResource)">@Html(Markdown.render(comment.contents, project))</div> |
... | ... | @@ -66,5 +67,6 @@ |
66 | 67 |
</div> |
67 | 68 |
</div> |
68 | 69 |
</li> |
70 |
+ } |
|
69 | 71 |
} |
70 | 72 |
</ul> |
--- app/views/common/commentUpdateForm.scala.html
+++ app/views/common/commentUpdateForm.scala.html
... | ... | @@ -4,11 +4,11 @@ |
4 | 4 |
* Copyright Yona & Yobi Authors & NAVER Corp. |
5 | 5 |
* https://yona.io |
6 | 6 |
**@ |
7 |
-@(comment:Comment, action:String, contents:String) |
|
7 |
+@(comment:Comment, action:String, contents:String, isAllowedUpdate:Boolean) |
|
8 | 8 |
@import utils.AccessControl._ |
9 | 9 |
|
10 | 10 |
<div id="comment-editform-@comment.id" class="comment-update-form"> |
11 |
- <form action="@action" method="post"> |
|
11 |
+ <form action="@action/@comment.id" method="post"> |
|
12 | 12 |
<input type="hidden" name="id" value="@comment.id"> |
13 | 13 |
|
14 | 14 |
<div class="write-comment-box"> |
... | ... | @@ -25,7 +25,7 @@ |
25 | 25 |
</span> |
26 | 26 |
} |
27 | 27 |
<button type="button" class="ybtn ybtn-cancel" data-comment-id="@comment.id">@Messages("button.cancel")</button> |
28 |
- @if(isAllowed(UserApp.currentUser(), comment.asResource(), Operation.UPDATE)) { |
|
28 |
+ @if(isAllowedUpdate) { |
|
29 | 29 |
<button type="submit" class="ybtn ybtn-info">@Messages("button.save")</button> |
30 | 30 |
} |
31 | 31 |
</div> |
--- app/views/common/editor.scala.html
+++ app/views/common/editor.scala.html
... | ... | @@ -33,6 +33,9 @@ |
33 | 33 |
<a href="#preview-@wrapId" data-toggle="tab" data-mode="preview">@Messages("common.editor.preview")</a> |
34 | 34 |
</li> |
35 | 35 |
<li> |
36 |
+ <div class="task-list-button"><button type="button" class="add-task-list-button ybtn ybtn-mini ybtn-danger">Add Task List</button></div> |
|
37 |
+ </li> |
|
38 |
+ <li> |
|
36 | 39 |
<div class="editor-notice-label"></div> |
37 | 40 |
</li> |
38 | 41 |
</ul> |
--- app/views/common/scripts.scala.html
+++ app/views/common/scripts.scala.html
... | ... | @@ -174,5 +174,26 @@ |
174 | 174 |
}); |
175 | 175 |
|
176 | 176 |
autosize($('textarea')); |
177 |
+ |
|
178 |
+ addTaskListButtonListener(); |
|
179 |
+ function addTaskListButtonListener() { |
|
180 |
+ $(".add-task-list-button").on('click', function () { |
|
181 |
+ var tasklistTemplate = "\n- [ ] Todo A\r\n- [ ] Todo B\r\n- [ ] Todo C"; |
|
182 |
+ var $textarea = $(this).closest("form").find("textarea"); |
|
183 |
+ var cursorPos = $textarea.prop('selectionStart'); |
|
184 |
+ var text = $textarea.val(); |
|
185 |
+ |
|
186 |
+ if(cursorPos === 0 && text && text.length > 0){ |
|
187 |
+ cursorPos = text.length; |
|
188 |
+ } |
|
189 |
+ |
|
190 |
+ var textBefore = text.substring(0, cursorPos); |
|
191 |
+ var textAfter = text.substring(cursorPos, text.length); |
|
192 |
+ |
|
193 |
+ $textarea.val(textBefore + tasklistTemplate + textAfter); |
|
194 |
+ $textarea.focus(); |
|
195 |
+ }); |
|
196 |
+ } |
|
197 |
+ |
|
177 | 198 |
}); |
178 | 199 |
</script> |
+++ app/views/common/tasklistBar.scala.html
... | ... | @@ -0,0 +1,12 @@ |
1 | +@** | |
2 | +* Yona, 21st Century Project Hosting SW | |
3 | +* | |
4 | +* Copyright Yona & Yobi Authors & NAVER Corp. & NAVER LABS Corp. | |
5 | +* https://yona.io | |
6 | +**@ | |
7 | +<div class="tasklist"> | |
8 | + <div class="task-title">Tasks<span class="done-counter"></span></div> | |
9 | + <div class="task-progress"> | |
10 | + <div class="bar red" style="width: 0;" title="Tasklist"></div> | |
11 | + </div> | |
12 | +</div> |
--- app/views/help/markdown.scala.html
+++ app/views/help/markdown.scala.html
... | ... | @@ -1,22 +1,8 @@ |
1 | 1 |
@** |
2 |
-* Yobi, Project Hosting SW |
|
2 |
+* Yona, 21st Century Project Hosting SW |
|
3 | 3 |
* |
4 |
-* Copyright 2013 NAVER Corp. |
|
5 |
-* http://yobi.io |
|
6 |
-* |
|
7 |
-* @author Suwon Chae |
|
8 |
-* |
|
9 |
-* Licensed under the Apache License, Version 2.0 (the "License"); |
|
10 |
-* you may not use this file except in compliance with the License. |
|
11 |
-* You may obtain a copy of the License at |
|
12 |
-* |
|
13 |
-* http://www.apache.org/licenses/LICENSE-2.0 |
|
14 |
-* |
|
15 |
-* Unless required by applicable law or agreed to in writing, software |
|
16 |
-* distributed under the License is distributed on an "AS IS" BASIS, |
|
17 |
-* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|
18 |
-* See the License for the specific language governing permissions and |
|
19 |
-* limitations under the License. |
|
4 |
+* Copyright Yona & Yobi Authors & NAVER Corp. & NAVER LABS Corp. |
|
5 |
+* https://yona.io |
|
20 | 6 |
**@ |
21 | 7 |
<div class="markdown-help"> |
22 | 8 |
<ul class="markdown-help-nav"> |
... | ... | @@ -28,6 +14,7 @@ |
28 | 14 |
<li class="help-nav" data-toggle="markdown-help" data-target="markdownHeaders">Headers</li> |
29 | 15 |
<li class="help-nav" data-toggle="markdown-help" data-target="markdownLinks">Links</li> |
30 | 16 |
<li class="help-nav" data-toggle="markdown-help" data-target="markdownLists">Lists</li> |
17 |
+ <li class="help-nav" data-toggle="markdown-help" data-target="markdownTaskList">TaskList</li> |
|
31 | 18 |
<li class="help-nav" data-toggle="markdown-help" data-target="markdownImages">Images</li> |
32 | 19 |
<li class="help-nav" data-toggle="markdown-help" data-target="markdownBlockquotes">Blockquotes</li> |
33 | 20 |
<li class="help-nav" data-toggle="markdown-help" data-target="markdownCodes">Codes</li> |
... | ... | @@ -105,6 +92,36 @@ |
105 | 92 |
</div> |
106 | 93 |
</div> |
107 | 94 |
</li> |
95 |
+ <li class="markdown-help-item markdownTaskList"> |
|
96 |
+ <div class="row-fluid thead"> |
|
97 |
+ <div class="span6">Markdown Input</div> |
|
98 |
+ <div class="span6">Markdown Output</div> |
|
99 |
+ </div> |
|
100 |
+ <div class="row-fluid markdwon-syntax-wrap"> |
|
101 |
+ <div class="span6 markdwon-syntax"> |
|
102 |
+ <pre> |
|
103 |
+- [ ] Todos |
|
104 |
+ - [x] To do A |
|
105 |
+ - [ ] To do B |
|
106 |
+ - [ ] To do C |
|
107 |
+ </pre> |
|
108 |
+ </div> |
|
109 |
+ <div class="span6"> |
|
110 |
+ <div class="markdown-wrap"> |
|
111 |
+ <ul> |
|
112 |
+ <li> |
|
113 |
+ <input type="checkbox"> Todos |
|
114 |
+ <ul> |
|
115 |
+ <li><input type="checkbox" checked> To do A</li> |
|
116 |
+ <li><input type="checkbox"> To do B</li> |
|
117 |
+ <li><input type="checkbox"> To do C</li> |
|
118 |
+ </ul> |
|
119 |
+ </li> |
|
120 |
+ </ul> |
|
121 |
+ </div> |
|
122 |
+ </div> |
|
123 |
+ </div> |
|
124 |
+ </li> |
|
108 | 125 |
<li class="markdown-help-item markdownImages" > |
109 | 126 |
<div class="row-fluid thead"> |
110 | 127 |
<div class="span6">Markdown Input</div> |
--- app/views/issue/partial_comment.scala.html
+++ app/views/issue/partial_comment.scala.html
... | ... | @@ -22,6 +22,7 @@ |
22 | 22 |
@VOTER_AVATAR_SHOW_LIMIT = @{ 5 } |
23 | 23 |
|
24 | 24 |
<li class="comment @isAuthorComment(comment.authorLoginId)" id="comment-@comment.id"> |
25 |
+@defining(isAllowed(UserApp.currentUser(), comment.asResource(), Operation.UPDATE)) { isAllowedUpdate => |
|
25 | 26 |
@common.childCommentsAnchorDiv(issue, comment) |
26 | 27 |
<div class="comment-avatar"> |
27 | 28 |
<a href="@userInfo(comment.authorLoginId)" class="avatar-wrap" data-toggle="tooltip" data-placement="top" title="@comment.authorLoginId"> |
... | ... | @@ -98,12 +99,14 @@ |
98 | 99 |
</span> |
99 | 100 |
</div> |
100 | 101 |
|
101 |
- @common.commentUpdateForm(comment, routes.IssueApp.newComment(project.owner, project.name, issue.getNumber).toString(), comment.contents) |
|
102 |
+ @common.commentUpdateForm(comment, routes.IssueApp.newComment(project.owner, project.name, issue.getNumber).toString(), comment.contents, isAllowedUpdate) |
|
102 | 103 |
|
103 | 104 |
<div id="comment-body-@comment.id"> |
104 |
- <div class="comment-body markdown-wrap" data-via-email="@OriginalEmail.exists(comment.asResource)">@Html(Markdown.render(comment.contents, project))</div> |
|
105 |
+ @common.tasklistBar() |
|
106 |
+ <div class="comment-body markdown-wrap" data-allowed-update="@isAllowedUpdate" data-via-email="@OriginalEmail.exists(comment.asResource)">@Html(Markdown.render(comment.contents, project))</div> |
|
105 | 107 |
<div class="attachments pull-right" data-attachments="@toJson(AttachmentApp.getFileList(ResourceType.ISSUE_COMMENT.toString(), comment.id.toString()))"></div> |
106 | 108 |
</div> |
107 | 109 |
</div> |
108 | 110 |
@common.childComments(issue, comment, ResourceType.ISSUE_COMMENT) |
111 |
+} |
|
109 | 112 |
</li> |
--- app/views/issue/view.scala.html
+++ app/views/issue/view.scala.html
... | ... | @@ -97,6 +97,10 @@ |
97 | 97 |
FavoriteIssue.findByIssueId(UserApp.currentUser().id, issue.id) != null |
98 | 98 |
} |
99 | 99 |
|
100 |
+@isAllowedUpdate = @{ |
|
101 |
+ isAllowed(UserApp.currentUser(), issue.asResource(), Operation.UPDATE) |
|
102 |
+} |
|
103 |
+ |
|
100 | 104 |
@projectLayout(titleForOGTag, project, utils.MenuType.ISSUE){ |
101 | 105 |
@projectMenu(project, utils.MenuType.ISSUE, "main-menu-only") |
102 | 106 |
<div class="page-wrap-outer"> |
... | ... | @@ -158,7 +162,15 @@ |
158 | 162 |
@if(StringUtils.isEmpty(issue.body)){ |
159 | 163 |
<div class="content empty-content"></div> |
160 | 164 |
} else { |
161 |
- <div class="content markdown-wrap">@Html(Markdown.render(issue.body, issue.project))</div> |
|
165 |
+ <div id="issue-@issue.getNumber" class="hide"> |
|
166 |
+ <form action="@api.routes.IssueApi.updateIssueContent(project.owner, project.name, issue.getNumber)"> |
|
167 |
+ <textarea>@issue.body</textarea> |
|
168 |
+ </form> |
|
169 |
+ </div> |
|
170 |
+ <div id="issue-body-@issue.getNumber"> |
|
171 |
+ @common.tasklistBar() |
|
172 |
+ <div class="content markdown-wrap" data-allowed-update="@isAllowedUpdate">@Html(Markdown.render(issue.body, issue.project))</div> |
|
173 |
+ </div> |
|
162 | 174 |
} |
163 | 175 |
<div class="attachments" id="attachments" data-attachments="@toJson(AttachmentApp.getFileList(ResourceType.ISSUE_POST.toString(), issue.id.toString()))"></div> |
164 | 176 |
<div class="board-actrow right-txt"> |
... | ... | @@ -174,7 +186,7 @@ |
174 | 186 |
} |
175 | 187 |
</button> |
176 | 188 |
} |
177 |
- @if(isAllowed(UserApp.currentUser(), issue.asResource(), Operation.UPDATE) && !hasSharer) { |
|
189 |
+ @if(isAllowedUpdate && !hasSharer) { |
|
178 | 190 |
<button id="issue-share-button" type="button" class="ybtn" data-toggle="popover" data-trigger="hover" data-placement="top" data-content="@Messages("issue.sharer.description")">@Messages("button.share.issue")</button> |
179 | 191 |
} |
180 | 192 |
|
... | ... | @@ -206,7 +218,7 @@ |
206 | 218 |
} |
207 | 219 |
|
208 | 220 |
<span class="act-row"> |
209 |
- @if(isAllowed(UserApp.currentUser(), issue.asResource(), Operation.UPDATE)) { |
|
221 |
+ @if(isAllowedUpdate) { |
|
210 | 222 |
<button type="button" class="icon btn-transparent-with-fontsize-lineheight ml10 pt5px" data-toggle="tooltip" title="@Messages("button.edit")" onclick="window.location='@routes.IssueApp.editIssueForm(project.owner, project.name, issue.getNumber)'"><i class="yobicon-edit-2"></i></button> |
211 | 223 |
} else { |
212 | 224 |
<a href="@routes.IssueApp.editIssueForm(project.owner, project.name, issue.getNumber)"><button type="button" class="icon btn-transparent-with-fontsize-lineheight ml10 pt5px" data-toggle="tooltip" title="@Messages("button.show.original")"><i class="yobicon-edit-2"></i></button></a> |
... | ... | @@ -226,7 +238,7 @@ |
226 | 238 |
@Messages("issue.sharer") <span class="num issue-sharer-count">@if(issue.sharers.size > 0) { @issue.sharers.size }</span> |
227 | 239 |
</dt> |
228 | 240 |
<dd id="sharer-list" class="@if(!hasSharer){hideFromDisplayOnly}"> |
229 |
- @if(isAllowed(UserApp.currentUser(), issue.asResource(), Operation.UPDATE)) { |
|
241 |
+ @if(isAllowedUpdate) { |
|
230 | 242 |
<input type="hidden" class="bigdrop width100p" id="issueSharer" name="issueSharer" placeholder="@Messages("issue.sharer.select")" value="@sharers" title=""> |
231 | 243 |
} else { |
232 | 244 |
@for(sharer <- issue.getSortedSharer){ |
... | ... | @@ -276,7 +288,7 @@ |
276 | 288 |
<dt>@Messages("issue.assignee")</dt> |
277 | 289 |
|
278 | 290 |
<dd> |
279 |
- @if(isAllowed(UserApp.currentUser(), issue.asResource(), Operation.UPDATE)) { |
|
291 |
+ @if(isAllowedUpdate) { |
|
280 | 292 |
@partial_assignee(project, issue) |
281 | 293 |
} else { |
282 | 294 |
@if(hasAssignee){ |
... | ... | @@ -309,7 +321,7 @@ |
309 | 321 |
</a> |
310 | 322 |
} else { |
311 | 323 |
@defining(issue.milestone != null) { hasMilestone => |
312 |
- @if(isAllowed(UserApp.currentUser(), issue.asResource(), Operation.UPDATE)) { |
|
324 |
+ @if(isAllowedUpdate) { |
|
313 | 325 |
<select id="milestone" name="milestone.id" |
314 | 326 |
data-toggle="select2" data-format="milestone" data-container-css-class="fullsize"> |
315 | 327 |
<option value="@Milestone.NULL_MILESTONE_ID" @if(!hasMilestone){ selected }> |
... | ... | @@ -366,7 +378,7 @@ |
366 | 378 |
</span> |
367 | 379 |
</dt> |
368 | 380 |
<dd> |
369 |
- @if(isAllowed(UserApp.currentUser(), issue.asResource(), Operation.UPDATE)) { |
|
381 |
+ @if(isAllowedUpdate) { |
|
370 | 382 |
<div class="search search-bar"> |
371 | 383 |
<input type="text" name="dueDate" value="@issue.getDueDateString" class="textbox full" autocomplete="off" data-toggle="calendar"> |
372 | 384 |
<button type="button" class="search-btn btn-calendar"><i class="yobicon-calendar2"></i></button> |
... | ... | @@ -383,14 +395,14 @@ |
383 | 395 |
|
384 | 396 |
@**<!-- labels -->**@ |
385 | 397 |
@if(!IssueLabel.findByProject(project).isEmpty){ |
386 |
- @if(isAllowed(UserApp.currentUser(), issue.asResource(), Operation.UPDATE)){ |
|
398 |
+ @if(isAllowedUpdate){ |
|
387 | 399 |
@partial_select_label(IssueLabel.findByProject(project), issue.getLabelIds, "", "", project) |
388 | 400 |
} else { |
389 | 401 |
@partial_show_selected_label(issue.labels.toList, routes.IssueApp.issues(project.owner, project.name, issue.state.state(), "html", 1).toString) |
390 | 402 |
} |
391 | 403 |
} |
392 | 404 |
<div class="act-row right-menu-icons"> |
393 |
- @if(isAllowed(UserApp.currentUser(), issue.asResource(), Operation.UPDATE)) { |
|
405 |
+ @if(isAllowedUpdate) { |
|
394 | 406 |
<button type="button" class="icon btn-transparent-with-fontsize-lineheight ml10 pt5px" data-toggle="tooltip" title="@Messages("button.edit")" onclick="window.location='@routes.IssueApp.editIssueForm(project.owner, project.name, issue.getNumber)'"><i class="yobicon-edit-2"></i></button> |
395 | 407 |
} else { |
396 | 408 |
<a href="@routes.IssueApp.editIssueForm(project.owner, project.name, issue.getNumber)"><button type="button" class="icon btn-transparent-with-fontsize-lineheight ml10 pt5px" data-toggle="tooltip" title="@Messages("button.show.original")"><i class="yobicon-edit-2"></i></button></a> |
... | ... | @@ -450,6 +462,8 @@ |
450 | 462 |
<script type="text/javascript" src="@routes.Assets.at("javascripts/lib/elevator/jquery.elevator.js")"></script> |
451 | 463 |
<script type="text/javascript" src="@routes.Assets.at("javascripts/service/yona.issue.Assginee.js")"></script> |
452 | 464 |
<script type="text/javascript" src="@routes.Assets.at("javascripts/service/yona.issue.Sharer.js")"></script> |
465 |
+<script type="text/javascript" src="@routes.Assets.at("javascripts/common/yona.Sha1.js")"></script> |
|
466 |
+<script type="text/javascript" src="@routes.Assets.at("javascripts/common/yona.Tasklist.js")"></script> |
|
453 | 467 |
<script type="text/javascript"> |
454 | 468 |
$(function(){ |
455 | 469 |
// yobi.issue.View |
... | ... | @@ -471,7 +485,7 @@ |
471 | 485 |
@if(project.menuSetting.issue) { |
472 | 486 |
,"N": "@routes.IssueApp.newIssueForm(project.owner, project.name)" |
473 | 487 |
} |
474 |
- @if(isAllowed(UserApp.currentUser(), issue.asResource(), Operation.UPDATE)) { |
|
488 |
+ @if(isAllowedUpdate) { |
|
475 | 489 |
,"E": "@routes.IssueApp.editIssueForm(project.owner, project.name, issue.getNumber)" |
476 | 490 |
} |
477 | 491 |
}); |
--- conf/routes
+++ conf/routes
... | ... | @@ -65,6 +65,7 @@ |
65 | 65 |
GET /-_-api/v1/owners/:owner/projects/:projectName/issues/:number controllers.api.IssueApi.getIssue(owner: String, projectName: String, number: Long) |
66 | 66 |
PUT /-_-api/v1/owners/:owner/projects/:projectName/issues/:number controllers.api.IssueApi.updateIssue(owner: String, projectName: String, number: Long) |
67 | 67 |
PATCH /-_-api/v1/owners/:owner/projects/:projectName/issues/:number controllers.api.IssueApi.updateIssueState(owner: String, projectName: String, number: Long) |
68 |
+PATCH /-_-api/v1/owners/:owner/projects/:projectName/issues/:number/content controllers.api.IssueApi.updateIssueContent(owner: String, projectName: String, number: Long) |
|
68 | 69 |
GET /-_-api/v1/users controllers.UserApp.users(query: String ?= "") |
69 | 70 |
POST /-_-api/v1/users controllers.api.UserApi.newUser() |
70 | 71 |
POST /-_-api/v1/users/token controllers.api.UserApi.newToken() |
... | ... | @@ -255,6 +256,8 @@ |
255 | 256 |
GET /:user/:project/issue/$number<[0-9]+>/nextstate controllers.IssueApp.nextState(user, project, number:Long) |
256 | 257 |
DELETE /:user/:project/issue/$number<[0-9]+>/delete controllers.IssueApp.deleteIssue(user, project, number:Long) |
257 | 258 |
POST /:user/:project/issue/$number<[0-9]+>/comments controllers.IssueApp.newComment(user, project, number:Long) |
259 |
+POST /:user/:project/issue/$number<[0-9]+>/comments/:commentId controllers.IssueApp.updateComment(user, project, number:Long, commentId:Long) |
|
260 |
+PATCH /:user/:project/issue/$number<[0-9]+>/comments/:commentId controllers.api.IssueApi.updateIssueComment(user, project, number:Long, commentId:Long) |
|
258 | 261 |
DELETE /:user/:project/issue/$number<[0-9]+>/comment/:commentId/delete controllers.IssueApp.deleteComment(user, project, number:Long, commentId:Long) |
259 | 262 |
GET /:user/:project/issue/$number<[0-9]+>/timeline controllers.IssueApp.timeline(user, project, number:Long) |
260 | 263 |
|
+++ public/javascripts/common/yona.Sha1.js
... | ... | @@ -0,0 +1,9 @@ |
1 | +/* | |
2 | + * [js-sha1]{@link https://github.com/emn178/js-sha1} | |
3 | + * | |
4 | + * @version 0.6.0 | |
5 | + * @author Chen, Yi-Cyuan [emn178@gmail.com] | |
6 | + * @copyright Chen, Yi-Cyuan 2014-2017 | |
7 | + * @license MIT | |
8 | + */ | |
9 | +!function(){"use strict";function t(t){t?(f[0]=f[16]=f[1]=f[2]=f[3]=f[4]=f[5]=f[6]=f[7]=f[8]=f[9]=f[10]=f[11]=f[12]=f[13]=f[14]=f[15]=0,this.blocks=f):this.blocks=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],this.h0=1732584193,this.h1=4023233417,this.h2=2562383102,this.h3=271733878,this.h4=3285377520,this.block=this.start=this.bytes=this.hBytes=0,this.finalized=this.hashed=!1,this.first=!0}var h="object"==typeof window?window:{},s=!h.JS_SHA1_NO_NODE_JS&&"object"==typeof process&&process.versions&&process.versions.node;s&&(h=global);var i=!h.JS_SHA1_NO_COMMON_JS&&"object"==typeof module&&module.exports,e="function"==typeof define&&define.amd,r="0123456789abcdef".split(""),o=[-2147483648,8388608,32768,128],n=[24,16,8,0],a=["hex","array","digest","arrayBuffer"],f=[],u=function(h){return function(s){return new t(!0).update(s)[h]()}},c=function(){var h=u("hex");s&&(h=p(h)),h.create=function(){return new t},h.update=function(t){return h.create().update(t)};for(var i=0;i<a.length;++i){var e=a[i];h[e]=u(e)}return h},p=function(t){var h=eval("require('crypto')"),s=eval("require('buffer').Buffer"),i=function(i){if("string"==typeof i)return h.createHash("sha1").update(i,"utf8").digest("hex");if(i.constructor===ArrayBuffer)i=new Uint8Array(i);else if(void 0===i.length)return t(i);return h.createHash("sha1").update(new s(i)).digest("hex")};return i};t.prototype.update=function(t){if(!this.finalized){var s="string"!=typeof t;s&&t.constructor===h.ArrayBuffer&&(t=new Uint8Array(t));for(var i,e,r=0,o=t.length||0,a=this.blocks;r<o;){if(this.hashed&&(this.hashed=!1,a[0]=this.block,a[16]=a[1]=a[2]=a[3]=a[4]=a[5]=a[6]=a[7]=a[8]=a[9]=a[10]=a[11]=a[12]=a[13]=a[14]=a[15]=0),s)for(e=this.start;r<o&&e<64;++r)a[e>>2]|=t[r]<<n[3&e++];else for(e=this.start;r<o&&e<64;++r)(i=t.charCodeAt(r))<128?a[e>>2]|=i<<n[3&e++]:i<2048?(a[e>>2]|=(192|i>>6)<<n[3&e++],a[e>>2]|=(128|63&i)<<n[3&e++]):i<55296||i>=57344?(a[e>>2]|=(224|i>>12)<<n[3&e++],a[e>>2]|=(128|i>>6&63)<<n[3&e++],a[e>>2]|=(128|63&i)<<n[3&e++]):(i=65536+((1023&i)<<10|1023&t.charCodeAt(++r)),a[e>>2]|=(240|i>>18)<<n[3&e++],a[e>>2]|=(128|i>>12&63)<<n[3&e++],a[e>>2]|=(128|i>>6&63)<<n[3&e++],a[e>>2]|=(128|63&i)<<n[3&e++]);this.lastByteIndex=e,this.bytes+=e-this.start,e>=64?(this.block=a[16],this.start=e-64,this.hash(),this.hashed=!0):this.start=e}return this.bytes>4294967295&&(this.hBytes+=this.bytes/4294967296<<0,this.bytes=this.bytes%4294967296),this}},t.prototype.finalize=function(){if(!this.finalized){this.finalized=!0;var t=this.blocks,h=this.lastByteIndex;t[16]=this.block,t[h>>2]|=o[3&h],this.block=t[16],h>=56&&(this.hashed||this.hash(),t[0]=this.block,t[16]=t[1]=t[2]=t[3]=t[4]=t[5]=t[6]=t[7]=t[8]=t[9]=t[10]=t[11]=t[12]=t[13]=t[14]=t[15]=0),t[14]=this.hBytes<<3|this.bytes>>>29,t[15]=this.bytes<<3,this.hash()}},t.prototype.hash=function(){var t,h,s=this.h0,i=this.h1,e=this.h2,r=this.h3,o=this.h4,n=this.blocks;for(t=16;t<80;++t)h=n[t-3]^n[t-8]^n[t-14]^n[t-16],n[t]=h<<1|h>>>31;for(t=0;t<20;t+=5)s=(h=(i=(h=(e=(h=(r=(h=(o=(h=s<<5|s>>>27)+(i&e|~i&r)+o+1518500249+n[t]<<0)<<5|o>>>27)+(s&(i=i<<30|i>>>2)|~s&e)+r+1518500249+n[t+1]<<0)<<5|r>>>27)+(o&(s=s<<30|s>>>2)|~o&i)+e+1518500249+n[t+2]<<0)<<5|e>>>27)+(r&(o=o<<30|o>>>2)|~r&s)+i+1518500249+n[t+3]<<0)<<5|i>>>27)+(e&(r=r<<30|r>>>2)|~e&o)+s+1518500249+n[t+4]<<0,e=e<<30|e>>>2;for(;t<40;t+=5)s=(h=(i=(h=(e=(h=(r=(h=(o=(h=s<<5|s>>>27)+(i^e^r)+o+1859775393+n[t]<<0)<<5|o>>>27)+(s^(i=i<<30|i>>>2)^e)+r+1859775393+n[t+1]<<0)<<5|r>>>27)+(o^(s=s<<30|s>>>2)^i)+e+1859775393+n[t+2]<<0)<<5|e>>>27)+(r^(o=o<<30|o>>>2)^s)+i+1859775393+n[t+3]<<0)<<5|i>>>27)+(e^(r=r<<30|r>>>2)^o)+s+1859775393+n[t+4]<<0,e=e<<30|e>>>2;for(;t<60;t+=5)s=(h=(i=(h=(e=(h=(r=(h=(o=(h=s<<5|s>>>27)+(i&e|i&r|e&r)+o-1894007588+n[t]<<0)<<5|o>>>27)+(s&(i=i<<30|i>>>2)|s&e|i&e)+r-1894007588+n[t+1]<<0)<<5|r>>>27)+(o&(s=s<<30|s>>>2)|o&i|s&i)+e-1894007588+n[t+2]<<0)<<5|e>>>27)+(r&(o=o<<30|o>>>2)|r&s|o&s)+i-1894007588+n[t+3]<<0)<<5|i>>>27)+(e&(r=r<<30|r>>>2)|e&o|r&o)+s-1894007588+n[t+4]<<0,e=e<<30|e>>>2;for(;t<80;t+=5)s=(h=(i=(h=(e=(h=(r=(h=(o=(h=s<<5|s>>>27)+(i^e^r)+o-899497514+n[t]<<0)<<5|o>>>27)+(s^(i=i<<30|i>>>2)^e)+r-899497514+n[t+1]<<0)<<5|r>>>27)+(o^(s=s<<30|s>>>2)^i)+e-899497514+n[t+2]<<0)<<5|e>>>27)+(r^(o=o<<30|o>>>2)^s)+i-899497514+n[t+3]<<0)<<5|i>>>27)+(e^(r=r<<30|r>>>2)^o)+s-899497514+n[t+4]<<0,e=e<<30|e>>>2;this.h0=this.h0+s<<0,this.h1=this.h1+i<<0,this.h2=this.h2+e<<0,this.h3=this.h3+r<<0,this.h4=this.h4+o<<0},t.prototype.hex=function(){this.finalize();var t=this.h0,h=this.h1,s=this.h2,i=this.h3,e=this.h4;return r[t>>28&15]+r[t>>24&15]+r[t>>20&15]+r[t>>16&15]+r[t>>12&15]+r[t>>8&15]+r[t>>4&15]+r[15&t]+r[h>>28&15]+r[h>>24&15]+r[h>>20&15]+r[h>>16&15]+r[h>>12&15]+r[h>>8&15]+r[h>>4&15]+r[15&h]+r[s>>28&15]+r[s>>24&15]+r[s>>20&15]+r[s>>16&15]+r[s>>12&15]+r[s>>8&15]+r[s>>4&15]+r[15&s]+r[i>>28&15]+r[i>>24&15]+r[i>>20&15]+r[i>>16&15]+r[i>>12&15]+r[i>>8&15]+r[i>>4&15]+r[15&i]+r[e>>28&15]+r[e>>24&15]+r[e>>20&15]+r[e>>16&15]+r[e>>12&15]+r[e>>8&15]+r[e>>4&15]+r[15&e]},t.prototype.toString=t.prototype.hex,t.prototype.digest=function(){this.finalize();var t=this.h0,h=this.h1,s=this.h2,i=this.h3,e=this.h4;return[t>>24&255,t>>16&255,t>>8&255,255&t,h>>24&255,h>>16&255,h>>8&255,255&h,s>>24&255,s>>16&255,s>>8&255,255&s,i>>24&255,i>>16&255,i>>8&255,255&i,e>>24&255,e>>16&255,e>>8&255,255&e]},t.prototype.array=t.prototype.digest,t.prototype.arrayBuffer=function(){this.finalize();var t=new ArrayBuffer(20),h=new DataView(t);return h.setUint32(0,this.h0),h.setUint32(4,this.h1),h.setUint32(8,this.h2),h.setUint32(12,this.h3),h.setUint32(16,this.h4),t};var y=c();i?module.exports=y:(h.sha1=y,e&&define(function(){return y}))}();(No newline at end of file) |
+++ public/javascripts/common/yona.Tasklist.js
... | ... | @@ -0,0 +1,145 @@ |
1 | +/** | |
2 | + * Yona, 21st Century Project Hosting SW | |
3 | + * <p> | |
4 | + * Copyright Yona & Yobi Authors & NAVER Corp. & NAVER LABS Corp. | |
5 | + * https://yona.io | |
6 | + **/ | |
7 | + | |
8 | +// At present, using .val() on textarea elements strips carriage return characters | |
9 | +// https://stackoverflow.com/a/8601601/1450196 | |
10 | +$.valHooks.textarea = { | |
11 | + get: function( elem ) { | |
12 | + return elem.value.replace( /\r?\n/g, "\r\n" ); | |
13 | + } | |
14 | +}; | |
15 | + | |
16 | +$(function () { | |
17 | + var $markdownWrap = $(".markdown-wrap"); | |
18 | + var inputCheckBox = "input[type='checkbox']"; | |
19 | + | |
20 | + checkTasklistDoneCount($markdownWrap); | |
21 | + disableCheckboxIfNeeds($markdownWrap); | |
22 | + | |
23 | + $markdownWrap.find(inputCheckBox).each(function () { | |
24 | + var $this = $(this); | |
25 | + var $parent = $this.closest(); | |
26 | + | |
27 | + $parent | |
28 | + .click(function () { | |
29 | + $this.trigger('click'); | |
30 | + }) | |
31 | + .hover(function(){ | |
32 | + $(this).css({ cursor: 'pointer' }); | |
33 | + }); | |
34 | + }); | |
35 | + | |
36 | + $markdownWrap.find(inputCheckBox).on("click", function () { | |
37 | + var $this = $(this); | |
38 | + var $form = $this.closest("div[id]").prev().find("form"); | |
39 | + var url = $form.attr("action"); | |
40 | + var originalText = $form.find("textarea").val(); | |
41 | + checkTask($this); | |
42 | + | |
43 | + var text = $form.find("textarea").val(); | |
44 | + | |
45 | + console.log(sha1(originalText), originalText.length); | |
46 | + | |
47 | + $.ajax({ | |
48 | + method: "PATCH", | |
49 | + url: url, | |
50 | + contentType: "application/json", | |
51 | + data: JSON.stringify({ content: text, sha1: sha1(originalText.trim()) }), | |
52 | + beforeSend: function() { | |
53 | + NProgress.start(); | |
54 | + } | |
55 | + }) | |
56 | + .done(function (msg) { | |
57 | + NProgress.done(); | |
58 | + checkTasklistDoneCount($markdownWrap); | |
59 | + }) | |
60 | + .fail(function(jqXHR, textStatus){ | |
61 | + var response = JSON.parse(jqXHR.responseText); | |
62 | + var message = '[' + jqXHR.statusText + '] ' + response.message + '\n\nRefresh the page!'; | |
63 | + $yobi.showAlert(message); | |
64 | + NProgress.done(); | |
65 | + }); | |
66 | + | |
67 | + }); | |
68 | + | |
69 | + function checkTask(that, checked) { | |
70 | + var $this = that; | |
71 | + var isChecked; | |
72 | + if(checked === undefined) { | |
73 | + isChecked = $this.prop("checked"); | |
74 | + } else { | |
75 | + isChecked = checked; | |
76 | + } | |
77 | + | |
78 | + $this.prop('checked', isChecked); | |
79 | + | |
80 | + var $parent = $this.closest(".markdown-wrap"); | |
81 | + var index = $parent.find(inputCheckBox).index($this); | |
82 | + var $form = $this.closest("div[id]").prev().find("form"); | |
83 | + var $textarea = $form.find("textarea"); | |
84 | + var text = $textarea.val(); | |
85 | + | |
86 | + var counter = 0; | |
87 | + // See: https://regex101.com/r/uIC2RM/2 | |
88 | + text = text.replace(/^([ ]*[-+*] \[[ xX]?])([ ]?.+)/gm, function replacer(match, checkbox, text){ | |
89 | + var composedText = checkbox + text; | |
90 | + if(index === counter) { | |
91 | + if(isChecked) { | |
92 | + composedText = checkbox.replace(/\[[ ]?]/, "[x]") + text | |
93 | + } else { | |
94 | + composedText = checkbox.replace(/\[[xX]?]/, "[ ]") + text | |
95 | + } | |
96 | + } | |
97 | + counter++; | |
98 | + return composedText; | |
99 | + }); | |
100 | + | |
101 | + $textarea.val(text); | |
102 | + $this.next().find(inputCheckBox).each(function () { | |
103 | + checkTask($(this), isChecked); | |
104 | + }); | |
105 | + } | |
106 | + | |
107 | + function checkTasklistDoneCount($target) { | |
108 | + $target.each(function( index ) { | |
109 | + var $this = $(this); | |
110 | + var total = 0; | |
111 | + var checked = 0; | |
112 | + $this.find(inputCheckBox).each(function () { | |
113 | + total++; | |
114 | + if($(this).prop("checked")) { | |
115 | + checked++; | |
116 | + } | |
117 | + }); | |
118 | + var $tasklist = $this.prev(); | |
119 | + var percentage = checked / total * 100; | |
120 | + $tasklist.find(".done-counter").html("(" + checked + "/" + total + ")"); | |
121 | + $tasklist.find(".bar").width(percentage + "%"); | |
122 | + $tasklist.find(".task-title").width(percentage + "%"); | |
123 | + if(total > 0) { | |
124 | + $tasklist.addClass("task-show"); | |
125 | + } | |
126 | + if(percentage === 100) { | |
127 | + $tasklist.find(".bar").removeClass("red").addClass("green"); | |
128 | + } else { | |
129 | + $tasklist.find(".bar").removeClass("green").addClass("red"); | |
130 | + } | |
131 | + }); | |
132 | + } | |
133 | + | |
134 | + function disableCheckboxIfNeeds($target){ | |
135 | + $target.each(function() { | |
136 | + var $this = $(this); | |
137 | + if($this.data("allowedUpdate") !== true) { | |
138 | + $this.find(inputCheckBox).each(function () { | |
139 | + $(this).prop("disabled", true); | |
140 | + }); | |
141 | + } | |
142 | + }); | |
143 | + } | |
144 | + | |
145 | +}); |
--- public/javascripts/lib/marked.js
+++ public/javascripts/lib/marked.js
... | ... | @@ -377,11 +377,11 @@ |
377 | 377 |
} |
378 | 378 |
|
379 | 379 |
// Check for task list items |
380 |
- istask = /^\[[ xX]\] /.test(item); |
|
380 |
+ istask = /^\[[ xX]?\] /.test(item); |
|
381 | 381 |
ischecked = undefined; |
382 | 382 |
if (istask) { |
383 |
- ischecked = item[1] !== ' '; |
|
384 |
- item = item.replace(/^\[[ xX]\] +/, ''); |
|
383 |
+ ischecked = item[1] !== ' ' && item[1] !== ']'; |
|
384 |
+ item = item.replace(/^\[[ xX]?\] +/, ''); |
|
385 | 385 |
} |
386 | 386 |
|
387 | 387 |
t = { |
... | ... | @@ -965,13 +965,13 @@ |
965 | 965 |
}; |
966 | 966 |
|
967 | 967 |
Renderer.prototype.listitem = function(text) { |
968 |
- return '<li>' + text + '</li>\n'; |
|
968 |
+ return '<li>' + text.replace(/<[/]?p[^>]*>/g,"") + '</li>\n'; |
|
969 | 969 |
}; |
970 | 970 |
|
971 | 971 |
Renderer.prototype.checkbox = function(checked) { |
972 | 972 |
return '<input ' |
973 | 973 |
+ (checked ? 'checked="" ' : '') |
974 |
- + 'disabled="" type="checkbox"' |
|
974 |
+ + ' type="checkbox"' |
|
975 | 975 |
+ (this.options.xhtml ? ' /' : '') |
976 | 976 |
+ '> '; |
977 | 977 |
} |
+++ public/javascripts/lib/tasklist/gfm-task-list.css
... | ... | @@ -0,0 +1,2 @@ |
1 | +.contains-task-list{list-style-type:disc;margin-top:10px;padding-left:0}.contains-task-list input[type=checkbox]{margin-right:5px}.contains-task-list .task-list-item{list-style-type:none;padding-right:15px;padding-left:42px;margin-right:-15px;margin-left:-15px;border:1px solid transparent}.contains-task-list .task-list-item .contains-task-list{margin-top:4px}.contains-task-list .task-list-item .task-list-item-checkbox{margin:0 .2em .25em -1.6em;vertical-align:middle} | |
2 | +/*# sourceMappingURL=gfm-task-list.min.css.map*/(No newline at end of file) |
+++ public/javascripts/lib/tasklist/gfm-task-list.js
... | ... | @@ -0,0 +1,182 @@ |
1 | +/******/ (function(modules) { // webpackBootstrap | |
2 | + /******/ // The module cache | |
3 | + /******/ var installedModules = {}; | |
4 | + /******/ | |
5 | + /******/ // The require function | |
6 | + /******/ function __webpack_require__(moduleId) { | |
7 | + /******/ | |
8 | + /******/ // Check if module is in cache | |
9 | + /******/ if(installedModules[moduleId]) | |
10 | + /******/ return installedModules[moduleId].exports; | |
11 | + /******/ | |
12 | + /******/ // Create a new module (and put it into the cache) | |
13 | + /******/ var module = installedModules[moduleId] = { | |
14 | + /******/ exports: {}, | |
15 | + /******/ id: moduleId, | |
16 | + /******/ loaded: false | |
17 | + /******/ }; | |
18 | + /******/ | |
19 | + /******/ // Execute the module function | |
20 | + /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); | |
21 | + /******/ | |
22 | + /******/ // Flag the module as loaded | |
23 | + /******/ module.loaded = true; | |
24 | + /******/ | |
25 | + /******/ // Return the exports of the module | |
26 | + /******/ return module.exports; | |
27 | + /******/ } | |
28 | + /******/ | |
29 | + /******/ | |
30 | + /******/ // expose the modules object (__webpack_modules__) | |
31 | + /******/ __webpack_require__.m = modules; | |
32 | + /******/ | |
33 | + /******/ // expose the module cache | |
34 | + /******/ __webpack_require__.c = installedModules; | |
35 | + /******/ | |
36 | + /******/ // __webpack_public_path__ | |
37 | + /******/ __webpack_require__.p = ""; | |
38 | + /******/ | |
39 | + /******/ // Load entry module and return exports | |
40 | + /******/ return __webpack_require__(0); | |
41 | + /******/ }) | |
42 | +/************************************************************************/ | |
43 | +/******/ ([ | |
44 | + /* 0 */ | |
45 | + /***/ function(module, exports, __webpack_require__) { | |
46 | + | |
47 | + /* WEBPACK VAR INJECTION */(function($) {"use strict"; | |
48 | + __webpack_require__(2); | |
49 | + var GFMTaskList = (function () { | |
50 | + function GFMTaskList($element, settings) { | |
51 | + var _this = this; | |
52 | + this.incomplete = "[ ]"; | |
53 | + this.complete = "[x]"; | |
54 | + this.incompletePattern = RegExp(this.escapePattern(this.incomplete)); | |
55 | + this.completePattern = RegExp(this.escapePattern(this.complete)); | |
56 | + this.itemPattern = RegExp("^(?:\\s*[-+*]|(?:\\d+\\.))?\\s*(" + this.escapePattern(this.complete) + "|" + this.escapePattern(this.incomplete) + ")(?=\\s)"); | |
57 | + this.codeFencesPattern = /^`{3}(?:\s*\w+)?[\S\s].*[\S\s]^`{3}$/mg; | |
58 | + this.itemsInParasPattern = RegExp("^(" + this.escapePattern(this.complete) + "|" + this.escapePattern(this.incomplete) + ").+$", 'g'); | |
59 | + this.$element = $element; | |
60 | + this.$markdownContainer = this.$element.find(settings.markdownContainer); | |
61 | + this.$renderedContainer = this.$element.find(settings.renderedContainer); | |
62 | + this.onUpdate = function (event) { | |
63 | + var update = _this.updateTaskList($(event.target)); | |
64 | + if (update) | |
65 | + settings.onUpdate(update); | |
66 | + }; | |
67 | + this.$renderedContainer.on('change', '.task-list-item-checkbox', this.onUpdate); | |
68 | + this.enable(); | |
69 | + } | |
70 | + GFMTaskList.prototype.destroy = function () { | |
71 | + this.$renderedContainer.off('change', '.task-list-item-checkbox', this.onUpdate); | |
72 | + }; | |
73 | + GFMTaskList.prototype.enable = function () { | |
74 | + this.$renderedContainer | |
75 | + .find('.task-list-item').addClass('enabled') | |
76 | + .find('.task-list-item-checkbox').attr('disabled', null); | |
77 | + this.$element.trigger('tasklist:enabled'); | |
78 | + }; | |
79 | + GFMTaskList.prototype.disable = function () { | |
80 | + this.$renderedContainer | |
81 | + .find('.task-list-item').removeClass('enabled') | |
82 | + .find('.task-list-item-checkbox').attr('disabled', 'disabled'); | |
83 | + this.$element.trigger('tasklist:disabled'); | |
84 | + }; | |
85 | + GFMTaskList.prototype.updateTaskListItem = function (source, itemIndex, checked) { | |
86 | + var clean = source | |
87 | + .replace(/\r/g, '') | |
88 | + .replace(this.codeFencesPattern, '') | |
89 | + .replace(this.itemsInParasPattern, '') | |
90 | + .split("\n"); | |
91 | + var index = 0; | |
92 | + var updatedMarkdown = []; | |
93 | + for (var _i = 0, _a = source.split('\n'); _i < _a.length; _i++) { | |
94 | + var line = _a[_i]; | |
95 | + if (clean.indexOf(line) >= 0 && this.itemPattern.test(line)) { | |
96 | + index++; | |
97 | + if (index === itemIndex) { | |
98 | + if (checked) { | |
99 | + line = line.replace(this.incompletePattern, this.complete); | |
100 | + } | |
101 | + else { | |
102 | + line = line.replace(this.completePattern, this.incomplete); | |
103 | + } | |
104 | + } | |
105 | + } | |
106 | + updatedMarkdown.push(line); | |
107 | + } | |
108 | + return updatedMarkdown.join('\n'); | |
109 | + }; | |
110 | + ; | |
111 | + GFMTaskList.prototype.updateTaskList = function ($item) { | |
112 | + var index = 1 + this.$renderedContainer.find('.task-list-item-checkbox').index($item); | |
113 | + var checked = $item.prop('checked'); | |
114 | + var event = $.Event('tasklist:change'); | |
115 | + this.$element.trigger(event, [index, checked]); | |
116 | + if (event.isDefaultPrevented()) | |
117 | + return; | |
118 | + var updatedMarkdown = this.updateTaskListItem(this.$markdownContainer.val(), index, checked); | |
119 | + this.$markdownContainer.val(updatedMarkdown); | |
120 | + this.$markdownContainer.trigger('change'); | |
121 | + this.$markdownContainer.trigger('tasklist:changed', [index, checked]); | |
122 | + return updatedMarkdown; | |
123 | + }; | |
124 | + ; | |
125 | + GFMTaskList.prototype.escapePattern = function (str) { | |
126 | + return str | |
127 | + .replace(/([\[\]])/g, '\\$1') | |
128 | + .replace(/\s/, '\\s') | |
129 | + .replace('x', '[xX]'); | |
130 | + }; | |
131 | + return GFMTaskList; | |
132 | + }()); | |
133 | + var jQuery; | |
134 | + (function (jQuery) { | |
135 | + $.fn.gfmTaskList = function (action) { | |
136 | + var instance = $.data(this, GFMTaskList.name); | |
137 | + if (typeof action === 'string') { | |
138 | + if (!instance) { | |
139 | + throw new Error("Must construct gfmTaskList before calling methods on it."); | |
140 | + } | |
141 | + instance[action](); | |
142 | + return this; | |
143 | + } | |
144 | + var settings; | |
145 | + if (typeof action === 'object') { | |
146 | + settings = action; | |
147 | + var requiredKeys = ['renderedContainer', 'markdownContainer']; | |
148 | + var keys_1 = Object.keys(settings); | |
149 | + requiredKeys.forEach(function (requiredKey) { | |
150 | + if (keys_1.indexOf(requiredKey) === -1) { | |
151 | + throw new Error("Missing key '" + requiredKey + "'"); | |
152 | + } | |
153 | + }); | |
154 | + action = undefined; | |
155 | + } | |
156 | + else { | |
157 | + throw new Error("Must pass an object to $.fn.gfmTaskList()."); | |
158 | + } | |
159 | + if (instance) | |
160 | + instance.destroy(); | |
161 | + instance = $.data(this, GFMTaskList.name, new GFMTaskList(this, settings)); | |
162 | + return this; | |
163 | + }; | |
164 | + })(jQuery || (jQuery = {})); | |
165 | + | |
166 | + /* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(1))) | |
167 | + | |
168 | + /***/ }, | |
169 | + /* 1 */ | |
170 | + /***/ function(module, exports) { | |
171 | + | |
172 | + module.exports = jQuery; | |
173 | + | |
174 | + /***/ }, | |
175 | + /* 2 */ | |
176 | + /***/ function(module, exports) { | |
177 | + | |
178 | + // removed by extract-text-webpack-plugin | |
179 | + | |
180 | + /***/ } | |
181 | + /******/ ]); | |
182 | +//# sourceMappingURL=gfm-task-list.js.map(No newline at end of file) |
Add a comment
Delete comment
Once you delete this comment, you won't be able to recover it. Are you sure you want to delete this comment?