diff --git a/js/components/ascribe_detail/further_details.js b/js/components/ascribe_detail/further_details.js index 54e696c9..b1d9c637 100644 --- a/js/components/ascribe_detail/further_details.js +++ b/js/components/ascribe_detail/further_details.js @@ -58,39 +58,42 @@ let FurtherDetails = React.createClass({ }, render() { + const { editable, extraData, otherData, pieceId } = this.props; + return ( + pieceId={pieceId} /> + pieceId={pieceId} /> + pieceId={pieceId} />
diff --git a/js/components/ascribe_forms/form_piece_extradata.js b/js/components/ascribe_forms/form_piece_extradata.js index eb1ab94c..9ba53f3b 100644 --- a/js/components/ascribe_forms/form_piece_extradata.js +++ b/js/components/ascribe_forms/form_piece_extradata.js @@ -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 (
+ handleSuccess={handleSuccess} + url={requests.prepareUrl(ApiUrls.piece_extradata, { piece_id: pieceId })}> diff --git a/js/components/ascribe_forms/input_textarea_toggable.js b/js/components/ascribe_forms/input_textarea_toggable.js index 0be8b87a..05a1f011 100644 --- a/js/components/ascribe_forms/input_textarea_toggable.js +++ b/js/components/ascribe_forms/input_textarea_toggable.js @@ -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 ( + onBlur={onBlur} + placeholder={placeholder} /> ); } else { - textarea =
{this.state.value}
; + // Can only convert links when not editable, as textarea does not support anchors + return
{convertLinks ? anchorize(value) : value}
; } - - return textarea; } }); diff --git a/js/components/whitelabel/wallet/components/cyland/cyland_forms/cyland_additional_data_form.js b/js/components/whitelabel/wallet/components/cyland/cyland_forms/cyland_additional_data_form.js index 6e643134..c41caa76 100644 --- a/js/components/whitelabel/wallet/components/cyland/cyland_forms/cyland_additional_data_form.js +++ b/js/components/whitelabel/wallet/components/cyland/cyland_forms/cyland_additional_data_form.js @@ -134,6 +134,7 @@ let CylandAdditionalDataForm = React.createClass({ expanded={!disabled || !!extraData.artist_contact_information}>
diff --git a/js/components/whitelabel/wallet/components/ikonotv/ikonotv_forms/ikonotv_artist_details_form.js b/js/components/whitelabel/wallet/components/ikonotv/ikonotv_forms/ikonotv_artist_details_form.js index 598f5bd0..49ead8ec 100644 --- a/js/components/whitelabel/wallet/components/ikonotv/ikonotv_forms/ikonotv_artist_details_form.js +++ b/js/components/whitelabel/wallet/components/ikonotv/ikonotv_forms/ikonotv_artist_details_form.js @@ -110,6 +110,7 @@ let IkonotvArtistDetailsForm = React.createClass({ expanded={!disabled || !!extraData.artist_website}> @@ -119,6 +120,7 @@ let IkonotvArtistDetailsForm = React.createClass({ expanded={!disabled || !!extraData.gallery_website}> @@ -128,6 +130,7 @@ let IkonotvArtistDetailsForm = React.createClass({ expanded={!disabled || !!extraData.additional_websites}> diff --git a/js/utils/dom_utils.js b/js/utils/dom_utils.js index d009f90f..f0cd852c 100644 --- a/js/utils/dom_utils.js +++ b/js/utils/dom_utils.js @@ -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]); } } -} \ No newline at end of file +} + +/** + * 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 = ({matchedStr}); + } else if (!matchedStrIsEmail && replaceLink) { + anchorizedMatch = ({matchedStr}); + } + + // 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; + } +} diff --git a/js/utils/regex_utils.js b/js/utils/regex_utils.js index af948b2b..49412d07 100644 --- a/js/utils/regex_utils.js +++ b/js/utils/regex_utils.js @@ -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()); } diff --git a/sass/ascribe_textarea.scss b/sass/ascribe_textarea.scss index bcd0502a..e241e442 100644 --- a/sass/ascribe_textarea.scss +++ b/sass/ascribe_textarea.scss @@ -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;