mirror of
https://github.com/discourse/discourse.git
synced 2024-11-25 09:42:07 +08:00
FEATURE: Embed topics list on remote sites via Javascript API. (#8008)
This adds support for a `<d-topics-list>` tag you can embed in your site that will be rendered as a list of discourse topics. Any attributes on the tag will be passed as filters. For example: `<d-topics-list discourse-url="URL" category="1234">` will filter to category 1234. To use this feature, enable the `embed topics list` site setting. Then on the site you want to embed, include the following javascript: `<script src="http://URL/javascripts/embed-topics.js"></script>` Where `URL` is your discourse forum's URL. Then include the `<d-topics-list discourse-url="URL">` tag in your HTML document and it will be replaced with the list of topics.
This commit is contained in:
parent
fafc6bcde0
commit
23367e79ea
|
@ -24,17 +24,20 @@
|
||||||
|
|
||||||
window.onload = function() {
|
window.onload = function() {
|
||||||
// get state info from data attribute
|
// get state info from data attribute
|
||||||
var header = document.querySelector("header");
|
var embedState = document.querySelector("[data-embed-state]");
|
||||||
var state = "unknown";
|
var state = "unknown";
|
||||||
if (header) {
|
var embedId = null;
|
||||||
state = header.getAttribute("data-embed-state");
|
if (embedState) {
|
||||||
|
state = embedState.getAttribute("data-embed-state");
|
||||||
|
embedId = embedState.getAttribute("data-embed-id");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send a post message with our loaded height and state
|
// Send a post message with our loaded height and state
|
||||||
postUp({
|
postUp({
|
||||||
type: "discourse-resize",
|
type: "discourse-resize",
|
||||||
height: document["body"].offsetHeight,
|
height: document["body"].offsetHeight,
|
||||||
state: state
|
state,
|
||||||
|
embedId
|
||||||
});
|
});
|
||||||
|
|
||||||
var postLinks = document.querySelectorAll("a[data-link-to-post]"),
|
var postLinks = document.querySelectorAll("a[data-link-to-post]"),
|
||||||
|
|
|
@ -175,3 +175,19 @@ aside.onebox {
|
||||||
div.lightbox-wrapper {
|
div.lightbox-wrapper {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.topics-list {
|
||||||
|
width: 100%;
|
||||||
|
.topic-list-item {
|
||||||
|
td {
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-link a {
|
||||||
|
color: $primary;
|
||||||
|
}
|
||||||
|
.main-link a:visited {
|
||||||
|
color: $primary-medium;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class EmbedController < ApplicationController
|
class EmbedController < ApplicationController
|
||||||
|
include TopicQueryParams
|
||||||
|
|
||||||
skip_before_action :check_xhr, :preload_json, :verify_authenticity_token
|
skip_before_action :check_xhr, :preload_json, :verify_authenticity_token
|
||||||
|
|
||||||
before_action :ensure_embeddable, except: [ :info ]
|
before_action :ensure_embeddable, except: [ :info, :topics ]
|
||||||
before_action :get_embeddable_css_class, except: [ :info ]
|
before_action :get_embeddable_css_class, except: [ :info, :topics ]
|
||||||
before_action :ensure_api_request, only: [ :info ]
|
before_action :ensure_api_request, only: [ :info ]
|
||||||
|
|
||||||
layout 'embed'
|
layout 'embed'
|
||||||
|
@ -16,7 +18,26 @@ class EmbedController < ApplicationController
|
||||||
@show_reason = true
|
@show_reason = true
|
||||||
@hosts = EmbeddableHost.all
|
@hosts = EmbeddableHost.all
|
||||||
end
|
end
|
||||||
render 'embed_error'
|
render 'embed_error', status: 400
|
||||||
|
end
|
||||||
|
|
||||||
|
def topics
|
||||||
|
discourse_expires_in 1.minute
|
||||||
|
|
||||||
|
response.headers['X-Frame-Options'] = "ALLOWALL"
|
||||||
|
unless SiteSetting.embed_topics_list?
|
||||||
|
render 'embed_topics_error', status: 400
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
if @embed_id = params[:discourse_embed_id]
|
||||||
|
raise Discourse::InvalidParameters.new(:embed_id) unless @embed_id =~ /^de\-[a-zA-Z0-9]+$/
|
||||||
|
end
|
||||||
|
|
||||||
|
list_options = build_topic_list_options
|
||||||
|
list_options[:per_page] = params[:per_page].to_i if params.has_key?(:per_page)
|
||||||
|
topic_query = TopicQuery.new(current_user, list_options)
|
||||||
|
@list = topic_query.list_latest
|
||||||
end
|
end
|
||||||
|
|
||||||
def comments
|
def comments
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
require_dependency 'topic_list_responder'
|
require_dependency 'topic_list_responder'
|
||||||
|
require_dependency 'topic_query_params'
|
||||||
|
|
||||||
class ListController < ApplicationController
|
class ListController < ApplicationController
|
||||||
include TopicListResponder
|
include TopicListResponder
|
||||||
|
include TopicQueryParams
|
||||||
|
|
||||||
skip_before_action :check_xhr
|
skip_before_action :check_xhr
|
||||||
|
|
||||||
|
@ -376,28 +378,6 @@ class ListController < ApplicationController
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def build_topic_list_options
|
|
||||||
options = {}
|
|
||||||
params[:tags] = [params[:tag_id].parameterize] if params[:tag_id].present? && guardian.can_tag_pms?
|
|
||||||
|
|
||||||
TopicQuery.public_valid_options.each do |key|
|
|
||||||
if params.key?(key)
|
|
||||||
val = options[key] = params[key]
|
|
||||||
if !TopicQuery.validate?(key, val)
|
|
||||||
raise Discourse::InvalidParameters.new key
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# hacky columns get special handling
|
|
||||||
options[:topic_ids] = param_to_integer_list(:topic_ids)
|
|
||||||
if options[:no_subcategories] == 'true'
|
|
||||||
options[:no_subcategories] = true
|
|
||||||
end
|
|
||||||
|
|
||||||
options
|
|
||||||
end
|
|
||||||
|
|
||||||
def list_target_user
|
def list_target_user
|
||||||
if params[:user_id] && guardian.is_staff?
|
if params[:user_id] && guardian.is_staff?
|
||||||
User.find(params[:user_id].to_i)
|
User.find(params[:user_id].to_i)
|
||||||
|
|
8
app/views/embed/embed_topics_error.html.erb
Normal file
8
app/views/embed/embed_topics_error.html.erb
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
<header class='discourse' data-embed-state='error'>
|
||||||
|
<h3><%= t 'embed.error' %></h3>
|
||||||
|
<%= link_to(image_tag(SiteSetting.site_logo_url, class: 'logo'), Discourse.base_url) %>
|
||||||
|
<div class='clearfix'></div>
|
||||||
|
</header>
|
||||||
|
<div class='embed-error'>
|
||||||
|
<%= t('embed.error_topics') %>
|
||||||
|
</div>
|
11
app/views/embed/topics.html.erb
Normal file
11
app/views/embed/topics.html.erb
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
<%- if @list && @list.topics.present? %>
|
||||||
|
<table class='topics-list' data-embed-state='loaded' <%- if @embed_id %>data-embed-id="<%= @embed_id %>"<%- end %>>
|
||||||
|
<%- @list.topics.each do |t| %>
|
||||||
|
<tr class='topic-list-item'>
|
||||||
|
<td class='main-link'>
|
||||||
|
<a target="_parent" href="<%= t.url %>" class="title raw-link raw-topic-link" data-topic-id="<%= t.id %>"><%= t.title %></a>
|
||||||
|
</td>
|
||||||
|
</div>
|
||||||
|
<%- end %>
|
||||||
|
</table>
|
||||||
|
<%- end %>
|
|
@ -268,6 +268,7 @@ en:
|
||||||
continue: "Continue Discussion"
|
continue: "Continue Discussion"
|
||||||
error: "Error Embedding"
|
error: "Error Embedding"
|
||||||
referer: "Referer:"
|
referer: "Referer:"
|
||||||
|
error_topics: "The `embed topics list` site setting was not enabled"
|
||||||
mismatch: "The referer was either not sent, or did not match any of the following hosts:"
|
mismatch: "The referer was either not sent, or did not match any of the following hosts:"
|
||||||
no_hosts: "No hosts were set up for embedding."
|
no_hosts: "No hosts were set up for embedding."
|
||||||
configure: "Configure Embedding"
|
configure: "Configure Embedding"
|
||||||
|
@ -1943,6 +1944,7 @@ en:
|
||||||
autohighlight_all_code: "Force apply code highlighting to all preformatted code blocks even when they didn't explicitly specify the language."
|
autohighlight_all_code: "Force apply code highlighting to all preformatted code blocks even when they didn't explicitly specify the language."
|
||||||
highlighted_languages: "Included syntax highlighting rules. (Warning: including too many languages may impact performance) see: <a href='https://highlightjs.org/static/demo/' target='_blank'>https://highlightjs.org/static/demo</a> for a demo"
|
highlighted_languages: "Included syntax highlighting rules. (Warning: including too many languages may impact performance) see: <a href='https://highlightjs.org/static/demo/' target='_blank'>https://highlightjs.org/static/demo</a> for a demo"
|
||||||
|
|
||||||
|
embed_topics_list: "Support HTML embedding of topics lists"
|
||||||
embed_truncate: "Truncate the embedded posts."
|
embed_truncate: "Truncate the embedded posts."
|
||||||
embed_support_markdown: "Support Markdown formatting for embedded posts."
|
embed_support_markdown: "Support Markdown formatting for embedded posts."
|
||||||
embed_whitelist_selector: "A comma separated list of CSS elements that are allowed in embeds."
|
embed_whitelist_selector: "A comma separated list of CSS elements that are allowed in embeds."
|
||||||
|
|
|
@ -711,6 +711,7 @@ Discourse::Application.routes.draw do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
get 'embed/topics' => 'embed#topics'
|
||||||
get 'embed/comments' => 'embed#comments'
|
get 'embed/comments' => 'embed#comments'
|
||||||
get 'embed/count' => 'embed#count'
|
get 'embed/count' => 'embed#count'
|
||||||
get 'embed/info' => 'embed#info'
|
get 'embed/info' => 'embed#info'
|
||||||
|
|
|
@ -849,6 +849,7 @@ posting:
|
||||||
choices:
|
choices:
|
||||||
- 4-spaces-indent
|
- 4-spaces-indent
|
||||||
- code-fences
|
- code-fences
|
||||||
|
embed_topics_list: false
|
||||||
embed_truncate:
|
embed_truncate:
|
||||||
default: true
|
default: true
|
||||||
embed_support_markdown:
|
embed_support_markdown:
|
||||||
|
|
25
lib/topic_query_params.rb
Normal file
25
lib/topic_query_params.rb
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module TopicQueryParams
|
||||||
|
def build_topic_list_options
|
||||||
|
options = {}
|
||||||
|
params[:tags] = [params[:tag_id].parameterize] if params[:tag_id].present? && guardian.can_tag_pms?
|
||||||
|
|
||||||
|
TopicQuery.public_valid_options.each do |key|
|
||||||
|
if params.key?(key)
|
||||||
|
val = options[key] = params[key]
|
||||||
|
if !TopicQuery.validate?(key, val)
|
||||||
|
raise Discourse::InvalidParameters.new key
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# hacky columns get special handling
|
||||||
|
options[:topic_ids] = param_to_integer_list(:topic_ids)
|
||||||
|
if options[:no_subcategories] == 'true'
|
||||||
|
options[:no_subcategories] = true
|
||||||
|
end
|
||||||
|
|
||||||
|
options
|
||||||
|
end
|
||||||
|
end
|
47
public/javascripts/embed-topics.js
Normal file
47
public/javascripts/embed-topics.js
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
(function() {
|
||||||
|
function postMessageReceived(e) {
|
||||||
|
if (!e) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.data && e.data.type === "discourse-resize" && e.data.embedId) {
|
||||||
|
var elem = document.getElementById(e.data.embedId);
|
||||||
|
if (elem) {
|
||||||
|
elem.height = e.data.height + "px";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window.addEventListener("message", postMessageReceived, false);
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", function(event) {
|
||||||
|
var lists = document.querySelectorAll("d-topics-list");
|
||||||
|
|
||||||
|
for (var i = 0; i < lists.length; i++) {
|
||||||
|
var list = lists[i];
|
||||||
|
var url = list.getAttribute("discourse-url");
|
||||||
|
if (!url || url.length === 0) {
|
||||||
|
console.error("Error, `data-discourse-url` was not found");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
var frameId =
|
||||||
|
"de-" +
|
||||||
|
Math.random()
|
||||||
|
.toString(36)
|
||||||
|
.substr(2, 9);
|
||||||
|
var params = ["discourse_embed_id=" + frameId];
|
||||||
|
list.removeAttribute("discourse-url");
|
||||||
|
|
||||||
|
for (var j = 0; j < list.attributes.length; j++) {
|
||||||
|
var attr = list.attributes[j];
|
||||||
|
params.push(attr.name.replace("-", "_") + "=" + attr.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
var iframe = document.createElement("iframe");
|
||||||
|
iframe.src = url + "/embed/topics?" + params.join("&");
|
||||||
|
iframe.id = frameId;
|
||||||
|
iframe.frameBorder = 0;
|
||||||
|
iframe.scrolling = "no";
|
||||||
|
list.appendChild(iframe);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})();
|
|
@ -70,6 +70,33 @@ describe EmbedController do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context "#topics" do
|
||||||
|
it "raises an error when not enabled" do
|
||||||
|
get '/embed/topics?embed_id=de-1234'
|
||||||
|
expect(response.status).to eq(400)
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when enabled" do
|
||||||
|
before do
|
||||||
|
SiteSetting.embed_topics_list = true
|
||||||
|
end
|
||||||
|
|
||||||
|
it "raises an error with a weird id" do
|
||||||
|
get '/embed/topics?discourse_embed_id=../asdf/-1234', headers: headers
|
||||||
|
expect(response.status).to eq(400)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns a list of topics" do
|
||||||
|
topic = Fabricate(:topic)
|
||||||
|
get '/embed/topics?discourse_embed_id=de-1234', headers: headers
|
||||||
|
expect(response.status).to eq(200)
|
||||||
|
expect(response.headers['X-Frame-Options']).to eq("ALLOWALL")
|
||||||
|
expect(response.body).to match("data-embed-id=\"de-1234\"")
|
||||||
|
expect(response.body).to match("data-topic-id=\"#{topic.id}\"")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context "with a host" do
|
context "with a host" do
|
||||||
let!(:embeddable_host) { Fabricate(:embeddable_host) }
|
let!(:embeddable_host) { Fabricate(:embeddable_host) }
|
||||||
let(:headers) { { 'REFERER' => embed_url } }
|
let(:headers) { { 'REFERER' => embed_url } }
|
||||||
|
|
Loading…
Reference in New Issue
Block a user