mirror of
https://github.com/ascribe/onion.git
synced 2024-12-22 09:23:13 +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() {
|
||||
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>
|
||||
|
@ -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 />
|
||||
|
@ -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;
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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());
|
||||
}
|
||||
|
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user