Implement comment preview feature (#223)
* Create #preview endpoint for CommentController * Create comment preview button WIP * Implement preview feature for reply comment W * Style preview button CSS WIP * Style preview button CSS WIP * Tweak markdown toggle logic and add styling * Update comment feature spec * Add error handling for comment preview & fix lint * Extract handleCommentPreview into it's own file * Adjust functionality for comment preview [WIP] * Modify comment preview functionality and CSS * Adjust code style based on linter
This commit is contained in:
parent
21b24f12fe
commit
4bc9020956
12 changed files with 226 additions and 61 deletions
|
|
@ -37,6 +37,7 @@ function callInitalizers(){
|
|||
initializeAnalytics();
|
||||
initializeCommentDropdown();
|
||||
initializeSettings();
|
||||
initializeCommentPreview();
|
||||
initializeAdditionalContentBoxes();
|
||||
if (!initializeLiveArticle.called){
|
||||
initializeLiveArticle();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,50 @@
|
|||
function initializeCommentPreview() {
|
||||
if (document.getElementById('preview-button')) {
|
||||
document.getElementById('preview-button').onclick = handleCommentPreview;
|
||||
}
|
||||
}
|
||||
|
||||
function handleCommentPreview(e) {
|
||||
e.preventDefault();
|
||||
var form = e.target.parentElement.parentElement;
|
||||
var textArea = form.querySelector('textarea');
|
||||
if (textArea.value !== '') {
|
||||
var previewDiv = form.querySelector('.comment-preview-div');
|
||||
var previewButton = form.querySelector('.comment-action-preview');
|
||||
if (previewButton.innerHTML.indexOf('PREVIEW') > -1) {
|
||||
textArea.classList.toggle('preview-loading');
|
||||
getAndShowPreview(previewDiv, textArea);
|
||||
previewButton.innerHTML = 'MARKDOWN';
|
||||
} else {
|
||||
previewDiv.classList.toggle('preview-toggle');
|
||||
textArea.classList.toggle('preview-toggle');
|
||||
previewButton.innerHTML = 'PREVIEW';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getAndShowPreview(previewDiv, textArea) {
|
||||
var commentMarkdown = textArea.value;
|
||||
var payload = JSON.stringify({
|
||||
comment: {
|
||||
body_markdown: commentMarkdown,
|
||||
},
|
||||
});
|
||||
getCsrfToken()
|
||||
.then(sendFetch('comment-preview', payload))
|
||||
.then(function(response) {
|
||||
return response.json();
|
||||
})
|
||||
.then(successCb)
|
||||
.catch(function(err) {
|
||||
console.log('error!');
|
||||
console.log(err);
|
||||
});
|
||||
|
||||
function successCb(body) {
|
||||
previewDiv.classList.toggle('preview-toggle');
|
||||
textArea.classList.toggle('preview-loading');
|
||||
textArea.classList.toggle('preview-toggle');
|
||||
previewDiv.innerHTML = body.processed_html;
|
||||
}
|
||||
}
|
||||
|
|
@ -228,6 +228,9 @@ function handleCommentSubmit(event) {
|
|||
mainCommentsForm.classList.remove("submitting");
|
||||
textArea.value = "";
|
||||
textArea.classList = "";
|
||||
var preview = document.getElementById("preview-div");
|
||||
preview.classList.add("preview-toggle");
|
||||
preview.innerHTML = "";
|
||||
var container = document.getElementById("comment-trees-container");
|
||||
container.insertBefore(newNode, container.firstChild);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,10 @@ function buildCommentFormHTML(commentableId, commentableType, parentId) {
|
|||
</div>'
|
||||
}
|
||||
var randomIdNumber = Math.floor(Math.random() * 1991)
|
||||
|
||||
var previewDiv = '<div class="preview-toggle comment-preview-div body"></div>'
|
||||
var previewButton = '<button id="preview-button" class="comment-action-button comment-action-preview" onclick="handleCommentPreview(event)">PREVIEW</button>'
|
||||
|
||||
return '<form class="new_comment" onsubmit="handleCommentSubmit.bind(this)(event)" id="new-comment-' + parentId + '" action="/comments" accept-charset="UTF-8" method="post" data-comment-id="'+parentId+'">\
|
||||
<input name="utf8" type="hidden" value="✓" />\
|
||||
<input type="hidden" name="authenticity_token" value="' + authToken + '">\
|
||||
|
|
@ -16,6 +20,7 @@ function buildCommentFormHTML(commentableId, commentableType, parentId) {
|
|||
<input value="' + commentableType + '" type="hidden" name="comment[commentable_type]" id="comment_commentable_type" />\
|
||||
<input value="' + parentId + '" type="hidden" name="comment[parent_id]" id="comment_parent_id" />\
|
||||
<textarea id="textarea-for-' + parentId + '" class="embiggened" name="comment[body_markdown]" id="comment_body_markdown" required></textarea>\
|
||||
'+previewDiv+'\
|
||||
'+codeOfConductHTML+'\
|
||||
<a href="/p/editor_guide" class="markdown-guide" target="_blank" title="Markdown Guide">\
|
||||
<img class="icon-image" src="<%= asset_path('info.svg') %>" />\
|
||||
|
|
@ -29,8 +34,11 @@ function buildCommentFormHTML(commentableId, commentableType, parentId) {
|
|||
<input type="submit" id="image-upload-submit-' + randomIdNumber + '" value="Upload" style="display:none">\
|
||||
<input class="uploaded-image" id="uploaded-image-' + randomIdNumber + '" />\
|
||||
</div>\
|
||||
<a href="#" class="cancel" onclick="cancel(event,this)">CANCEL</a>\
|
||||
<input type="submit" name="commit" value="SUBMIT" />\
|
||||
<div class="actions reply-actions">\
|
||||
<a href="#" class="cancel" onclick="cancel(event,this)">CANCEL</a>\
|
||||
'+previewButton+'\
|
||||
<input type="submit" class="comment-action-button" name="commit" value="SUBMIT" />\
|
||||
</div>\
|
||||
</form>';
|
||||
}
|
||||
|
||||
|
|
@ -38,4 +46,4 @@ function cancel(event, el) {
|
|||
event.preventDefault();
|
||||
replaceActionButts(el.parentNode.parentNode.parentNode.parentNode);
|
||||
initializeCommentsPage();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -61,6 +61,18 @@ function sendFetch(switchStatement, body) {
|
|||
credentials: 'same-origin',
|
||||
});
|
||||
};
|
||||
case 'comment-preview':
|
||||
return function(csrfToken) {
|
||||
return window.fetch('/comments/preview', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-Token': csrfToken,
|
||||
},
|
||||
body: body,
|
||||
credentials: 'same-origin',
|
||||
});
|
||||
};
|
||||
default:
|
||||
console.log('A wrong switchStatement was used.');
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -125,7 +125,7 @@ a.header-link{
|
|||
position: absolute;
|
||||
left:calc(0.2vw + 43px);
|
||||
bottom:0px;
|
||||
width:calc(100% - 145px);
|
||||
width: initial;
|
||||
text-align:left;
|
||||
.uploaded-image {
|
||||
vertical-align:11px;
|
||||
|
|
@ -248,9 +248,57 @@ a.header-link{
|
|||
&.embiggened-max{
|
||||
height:calc(30vw + 18px);
|
||||
}
|
||||
&.preview-loading{
|
||||
background: white url(image_path('loading-ellipsis.svg')) no-repeat center center;
|
||||
background-size: 50px;
|
||||
}
|
||||
}
|
||||
.preview-toggle {
|
||||
display: none;
|
||||
}
|
||||
.comment-preview-div {
|
||||
padding: 10px 20px 2px;
|
||||
min-height:calc(13vw + 6px);
|
||||
text-align: left;
|
||||
font-size: 19px;
|
||||
font-family: $helvetica;
|
||||
background: lighten($yellow, 19%);
|
||||
box-shadow: $shadow;
|
||||
width: calc(100% - 80px);
|
||||
margin: 19px auto 8px;
|
||||
border: 2px solid $yellow;
|
||||
p.loading-message{
|
||||
opacity:0.6;
|
||||
}
|
||||
pre {
|
||||
overflow-x: auto;
|
||||
}
|
||||
}
|
||||
.actions{
|
||||
text-align:right;
|
||||
|
||||
.comment-action-button {
|
||||
font-family: $helvetica-condensed;
|
||||
font-stretch:condensed;
|
||||
color:white;
|
||||
border:0px;
|
||||
font-size:13px;
|
||||
font-weight:500;
|
||||
margin-top:3px;
|
||||
padding:5px 12px;
|
||||
border-radius:3px;
|
||||
cursor:pointer;
|
||||
appearance: none;
|
||||
}
|
||||
|
||||
.comment-action-preview {
|
||||
background: $dark-gray;
|
||||
z-index: 10;
|
||||
}
|
||||
}
|
||||
.reply-actions {
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
.code-of-conduct {
|
||||
display: block;
|
||||
|
|
@ -272,17 +320,6 @@ a.header-link{
|
|||
input[type="submit"]{
|
||||
margin-right:19px;
|
||||
background: $bold-blue;
|
||||
font-family: $helvetica-condensed;
|
||||
font-stretch:condensed;
|
||||
color:white;
|
||||
border:0px;
|
||||
font-size:13px;
|
||||
font-weight:500;
|
||||
margin-top:3px;
|
||||
padding:5px 12px;
|
||||
border-radius:3px;
|
||||
cursor:pointer;
|
||||
appearance: none;
|
||||
}
|
||||
&.submitting{
|
||||
input[type="submit"]{
|
||||
|
|
@ -677,7 +714,7 @@ a.header-link{
|
|||
height:20px;
|
||||
}
|
||||
.editor-image-upload{
|
||||
width:calc(100% - 150px);
|
||||
width: initial;
|
||||
text-align:left;
|
||||
.uploaded-image {
|
||||
width: calc( 93% - 54px);
|
||||
|
|
@ -747,6 +784,7 @@ a.header-link{
|
|||
input[type="submit"]{
|
||||
padding:5px 8px;
|
||||
margin-bottom:5px;
|
||||
margin-right:7px;
|
||||
}
|
||||
|
||||
.cancel{
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
class CommentsController < ApplicationController
|
||||
before_action :set_comment, only: [ :update, :destroy]
|
||||
before_action :set_comment, only: %i[update destroy]
|
||||
before_action :set_cache_control_headers, only: [:index]
|
||||
before_action :raise_banned, only: [:create,:update]
|
||||
before_action :raise_banned, only: %i[create update]
|
||||
before_action :authenticate_user!, only: %i[preview create]
|
||||
|
||||
# GET /comments
|
||||
# GET /comments.json
|
||||
|
|
@ -16,7 +17,7 @@ class CommentsController < ApplicationController
|
|||
|
||||
if @podcast
|
||||
@user = @podcast
|
||||
@commentable = @user.podcast_episodes.find_by_slug(params[:slug]) or not_found
|
||||
(@commentable = @user.podcast_episodes.find_by_slug(params[:slug])) || not_found
|
||||
else
|
||||
@user = User.find_by_username(params[:username]) ||
|
||||
Organization.find_by_slug(params[:username]) ||
|
||||
|
|
@ -51,10 +52,6 @@ class CommentsController < ApplicationController
|
|||
# POST /comments
|
||||
# POST /comments.json
|
||||
def create
|
||||
unless current_user
|
||||
redirect_to "/"
|
||||
return
|
||||
end
|
||||
raise if RateLimitChecker.new(current_user).limit_by_situation("comment_creation")
|
||||
@comment = Comment.new(comment_params)
|
||||
@comment.user_id = current_user.id
|
||||
|
|
@ -101,7 +98,7 @@ class CommentsController < ApplicationController
|
|||
# PATCH/PUT /comments/1.json
|
||||
def update
|
||||
raise unless @comment.user_id == current_user.id
|
||||
if @comment.update(comment_update_params.merge({ edited_at: DateTime.now }))
|
||||
if @comment.update(comment_update_params.merge(edited_at: DateTime.now))
|
||||
Mention.create_all(@comment)
|
||||
redirect_to "#{@comment.commentable.path}/comments/#{@comment.id_code_generated}", notice: "Comment was successfully updated."
|
||||
else
|
||||
|
|
@ -125,29 +122,43 @@ class CommentsController < ApplicationController
|
|||
end
|
||||
|
||||
def delete_confirm
|
||||
unless current_user && Comment.where(id: params[:id_code].to_i(26), user_id: current_user.id ).first
|
||||
unless current_user && Comment.where(id: params[:id_code].to_i(26), user_id: current_user.id).first
|
||||
@comment = Comment.find(params[:id_code].to_i(26))
|
||||
redirect_to @comment.path
|
||||
return
|
||||
end
|
||||
@comment = Comment.where(id: params[:id_code].to_i(26), user_id: current_user.id ).first
|
||||
@comment = Comment.where(id: params[:id_code].to_i(26), user_id: current_user.id).first
|
||||
end
|
||||
|
||||
def preview
|
||||
begin
|
||||
fixed_body_markdown = MarkdownFixer.fix_for_preview(comment_params[:body_markdown])
|
||||
parsed_markdown = MarkdownParser.new(fixed_body_markdown)
|
||||
processed_html = parsed_markdown.finalize
|
||||
rescue StandardError => e
|
||||
processed_html = "<p>😔 There was a error in your markdown</p><hr><p>#{e}</p>"
|
||||
end
|
||||
respond_to do |format|
|
||||
format.json { render json: { processed_html: processed_html }, status: 200 }
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
# Use callbacks to share common setup or constraints between actions.
|
||||
def set_comment
|
||||
@comment = Comment.find(params[:id])
|
||||
end
|
||||
|
||||
# Never trust parameters from the scary internet, only allow the white list through.
|
||||
def comment_params
|
||||
params.require(:comment).permit(:body_markdown,
|
||||
:commentable_id,
|
||||
:commentable_type,
|
||||
:parent_id)
|
||||
end
|
||||
# Use callbacks to share common setup or constraints between actions.
|
||||
def set_comment
|
||||
@comment = Comment.find(params[:id])
|
||||
end
|
||||
|
||||
def comment_update_params
|
||||
params.require(:comment).permit(:body_markdown)
|
||||
end
|
||||
# Never trust parameters from the scary internet, only allow the white list through.
|
||||
def comment_params
|
||||
params.require(:comment).permit(:body_markdown,
|
||||
:commentable_id,
|
||||
:commentable_type,
|
||||
:parent_id)
|
||||
end
|
||||
|
||||
def comment_update_params
|
||||
params.require(:comment).permit(:body_markdown)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
class HexComparer
|
||||
|
||||
attr_accessor :hexes, :amount
|
||||
def initialize(hexes, amount=1)
|
||||
def initialize(hexes, amount = 1)
|
||||
@hexes = hexes
|
||||
@amount = amount
|
||||
end
|
||||
|
||||
def order
|
||||
hexes.sort
|
||||
end
|
||||
end
|
||||
|
||||
def smallest
|
||||
order.first
|
||||
|
|
|
|||
|
|
@ -26,7 +26,9 @@
|
|||
id: "text-area",
|
||||
readonly: !user_signed_in?,
|
||||
autofocus: @comment.persisted?,
|
||||
required: true %>
|
||||
required: true
|
||||
%>
|
||||
<div class="preview-toggle comment-preview-div" id="preview-div"></div>
|
||||
</div>
|
||||
<div class="code-of-conduct" id="toggle-code-of-conduct-checkbox">
|
||||
</div>
|
||||
|
|
@ -45,7 +47,8 @@
|
|||
</div>
|
||||
<% end %>
|
||||
<div class="actions" id="submit-wrapper">
|
||||
<%= f.submit "SUBMIT", id: "submit-button", onclick:"validateField(event)" %>
|
||||
<button id="preview-button" class="comment-action-button comment-action-preview">PREVIEW</button>
|
||||
<%= f.submit "SUBMIT", id: "submit-button", onclick:"validateField(event)", class:"comment-action-button" %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
|
|
|
|||
|
|
@ -155,6 +155,7 @@ Rails.application.routes.draw do
|
|||
get "/sponsors" => "pages#sponsors"
|
||||
get "/search" => "stories#search"
|
||||
post "articles/preview" => "articles#preview"
|
||||
post "comments/preview" => "comments#preview"
|
||||
get "/freestickers" => "giveaways#new"
|
||||
get "/freestickers/edit" => "giveaways#edit"
|
||||
get "/scholarship", to: redirect("/p/scholarships")
|
||||
|
|
|
|||
|
|
@ -1,30 +1,44 @@
|
|||
require "rails_helper"
|
||||
|
||||
feature "Creating Comment" do
|
||||
RSpec.describe "Creating Comment", type: :feature, js: true do
|
||||
let(:user) { create(:user) }
|
||||
let(:raw_comment) { Faker::Lorem.paragraph }
|
||||
let(:article) do
|
||||
create(:article, user_id: user.id, show_comments: true)
|
||||
end
|
||||
|
||||
background do
|
||||
before do
|
||||
login_via_session_as user
|
||||
end
|
||||
|
||||
# scenario "User fills out comment box normally" do
|
||||
# visit article.path.to_s
|
||||
# fill_in "text-area", with: raw_comment
|
||||
# click_button("SUBMIT")
|
||||
# expect(page).to have_css(".comment-action-buttons")
|
||||
# expect(page).to have_text(raw_comment)
|
||||
# end
|
||||
it "User fills out comment box normally" do
|
||||
visit article.path.to_s
|
||||
fill_in "text-area", with: raw_comment
|
||||
find(".checkbox").click
|
||||
click_button("SUBMIT")
|
||||
expect(page).to have_text(raw_comment)
|
||||
end
|
||||
|
||||
# scenario "User replies to a comment" do
|
||||
# create(:comment, commentable_id: article.id, user_id: user.id)
|
||||
# visit article.path.to_s
|
||||
# find(".toggle-reply-form").click
|
||||
# fill_in "text-area", with: raw_comment
|
||||
# click_button("SUBMIT")
|
||||
# expect(page).to have_text(raw_comment)
|
||||
# end
|
||||
it "User fill out commen box then click previews and submit" do
|
||||
visit article.path.to_s
|
||||
fill_in "text-area", with: raw_comment
|
||||
find(".checkbox").click
|
||||
click_button("PREVIEW")
|
||||
expect(page).to have_text(raw_comment)
|
||||
expect(page).to have_text("MARKDOWN")
|
||||
click_button("MARKDOWN")
|
||||
# expect(page).to have_text(raw_comment)
|
||||
expect(page).to have_text("PREVIEW")
|
||||
click_button("SUBMIT")
|
||||
expect(page).to have_text(raw_comment)
|
||||
end
|
||||
|
||||
it "User replies to a comment" do
|
||||
create(:comment, commentable_id: article.id, user_id: user.id)
|
||||
visit article.path.to_s
|
||||
find(".toggle-reply-form").click
|
||||
find(:xpath, "//div[@class='actions']/form[@class='new_comment']/textarea").set(raw_comment)
|
||||
find(:xpath, "//div[contains(@class, 'reply-actions')]/input[@name='commit']").click
|
||||
expect(page).to have_text(raw_comment)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -62,4 +62,28 @@ RSpec.describe "Comments", type: :request do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "POST /comments/preview" do
|
||||
it "returns 401 if user is not logged in" do
|
||||
post "/comments/preview", params: { comment: { body_markdown: "hi"}}, headers: { HTTP_ACCEPT: "application/json" }
|
||||
expect(response).to have_http_status(401)
|
||||
end
|
||||
|
||||
context "when logged-in" do
|
||||
before do
|
||||
login_as user
|
||||
post "/comments/preview",
|
||||
params: { comment: { body_markdown: "hi" } },
|
||||
headers: { HTTP_ACCEPT: "application/json" }
|
||||
end
|
||||
|
||||
it "returns 200 on good request" do
|
||||
expect(response).to have_http_status(200)
|
||||
end
|
||||
|
||||
it "returns json" do
|
||||
expect(response.content_type).to eq("application/json")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue