From d9531d94d56e7ea18c4a89fc2a85fea962dc901b Mon Sep 17 00:00:00 2001 From: Sam Saffron Date: Tue, 19 Feb 2013 17:57:14 +1100 Subject: [PATCH] basic lightbox support --- app/assets/images/border1.png | Bin 0 -> 1057 bytes app/assets/images/border2.png | Bin 0 -> 170 bytes app/assets/images/loading.gif | Bin 0 -> 9427 bytes app/assets/javascripts/discourse.js.coffee | 1 + .../components/click_track.js.coffee | 2 + .../discourse/components/lightbox.js.coffee | 8 ++ .../discourse/views/post_view.js.coffee | 1 + .../stylesheets/application/lightbox.scss | 50 ++++++++ app/models/post.rb | 4 +- app/models/site_customization.rb | 2 +- app/models/site_setting.rb | 2 +- config/locales/server.en.yml | 2 + lib/cooked_post_processor.rb | 114 ++++++++++++------ public/javascripts/jquery.colorbox-min.js | 6 + spec/components/cooked_post_processor_spec.rb | 43 ++++++- 15 files changed, 191 insertions(+), 44 deletions(-) create mode 100644 app/assets/images/border1.png create mode 100644 app/assets/images/border2.png create mode 100644 app/assets/images/loading.gif create mode 100644 app/assets/javascripts/discourse/components/lightbox.js.coffee create mode 100644 app/assets/stylesheets/application/lightbox.scss create mode 100644 public/javascripts/jquery.colorbox-min.js diff --git a/app/assets/images/border1.png b/app/assets/images/border1.png new file mode 100644 index 0000000000000000000000000000000000000000..0ddc704051b651f43cffb1326dee3ba563727acc GIT binary patch literal 1057 zcmV++1m63JP)r zAF%JpeZBR%PujJ&?cU$mzF@zQ4A^OFY-v$p7aBuKlp}eWDguO+1d;SiQl=<_wN?Ru z$}F)CZO*|QgDyN_U;rRDpw2LP$OLZD@3s^O2w|jPHUa@DKspfu0^h+*kZIDEfdC1N zo&f>EwiE)lfoMlz^h}7DN&?yjR1p>l!e}9cKf%uX+@UK4M1f#6=sL9_;DREMkOewV zfoO+dLI7s`|J0@eorTef?let$#kA*uOgea0n8ZlkOee&KKj{^r91I77A-eH8ps|f4 zR&?ZffL&>V(CCVRrvEA8dEmR#;6Zc*yd|vTX9rg5T7axEP5v+4Zntml@9#g&NiFW~ z?mj*|Jp4E|`@kX*eYOBf8o9vM2x2{|-tBguH=E5{t@UOONZ2e{4& z;l%aj`Xu76lRR)cL2`%#edS`pxl`^D?M4w&McV@gar&!0#ef2FI8(b z#DWlmm{Y_y#PX{mPj?a|R4~TXasN0V$FYKyxRUf(A#@Ibt8K`1v19a7GHVLjpv*rykJg@YO(c90K=lUwwq8p*)1_eipDm!ER2ms;1lNLjj`TTFBVX&~i5Rt@CB zKy)o2>smmrY5{rGKrRgAbrreRZnyhd1&FOAkAKnU>`_;KpIE`X&?=xfi#c? z(m)zW11T>$=X;6&QrscWyDBVl)L*rL{B9r@2J%=7$e|XHz7~+7fn1QRR3&ddR0UFn b{1IRPnb~f(1W%SC00000NkvXXu0mjf?Cjqi literal 0 HcmV?d00001 diff --git a/app/assets/images/border2.png b/app/assets/images/border2.png new file mode 100644 index 0000000000000000000000000000000000000000..aa62a0b724371d1f0a8e183c5f3707d2f7aecd63 GIT binary patch literal 170 zcmeAS@N?(olHy`uVBq!ia0vp^MnKHS!3HGfT>QHUNJ*BsMwA5SrjnPU>t&Bgig1lwFnk>0^Moe(#2j_SDR=qm2cFWSad{!^LK345wm0*~eFZqAj Sx%>#AxeT7JelF{r5}E+(L_S*p literal 0 HcmV?d00001 diff --git a/app/assets/images/loading.gif b/app/assets/images/loading.gif new file mode 100644 index 0000000000000000000000000000000000000000..602ce3c3ae9182650eca594ff76e4c27498f39e0 GIT binary patch literal 9427 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 + $('a.lightbox', $elem).each (i, e) => + $LAB.script("/javascripts/jquery.colorbox-min.js").wait -> + $(e).colorbox() diff --git a/app/assets/javascripts/discourse/views/post_view.js.coffee b/app/assets/javascripts/discourse/views/post_view.js.coffee index af086403084..8a5c787b845 100644 --- a/app/assets/javascripts/discourse/views/post_view.js.coffee +++ b/app/assets/javascripts/discourse/views/post_view.js.coffee @@ -212,6 +212,7 @@ window.Discourse.PostView = Ember.View.extend # Add syntax highlighting Discourse.SyntaxHighlighting.apply($post) + Discourse.Lightbox.apply($post) # If we're scrolling upwards, adjust the scroll position accordingly if scrollTo = @get('post.scrollTo') diff --git a/app/assets/stylesheets/application/lightbox.scss b/app/assets/stylesheets/application/lightbox.scss new file mode 100644 index 00000000000..582f63efdfa --- /dev/null +++ b/app/assets/stylesheets/application/lightbox.scss @@ -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;} + diff --git a/app/models/post.rb b/app/models/post.rb index 520d2ce743d..80ff1fc9f4c 100644 --- a/app/models/post.rb +++ b/app/models/post.rb @@ -14,12 +14,10 @@ class Post < ActiveRecord::Base FLAG_THRESHOLD_REACHED_AGAIN = 2 end - versioned - rate_limit - acts_as_paranoid + after_recover :update_flagged_posts_count after_destroy :update_flagged_posts_count diff --git a/app/models/site_customization.rb b/app/models/site_customization.rb index fe338c8ff00..492f2782775 100644 --- a/app/models/site_customization.rb +++ b/app/models/site_customization.rb @@ -107,7 +107,7 @@ footer:after{ content: '#{error}' }" @lock.synchronize do style = self.where(key: key).first - style.ensure_stylesheet_on_disk! + style.ensure_stylesheet_on_disk! if style @cache[key] = style end end diff --git a/app/models/site_setting.rb b/app/models/site_setting.rb index 87d2aaae759..4146d7cd49b 100644 --- a/app/models/site_setting.rb +++ b/app/models/site_setting.rb @@ -81,7 +81,7 @@ class SiteSetting < ActiveRecord::Base setting(:max_flags_per_day, 20) setting(:max_edits_per_day, 30) setting(:max_favorites_per_day, 20) - + setting(:auto_link_images_wider_than, 50) setting(:email_time_window_mins, 5) diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 64e39f82cb7..880a7580170 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -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_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." flush_timings_secs: "How frequently we flush timing data to the server, in seconds." diff --git a/lib/cooked_post_processor.rb b/lib/cooked_post_processor.rb index 25bba717601..99d4624988b 100644 --- a/lib/cooked_post_processor.rb +++ b/lib/cooked_post_processor.rb @@ -5,11 +5,13 @@ require_dependency 'oneboxer' class CookedPostProcessor + def initialize(post, opts={}) @dirty = false @opts = opts @post = post @doc = Nokogiri::HTML(post.cooked) + @size_cache = {} end def dirty? @@ -33,42 +35,82 @@ class CookedPostProcessor # First let's consider the images def post_process_images 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' - if @post.post_number == 1 - img = images.first - @post.topic.update_column :image_url, img['src'] if img['src'].present? - end + # Extract the first image from the first post and use it as the 'topic image' + if @post.post_number == 1 + img = images.first + @post.topic.update_column :image_url, img['src'] if img['src'].present? + end - images.each do |img| - if img['src'].present? + images.each do |img| + src = img['src'] + src = Discourse.base_url + src if src[0] == "/" - # If we provided some image sizes, look those up first - if @opts[:image_sizes].present? - if dim = @opts[:image_sizes][img['src']] - w, h = ImageSizer.resize(dim['width'], dim['height']) - img.set_attribute 'width', w.to_s - img.set_attribute 'height', h.to_s - @dirty = true - end - end + if src.present? && (img['width'].blank? || img['height'].blank?) - # If the image has no width or height, figure them out. - if img['width'].blank? or img['height'].blank? - dim = CookedPostProcessor.image_dimensions(img['src']) - if dim.present? - img.set_attribute 'width', dim[0].to_s - img.set_attribute 'height', dim[1].to_s - @dirty = true - end - end + w,h = + get_size_from_image_sizes(src, @opts[:image_sizes]) || + image_dimensions(src) + if w && h + img['width'] = w.to_s + img['height'] = h.to_s + @dirty = true end end - end + + if src.present? + if src != img['src'] + img['src'] = src + @dirty = true + end + convert_to_link!(img) + img.set_attribute('src', optimize_image(src)) + end + + end end + def optimize_image(src) + src + 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 return unless @doc.present? @@ -80,14 +122,18 @@ class CookedPostProcessor @doc.try(:to_html) end - # Retrieve the image dimensions for a url - def self.image_dimensions(url) - return nil unless SiteSetting.crawl_images? - uri = URI.parse(url) - return nil unless %w(http https).include?(uri.scheme) - w, h = FastImage.size(url) + def get_size(url) + return nil unless SiteSetting.crawl_images? || url.start_with?(Discourse.base_url) + @size_cache[url] ||= FastImage.size(url) + end - 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 diff --git a/public/javascripts/jquery.colorbox-min.js b/public/javascripts/jquery.colorbox-min.js new file mode 100644 index 00000000000..0a3f248a0c8 --- /dev/null +++ b/public/javascripts/jquery.colorbox-min.js @@ -0,0 +1,6 @@ +/*! + jQuery ColorBox v1.4.3 - 2013-02-18 + (c) 2013 Jack Moore - jacklmoore.com/colorbox + license: http://www.opensource.org/licenses/mit-license.php +*/ +(function(e,t,i){function o(i,o,n){var r=t.createElement(i);return o&&(r.id=Y+o),n&&(r.style.cssText=n),e(r)}function n(e){var t=T.length,i=(A+e)%t;return 0>i?t+i:i}function r(e,t){return Math.round((/%/.test(e)?("x"===t?k.width():k.height())/100:1)*parseInt(e,10))}function h(e,t){return e.photo||e.photoRegex.test(t)}function l(e,t){return e.retinaUrl&&i.devicePixelRatio>1?t.replace(e.photoRegex,e.retinaSuffix):t}function s(e){"contains"in w[0]&&!w[0].contains(e.target)&&(e.stopPropagation(),w.focus())}function a(){var t,i=e.data(N,V);null==i?(K=e.extend({},J),console&&console.log&&console.log("Error: cboxElement missing settings object")):K=e.extend({},i);for(t in K)e.isFunction(K[t])&&"on"!==t.slice(0,2)&&(K[t]=K[t].call(N));K.rel=K.rel||N.rel||e(N).data("rel")||"nofollow",K.href=K.href||e(N).attr("href"),K.title=K.title||N.title,"string"==typeof K.href&&(K.href=e.trim(K.href))}function d(i,o){e(t).trigger(i),at.trigger(i),e.isFunction(o)&&o.call(N)}function c(){var e,t,i,o,n,r=Y+"Slideshow_",h="click."+Y;K.slideshow&&T[1]?(t=function(){clearTimeout(e)},i=function(){(K.loop||T[A+1])&&(e=setTimeout(G.next,K.slideshowSpeed))},o=function(){M.html(K.slideshowStop).unbind(h).one(h,n),at.bind(it,i).bind(tt,t).bind(ot,n),w.removeClass(r+"off").addClass(r+"on")},n=function(){t(),at.unbind(it,i).unbind(tt,t).unbind(ot,n),M.html(K.slideshowStart).unbind(h).one(h,function(){G.next(),o()}),w.removeClass(r+"on").addClass(r+"off")},K.slideshowAuto?o():n()):w.removeClass(r+"off "+r+"on")}function u(i){U||(N=i,a(),T=e(N),A=0,"nofollow"!==K.rel&&(T=e("."+Z).filter(function(){var t,i=e.data(this,V);return i&&(t=e(this).data("rel")||i.rel||this.rel),t===K.rel}),A=T.index(N),-1===A&&(T=T.add(N),A=T.length-1)),m.css({opacity:parseFloat(K.opacity),cursor:K.overlayClose?"pointer":"auto",visibility:"visible"}).show(),j||(j=q=!0,w.css({visibility:"hidden",display:"block"}),E=o(dt,"LoadedContent","width:0; height:0; overflow:hidden").appendTo(v),_=x.height()+C.height()+v.outerHeight(!0)-v.height(),z=y.width()+b.width()+v.outerWidth(!0)-v.width(),D=E.outerHeight(!0),B=E.outerWidth(!0),K.w=r(K.initialWidth,"x"),K.h=r(K.initialHeight,"y"),G.position(),lt&&k.bind("resize."+st+" scroll."+st,function(){m.css({width:k.width(),height:k.height(),top:k.scrollTop(),left:k.scrollLeft()})}).trigger("resize."+st),c(),d(et,K.onOpen),P.add(W).hide(),R.html(K.close).show(),w.focus(),t.addEventListener&&(t.addEventListener("focus",s,!0),at.one(nt,function(){t.removeEventListener("focus",s,!0)})),K.returnFocus&&at.one(nt,function(){e(N).focus()})),G.load(!0))}function f(){!w&&t.body&&(X=!1,k=e(i),w=o(dt).attr({id:V,"class":ht?Y+(lt?"IE6":"IE"):"",role:"dialog",tabindex:"-1"}).hide(),m=o(dt,"Overlay",lt?"position:absolute":"").hide(),L=o(dt,"LoadingOverlay").add(o(dt,"LoadingGraphic")),g=o(dt,"Wrapper"),v=o(dt,"Content").append(W=o(dt,"Title"),H=o(dt,"Current"),F=o("button","Previous"),S=o("button","Next"),M=o("button","Slideshow"),L,R=o("button","Close")),g.append(o(dt).append(o(dt,"TopLeft"),x=o(dt,"TopCenter"),o(dt,"TopRight")),o(dt,!1,"clear:left").append(y=o(dt,"MiddleLeft"),v,b=o(dt,"MiddleRight")),o(dt,!1,"clear:left").append(o(dt,"BottomLeft"),C=o(dt,"BottomCenter"),o(dt,"BottomRight"))).find("div div").css({"float":"left"}),I=o(dt,!1,"position:absolute; width:9999px; visibility:hidden; display:none"),P=S.add(F).add(H).add(M),e(t.body).append(m,w.append(g,I)))}function p(){function i(e){e.which>1||e.shiftKey||e.altKey||e.metaKey||(e.preventDefault(),u(this))}return w?(X||(X=!0,S.click(function(){G.next()}),F.click(function(){G.prev()}),R.click(function(){G.close()}),m.click(function(){K.overlayClose&&G.close()}),e(t).bind("keydown."+Y,function(e){var t=e.keyCode;j&&K.escKey&&27===t&&(e.preventDefault(),G.close()),j&&K.arrowKey&&T[1]&&!e.altKey&&(37===t?(e.preventDefault(),F.click()):39===t&&(e.preventDefault(),S.click()))}),e.isFunction(e.fn.on)?e(t).on("click."+Y,"."+Z,i):e("."+Z).live("click."+Y,i)),!0):!1}var m,w,g,v,x,y,b,C,T,k,E,I,L,W,H,M,S,F,R,P,K,_,z,D,B,N,A,O,j,q,U,$,G,Q,X,J={transition:"elastic",speed:300,width:!1,initialWidth:"600",innerWidth:!1,maxWidth:!1,height:!1,initialHeight:"450",innerHeight:!1,maxHeight:!1,scalePhotos:!0,scrolling:!0,inline:!1,html:!1,iframe:!1,fastIframe:!0,photo:!1,href:!1,title:!1,rel:!1,opacity:.9,preloading:!0,className:!1,retinaImage:!1,retinaUrl:!1,retinaSuffix:"@2x.$1",current:"image {current} of {total}",previous:"previous",next:"next",close:"close",xhrError:"This content failed to load.",imgError:"This image failed to load.",open:!1,returnFocus:!0,reposition:!0,loop:!0,slideshow:!1,slideshowAuto:!0,slideshowSpeed:2500,slideshowStart:"start slideshow",slideshowStop:"stop slideshow",photoRegex:/\.(gif|png|jp(e|g|eg)|bmp|ico)((#|\?).*)?$/i,onOpen:!1,onLoad:!1,onComplete:!1,onCleanup:!1,onClosed:!1,overlayClose:!0,escKey:!0,arrowKey:!0,top:!1,bottom:!1,left:!1,right:!1,fixed:!1,data:void 0},V="colorbox",Y="cbox",Z=Y+"Element",et=Y+"_open",tt=Y+"_load",it=Y+"_complete",ot=Y+"_cleanup",nt=Y+"_closed",rt=Y+"_purge",ht=!e.support.leadingWhitespace,lt=ht&&!i.XMLHttpRequest,st=Y+"_IE6",at=e({}),dt="div";e.colorbox||(e(f),G=e.fn[V]=e[V]=function(t,i){var o=this;if(t=t||{},f(),p()){if(e.isFunction(o))o=e(""),t.open=!0;else if(!o[0])return o;i&&(t.onComplete=i),o.each(function(){e.data(this,V,e.extend({},e.data(this,V)||J,t))}).addClass(Z),(e.isFunction(t.open)&&t.open.call(o)||t.open)&&u(o[0])}return o},G.position=function(e,t){function i(e){x[0].style.width=C[0].style.width=v[0].style.width=parseInt(e.style.width,10)-z+"px",v[0].style.height=y[0].style.height=b[0].style.height=parseInt(e.style.height,10)-_+"px"}var o,n,h,l=0,s=0,a=w.offset();k.unbind("resize."+Y),w.css({top:-9e4,left:-9e4}),n=k.scrollTop(),h=k.scrollLeft(),K.fixed&&!lt?(a.top-=n,a.left-=h,w.css({position:"fixed"})):(l=n,s=h,w.css({position:"absolute"})),s+=K.right!==!1?Math.max(k.width()-K.w-B-z-r(K.right,"x"),0):K.left!==!1?r(K.left,"x"):Math.round(Math.max(k.width()-K.w-B-z,0)/2),l+=K.bottom!==!1?Math.max(k.height()-K.h-D-_-r(K.bottom,"y"),0):K.top!==!1?r(K.top,"y"):Math.round(Math.max(k.height()-K.h-D-_,0)/2),w.css({top:a.top,left:a.left,visibility:"visible"}),e=w.width()===K.w+B&&w.height()===K.h+D?0:e||0,g[0].style.width=g[0].style.height="9999px",o={width:K.w+B+z,height:K.h+D+_,top:l,left:s},0===e&&w.css(o),w.dequeue().animate(o,{duration:e,complete:function(){i(this),q=!1,g[0].style.width=K.w+B+z+"px",g[0].style.height=K.h+D+_+"px",K.reposition&&setTimeout(function(){k.bind("resize."+Y,G.position)},1),t&&t()},step:function(){i(this)}})},G.resize=function(e){j&&(e=e||{},e.width&&(K.w=r(e.width,"x")-B-z),e.innerWidth&&(K.w=r(e.innerWidth,"x")),E.css({width:K.w}),e.height&&(K.h=r(e.height,"y")-D-_),e.innerHeight&&(K.h=r(e.innerHeight,"y")),e.innerHeight||e.height||(E.css({height:"auto"}),K.h=E.height()),E.css({height:K.h}),G.position("none"===K.transition?0:K.speed))},G.prep=function(t){function i(){return K.w=K.w||E.width(),K.w=K.mw&&K.mw1?("string"==typeof K.current&&H.html(K.current.replace("{current}",A+1).replace("{total}",s)).show(),S[K.loop||s-1>A?"show":"hide"]().html(K.next),F[K.loop||A?"show":"hide"]().html(K.previous),K.slideshow&&M.show(),K.preloading&&e.each([n(-1),n(1)],function(){var t,i,o=T[this],n=e.data(o,V);n&&n.href?(t=n.href,e.isFunction(t)&&(t=t.call(o))):t=e(o).attr("href"),t&&h(n,t)&&(t=l(n,t),i=new Image,i.src=t)})):P.hide(),K.iframe?(i=o("iframe")[0],c in i&&(i[c]=0),u in i&&(i[u]="true"),K.scrolling||(i.scrolling="no"),e(i).attr({src:K.href,name:(new Date).getTime(),"class":Y+"Iframe",allowFullScreen:!0,webkitAllowFullScreen:!0,mozallowfullscreen:!0}).one("load",r).appendTo(E),at.one(rt,function(){i.src="//about:blank"}),K.fastIframe&&e(i).trigger("load")):r(),"fade"===K.transition?w.fadeTo(a,1,t):t())},"fade"===K.transition?w.fadeTo(a,0,function(){G.position(0,s)}):G.position(a,s)}},G.load=function(t){var n,s,c,u=G.prep;q=!0,O=!1,N=T[A],t||a(),Q&&w.add(m).removeClass(Q),K.className&&w.add(m).addClass(K.className),Q=K.className,d(rt),d(tt,K.onLoad),K.h=K.height?r(K.height,"y")-D-_:K.innerHeight&&r(K.innerHeight,"y"),K.w=K.width?r(K.width,"x")-B-z:K.innerWidth&&r(K.innerWidth,"x"),K.mw=K.w,K.mh=K.h,K.maxWidth&&(K.mw=r(K.maxWidth,"x")-B-z,K.mw=K.w&&K.w1&&(O.height=O.height/i.devicePixelRatio,O.width=O.width/i.devicePixelRatio),K.scalePhotos&&(s=function(){O.height-=O.height*e,O.width-=O.width*e},K.mw&&O.width>K.mw&&(e=(O.width-K.mw)/O.width,s()),K.mh&&O.height>K.mh&&(e=(O.height-K.mh)/O.height,s())),K.h&&(O.style.marginTop=Math.max(K.mh-O.height,0)/2+"px"),T[1]&&(K.loop||T[A+1])&&(O.style.cursor="pointer",O.onclick=function(){G.next()}),ht&&(O.style.msInterpolationMode="bicubic"),setTimeout(function(){u(O)},1)}),setTimeout(function(){O.src=n},1)):n&&I.load(n,K.data,function(t,i){u("error"===i?o(dt,"Error").html(K.xhrError):e(this).contents())})},G.next=function(){!q&&T[1]&&(K.loop||T[A+1])&&(A=n(1),G.load())},G.prev=function(){!q&&T[1]&&(K.loop||A)&&(A=n(-1),G.load())},G.close=function(){j&&!U&&(U=!0,j=!1,d(ot,K.onCleanup),k.unbind("."+Y+" ."+st),m.fadeTo(200,0),w.stop().fadeTo(300,0,function(){w.add(m).css({opacity:1,cursor:"auto"}).hide(),d(rt),E.empty().remove(),setTimeout(function(){U=!1,d(nt,K.onClosed)},1)}))},G.remove=function(){e([]).add(w).add(m).remove(),w=null,e("."+Z).removeData(V).removeClass(Z),e(t).unbind("click."+Y)},G.element=function(){return e(N)},G.settings=J)})(jQuery,document,window); \ No newline at end of file diff --git a/spec/components/cooked_post_processor_spec.rb b/spec/components/cooked_post_processor_spec.rb index 96904db6582..901a309b241 100644 --- a/spec/components/cooked_post_processor_spec.rb +++ b/spec/components/cooked_post_processor_spec.rb @@ -3,6 +3,11 @@ require 'spec_helper' require 'cooked_post_processor' describe CookedPostProcessor do + let :cpp do + post = Fabricate.build(:post_with_youtube) + post.id = 123 + CookedPostProcessor.new(post) + end context 'process_onebox' do @@ -39,10 +44,11 @@ EXPECTED @topic = Fabricate(:topic) @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.expects(:get_size).returns([111,222]) end 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 end @@ -55,7 +61,8 @@ EXPECTED context 'with unsized images in the post' 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) end @@ -72,10 +79,36 @@ EXPECTED end end + context 'link convertor' do + before do + SiteSetting.stubs(:crawl_images?).returns(true) + end + + let :post_with_img do + Fabricate.build(:post, cooked: '

') + 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 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 context 'with valid url' do @@ -86,13 +119,13 @@ EXPECTED it "doesn't call fastimage if image crawling is disabled" do SiteSetting.expects(:crawl_images?).returns(false) FastImage.expects(:size).never - CookedPostProcessor.image_dimensions(@url) + cpp.image_dimensions(@url) end it "calls fastimage if image crawling is enabled" do SiteSetting.expects(:crawl_images?).returns(true) FastImage.expects(:size).with(@url) - CookedPostProcessor.image_dimensions(@url) + cpp.image_dimensions(@url) end end end