1
0
mirror of https://github.com/ascribe/onion.git synced 2025-01-03 10:25:08 +01:00

Add anchorize utility to textareas

And Tim thought this was going to be a piece of cake… next time he can
play with the url regex.
This commit is contained in:
Brett Sun 2016-01-07 18:29:55 +01:00
parent 2acf3f4056
commit 74a587906b
5 changed files with 181 additions and 58 deletions

View File

@ -32,13 +32,13 @@ let FurtherDetails = React.createClass({
};
},
showNotification(){
showNotification() {
this.props.handleSuccess();
let notification = new GlobalNotificationModel('Details updated', 'success');
const notification = new GlobalNotificationModel('Details updated', 'success');
GlobalNotificationActions.appendGlobalNotification(notification);
},
submitFile(file){
submitFile(file) {
this.setState({
otherDataKey: file.key
});
@ -51,6 +51,8 @@ let FurtherDetails = React.createClass({
},
render() {
const { editable, extraData, otherData, pieceId } = this.props;
return (
<Row>
<Col md={12} className="ascribe-edition-personal-note">
@ -58,33 +60,33 @@ let FurtherDetails = React.createClass({
name='artist_contact_info'
title='Artist Contact Info'
handleSuccess={this.showNotification}
editable={this.props.editable}
pieceId={this.props.pieceId}
extraData={this.props.extraData}
/>
editable={editable}
pieceId={pieceId}
extraData={extraData}
convertLinks />
<PieceExtraDataForm
name='display_instructions'
title='Display Instructions'
handleSuccess={this.showNotification}
editable={this.props.editable}
pieceId={this.props.pieceId}
extraData={this.props.extraData} />
editable={editable}
pieceId={pieceId}
extraData={extraData} />
<PieceExtraDataForm
name='technology_details'
title='Technology Details'
handleSuccess={this.showNotification}
editable={this.props.editable}
pieceId={this.props.pieceId}
extraData={this.props.extraData} />
editable={editable}
pieceId={pieceId}
extraData={extraData} />
<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

@ -18,38 +18,43 @@ let PieceExtraDataForm = React.createClass({
handleSuccess: React.PropTypes.func,
name: React.PropTypes.string,
title: React.PropTypes.string,
convertLinks: React.PropTypes.bool,
editable: React.PropTypes.bool
},
getFormData() {
let extradata = {};
extradata[this.props.name] = this.refs.form.refs[this.props.name].state.value;
return {
extradata: extradata,
extradata: {
[this.props.name]: this.refs.form.refs[this.props.name].state.value
},
piece_id: this.props.pieceId
};
},
render() {
let defaultValue = this.props.extraData[this.props.name] || '';
if (defaultValue.length === 0 && !this.props.editable){
const { convertLinks, editable, extraData, handleSuccess, name, pieceId, title } = this.props;
const defaultValue = this.props.extraData[this.props.name] || '';
if (defaultValue.length === 0 && !editable){
return null;
}
let url = requests.prepareUrl(ApiUrls.piece_extradata, {piece_id: this.props.pieceId});
const url = requests.prepareUrl(ApiUrls.piece_extradata, {piece_id: pieceId});
return (
<Form
ref='form'
url={url}
handleSuccess={this.props.handleSuccess}
handleSuccess={handleSuccess}
getFormData={this.getFormData}
disabled={!this.props.editable}>
disabled={!editable}>
<Property
name={this.props.name}
label={this.props.title}>
name={name}
label={title}>
<InputTextAreaToggable
rows={1}
convertLinks={convertLinks}
defaultValue={defaultValue}
placeholder={getLangText('Fill in%s', ' ') + this.props.title}
placeholder={getLangText('Fill in%s', ' ') + title}
required />
</Property>
<hr />

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

@ -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());
}