mirror of
https://github.com/redmine/redmine.git
synced 2025-10-26 00:36:14 +02:00
Reimplement partial quote feature using Stimulus JS (#42515).
Patch by Katsuya HIDAKA (user:hidakatsuya). git-svn-id: https://svn.redmine.org/redmine/trunk@23854 e93f8b46-1217-0410-a6f0-8f06a7374b81
This commit is contained in:
1
Gemfile
1
Gemfile
@@ -22,6 +22,7 @@ gem 'commonmarker', '~> 2.3.0'
|
||||
gem "doorkeeper", "~> 5.8.2"
|
||||
gem "bcrypt", require: false
|
||||
gem "doorkeeper-i18n", "~> 5.2"
|
||||
gem "requestjs-rails", "~> 0.0.13"
|
||||
|
||||
# Ruby Standard Gems
|
||||
gem 'csv', '~> 3.3.2'
|
||||
|
||||
@@ -1,21 +1,6 @@
|
||||
function quoteReply(path, selectorForContentElement, textFormatting) {
|
||||
const contentElement = $(selectorForContentElement).get(0);
|
||||
const selectedRange = QuoteExtractor.extract(contentElement);
|
||||
|
||||
let formatter;
|
||||
|
||||
if (textFormatting === 'common_mark') {
|
||||
formatter = new QuoteCommonMarkFormatter();
|
||||
} else {
|
||||
formatter = new QuoteTextFormatter();
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: path,
|
||||
type: 'post',
|
||||
data: { quote: formatter.format(selectedRange) }
|
||||
});
|
||||
}
|
||||
import { Controller } from '@hotwired/stimulus'
|
||||
import TurndownService from 'turndown'
|
||||
import { post } from '@rails/request.js'
|
||||
|
||||
class QuoteExtractor {
|
||||
static extract(targetElement) {
|
||||
@@ -214,3 +199,26 @@ class QuoteCommonMarkFormatter {
|
||||
return htmlFragment.innerHTML;
|
||||
}
|
||||
}
|
||||
|
||||
export default class extends Controller {
|
||||
static targets = [ 'content' ];
|
||||
|
||||
quote(event) {
|
||||
const { url, textFormatting } = event.params;
|
||||
const selectedRange = QuoteExtractor.extract(this.contentTarget);
|
||||
|
||||
let formatter;
|
||||
|
||||
if (textFormatting === 'common_mark') {
|
||||
formatter = new QuoteCommonMarkFormatter();
|
||||
} else {
|
||||
formatter = new QuoteTextFormatter();
|
||||
}
|
||||
|
||||
post(url, {
|
||||
body: JSON.stringify({ quote: formatter.format(selectedRange) }),
|
||||
contentType: 'application/json',
|
||||
responseKind: 'script'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
8
app/assets/javascripts/turndown-7.2.0.min.js
vendored
8
app/assets/javascripts/turndown-7.2.0.min.js
vendored
File diff suppressed because one or more lines are too long
@@ -46,7 +46,7 @@ module JournalsHelper
|
||||
if journal.notes.present?
|
||||
if options[:reply_links]
|
||||
url = quoted_issue_path(issue, :journal_id => journal, :journal_indice => indice)
|
||||
links << quote_reply(url, "#journal-#{journal.id}-notes", icon_only: true)
|
||||
links << quote_reply_button(url: url, icon_only: true)
|
||||
end
|
||||
if journal.editable_by?(User.current)
|
||||
links << link_to(sprite_icon('edit', l(:button_edit)),
|
||||
@@ -69,7 +69,8 @@ module JournalsHelper
|
||||
end
|
||||
|
||||
def render_notes(issue, journal, options={})
|
||||
content_tag('div', textilizable(journal, :notes), :id => "journal-#{journal.id}-notes", :class => "wiki")
|
||||
content_tag('div', textilizable(journal, :notes),
|
||||
id: "journal-#{journal.id}-notes", class: "wiki", data: { quote_reply_target: 'content' })
|
||||
end
|
||||
|
||||
def render_private_notes_indicator(journal)
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
<% content_for :header_tags do %>
|
||||
<%= javascripts_for_quote_reply_include_tag %>
|
||||
<% end %>
|
||||
|
||||
<%= render :partial => 'action_menu' %>
|
||||
|
||||
<h2 class="inline-block"><%= issue_heading(@issue) %></h2><%= issue_status_type_badge(@issue.status) %>
|
||||
@@ -96,13 +92,13 @@ end %>
|
||||
|
||||
<% if @issue.description? %>
|
||||
<hr />
|
||||
<div class="description">
|
||||
<div class="description" data-controller="quote-reply">
|
||||
<div class="contextual">
|
||||
<%= quote_reply(quoted_issue_path(@issue), '#issue_description_wiki') if @issue.notes_addable? %>
|
||||
<%= quote_reply_button(url: quoted_issue_path(@issue)) if @issue.notes_addable? %>
|
||||
</div>
|
||||
|
||||
<p><strong><%=l(:field_description)%></strong></p>
|
||||
<div id="issue_description_wiki" class="wiki">
|
||||
<div id="issue_description_wiki" class="wiki" data-quote-reply-target="content">
|
||||
<%= textilizable @issue, :description, :attachments => @issue.attachments %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
<% reply_links = issue.notes_addable? -%>
|
||||
<% for journal in journals %>
|
||||
<div id="change-<%= journal.id %>" class="<%= journal.css_classes %>">
|
||||
<div id="change-<%= journal.id %>" class="<%= journal.css_classes %>" data-controller="quote-reply">
|
||||
<div id="note-<%= journal.indice %>" class="note">
|
||||
<div class="contextual">
|
||||
<span class="journal-actions"><%= render_journal_actions(issue, journal, :reply_links => reply_links) %></span>
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
<% content_for :header_tags do %>
|
||||
<%= javascripts_for_quote_reply_include_tag %>
|
||||
<% end %>
|
||||
|
||||
<%= board_breadcrumb(@message) %>
|
||||
|
||||
<div class="contextual">
|
||||
<div data-controller="quote-reply">
|
||||
<div class="contextual">
|
||||
<%= watcher_link(@topic, User.current) %>
|
||||
<%= quote_reply(
|
||||
url_for(:action => 'quote', :id => @topic, :format => 'js'),
|
||||
'#message_topic_wiki'
|
||||
<%= quote_reply_button(
|
||||
url: url_for(action: 'quote', id: @topic, format: 'js')
|
||||
) if !@topic.locked? && authorize_for('messages', 'reply') %>
|
||||
<%= link_to(
|
||||
sprite_icon('edit', l(:button_edit)),
|
||||
@@ -21,20 +17,21 @@
|
||||
:method => :post,
|
||||
:data => {:confirm => l(:text_are_you_sure)},
|
||||
:class => 'icon icon-del'
|
||||
) if @message.destroyable_by?(User.current) %>
|
||||
</div>
|
||||
) if @message.destroyable_by?(User.current) %>
|
||||
</div>
|
||||
|
||||
<h2><%= avatar(@topic.author) %><%= @topic.subject %></h2>
|
||||
<h2><%= avatar(@topic.author) %><%= @topic.subject %></h2>
|
||||
|
||||
<div class="message">
|
||||
<div class="reaction">
|
||||
<%= reaction_button @topic %>
|
||||
</div>
|
||||
<p><span class="author"><%= authoring @topic.created_on, @topic.author %></span></p>
|
||||
<div id="message_topic_wiki" class="wiki">
|
||||
<%= textilizable(@topic, :content) %>
|
||||
</div>
|
||||
<%= link_to_attachments @topic, :author => false, :thumbnails => true %>
|
||||
<div class="message">
|
||||
<div class="reaction">
|
||||
<%= reaction_button @topic %>
|
||||
</div>
|
||||
<p><span class="author"><%= authoring @topic.created_on, @topic.author %></span></p>
|
||||
<div id="message_topic_wiki" class="wiki" data-quote-reply-target="content">
|
||||
<%= textilizable(@topic, :content) %>
|
||||
</div>
|
||||
<%= link_to_attachments @topic, :author => false, :thumbnails => true %>
|
||||
</div>
|
||||
</div>
|
||||
<br />
|
||||
|
||||
@@ -45,12 +42,11 @@
|
||||
<p><%= toggle_link l(:button_reply), "reply", :focus => 'message_content', :scroll => "message_content" %></p>
|
||||
<% end %>
|
||||
<% @replies.each do |message| %>
|
||||
<div class="message reply" id="<%= "message-#{message.id}" %>">
|
||||
<div class="message reply" id="<%= "message-#{message.id}" %>" data-controller="quote-reply">
|
||||
<div class="contextual">
|
||||
<%= reaction_button message %>
|
||||
<%= quote_reply(
|
||||
url_for(:action => 'quote', :id => message, :format => 'js'),
|
||||
"#message-#{message.id} .wiki",
|
||||
<%= quote_reply_button(
|
||||
url: url_for(action: 'quote', id: message, format: 'js'),
|
||||
icon_only: true
|
||||
) if !@topic.locked? && authorize_for('messages', 'reply') %>
|
||||
<%= link_to(
|
||||
@@ -74,7 +70,9 @@
|
||||
-
|
||||
<%= authoring message.created_on, message.author %>
|
||||
</h4>
|
||||
<div class="wiki"><%= textilizable message, :content, :attachments => message.attachments %></div>
|
||||
<div class="wiki" data-quote-reply-target="content">
|
||||
<%= textilizable message, :content, :attachments => message.attachments %>
|
||||
</div>
|
||||
<%= link_to_attachments message, :author => false, :thumbnails => true %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
@@ -5,4 +5,5 @@
|
||||
pin "application"
|
||||
pin "@hotwired/stimulus", to: "stimulus.min.js"
|
||||
pin "@hotwired/stimulus-loading", to: "stimulus-loading.js"
|
||||
pin "turndown" # @7.2.0
|
||||
pin_all_from "app/javascript/controllers", under: "controllers"
|
||||
|
||||
@@ -20,21 +20,18 @@
|
||||
module Redmine
|
||||
module QuoteReply
|
||||
module Helper
|
||||
def javascripts_for_quote_reply_include_tag
|
||||
javascript_include_tag 'turndown-7.2.0.min', 'quote_reply'
|
||||
end
|
||||
def quote_reply_button(url:, icon_only: false)
|
||||
button_params = {
|
||||
data: {
|
||||
action: 'quote-reply#quote',
|
||||
quote_reply_url_param: url,
|
||||
quote_reply_text_formatting_param: Setting.text_formatting
|
||||
},
|
||||
class: "#{icon_only ? "icon-only" : "icon"} icon-quote"
|
||||
}
|
||||
button_params[:title] = l(:button_quote) if icon_only
|
||||
|
||||
def quote_reply(url, selector_for_content, icon_only: false)
|
||||
quote_reply_function = "quoteReply('#{j url}', '#{j selector_for_content}', '#{j Setting.text_formatting}')"
|
||||
|
||||
html_options = { class: "#{icon_only ? "icon-only" : "icon"} icon-quote" }
|
||||
html_options[:title] = l(:button_quote) if icon_only
|
||||
|
||||
link_to_function(
|
||||
sprite_icon('quote-filled', l(:button_quote), icon_only: icon_only, style: :filled),
|
||||
quote_reply_function,
|
||||
html_options
|
||||
)
|
||||
link_to sprite_icon('quote-filled', l(:button_quote), icon_only: icon_only, style: :filled), '#', button_params
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ require_relative '../application_system_test_case'
|
||||
class MessagesTest < ApplicationSystemTestCase
|
||||
def test_reply_to_topic_message
|
||||
with_text_formatting 'common_mark' do
|
||||
within '#content > .contextual' do
|
||||
within '#content > [data-controller="quote-reply"]' do
|
||||
click_link 'Quote'
|
||||
end
|
||||
|
||||
@@ -64,7 +64,7 @@ class MessagesTest < ApplicationSystemTestCase
|
||||
window.getSelection().addRange(range);
|
||||
JS
|
||||
|
||||
within '#content > .contextual' do
|
||||
within '#content > [data-controller="quote-reply"]' do
|
||||
click_link 'Quote'
|
||||
end
|
||||
|
||||
|
||||
@@ -23,18 +23,18 @@ class QuoteReplyHelperTest < ActionView::TestCase
|
||||
include ERB::Util
|
||||
include Redmine::QuoteReply::Helper
|
||||
|
||||
def test_quote_reply
|
||||
def test_quote_reply_button
|
||||
with_locale 'en' do
|
||||
url = quoted_issue_path(issues(:issues_001))
|
||||
|
||||
a_tag = quote_reply(url, '#issue_description_wiki')
|
||||
assert_includes a_tag, %|onclick="#{h "quoteReply('/issues/1/quoted', '#issue_description_wiki', 'common_mark'); return false;"}"|
|
||||
assert_includes a_tag, %|class="icon icon-quote"|
|
||||
assert_not_includes a_tag, 'title='
|
||||
html = quote_reply_button(url: url)
|
||||
assert_select_in html,
|
||||
'a[data-quote-reply-url-param=?][data-quote-reply-text-formatting-param=?]:not([title])',
|
||||
url, Setting.text_formatting
|
||||
|
||||
# When icon_only is true
|
||||
a_tag = quote_reply(url, '#issue_description_wiki', icon_only: true)
|
||||
assert_includes a_tag, %|title="Quote"|
|
||||
html = quote_reply_button(url: url, icon_only: true)
|
||||
assert_select_in html, 'a.icon-only.icon-quote[title=?]', 'Quote'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
110
vendor/javascript/turndown.js
vendored
Normal file
110
vendor/javascript/turndown.js
vendored
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user