mirror of
https://github.com/codeninjasllc/discourse.git
synced 2024-11-27 17:46:05 -05:00
basic lightbox support
This commit is contained in:
parent
14c0b96d55
commit
d9531d94d5
15 changed files with 191 additions and 44 deletions
BIN
app/assets/images/border1.png
Normal file
BIN
app/assets/images/border1.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1 KiB |
BIN
app/assets/images/border2.png
Normal file
BIN
app/assets/images/border2.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 170 B |
BIN
app/assets/images/loading.gif
Normal file
BIN
app/assets/images/loading.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.2 KiB |
|
@ -134,6 +134,7 @@ window.Discourse = Ember.Application.createWithMixins
|
||||||
return if href is '#'
|
return if href is '#'
|
||||||
return if $currentTarget.attr('target')
|
return if $currentTarget.attr('target')
|
||||||
return if $currentTarget.data('auto-route')
|
return if $currentTarget.data('auto-route')
|
||||||
|
return if $currentTarget.hasClass('lightbox')
|
||||||
return if href.indexOf("mailto:") is 0
|
return if href.indexOf("mailto:") is 0
|
||||||
|
|
||||||
if href.match(/^http[s]?:\/\//i) && !href.match new RegExp("^http:\\/\\/" + window.location.hostname,"i")
|
if href.match(/^http[s]?:\/\//i) && !href.match new RegExp("^http:\\/\\/" + window.location.hostname,"i")
|
||||||
|
|
|
@ -6,6 +6,8 @@ window.Discourse.ClickTrack =
|
||||||
|
|
||||||
$a = $(e.currentTarget)
|
$a = $(e.currentTarget)
|
||||||
|
|
||||||
|
return if $a.hasClass('lightbox')
|
||||||
|
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
|
||||||
# We don't track clicks on quote back buttons
|
# We don't track clicks on quote back buttons
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
# Helper object for light boxes. Uses highlight.js which is loaded
|
||||||
|
# on demand.
|
||||||
|
window.Discourse.Lightbox =
|
||||||
|
|
||||||
|
apply: ($elem) ->
|
||||||
|
$('a.lightbox', $elem).each (i, e) =>
|
||||||
|
$LAB.script("/javascripts/jquery.colorbox-min.js").wait ->
|
||||||
|
$(e).colorbox()
|
|
@ -212,6 +212,7 @@ window.Discourse.PostView = Ember.View.extend
|
||||||
|
|
||||||
# Add syntax highlighting
|
# Add syntax highlighting
|
||||||
Discourse.SyntaxHighlighting.apply($post)
|
Discourse.SyntaxHighlighting.apply($post)
|
||||||
|
Discourse.Lightbox.apply($post)
|
||||||
|
|
||||||
# If we're scrolling upwards, adjust the scroll position accordingly
|
# If we're scrolling upwards, adjust the scroll position accordingly
|
||||||
if scrollTo = @get('post.scrollTo')
|
if scrollTo = @get('post.scrollTo')
|
||||||
|
|
50
app/assets/stylesheets/application/lightbox.scss
Normal file
50
app/assets/stylesheets/application/lightbox.scss
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
/*
|
||||||
|
ColorBox Core Style:
|
||||||
|
The following CSS is consistent between example themes and should not be altered.
|
||||||
|
*/
|
||||||
|
#colorbox, #cboxOverlay, #cboxWrapper{position:absolute; top:0; left:0; z-index:9999; overflow:hidden;}
|
||||||
|
#cboxOverlay{position:fixed; width:100%; height:100%;}
|
||||||
|
#cboxMiddleLeft, #cboxBottomLeft{clear:left;}
|
||||||
|
#cboxContent{position:relative;}
|
||||||
|
#cboxLoadedContent{overflow:auto; -webkit-overflow-scrolling: touch;}
|
||||||
|
#cboxTitle{margin:0;}
|
||||||
|
#cboxLoadingOverlay, #cboxLoadingGraphic{position:absolute; top:0; left:0; width:100%; height:100%;}
|
||||||
|
#cboxPrevious, #cboxNext, #cboxClose, #cboxSlideshow{cursor:pointer;}
|
||||||
|
.cboxPhoto{float:left; margin:auto; border:0; display:block; max-width:none;}
|
||||||
|
.cboxIframe{width:100%; height:100%; display:block; border:0;}
|
||||||
|
#colorbox, #cboxContent, #cboxLoadedContent{box-sizing:content-box; -moz-box-sizing:content-box; -webkit-box-sizing:content-box;}
|
||||||
|
|
||||||
|
/*
|
||||||
|
User Style:
|
||||||
|
Change the following styles to modify the appearance of ColorBox. They are
|
||||||
|
ordered & tabbed in a way that represents the nesting of the generated HTML.
|
||||||
|
*/
|
||||||
|
#cboxOverlay{background:#fff;}
|
||||||
|
#colorbox{outline:0;}
|
||||||
|
#cboxTopLeft{width:25px; height:25px; background:image-url("images/border1.png") no-repeat 0 0;}
|
||||||
|
#cboxTopCenter{height:25px; background:image-url("images/border1.png") repeat-x 0 -50px;}
|
||||||
|
#cboxTopRight{width:25px; height:25px; background:image-url("images/border1.png") no-repeat -25px 0;}
|
||||||
|
#cboxBottomLeft{width:25px; height:25px; background:image-url("images/border1.png") no-repeat 0 -25px;}
|
||||||
|
#cboxBottomCenter{height:25px; background:image-url("images/border1.png") repeat-x 0 -75px;}
|
||||||
|
#cboxBottomRight{width:25px; height:25px; background:image-url("images/border1.png") no-repeat -25px -25px;}
|
||||||
|
#cboxMiddleLeft{width:25px; background:image-url("images/border2.png") repeat-y 0 0;}
|
||||||
|
#cboxMiddleRight{width:25px; background:image-url("images/border2.png") repeat-y -25px 0;}
|
||||||
|
#cboxContent{background:#fff; overflow:hidden;}
|
||||||
|
.cboxIframe{background:#fff;}
|
||||||
|
#cboxError{padding:50px; border:1px solid #ccc;}
|
||||||
|
#cboxLoadedContent{margin-bottom:20px;}
|
||||||
|
#cboxTitle{position:absolute; bottom:0px; left:0; text-align:center; width:100%; color:#999;}
|
||||||
|
#cboxCurrent{position:absolute; bottom:0px; left:100px; color:#999;}
|
||||||
|
#cboxLoadingOverlay{background:#fff image-url("images/loading.gif") no-repeat 5px 5px;}
|
||||||
|
|
||||||
|
/* these elements are buttons, and may need to have additional styles reset to avoid unwanted base styles */
|
||||||
|
#cboxPrevious, #cboxNext, #cboxSlideshow, #cboxClose {border:0; padding:0; margin:0; overflow:visible; width:auto; background:none; }
|
||||||
|
|
||||||
|
/* avoid outlines on :active (mouseclick), but preserve outlines on :focus (tabbed navigating) */
|
||||||
|
#cboxPrevious:active, #cboxNext:active, #cboxSlideshow:active, #cboxClose:active {outline:0;}
|
||||||
|
|
||||||
|
#cboxSlideshow{position:absolute; bottom:0px; right:42px; color:#444;}
|
||||||
|
#cboxPrevious{position:absolute; bottom:0px; left:0; color:#444;}
|
||||||
|
#cboxNext{position:absolute; bottom:0px; left:63px; color:#444;}
|
||||||
|
#cboxClose{position:absolute; bottom:0; right:0; display:block; color:#444;}
|
||||||
|
|
|
@ -14,12 +14,10 @@ class Post < ActiveRecord::Base
|
||||||
FLAG_THRESHOLD_REACHED_AGAIN = 2
|
FLAG_THRESHOLD_REACHED_AGAIN = 2
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
versioned
|
versioned
|
||||||
|
|
||||||
rate_limit
|
rate_limit
|
||||||
|
|
||||||
acts_as_paranoid
|
acts_as_paranoid
|
||||||
|
|
||||||
after_recover :update_flagged_posts_count
|
after_recover :update_flagged_posts_count
|
||||||
after_destroy :update_flagged_posts_count
|
after_destroy :update_flagged_posts_count
|
||||||
|
|
||||||
|
|
|
@ -107,7 +107,7 @@ footer:after{ content: '#{error}' }"
|
||||||
|
|
||||||
@lock.synchronize do
|
@lock.synchronize do
|
||||||
style = self.where(key: key).first
|
style = self.where(key: key).first
|
||||||
style.ensure_stylesheet_on_disk!
|
style.ensure_stylesheet_on_disk! if style
|
||||||
@cache[key] = style
|
@cache[key] = style
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -81,7 +81,7 @@ class SiteSetting < ActiveRecord::Base
|
||||||
setting(:max_flags_per_day, 20)
|
setting(:max_flags_per_day, 20)
|
||||||
setting(:max_edits_per_day, 30)
|
setting(:max_edits_per_day, 30)
|
||||||
setting(:max_favorites_per_day, 20)
|
setting(:max_favorites_per_day, 20)
|
||||||
|
setting(:auto_link_images_wider_than, 50)
|
||||||
|
|
||||||
setting(:email_time_window_mins, 5)
|
setting(:email_time_window_mins, 5)
|
||||||
|
|
||||||
|
|
|
@ -338,6 +338,8 @@ en:
|
||||||
basic_requires_read_posts: "How many posts a user needs to have read to be promoted to basic level"
|
basic_requires_read_posts: "How many posts a user needs to have read to be promoted to basic level"
|
||||||
basic_requires_time_spent_mins: "How many mins a user needs to have spent reading posts to be promoted to basic level"
|
basic_requires_time_spent_mins: "How many mins a user needs to have spent reading posts to be promoted to basic level"
|
||||||
|
|
||||||
|
auto_link_images_wider_than: "How wide does an image need to be, to be considered for auto link and lightbox treatment"
|
||||||
|
|
||||||
email_time_window_mins: "How many minutes we wait before sending a user mail, to give them a chance to see it first."
|
email_time_window_mins: "How many minutes we wait before sending a user mail, to give them a chance to see it first."
|
||||||
flush_timings_secs: "How frequently we flush timing data to the server, in seconds."
|
flush_timings_secs: "How frequently we flush timing data to the server, in seconds."
|
||||||
|
|
||||||
|
|
|
@ -5,11 +5,13 @@ require_dependency 'oneboxer'
|
||||||
|
|
||||||
class CookedPostProcessor
|
class CookedPostProcessor
|
||||||
|
|
||||||
|
|
||||||
def initialize(post, opts={})
|
def initialize(post, opts={})
|
||||||
@dirty = false
|
@dirty = false
|
||||||
@opts = opts
|
@opts = opts
|
||||||
@post = post
|
@post = post
|
||||||
@doc = Nokogiri::HTML(post.cooked)
|
@doc = Nokogiri::HTML(post.cooked)
|
||||||
|
@size_cache = {}
|
||||||
end
|
end
|
||||||
|
|
||||||
def dirty?
|
def dirty?
|
||||||
|
@ -33,7 +35,7 @@ class CookedPostProcessor
|
||||||
# First let's consider the images
|
# First let's consider the images
|
||||||
def post_process_images
|
def post_process_images
|
||||||
images = @doc.search("img")
|
images = @doc.search("img")
|
||||||
if images.present?
|
return unless images.present?
|
||||||
|
|
||||||
# Extract the first image from the first post and use it as the 'topic image'
|
# Extract the first image from the first post and use it as the 'topic image'
|
||||||
if @post.post_number == 1
|
if @post.post_number == 1
|
||||||
|
@ -42,33 +44,73 @@ class CookedPostProcessor
|
||||||
end
|
end
|
||||||
|
|
||||||
images.each do |img|
|
images.each do |img|
|
||||||
if img['src'].present?
|
src = img['src']
|
||||||
|
src = Discourse.base_url + src if src[0] == "/"
|
||||||
|
|
||||||
# If we provided some image sizes, look those up first
|
if src.present? && (img['width'].blank? || img['height'].blank?)
|
||||||
if @opts[:image_sizes].present?
|
|
||||||
if dim = @opts[:image_sizes][img['src']]
|
w,h =
|
||||||
w, h = ImageSizer.resize(dim['width'], dim['height'])
|
get_size_from_image_sizes(src, @opts[:image_sizes]) ||
|
||||||
img.set_attribute 'width', w.to_s
|
image_dimensions(src)
|
||||||
img.set_attribute 'height', h.to_s
|
|
||||||
|
if w && h
|
||||||
|
img['width'] = w.to_s
|
||||||
|
img['height'] = h.to_s
|
||||||
@dirty = true
|
@dirty = true
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# If the image has no width or height, figure them out.
|
if src.present?
|
||||||
if img['width'].blank? or img['height'].blank?
|
if src != img['src']
|
||||||
dim = CookedPostProcessor.image_dimensions(img['src'])
|
img['src'] = src
|
||||||
if dim.present?
|
|
||||||
img.set_attribute 'width', dim[0].to_s
|
|
||||||
img.set_attribute 'height', dim[1].to_s
|
|
||||||
@dirty = true
|
@dirty = true
|
||||||
end
|
end
|
||||||
|
convert_to_link!(img)
|
||||||
|
img.set_attribute('src', optimize_image(src))
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
def optimize_image(src)
|
||||||
|
src
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def convert_to_link!(img)
|
||||||
|
src = img["src"]
|
||||||
|
width = img["width"].to_i
|
||||||
|
height = img["height"].to_i
|
||||||
|
|
||||||
|
return unless src.present? && width > SiteSetting.auto_link_images_wider_than
|
||||||
|
|
||||||
|
original_width, original_height = get_size(src)
|
||||||
|
|
||||||
|
return unless original_width.to_i > width && original_height.to_i > height
|
||||||
|
|
||||||
|
parent = img.parent
|
||||||
|
while parent
|
||||||
|
return if parent.name == "a"
|
||||||
|
break unless parent.respond_to? :parent
|
||||||
|
parent = parent.parent
|
||||||
|
end
|
||||||
|
|
||||||
|
# not a hyperlink so we can apply
|
||||||
|
a = Nokogiri::XML::Node.new "a", @doc
|
||||||
|
img.add_next_sibling(a)
|
||||||
|
a["href"] = src
|
||||||
|
a["class"] = "lightbox"
|
||||||
|
a.add_child(img)
|
||||||
|
@dirty = true
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_size_from_image_sizes(src, image_sizes)
|
||||||
|
if image_sizes.present?
|
||||||
|
if dim = image_sizes[src]
|
||||||
|
ImageSizer.resize(dim['width'], dim['height'])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def post_process
|
def post_process
|
||||||
return unless @doc.present?
|
return unless @doc.present?
|
||||||
|
@ -80,14 +122,18 @@ class CookedPostProcessor
|
||||||
@doc.try(:to_html)
|
@doc.try(:to_html)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Retrieve the image dimensions for a url
|
def get_size(url)
|
||||||
def self.image_dimensions(url)
|
return nil unless SiteSetting.crawl_images? || url.start_with?(Discourse.base_url)
|
||||||
return nil unless SiteSetting.crawl_images?
|
@size_cache[url] ||= FastImage.size(url)
|
||||||
uri = URI.parse(url)
|
end
|
||||||
return nil unless %w(http https).include?(uri.scheme)
|
|
||||||
w, h = FastImage.size(url)
|
|
||||||
|
|
||||||
ImageSizer.resize(w, h)
|
# Retrieve the image dimensions for a url
|
||||||
|
def image_dimensions(url)
|
||||||
|
uri = URI.parse(url)
|
||||||
|
|
||||||
|
return nil unless %w(http https).include?(uri.scheme)
|
||||||
|
w, h = get_size(url)
|
||||||
|
ImageSizer.resize(w, h) if w && h
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
6
public/javascripts/jquery.colorbox-min.js
vendored
Normal file
6
public/javascripts/jquery.colorbox-min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
|
@ -3,6 +3,11 @@ require 'spec_helper'
|
||||||
require 'cooked_post_processor'
|
require 'cooked_post_processor'
|
||||||
|
|
||||||
describe CookedPostProcessor do
|
describe CookedPostProcessor do
|
||||||
|
let :cpp do
|
||||||
|
post = Fabricate.build(:post_with_youtube)
|
||||||
|
post.id = 123
|
||||||
|
CookedPostProcessor.new(post)
|
||||||
|
end
|
||||||
|
|
||||||
context 'process_onebox' do
|
context 'process_onebox' do
|
||||||
|
|
||||||
|
@ -39,10 +44,11 @@ EXPECTED
|
||||||
@topic = Fabricate(:topic)
|
@topic = Fabricate(:topic)
|
||||||
@post = Fabricate.build(:post_with_image_url, topic: @topic, user: @topic.user)
|
@post = Fabricate.build(:post_with_image_url, topic: @topic, user: @topic.user)
|
||||||
@cpp = CookedPostProcessor.new(@post, :image_sizes => {'http://www.forumwarz.com/images/header/logo.png' => {'width' => 111, 'height' => 222}})
|
@cpp = CookedPostProcessor.new(@post, :image_sizes => {'http://www.forumwarz.com/images/header/logo.png' => {'width' => 111, 'height' => 222}})
|
||||||
|
@cpp.expects(:get_size).returns([111,222])
|
||||||
end
|
end
|
||||||
|
|
||||||
it "doesn't call image_dimensions because it knows the size" do
|
it "doesn't call image_dimensions because it knows the size" do
|
||||||
CookedPostProcessor.expects(:image_dimensions).never
|
@cpp.expects(:image_dimensions).never
|
||||||
@cpp.post_process_images
|
@cpp.post_process_images
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -55,7 +61,8 @@ EXPECTED
|
||||||
|
|
||||||
context 'with unsized images in the post' do
|
context 'with unsized images in the post' do
|
||||||
before do
|
before do
|
||||||
CookedPostProcessor.expects(:image_dimensions).returns([123, 456])
|
FastImage.stubs(:size).returns([123, 456])
|
||||||
|
CookedPostProcessor.any_instance.expects(:image_dimensions).returns([123, 456])
|
||||||
@post = Fabricate(:post_with_images)
|
@post = Fabricate(:post_with_images)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -72,10 +79,36 @@ EXPECTED
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'link convertor' do
|
||||||
|
before do
|
||||||
|
SiteSetting.stubs(:crawl_images?).returns(true)
|
||||||
|
end
|
||||||
|
|
||||||
|
let :post_with_img do
|
||||||
|
Fabricate.build(:post, cooked: '<p><img src="http://hello.com/image.png"></p>')
|
||||||
|
end
|
||||||
|
|
||||||
|
let :cpp_for_post do
|
||||||
|
CookedPostProcessor.new(post_with_img)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'convert img tags to links if they are sized down' do
|
||||||
|
cpp_for_post.expects(:get_size).returns([2000,2000]).twice
|
||||||
|
cpp_for_post.post_process
|
||||||
|
cpp_for_post.html.should =~ /a href/
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not convert img tags to links if they are small' do
|
||||||
|
cpp_for_post.expects(:get_size).returns([200,200]).twice
|
||||||
|
cpp_for_post.post_process
|
||||||
|
(cpp_for_post.html !~ /a href/).should be_true
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
context 'image_dimensions' do
|
context 'image_dimensions' do
|
||||||
it "returns unless called with a http or https url" do
|
it "returns unless called with a http or https url" do
|
||||||
CookedPostProcessor.image_dimensions('/tmp/image.jpg').should be_blank
|
cpp.image_dimensions('/tmp/image.jpg').should be_blank
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'with valid url' do
|
context 'with valid url' do
|
||||||
|
@ -86,13 +119,13 @@ EXPECTED
|
||||||
it "doesn't call fastimage if image crawling is disabled" do
|
it "doesn't call fastimage if image crawling is disabled" do
|
||||||
SiteSetting.expects(:crawl_images?).returns(false)
|
SiteSetting.expects(:crawl_images?).returns(false)
|
||||||
FastImage.expects(:size).never
|
FastImage.expects(:size).never
|
||||||
CookedPostProcessor.image_dimensions(@url)
|
cpp.image_dimensions(@url)
|
||||||
end
|
end
|
||||||
|
|
||||||
it "calls fastimage if image crawling is enabled" do
|
it "calls fastimage if image crawling is enabled" do
|
||||||
SiteSetting.expects(:crawl_images?).returns(true)
|
SiteSetting.expects(:crawl_images?).returns(true)
|
||||||
FastImage.expects(:size).with(@url)
|
FastImage.expects(:size).with(@url)
|
||||||
CookedPostProcessor.image_dimensions(@url)
|
cpp.image_dimensions(@url)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in a new issue