From fff0d9352b79605fea6624bf0d1a6aff9e4292af Mon Sep 17 00:00:00 2001 From: Taka Date: Wed, 25 Mar 2020 03:01:50 +0000 Subject: [PATCH] Allow images to be previewed. --- asl_articles/tests/test_image_preview.py | 74 +++++++ asl_articles/tests/utils.py | 13 ++ web/package-lock.json | 5 + web/package.json | 1 + .../jQuery/imageZoom/jquery.imageZoom.css | 48 +++++ .../jQuery/imageZoom/jquery.imageZoom.js | 195 ++++++++++++++++++ .../jQuery/imageZoom/jquery.imageZoom.png | Bin 0 -> 1755 bytes web/src/App.js | 6 +- web/src/ArticleSearchResult.js | 12 +- web/src/PreviewableImage.js | 72 +++++++ web/src/PublicationSearchResult.js | 12 +- web/src/PublisherSearchResult.js | 12 +- 12 files changed, 443 insertions(+), 7 deletions(-) create mode 100644 asl_articles/tests/test_image_preview.py create mode 100644 web/public/jQuery/imageZoom/jquery.imageZoom.css create mode 100644 web/public/jQuery/imageZoom/jquery.imageZoom.js create mode 100644 web/public/jQuery/imageZoom/jquery.imageZoom.png create mode 100644 web/src/PreviewableImage.js diff --git a/asl_articles/tests/test_image_preview.py b/asl_articles/tests/test_image_preview.py new file mode 100644 index 0000000..16811cc --- /dev/null +++ b/asl_articles/tests/test_image_preview.py @@ -0,0 +1,74 @@ +""" Test previewing images. """ + +import os + +from selenium.common.exceptions import ElementClickInterceptedException + +from asl_articles.search import SEARCH_ALL_PUBLISHERS, SEARCH_ALL_PUBLICATIONS, SEARCH_ALL_ARTICLES +from asl_articles.tests.test_publishers import create_publisher, edit_publisher +from asl_articles.tests.test_publications import create_publication, edit_publication +from asl_articles.tests.test_articles import create_article, edit_article +from asl_articles.tests.utils import init_tests, find_child, find_children, wait_for, \ + do_search, get_search_results, call_with_retry + +# --------------------------------------------------------------------- + +def test_image_preview( webdriver, flask_app, dbconn ): + """Test previewing images.""" + + # initialize + init_tests( webdriver, flask_app, dbconn ) + + def do_test( create, edit, refresh ): + + # create a new object + webdriver.refresh() + create() + results = get_search_results() + assert len(results) == 1 + sr = results[0] + + # add images to the object + # NOTE: We're testing that images in an object already on-screen is updated correctly. + fname = os.path.join( os.path.split(__file__)[0], "fixtures/images/1.gif" ) + description = 'foo bar' + edit( sr, fname, description ) + _check_previewable_images( sr ) + + # refresh the object + # NOTE: We're testing that images in an object loaded afresh is set up correctly. + webdriver.refresh() + wait_for( 2, lambda: find_child( "#search-form" ) ) + results = refresh() + assert len(results) == 1 + _check_previewable_images( results[0] ) + + # do the tests + do_test( + lambda: create_publisher( { "name": "Test publisher" } ), + lambda sr, fname, description: edit_publisher( sr, { "image": fname, "description": description } ), + lambda: do_search( SEARCH_ALL_PUBLISHERS ) + ) + do_test( + lambda: create_publication( { "name": "Test publication" } ), + lambda sr, fname, description: edit_publication( sr, { "image": fname, "description": description } ), + lambda: do_search( SEARCH_ALL_PUBLICATIONS ) + ) + do_test( + lambda: create_article( { "title": "Test article" } ), + lambda sr, fname, description: edit_article( sr, { "image": fname, "snippet": description } ), + lambda: do_search( SEARCH_ALL_ARTICLES ) + ) + +# --------------------------------------------------------------------- + +def _check_previewable_images( sr ): + """Check that previewable images are working correctly.""" + images = list( find_children( "a.preview img", sr ) ) + assert len(images) == 2 + for img in images: + assert find_child( ".jquery-image-zoom" ) is None + img.click() + preview = wait_for( 2, lambda: find_child( ".jquery-image-zoom" ) ) + call_with_retry( preview.click, [ElementClickInterceptedException] ) + wait_for( 2, lambda: find_child( ".jquery-image-zoom" ) is None ) diff --git a/asl_articles/tests/utils.py b/asl_articles/tests/utils.py index c793a43..8aa09c7 100644 --- a/asl_articles/tests/utils.py +++ b/asl_articles/tests/utils.py @@ -2,6 +2,7 @@ import os import json +import time import itertools import uuid import base64 @@ -469,6 +470,18 @@ def get_article_row( dbconn, article_id, fields ): # --------------------------------------------------------------------- +def call_with_retry( func, expected_exceptions, max_retries=10, delay=0.1 ): + """Try to call a function, with retries if it fails.""" + for _ in range(0,max_retries): + try: + return func() + except Exception as exc: #pylint: disable=broad-except + if type(exc) not in expected_exceptions: #pylint: disable=unidiomatic-typecheck + raise + time.sleep( delay ) + continue + assert False + def change_image( dlg, fname ): """Click on an image to change it.""" # NOTE: This is a bit tricky since we started overlaying the image with the "remove image" icon :-/ diff --git a/web/package-lock.json b/web/package-lock.json index 1e52950..573f0bb 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -8091,6 +8091,11 @@ } } }, + "jquery": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.4.1.tgz", + "integrity": "sha512-36+AdBzCL+y6qjw5Tx7HgzeGCzC81MDDgaUP8ld2zhx58HdqXGoBd+tHdrBMiyjGQs0Hxs/MLZTu/eHNJJuWPw==" + }, "js-levenshtein": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/js-levenshtein/-/js-levenshtein-1.1.6.tgz", diff --git a/web/package.json b/web/package.json index ea43dd5..0b0adcb 100644 --- a/web/package.json +++ b/web/package.json @@ -7,6 +7,7 @@ "@reach/menu-button": "^0.7.2", "axios": "^0.19.0", "http-proxy-middleware": "^0.20.0", + "jquery": "^3.4.1", "lodash.clone": "^4.5.0", "lodash.clonedeep": "^4.5.0", "lodash.isequal": "^4.5.0", diff --git a/web/public/jQuery/imageZoom/jquery.imageZoom.css b/web/public/jQuery/imageZoom/jquery.imageZoom.css new file mode 100644 index 0000000..c5af1cc --- /dev/null +++ b/web/public/jQuery/imageZoom/jquery.imageZoom.css @@ -0,0 +1,48 @@ +div.jquery-image-zoom { + line-height: 0; + font-size: 0; + + z-index: 10; + + border: 5px solid #fff; + background: #eee; /* TM 25jan15: Added this to make it easier to see images with transparent backgrounds. */ + margin: -5px; + + -webkit-box-shadow: 0 0 10px rgba(0, 0, 0, 0.3); + -moz-box-shadow: 0 0 10px rgba(0, 0, 0, 0.3); + box-shadow: 0 0 10px rgba(0, 0, 0, 0.3); +} + +div.jquery-image-zoom a { + background: url(/jQuery/imageZoom/jquery.imageZoom.png) no-repeat; + + display: block; + width: 25px; + height: 25px; + + position: absolute; + left: -17px; + top: -17px; + /* IE-users are prolly used to close-link in right-hand corner */ + *left: auto; + *right: -17px; + + text-decoration: none; + text-indent: -100000px; + outline: 0; + + z-index: 11; +} + +div.jquery-image-zoom a:hover { + background-position: left -25px; +} + +div.jquery-image-zoom img, +div.jquery-image-zoom embed, +div.jquery-image-zoom object, +div.jquery-image-zoom div { + width: 100%; + height: 100%; + margin: 0; +} diff --git a/web/public/jQuery/imageZoom/jquery.imageZoom.js b/web/public/jQuery/imageZoom/jquery.imageZoom.js new file mode 100644 index 0000000..f49a94a --- /dev/null +++ b/web/public/jQuery/imageZoom/jquery.imageZoom.js @@ -0,0 +1,195 @@ +/*** +@title: +Image Zoom + +@version: +2.0 + +@author: +Andreas Lagerkvist + +@date: +2008-08-31 + +@url: +http://andreaslagerkvist.com/jquery/image-zoom/ + +@license: +http://creativecommons.org/licenses/by/3.0/ + +@copyright: +2008 Andreas Lagerkvist (andreaslagerkvist.com) + +@requires: +jquery, jquery.imageZoom.css, jquery.imageZoom.png + +@does: +This plug-in makes links pointing to images open in the "Image Zoom". Clicking a link will zoom out the clicked image to its target-image. Click anywhere on the image or the close-button to zoom the image back in. Only ~3k minified. + +@howto: +jQuery(document.body).imageZoom(); Would make every link pointing to an image in the document open in the zoom. + +@exampleHTML: + + + + +@exampleJS: +// I don't run it because my site already uses imgZoom +// jQuery(document.body).imageZoom(); +***/ +jQuery.fn.imageZoom = function (conf) { + // Some config. If you set dontFadeIn: 0 and hideClicked: 0 imgzoom will act exactly like fancyzoom + var config = jQuery.extend({ + speed: 200, // Animation-speed of zoom + dontFadeIn: 1, // 1 = Do not fade in, 0 = Do fade in + hideClicked: 1, // Whether to hide the image that was clicked to bring up the imgzoom + imageMargin: 30, // Margin from image-edge to window-edge if image is larger than screen + className: 'jquery-image-zoom', + loading: 'Loading...' + }, conf); + config.doubleSpeed = config.speed / 4; // Used for fading in the close-button + + return this.click(function(e) { + // Make sure the target-element is a link (or an element inside a link) + var clickedElement = jQuery(e.target); // The element that was actually clicked + var clickedLink = clickedElement.is('a') ? clickedElement : clickedElement.parents('a'); // If it's not an a, check if any of its parents is + // TM MAR/20: Removed the check on the filename extension (it was looking for an image-type extension). + clickedLink = (clickedLink && clickedLink.is('a')) ? clickedLink : false; // If it was an a or child of an a, make sure it points to an image + var clickedImg = (clickedLink && clickedLink.find('img').length) ? clickedLink.find('img') : false; // See if the clicked link contains and image + + // Only continue if a link pointing to an image was clicked + if (clickedLink) { + // These functions are used when the imaeg starts and stops loading (displays either 'loading..' or fades out the clicked img slightly) + clickedLink.oldText = clickedLink.text(); + + clickedLink.setLoadingImg = function () { + if (clickedImg) { + clickedImg.css({opacity: '0.5'}); + } + else { + clickedLink.text(config.loading); + } + }; + + clickedLink.setNotLoadingImg = function () { + if (clickedImg) { + clickedImg.css({opacity: '1'}); + } + else { + clickedLink.text(clickedLink.oldText); + } + }; + + // The URI to the image we are going to display + var displayImgSrc = clickedLink.attr('href'); + + // If an imgzoom wiv this image is already open dont do nathin + if (jQuery('div.' + config.className + ' img[src="' + displayImgSrc + '"]').length) { + return false; + } + + // This function is run once the displayImgSrc-img has loaded (below) + var preloadOnload = function (pload) { + // The clicked-link is faded out during loading, fade it back in + clickedLink.setNotLoadingImg(); + + // Now set some vars we need + var dimElement = clickedImg ? clickedImg : clickedLink; // The element used to retrieve dimensions of imgzoom before zoom (either clicked link or img inside) + var hideClicked = clickedImg ? config.hideClicked : 0; // Whether to hide clicked link (set in config but always true for non-image-links) + var offset = dimElement.offset(); // Offset of clicked link (or image inside) + var imgzoomBefore = { // The dimensions of the imgzoom _before_ it is zoomed out + width: dimElement.outerWidth(), + height: dimElement.outerHeight(), + left: offset.left, + top: offset.top/*, + opacity: config.dontFadeIn*/ + }; + var imgzoom = jQuery('
').css('position', 'absolute').appendTo(document.body); // We don't want any class-name or any other contents part from the image when we calculate the new dimensions of the imgzoom + var imgzoomAfter = { // The dimensions of the imgzoom _after_ it is zoomed out + width: pload.width, + height: pload.height/*, + opacity: 1*/ + }; + var windowDim = { + width: jQuery(window).width(), + height: jQuery(window).height() + }; + // Make sure imgzoom isn't wider than screen + if (imgzoomAfter.width > (windowDim.width - config.imageMargin * 2)) { + var nWidth = windowDim.width - config.imageMargin * 2; + imgzoomAfter.height = (nWidth / imgzoomAfter.width) * imgzoomAfter.height; + imgzoomAfter.width = nWidth; + } + // Now make sure it isn't taller + if (imgzoomAfter.height > (windowDim.height - config.imageMargin * 2)) { + var nHeight = windowDim.height - config.imageMargin * 2; + imgzoomAfter.width = (nHeight / imgzoomAfter.height) * imgzoomAfter.width; + imgzoomAfter.height = nHeight; + } + // Center imgzoom + imgzoomAfter.left = (windowDim.width - imgzoomAfter.width) / 2 + jQuery(window).scrollLeft(); + imgzoomAfter.top = (windowDim.height - imgzoomAfter.height) / 2 + jQuery(window).scrollTop(); + var closeButton = jQuery('Close').appendTo(imgzoom).hide(); // The button that closes the imgzoom (we're adding this after the calculation of the dimensions) + + // Hide the clicked link if set so in config + if (hideClicked) { + clickedLink.css('visibility', 'hidden'); + } + + // Now animate the imgzoom from its small size to its large size, and then fade in the close-button + imgzoom.addClass(config.className).css(imgzoomBefore).animate(imgzoomAfter, config.speed, function () { + closeButton.fadeIn(config.doubleSpeed); + }); + + // This function closes the imgzoom + var hideImgzoom = function () { + closeButton.fadeOut(config.doubleSpeed, function () { + imgzoom.animate(imgzoomBefore, config.speed, function () { + clickedLink.css('visibility', 'visible'); + imgzoom.remove(); + }); + }); + + return false; + }; + + // Close imgzoom when you click the closeButton or the imgzoom + imgzoom.click(hideImgzoom); + closeButton.click(hideImgzoom); + }; + + // Preload image + var preload = new Image(); + + preload.src = displayImgSrc; + + if (preload.complete) { + preloadOnload(preload); + } + else { + clickedLink.setLoadingImg(); + + preload.onload = function () { + preloadOnload(preload); + }; + } + + // Finally return false from the click so the browser doesn't actually follow the link... + return false; + } + }); +}; + +// NOTE: We used to close up on ESC, but we want to do this on *any* keypress (e.g. if the user +// starts typing in the search query box) or click (e.g. if the user clicks on a menu). +$(document).keydown( () => { $("div.jquery-image-zoom a").click() ; } ) ; +$(document).click( () => { $("div.jquery-image-zoom a").click() ; } ) ; diff --git a/web/public/jQuery/imageZoom/jquery.imageZoom.png b/web/public/jQuery/imageZoom/jquery.imageZoom.png new file mode 100644 index 0000000000000000000000000000000000000000..2f5a3811d5399eb5c56dfa6779387db04bcebb82 GIT binary patch literal 1755 zcmV<11|<23P)P001%w1^@s6se*8p00004XF*Lt006JZ zHwB960000PbVXQnQ*UN;cVTj606}DLVr3vnZDD6+Qe|Oed2z{QJOBU&ZAnByRCwCV zSba!UXBdC3_g*!d@wJ?EbDywC4_ zzMp$7LI_&QvREtud2B%lvGn)%uU@`a%HbHEN zDG2)jDqmp6#>U2=gnLp(Muxb5|Gor*FUw3pTrStU%m9BF8rbc2v2EKnQCL_gY&M%D ztJNy6YuBz7m6equIy%|_Dl03KARH`;HkuX|ytv2=`}XZKHf`Oy)rh2%Cr=tc>FMcW ze0*F!Z)$2X_z@_gx3^b59~&Fnt$^S~BENt(l2PB3nwlz*6ADmfW+n?To}E5@S~1(< z!-sW~eWrjQEcfz@GiS~y8~D_$*@FiU459Fwao^tFF3sn;XVa!l@*NJ>t7AyJfpHCA z5x!pjYG`Pn^73+e`0z<$BlG2z^Cmw(-z!>m8Sv-x3W&eD#6Z`tUnfM4^6uKTYw{gJ zUU?2pp9l^KiBLe0eB*qLiHY$7+PHBeH8(dCB91g(c|CC803APm+>1XkF;QNjxl=L8 z;Nai}wj)P|r+rac1WJnHgW}?1#YgOD&pqrzE&I`Af&wom!-Qhb zo;?OoYip|l!okA}72rLuWSpL!KD98ySZGyMl{k9zsDO>-cizI3o0}_6oH!xYty^dC z^8wKUBna9=f)@xs1cQqi{1ruC#8slZyIVI^z0U&4{ezjf2C=CY1AyT0%nzGS5KbAK z;tOVGwT~P*60j1piF$f^^d>gSl{{cS;Zv}>y2D3DM@3Cdjo7|@yGTn*6S1+eGOJ5U zN~F{E1?{hx0{CjboQH^xLcR3+TWDBL4jO;+g2mso!b1c!$wi6SfdL{L5f&R78oCOJ zHVol?xLz0<8fwhS%5p8b$N%Fa(Ul;WmbyadKUe{WyL4FOTcXv=h;pHGVklAE9FYTa zkK-GnpoTlkW#7WBKN1~+(AUC=Hkk3C$WOY7ewrkDe+N;|qCoy2N-ab-^^@LlAh%W~1*y%v~1+pf}O9c7k}K zn;~l5Li8$b-9ABnCrU=?yabVvEmkYl0AMZ%$FfEr&Cg3v^u;eRycThP^NKF^0ZdfO z0OxS>4}t{Xbs79`+wGG78KT$W%Tm)2{({@0jgLup(GWl=RTdW%-%tBJM>YrpBio?v{bRM(1QX=ZSVJntVl+z>zeK z`*sf}qZ$+1th_&X@Sq$V06=YRZN|`X_wHSTpPg_HzSI&hU(uwUXfLB5LyKMNaF-ey zDmA%8qz-rZki(sGK^=RV;ZBzUP5!;%ZdTt*qz-o|_Hwv;)&yv=!`&*abs!s^icgwR zo7j#V8A~1RI67HYC(&7@3hyNP^&%{=Mu?Y^_-CDFSM+pRxpJlA1J(pxk?`}HAe1(5 z-mGZyvSvPLazTG)Eyo8)AY0RrMC(@oP&dyFMEiIKkVi1h8)Ya7`L%h1W zn(#YO^78>JkgR;f2_$4A(X>*#Pq?HBCd0TK=2S9EH*_s)k+U+Jx?cBkBg8>Un3#7Cs(Z@JN6L0zioA$%LVF3Wx zM77W$2M2>U#zs*f7xomdeu8RjLuAw;<@WM-7qn@CR^1?%*0d5JcnyTU0?O7x8a}-` zh7!Mvs4u9_$bB8ndndFw#Fg2fCWxY2Fi{J9@RsRc9{zf~4%UGI0&%_rfZ4EZEbdo9 x(l7+~ahTpDYJ|$Jry3-JUFrXqx$AEM1^`j@eVA4tGV=fc002ovPDHLkV1f;jL%{$5 literal 0 HcmV?d00001 diff --git a/web/src/App.js b/web/src/App.js index 5f4e4db..27d4d7c 100644 --- a/web/src/App.js +++ b/web/src/App.js @@ -12,12 +12,14 @@ import { PublicationSearchResult } from "./PublicationSearchResult" ; import { ArticleSearchResult } from "./ArticleSearchResult" ; import ModalForm from "./ModalForm"; import AskDialog from "./AskDialog" ; +import { PreviewableImage } from "./PreviewableImage" ; import { makeSmartBulletList } from "./utils.js" ; import { APP_NAME } from "./constants.js" ; import "./App.css" ; const axios = require( "axios" ) ; const queryString = require( "query-string" ) ; +window.$ = window.jQuery = require( "jquery" ) ; export let gAppRef = null ; @@ -147,7 +149,9 @@ export class App extends React.Component } componentDidMount() { - // install our key handler + + // initialize + PreviewableImage.initPreviewableImages() ; window.addEventListener( "keydown", this.onKeyDown.bind( this ) ) ; // check if the server started up OK diff --git a/web/src/ArticleSearchResult.js b/web/src/ArticleSearchResult.js index 3aa2adc..2937630 100644 --- a/web/src/ArticleSearchResult.js +++ b/web/src/ArticleSearchResult.js @@ -4,6 +4,7 @@ import { Menu, MenuList, MenuButton, MenuItem } from "@reach/menu-button" ; import { ArticleSearchResult2 } from "./ArticleSearchResult2.js" ; import "./ArticleSearchResult.css" ; import { PublicationSearchResult } from "./PublicationSearchResult.js" ; +import { PreviewableImage } from "./PreviewableImage.js" ; import { RatingStars } from "./RatingStars.js" ; import { gAppRef } from "./App.js" ; import { makeScenarioDisplayName, applyUpdatedVals, removeSpecialFields, makeCommaList, isLink } from "./utils.js" ; @@ -20,7 +21,9 @@ export class ArticleSearchResult extends React.Component // prepare the basic details const display_title = this.props.data[ "article_title!" ] || this.props.data.article_title ; const display_subtitle = this.props.data[ "article_subtitle!" ] || this.props.data.article_subtitle ; - const display_snippet = this.props.data[ "article_snippet!" ] || this.props.data.article_snippet ; + const display_snippet = PreviewableImage.adjustHtmlForPreviewableImages( + this.props.data[ "article_snippet!" ] || this.props.data.article_snippet + ) ; const pub = gAppRef.caches.publications[ this.props.data.pub_id ] ; const image_url = gAppRef.makeFlaskImageUrl( "article", this.props.data.article_image_id ) ; @@ -141,7 +144,7 @@ export class ArticleSearchResult extends React.Component { display_subtitle &&
}
- { image_url && Article. } + { image_url && }
@@ -152,6 +155,10 @@ export class ArticleSearchResult extends React.Component
) ; } + componentDidMount() { + PreviewableImage.activatePreviewableImages( this ) ; + } + onRatingChange( newRating, onFailed ) { axios.post( gAppRef.makeFlaskUrl( "/article/update-rating", null ), { article_id: this.props.data.article_id, @@ -205,6 +212,7 @@ export class ArticleSearchResult extends React.Component if ( newVals.imageData ) gAppRef.forceFlaskImageReload( "article", newVals.article_id ) ; this.forceUpdate() ; + PreviewableImage.activatePreviewableImages( this ) ; if ( resp.data.warnings ) gAppRef.showWarnings( "The article was updated OK.", resp.data.warnings ) ; else diff --git a/web/src/PreviewableImage.js b/web/src/PreviewableImage.js new file mode 100644 index 0000000..c671b02 --- /dev/null +++ b/web/src/PreviewableImage.js @@ -0,0 +1,72 @@ +import React from "react" ; +import ReactDOM from "react-dom" ; +import $ from "jquery" ; + +// -------------------------------------------------------------------- + +export class PreviewableImage extends React.Component +{ + // NOTE: While the "react-modal-image" component seems to work nicely, how can we use it + // on arbitrary images in user-defined content? + // This class is a wrapper around the jQuery-based imageZoom plugin. + + render() { + return ( + {this.props.altText} + ) ; + } + + static initPreviewableImages() { + // load the imageZoom script + $.getScript( "/jQuery/imageZoom/jquery.imageZoom.js" ) ; + // load the imageZoom CSS + let cssNode = document.createElement( "link" ) ; + cssNode.type = "text/css" ; + cssNode.rel = "stylesheet" ; + cssNode.href = "/jQuery/imageZoom/jquery.imageZoom.css" ; + let headNode = document.getElementsByTagName( "head" )[0] ; + headNode.appendChild( cssNode ) ; + } + + static adjustHtmlForPreviewableImages( html ) { + // FUDGE! The imageZoom plugin requires images to be wrapped with a tag. + // I was hoping to be able to let the user enable the preview functionality for images + // by simply adding a "preview" attribute to their tags, then locating them after render + // and dynamically wrapping them with the necessary tag, but React doesn't + // seem to like that :-/ + // We instead look for such images in the HTML returned to us by the backend server, and fix it up + // before rendering it. + + // initialize + if ( ! html ) + return "" ; + + // locate tags with a class of "preview", and wrap them in a . + let buf=[], pos=0 ; + const img_regex = /]*class\s*=\s*["']preview["'][^>]*>/g ; + const url_regex = /src\s*=\s*["'](.*?)['"]/ + for ( const match of html.matchAll( img_regex ) ) { + buf.push( html.substr( pos, match.index-pos ) ) ; + const match2 = url_regex.exec( match[0] ) ; + if ( match2 ) { + buf.push( + "", + match[0], + "" + ) ; + } else + buf.push( match[0] ) ; + pos = match.index + match[0].length ; + } + buf.push( html.substr( pos ) ) ; + + return buf.join( "" ) ; + } + + static activatePreviewableImages( rootNode ) { + // locate images marked as previewable and activate them + let $elems = $( ReactDOM.findDOMNode( rootNode ) ).find( "a.preview" ) ; + $elems.imageZoom() ; + } + +} diff --git a/web/src/PublicationSearchResult.js b/web/src/PublicationSearchResult.js index 172bad6..9ab3ee1 100644 --- a/web/src/PublicationSearchResult.js +++ b/web/src/PublicationSearchResult.js @@ -3,6 +3,7 @@ import { Link } from "react-router-dom" ; import { Menu, MenuList, MenuButton, MenuItem } from "@reach/menu-button" ; import "./PublicationSearchResult.css" ; import { PublicationSearchResult2 } from "./PublicationSearchResult2.js" ; +import { PreviewableImage } from "./PreviewableImage.js" ; import { PUBLICATION_EXCESS_ARTICLE_THRESHOLD } from "./constants.js" ; import { gAppRef } from "./App.js" ; import { makeCollapsibleList, pluralString, applyUpdatedVals, removeSpecialFields, isLink } from "./utils.js" ; @@ -17,7 +18,9 @@ export class PublicationSearchResult extends React.Component render() { // prepare the basic details - const display_description = this.props.data[ "pub_description!" ] || this.props.data.pub_description ; + const display_description = PreviewableImage.adjustHtmlForPreviewableImages( + this.props.data[ "pub_description!" ] || this.props.data.pub_description + ) ; const publ = gAppRef.caches.publishers[ this.props.data.publ_id ] ; const image_url = PublicationSearchResult.makeImageUrl( this.props.data ) ; @@ -108,7 +111,7 @@ export class PublicationSearchResult extends React.Component }
- { image_url && Publication. } + { image_url && }
{ makeCollapsibleList( "Articles", articles, PUBLICATION_EXCESS_ARTICLE_THRESHOLD, {float:"left",marginBottom:"0.25em"} ) }
@@ -119,6 +122,10 @@ export class PublicationSearchResult extends React.Component
) ; } + componentDidMount() { + PreviewableImage.activatePreviewableImages( this ) ; + } + static onNewPublication( notify ) { PublicationSearchResult2._doEditPublication( {}, null, (newVals,refs) => { axios.post( gAppRef.makeFlaskUrl( "/publication/create", {list:1} ), newVals ) @@ -163,6 +170,7 @@ export class PublicationSearchResult extends React.Component if ( newVals.imageData ) gAppRef.forceFlaskImageReload( "publication", newVals.pub_id ) ; this.forceUpdate() ; + PreviewableImage.activatePreviewableImages( this ) ; if ( resp.data.warnings ) gAppRef.showWarnings( "The publication was updated OK.", resp.data.warnings ) ; else diff --git a/web/src/PublisherSearchResult.js b/web/src/PublisherSearchResult.js index 0b07d60..882e11a 100644 --- a/web/src/PublisherSearchResult.js +++ b/web/src/PublisherSearchResult.js @@ -4,6 +4,7 @@ import { Menu, MenuList, MenuButton, MenuItem } from "@reach/menu-button" ; import { PublisherSearchResult2 } from "./PublisherSearchResult2.js" import "./PublisherSearchResult.css" ; import { PublicationSearchResult } from "./PublicationSearchResult.js" +import { PreviewableImage } from "./PreviewableImage.js" ; import { PUBLISHER_EXCESS_PUBLICATION_THRESHOLD } from "./constants.js" ; import { gAppRef } from "./App.js" ; import { makeCollapsibleList, pluralString, applyUpdatedVals, removeSpecialFields } from "./utils.js" ; @@ -19,7 +20,9 @@ export class PublisherSearchResult extends React.Component // prepare the basic details const display_name = this.props.data[ "publ_name!" ] || this.props.data.publ_name ; - const display_description = this.props.data[ "publ_description!" ] || this.props.data.publ_description ; + const display_description = PreviewableImage.adjustHtmlForPreviewableImages( + this.props.data[ "publ_description!" ] || this.props.data.publ_description + ) ; const image_url = gAppRef.makeFlaskImageUrl( "publisher", this.props.data.publ_image_id ) ; // prepare the publications @@ -72,13 +75,17 @@ export class PublisherSearchResult extends React.Component }
- { image_url && Publisher. } + { image_url && }
{ makeCollapsibleList( "Publications", pubs, PUBLISHER_EXCESS_PUBLICATION_THRESHOLD, {float:"left",marginBottom:"0.25em"} ) }
) ; } + componentDidMount() { + PreviewableImage.activatePreviewableImages( this ) ; + } + static onNewPublisher( notify ) { PublisherSearchResult2._doEditPublisher( {}, (newVals,refs) => { axios.post( gAppRef.makeFlaskUrl( "/publisher/create", {list:1} ), newVals ) @@ -115,6 +122,7 @@ export class PublisherSearchResult extends React.Component if ( newVals.imageData ) gAppRef.forceFlaskImageReload( "publisher", newVals.publ_id ) ; this.forceUpdate() ; + PreviewableImage.activatePreviewableImages( this ) ; if ( resp.data.warnings ) gAppRef.showWarnings( "The publisher was updated OK.", resp.data.warnings ) ; else