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:
commit
a23dd99b94
@ -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>
|
||||||
|
@ -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 />
|
||||||
|
@ -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;
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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());
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
Loading…
Reference in New Issue
Block a user