diff --git a/applications/app/controllers/ImageContentController.scala b/applications/app/controllers/ImageContentController.scala index c7d6679c8aa4..867d8ab81e08 100644 --- a/applications/app/controllers/ImageContentController.scala +++ b/applications/app/controllers/ImageContentController.scala @@ -1,24 +1,30 @@ package controllers +import com.gu.contentapi.client.Parameter +import com.gu.contentapi.client.model.SearchQueryBase import com.gu.contentapi.client.model.v1.{ItemResponse, Content => ApiContent} import common._ import conf.switches.Switches import contentapi.ContentApiClient import model._ import pages.ContentHtmlPage +import play.api.libs.ws.WSClient import play.api.mvc._ import services.ImageQuery import views.support.RenderOtherStatus +import play.api.libs.json._ +import conf.Configuration.contentApi import scala.concurrent.Future case class ImageContentPage(image: ImageContent, related: RelatedContent) extends ContentPage { - override lazy val item = image + override lazy val item: ImageContent = image } class ImageContentController( val contentApiClient: ContentApiClient, - val controllerComponents: ControllerComponents + val controllerComponents: ControllerComponents, + wsClient: WSClient )(implicit context: ApplicationContext) extends BaseController with RendersItemResponse with ImageQuery with Logging with ImplicitControllerExecutionContext { @@ -39,4 +45,26 @@ class ImageContentController( private def isSupported(c: ApiContent) = c.isImageContent override def canRender(i: ItemResponse): Boolean = i.content.exists(isSupported) + + def getNextLightboxJson(path: String, tag: String, direction: String): Action[AnyContent] = Action.async { implicit request => + + val capiQuery: ContentApiNavQuery = ContentApiNavQuery(currentId = path, direction=direction) + .tag(tag).showTags("all").showElements("image").pageSize(contentApi.nextPreviousPageSize) + + contentApiClient.thriftClient.getResponse(capiQuery).map { + response => + val lightboxJson = response.results.flatMap(result => Content(result) match { + case content: ImageContent => Some(content.lightBox.javascriptConfig) + case _ => None + }) + Cached(CacheTime.Default)(JsonComponent(JsArray(lightboxJson))) + } + } +} + +case class ContentApiNavQuery(parameterHolder: Map[String, Parameter] = Map.empty, currentId: String, direction: String) + extends SearchQueryBase[ContentApiNavQuery] { + def withParameters(parameterMap: Map[String, Parameter]): ContentApiNavQuery = copy(parameterMap) + + override def pathSegment: String = s"content/$currentId/$direction" } diff --git a/applications/conf/routes b/applications/conf/routes index c3428aa57aab..fc704708b103 100644 --- a/applications/conf/routes +++ b/applications/conf/routes @@ -61,6 +61,9 @@ GET /opt/reset GET /service-worker.js controllers.WebAppController.serviceWorker() GET /2015-06-24-manifest.json controllers.WebAppController.manifest() +GET /getnext/$tag<[\w\d-]*(/[\w\d-]*)?(/[\w\d-]*)?>/*path controllers.ImageContentController.getNextLightboxJson(path, tag, direction = "next") +GET /getprev/$tag<[\w\d-]*(/[\w\d-]*)?(/[\w\d-]*)?>/*path controllers.ImageContentController.getNextLightboxJson(path, tag, direction = "prev") + # Newspaper pages GET /theguardian controllers.NewspaperController.latestGuardianNewspaper() GET /theobserver controllers.NewspaperController.latestObserverNewspaper() @@ -89,6 +92,8 @@ GET /$path<[\w\d-]*(/[\w\d-]*)?/gallery/.*> GET /$path<[\w\d-]*(/[\w\d-]*)?/(cartoon|picture|graphic)/.*>.json controllers.ImageContentController.renderJson(path) GET /$path<[\w\d-]*(/[\w\d-]*)?/(cartoon|picture|graphic)/.*> controllers.ImageContentController.render(path) + + # Audio and Video paths GET /$path<[\w\d-]*(/[\w\d-]*)?/(video|audio)/.*>/info.json controllers.MediaController.renderInfoJson(path) GET /$path<[\w\d-]*(/[\w\d-]*)?/(video|audio)/.*>.json controllers.MediaController.renderJson(path) @@ -108,6 +113,9 @@ GET /$path GET /$shortCode

controllers.ShortUrlsController.redirectShortUrl(shortCode) GET /$shortCode

/:campaignCode controllers.ShortUrlsController.fetchCampaignAndRedirectShortCode(shortCode, campaignCode) + +#GET /getprevious/$path<[\w\d-]*(/[\w\d-]*)?(/[\w\d-]*)?> controllers.IndexController.render(path) + # Index pages for tags GET /$path<[\w\d-]*(/[\w\d-]*)?(/[\w\d-]*)?>/trails.json controllers.IndexController.renderTrailsJson(path) GET /$path<[\w\d-]*(/[\w\d-]*)?(/[\w\d-]*)?>/trails controllers.IndexController.renderTrails(path) diff --git a/applications/test/ImageContentControllerTest.scala b/applications/test/ImageContentControllerTest.scala index a7fc966ce5b2..aecbbb3a0936 100644 --- a/applications/test/ImageContentControllerTest.scala +++ b/applications/test/ImageContentControllerTest.scala @@ -17,7 +17,7 @@ import org.scalatest.{BeforeAndAfterAll, DoNotDiscover, FlatSpec, Matchers} val cartoonUrl = "commentisfree/cartoon/2013/jul/15/iain-duncan-smith-benefits-cap" val pictureUrl = "artanddesign/picture/2013/oct/08/photography" - lazy val imageContentController = new ImageContentController(testContentApiClient, play.api.test.Helpers.stubControllerComponents()) + lazy val imageContentController = new ImageContentController(testContentApiClient, play.api.test.Helpers.stubControllerComponents(), wsClient) "Image Content Controller" should "200 when content type is picture" in { val result = imageContentController.render(pictureUrl)(TestRequest(pictureUrl)) diff --git a/common/app/common/configuration.scala b/common/app/common/configuration.scala index 6df01b66afc3..aebd16e81381 100644 --- a/common/app/common/configuration.scala +++ b/common/app/common/configuration.scala @@ -212,6 +212,8 @@ class GuardianConfiguration extends Logging { ).flatten: _* ) } + + lazy val nextPreviousPageSize: Int = configuration.getIntegerProperty("content.api.nextPreviousPageSize").getOrElse(50) } object ophanApi { diff --git a/common/app/model/content.scala b/common/app/model/content.scala index d8525e9b47fe..c69a679e09dc 100644 --- a/common/app/model/content.scala +++ b/common/app/model/content.scala @@ -843,7 +843,9 @@ case class GenericLightbox( "srcsets" -> JsString(ImgSrc.srcset(container.images, GalleryMedia.lightbox)), "sizes" -> JsString(GalleryMedia.lightbox.sizes), "ratio" -> Try(JsNumber(img.width.toDouble / img.height.toDouble)).getOrElse(JsNumber(1)), - "role" -> JsString(img.role.toString) + "role" -> JsString(img.role.toString), + "parentContentId" -> JsString(properties.id), + "id" ->JsString(properties.id) //duplicated to simplify lightbox logic )) } JsObject(Seq( diff --git a/static/src/fonts/hinting-off/kerning-off/ascii/fonts.css b/static/src/fonts/hinting-off/kerning-off/ascii/fonts.css index 63bebecf647e..9edb6675ec39 100644 --- a/static/src/fonts/hinting-off/kerning-off/ascii/fonts.css +++ b/static/src/fonts/hinting-off/kerning-off/ascii/fonts.css @@ -7,7 +7,7 @@ For more information please visit Commercial Type at http://commercialtype.com or email us at info[at]commercialtype.com Copyright (C) 2013 Schwartzco Inc. - + */ @@ -280,7 +280,7 @@ font-style: normal; font-stretch: normal; } - + @font-face { font-family: 'GuardianAgateSans1Web'; src: url('GuardianAgateSans1Web/GuardianAgateSans1Web-RegularItalic.eot'); @@ -389,7 +389,7 @@ font-style: normal; font-stretch: normal; } - + @font-face { font-family: 'GuardianEgyptianWeb'; src: url('GuardianEgyptianWeb/GuardianEgyptianWeb-SemiboldItalic.eot'); @@ -416,8 +416,8 @@ font-style: normal; font-stretch: normal; } - - + + @font-face { font-family: 'GuardianEgyptianWeb'; @@ -433,7 +433,7 @@ } /* GUARDIAN TEXT EGYPTIAN */ - + @font-face { font-family: 'GuardianTextEgyptianWeb'; src: url('GuardianTextEgyptianWeb/GuardianTextEgyptianWeb-Regular.eot'); @@ -488,7 +488,7 @@ } - + @font-face { font-family: 'GuardianTextEgyptianWeb'; src: url('GuardianTextEgyptianWeb/GuardianTextEgyptianWeb-Bold.eot'); @@ -679,7 +679,7 @@ font-style: normal; font-stretch: normal; } - + @font-face { font-family: 'GuardianTextSansWeb'; @@ -734,4 +734,3 @@ font-style: normal; font-stretch: normal; } - \ No newline at end of file diff --git a/static/src/javascripts/projects/common/modules/gallery/lightbox.js b/static/src/javascripts/projects/common/modules/gallery/lightbox.js index 6fa9ef047aae..8b1515ab963b 100644 --- a/static/src/javascripts/projects/common/modules/gallery/lightbox.js +++ b/static/src/javascripts/projects/common/modules/gallery/lightbox.js @@ -18,6 +18,7 @@ import endslateTpl from 'raw-loader!common/views/content/endslate.html'; import loaderTpl from 'raw-loader!common/views/content/loader.html'; import shareButtonTpl from 'raw-loader!common/views/content/share-button.html'; import { loadCssPromise } from 'lib/load-css-promise'; +import fetch from 'lib/fetch'; type ImageJson = { caption: string, @@ -81,6 +82,7 @@ class GalleryLightbox { bodyScrollPosition: number; endslateEl: bonzo; endslate: Object; + startIndex: number; constructor(): void { // CONFIG @@ -295,16 +297,87 @@ class GalleryLightbox { this.fsm.trigger(event, data); } - loadGalleryfromJson(galleryJson: GalleryJson, startIndex: number): void { - this.index = startIndex; + static loadNextOrPrevious( + currentImageId: string, + direction: string + ): Promise { + const pathPrefix = direction === 'forwards' ? 'getnext' : 'getprev'; + const seriesTag = config + .get('page.nonKeywordTagIds', '') + .split(',') + .filter(tag => tag.includes('series'))[0]; + const fetchUrl = `/${pathPrefix}/${seriesTag}/${currentImageId}`; + return fetch(fetchUrl) + .then(response => response.json()) + .then(json => + // filter out non-lightbox items in the series + json.filter(image => image.images.length > 0) + ); + } - if (this.galleryJson && galleryJson.id === this.galleryJson.id) { + loadOrOpen(newGalleryJson: GalleryJson, index: number): void { + this.index = index; + if ( + this.galleryJson && + newGalleryJson.id === this.galleryJson.id && + newGalleryJson.images.length === this.galleryJson.images.length + ) { this.trigger('open'); } else { - this.trigger('loadJson', galleryJson); + this.trigger('loadJson', newGalleryJson); } } + fetchSurroundingJson(galleryJson: GalleryJson): Promise { + // if this is an image page, load series of images into the lightbox + if ( + config.get('page.contentType') === 'ImageContent' && + galleryJson.images.length < 2 + ) { + // store current path with leading slash removed + const currentId = window.location.pathname.substring(1); + // fetch next and previous images and load them + return Promise.all([ + GalleryLightbox.loadNextOrPrevious(currentId, 'forwards'), + GalleryLightbox.loadNextOrPrevious(currentId, 'backwards'), + ]) + .then(([nextResult, previousResult]) => ({ + next: nextResult.map(e => e.images[0]), + previous: previousResult.map(e => e.images[0]), + })) + .then(({ next, previous }) => { + // combine this image, and the ones before and after it, into one big array + const allImages = previous + .reverse() + .concat(galleryJson.images, next); + if (allImages.length < 2) { + bonzo([this.nextBtn, this.prevBtn]).hide(); + $( + '.gallery-lightbox__progress', + this.lightboxEl + ).hide(); + } + + galleryJson.images = allImages; + this.startIndex = previous.length + 1; + + return galleryJson; + }); + } + + return Promise.resolve(galleryJson); + } + + loadHtml(json: GalleryJson): void { + this.images = json.images || []; + const imagesHtml = json.images + .map((img, i) => this.generateImgHTML(img, i + 1)) + .join(''); + this.$contentEl.html(imagesHtml); + this.$images = $('.js-gallery-lightbox-img', this.$contentEl[0]); + this.$countEl.text(this.images.length); + } + loadSurroundingImages(index: number, count: number): void { let imageContent; let $img; @@ -435,19 +508,7 @@ class GalleryLightbox { }, loadJson(json: GalleryJson): void { this.galleryJson = json; - this.images = json.images || []; - this.$countEl.text(this.images.length); - - const imagesHtml = this.images - .map((img, i) => this.generateImgHTML(img, i + 1)) - .join(''); - - this.$contentEl.html(imagesHtml); - - this.$images = $( - '.js-gallery-lightbox-img', - this.$contentEl[0] - ); + this.loadHtml(json); if (this.showEndslate) { this.loadEndslate(); @@ -459,14 +520,6 @@ class GalleryLightbox { this.initSwipe(); } - if (this.galleryJson.images.length < 2) { - bonzo([this.nextBtn, this.prevBtn]).hide(); - $( - '.gallery-lightbox__progress', - this.lightboxEl - ).hide(); - } - this.state = 'image'; }, }, @@ -612,11 +665,9 @@ const init = (): void => { const images = config.get('page.lightboxImages'); if (images && images.images.length > 0) { - let lightbox; + const lightbox = new GalleryLightbox(); const galleryHash = window.location.hash; - let res; - bean.on(document.body, 'click', '.js-gallerythumbs', (e: Event) => { e.preventDefault(); @@ -628,25 +679,33 @@ const init = (): void => { const galleryIndex = Number.isNaN(parsedGalleryIndex) ? 1 : parsedGalleryIndex; // 1-based index - lightbox = lightbox || new GalleryLightbox(); - lightbox.loadGalleryfromJson(images, galleryIndex); + lightbox.fetchSurroundingJson(images).then(allImages => { + const startIndex = lightbox.startIndex + ? lightbox.startIndex + : galleryIndex; + lightbox.loadOrOpen(allImages, startIndex); + }); }); - lightbox = lightbox || new GalleryLightbox(); const galleryId = `/${config.get('page.pageId')}`; const match = /\?index=(\d+)/.exec(document.location.href); + const res = /^#(?:img-)?(\d+)$/.exec(galleryHash); + + let lightboxIndex = 0; if (match) { // index specified so launch lightbox at that index pushUrl({}, document.title, galleryId, true); // lets back work properly - lightbox.loadGalleryfromJson(images, parseInt(match[1], 10)); - } else { - res = /^#(?:img-)?(\d+)$/.exec(galleryHash); + lightboxIndex = parseInt(match[1], 10); + } else if (res) { + lightboxIndex = parseInt(res[1], 10); + } - if (res) { - lightbox.loadGalleryfromJson(images, parseInt(res[1], 10)); - } + if (lightboxIndex > 0) { + lightbox.fetchSurroundingJson(images).then(allImages => { + lightbox.loadOrOpen(allImages, lightboxIndex); + }); } } });