Merge pull request #98 from ascribe/AD-1417-turn-links-in-further-details-into-anchors

AD-1417 Turn links in further details into anchors
This commit is contained in:
Brett Sun 2016-01-20 12:41:36 +01:00
commit a23dd99b94
8 changed files with 173 additions and 47 deletions

View File

@ -58,39 +58,42 @@ let FurtherDetails = React.createClass({
},
render() {
const { editable, extraData, otherData, pieceId } = this.props;
return (
<Row>
<Col md={12} className="ascribe-edition-personal-note">
<PieceExtraDataForm
name='artist_contact_info'
title='Artist Contact Info'
convertLinks
editable={editable}
extraData={extraData}
handleSuccess={this.showNotification}
editable={this.props.editable}
pieceId={this.props.pieceId}
extraData={this.props.extraData} />
pieceId={pieceId} />
<PieceExtraDataForm
name='display_instructions'
title='Display Instructions'
editable={editable}
extraData={extraData}
handleSuccess={this.showNotification}
editable={this.props.editable}
pieceId={this.props.pieceId}
extraData={this.props.extraData} />
pieceId={pieceId} />
<PieceExtraDataForm
name='technology_details'
title='Technology Details'
editable={editable}
extraData={extraData}
handleSuccess={this.showNotification}
editable={this.props.editable}
pieceId={this.props.pieceId}
extraData={this.props.extraData} />
pieceId={pieceId} />
<Form>
<FurtherDetailsFileuploader
submitFile={this.submitFile}
setIsUploadReady={this.setIsUploadReady}
isReadyForFormSubmission={formSubmissionValidation.atLeastOneUploadedFile}
editable={this.props.editable}
editable={editable}
overrideForm={true}
pieceId={this.props.pieceId}
otherData={this.props.otherData}
pieceId={pieceId}
otherData={otherData}
multiple={true} />
</Form>
</Col>

View File

@ -16,9 +16,11 @@ let PieceExtraDataForm = React.createClass({
name: React.PropTypes.string.isRequired,
pieceId: React.PropTypes.number.isRequired,
convertLinks: React.PropTypes.bool,
editable: React.PropTypes.bool,
extraData: React.PropTypes.object,
handleSuccess: React.PropTypes.func,
name: React.PropTypes.string,
title: React.PropTypes.string
},
@ -32,7 +34,7 @@ let PieceExtraDataForm = React.createClass({
},
render() {
const { editable, extraData, handleSuccess, name, pieceId, title } = this.props;
const { convertLinks, editable, extraData, handleSuccess, name, pieceId, title } = this.props;
const defaultValue = (extraData && extraData[name]) || null;
if (!defaultValue && !editable) {
@ -42,15 +44,16 @@ let PieceExtraDataForm = React.createClass({
return (
<Form
ref='form'
url={requests.prepareUrl(ApiUrls.piece_extradata, { piece_id: pieceId })}
handleSuccess={handleSuccess}
disabled={!editable}
getFormData={this.getFormData}
disabled={!editable}>
handleSuccess={handleSuccess}
url={requests.prepareUrl(ApiUrls.piece_extradata, { piece_id: pieceId })}>
<Property
name={name}
label={title}>
<InputTextAreaToggable
rows={1}
convertLinks={convertLinks}
defaultValue={defaultValue}
placeholder={getLangText('Fill in%s', ' ') + title}
required />

View File

@ -4,17 +4,20 @@ import React from 'react';
import TextareaAutosize from 'react-textarea-autosize';
import { anchorize } from '../../utils/dom_utils';
let InputTextAreaToggable = React.createClass({
propTypes: {
autoFocus: React.PropTypes.bool,
disabled: React.PropTypes.bool,
rows: React.PropTypes.number.isRequired,
required: React.PropTypes.bool,
convertLinks: React.PropTypes.bool,
defaultValue: React.PropTypes.string,
placeholder: React.PropTypes.string,
disabled: React.PropTypes.bool,
onBlur: React.PropTypes.func,
onChange: React.PropTypes.func
onChange: React.PropTypes.func,
placeholder: React.PropTypes.string,
required: React.PropTypes.bool,
rows: React.PropTypes.number.isRequired
},
getInitialState() {
@ -36,7 +39,7 @@ let InputTextAreaToggable = React.createClass({
componentDidUpdate() {
// If the initial value of state.value is null, we want to set props.defaultValue
// as a value. In all other cases TextareaAutosize.onChange is updating.handleChange already
if(this.state.value === null && this.props.defaultValue) {
if (this.state.value === null && this.props.defaultValue) {
this.setState({
value: this.props.defaultValue
});
@ -49,28 +52,26 @@ let InputTextAreaToggable = React.createClass({
},
render() {
let className = 'form-control ascribe-textarea';
let textarea = null;
const { convertLinks, disabled, onBlur, placeholder, required, rows } = this.props;
const { value } = this.state;
if(!this.props.disabled) {
className = className + ' ascribe-textarea-editable';
textarea = (
if (!disabled) {
return (
<TextareaAutosize
ref='textarea'
className={className}
value={this.state.value}
rows={this.props.rows}
className='form-control ascribe-textarea ascribe-textarea-editable'
value={value}
rows={rows}
maxRows={10}
required={this.props.required}
required={required}
onChange={this.handleChange}
onBlur={this.props.onBlur}
placeholder={this.props.placeholder} />
onBlur={onBlur}
placeholder={placeholder} />
);
} else {
textarea = <pre className="ascribe-pre">{this.state.value}</pre>;
// Can only convert links when not editable, as textarea does not support anchors
return <pre className="ascribe-pre">{convertLinks ? anchorize(value) : value}</pre>;
}
return textarea;
}
});

View File

@ -134,6 +134,7 @@ let CylandAdditionalDataForm = React.createClass({
expanded={!disabled || !!extraData.artist_contact_information}>
<InputTextAreaToggable
rows={1}
convertLinks
defaultValue={extraData.artist_contact_information}
placeholder={getLangText('Enter the artist\'s contact information...')} />
</Property>

View File

@ -110,6 +110,7 @@ let IkonotvArtistDetailsForm = React.createClass({
expanded={!disabled || !!extraData.artist_website}>
<InputTextAreaToggable
rows={1}
convertLinks
defaultValue={extraData.artist_website}
placeholder={getLangText('The artist\'s website if present...')} />
</Property>
@ -119,6 +120,7 @@ let IkonotvArtistDetailsForm = React.createClass({
expanded={!disabled || !!extraData.gallery_website}>
<InputTextAreaToggable
rows={1}
convertLinks
defaultValue={extraData.gallery_website}
placeholder={getLangText('The website of any related Gallery or Museum')} />
</Property>
@ -128,6 +130,7 @@ let IkonotvArtistDetailsForm = React.createClass({
expanded={!disabled || !!extraData.additional_websites}>
<InputTextAreaToggable
rows={1}
convertLinks
defaultValue={extraData.additional_websites}
placeholder={getLangText('Enter additional Websites/Publications if any')} />
</Property>

View File

@ -1,5 +1,9 @@
'use strict';
import React from 'react';
import { getLinkRegex, isEmail } from './regex_utils';
/**
* Set the title in the browser window.
*/
@ -13,21 +17,24 @@ export function setDocumentTitle(title) {
* @param {object} elementAttributes: hash table containing the attributes of the relevant element
*/
function constructHeadElement(elementType, elementId, elementAttributes) {
let head = (document.head || document.getElementsByTagName('head')[0]);
let element = document.createElement(elementType);
let oldElement = document.getElementById(elementId);
const head = (document.head || document.getElementsByTagName('head')[0]);
const element = document.createElement(elementType);
const oldElement = document.getElementById(elementId);
element.setAttribute('id', elementId);
for (let k in elementAttributes){
for (let k in elementAttributes) {
try {
element.setAttribute(k, elementAttributes[k]);
}
catch(e){
} catch(e) {
console.warn(e.message);
}
}
if (oldElement) {
head.removeChild(oldElement);
}
head.appendChild(element);
}
@ -37,9 +44,68 @@ function constructHeadElement(elementType, elementId, elementAttributes) {
*/
export function constructHead(headObject){
for (let k in headObject){
let favicons = headObject[k];
const favicons = headObject[k];
for (let f in favicons){
constructHeadElement(k, f, favicons[f]);
}
}
}
}
/**
* Replaces the links and emails in a given string with anchor elements.
*
* @param {string} string String to anchorize
* @param {(object)} options Options object for anchorizing
* @param {(boolean)} emails Whether or not to replace emails (default: true)
* @param {(boolean)} links Whether or not to replace links (default: true)
* @param {(string)} target Anchor target attribute (default: '_blank')
* @return {string|React.element[]} Anchorized string as usable react element, either as an array of
* elements or just a string
*/
export function anchorize(string, { emails: replaceEmail = true, links: replaceLink = true, target = '_blank' } = {}) {
if (!replaceEmail && !replaceLink) {
return string;
}
const linkRegex = getLinkRegex();
const strWithAnchorElems = [];
let lastMatchIndex = 0;
let regexMatch;
while (regexMatch = linkRegex.exec(string)) {
const [ matchedStr, schemeName ] = regexMatch;
const matchedStrIsEmail = isEmail(matchedStr);
let anchorizedMatch;
if (matchedStrIsEmail && replaceEmail) {
anchorizedMatch = (<a href={`mailto:${matchedStr}`}>{matchedStr}</a>);
} else if (!matchedStrIsEmail && replaceLink) {
anchorizedMatch = (<a href={`${schemeName ? matchedStr : ('http://' + matchedStr)}`} target={target}>{matchedStr}</a>);
}
// We only need to add an element to the array and update the lastMatchIndex if we actually create an anchor
if (anchorizedMatch) {
// First add the string between the end of the last anchor text and the start of the current match
const currentMatchStartIndex = linkRegex.lastIndex - matchedStr.length;
if (lastMatchIndex !== currentMatchStartIndex) {
strWithAnchorElems.push(string.substring(lastMatchIndex, currentMatchStartIndex));
}
strWithAnchorElems.push(anchorizedMatch);
lastMatchIndex = linkRegex.lastIndex;
}
}
if (strWithAnchorElems.length) {
// Add the string between the end of the last anchor and the end of the string
if (lastMatchIndex !== string.length) {
strWithAnchorElems.push(string.substring(lastMatchIndex));
}
return strWithAnchorElems;
} else {
return string;
}
}

View File

@ -1,7 +1,56 @@
'use strict'
export function isEmail(string) {
// TODO: Create Unittests that test all functions
// We return new regexes everytime as opposed to using a constant regex because
// regexes with the global flag maintain internal iterators that can cause problems:
// http://bjorn.tipling.com/state-and-regular-expressions-in-javascript
// http://www.2ality.com/2013/08/regexp-g.html
export function getEmailRegex() {
// This is a bit of a weak test for an email, but you really can't win them all
// http://stackoverflow.com/questions/201323/using-a-regular-expression-to-validate-an-email-address
return !!string && string.match(/.*@.*\..*/);
return /.*@.*\..*/g;
}
export function getLinkRegex() {
// You really can't win them all with urls too (unless a 500 character regex that adheres
// to a strict interpretation of urls sounds like fun!)
// https://mathiasbynens.be/demo/url-regex
//
// This was initially based off of the one angular uses for its linky
// (https://github.com/angular/angular.js/blob/master/src/ngSanitize/filter/linky.js)...
// but then it evovled into its own thing to support capturing groups for filtering the
// hostname and other technically valid urls.
//
// Capturing groups:
// 1. URL scheme
// 2. URL without scheme
// 3. Host name
// 4. Path
// 5. Fragment
//
// Passes most tests of https://mathiasbynens.be/demo/url-regex, but provides a few false
// positives for some tests that are too strict (like `foo.com`). There are a few other
// false positives, such as `http://www.foo.bar./` but c'mon, that one's not my fault.
// I'd argue we would want to match that as a link anyway.
//
// Note: This also catches emails, as otherwise it would match the `ascribe.io` in `hi@ascribe.io`,
// producing (what I think is) more surprising behaviour than the alternative.
return /\b(https?:\/\/)?((?:www\.)?((?:[^\s.,;()\/]+\.)+[^\s$_!*()$&.,;=?+\/\#]+)((?:\/|\?|\/\?)[^\s#^`{}<>?"\[\]\/\|]+)*\/?(#[^\s#%^`{}<>?"\[\]\/\|]*)?)/g;
}
/**
* @param {string} string String to check
* @return {boolean} Whether string is an email or not
*/
export function isEmail(string) {
return !!string && string.match(getEmailRegex());
}
/**
* @param {string} string String to check
* @return {boolean} Whether string is an link or not
*/
export function isLink(string) {
return !!string && string.match(getLinkRegex());
}

View File

@ -14,7 +14,7 @@
font-family: inherit;
margin: 0;
padding: 0;
text-align: justify;
text-align: left;
white-space: -moz-pre-wrap;
white-space: -o-pre-wrap;
white-space: -pre-wrap;