1
0
mirror of https://github.com/ascribe/onion.git synced 2024-12-22 17:33:14 +01:00

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

View File

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

View File

@ -4,17 +4,20 @@ import React from 'react';
import TextareaAutosize from 'react-textarea-autosize'; import TextareaAutosize from 'react-textarea-autosize';
import { anchorize } from '../../utils/dom_utils';
let InputTextAreaToggable = React.createClass({ let InputTextAreaToggable = React.createClass({
propTypes: { propTypes: {
autoFocus: React.PropTypes.bool, autoFocus: React.PropTypes.bool,
disabled: React.PropTypes.bool, convertLinks: React.PropTypes.bool,
rows: React.PropTypes.number.isRequired,
required: React.PropTypes.bool,
defaultValue: React.PropTypes.string, defaultValue: React.PropTypes.string,
placeholder: React.PropTypes.string, disabled: React.PropTypes.bool,
onBlur: React.PropTypes.func, 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() { getInitialState() {
@ -36,7 +39,7 @@ let InputTextAreaToggable = React.createClass({
componentDidUpdate() { componentDidUpdate() {
// If the initial value of state.value is null, we want to set props.defaultValue // 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 // 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({ this.setState({
value: this.props.defaultValue value: this.props.defaultValue
}); });
@ -49,28 +52,26 @@ let InputTextAreaToggable = React.createClass({
}, },
render() { render() {
let className = 'form-control ascribe-textarea'; const { convertLinks, disabled, onBlur, placeholder, required, rows } = this.props;
let textarea = null; const { value } = this.state;
if(!this.props.disabled) { if (!disabled) {
className = className + ' ascribe-textarea-editable'; return (
textarea = (
<TextareaAutosize <TextareaAutosize
ref='textarea' ref='textarea'
className={className} className='form-control ascribe-textarea ascribe-textarea-editable'
value={this.state.value} value={value}
rows={this.props.rows} rows={rows}
maxRows={10} maxRows={10}
required={this.props.required} required={required}
onChange={this.handleChange} onChange={this.handleChange}
onBlur={this.props.onBlur} onBlur={onBlur}
placeholder={this.props.placeholder} /> placeholder={placeholder} />
); );
} else { } 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}> expanded={!disabled || !!extraData.artist_contact_information}>
<InputTextAreaToggable <InputTextAreaToggable
rows={1} rows={1}
convertLinks
defaultValue={extraData.artist_contact_information} defaultValue={extraData.artist_contact_information}
placeholder={getLangText('Enter the artist\'s contact information...')} /> placeholder={getLangText('Enter the artist\'s contact information...')} />
</Property> </Property>

View File

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

View File

@ -1,5 +1,9 @@
'use strict'; 'use strict';
import React from 'react';
import { getLinkRegex, isEmail } from './regex_utils';
/** /**
* Set the title in the browser window. * 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 * @param {object} elementAttributes: hash table containing the attributes of the relevant element
*/ */
function constructHeadElement(elementType, elementId, elementAttributes) { function constructHeadElement(elementType, elementId, elementAttributes) {
let head = (document.head || document.getElementsByTagName('head')[0]); const head = (document.head || document.getElementsByTagName('head')[0]);
let element = document.createElement(elementType); const element = document.createElement(elementType);
let oldElement = document.getElementById(elementId); const oldElement = document.getElementById(elementId);
element.setAttribute('id', elementId); element.setAttribute('id', elementId);
for (let k in elementAttributes){
for (let k in elementAttributes) {
try { try {
element.setAttribute(k, elementAttributes[k]); element.setAttribute(k, elementAttributes[k]);
} } catch(e) {
catch(e){
console.warn(e.message); console.warn(e.message);
} }
} }
if (oldElement) { if (oldElement) {
head.removeChild(oldElement); head.removeChild(oldElement);
} }
head.appendChild(element); head.appendChild(element);
} }
@ -37,9 +44,68 @@ function constructHeadElement(elementType, elementId, elementAttributes) {
*/ */
export function constructHead(headObject){ export function constructHead(headObject){
for (let k in headObject){ for (let k in headObject){
let favicons = headObject[k]; const favicons = headObject[k];
for (let f in favicons){ for (let f in favicons){
constructHeadElement(k, f, favicons[f]); 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' '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 // 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 // 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; font-family: inherit;
margin: 0; margin: 0;
padding: 0; padding: 0;
text-align: justify; text-align: left;
white-space: -moz-pre-wrap; white-space: -moz-pre-wrap;
white-space: -o-pre-wrap; white-space: -o-pre-wrap;
white-space: -pre-wrap; white-space: -pre-wrap;