Comparing version 0.2.31 to 0.2.32
@@ -10,2 +10,16 @@ 'use strict'; | ||
var PrettyTag = React.createClass({ | ||
displayName: 'PrettyTag', | ||
render: function render() { | ||
var classes = cx(_.extend({}, this.props.classes, { 'pretty-part': true })); | ||
return React.createElement( | ||
'span', | ||
{ className: classes, onClick: this.props.onClick }, | ||
this.props.children | ||
); | ||
} | ||
}); | ||
/* | ||
@@ -20,16 +34,2 @@ Editor for tagged text. Renders text like "hello {{firstName}}" | ||
Initially a read-only view using a simple div is shown. | ||
IMPLEMENTATION NOTE: | ||
To display the tags inside CodeMirror we are using CM's | ||
specialCharPlaceholder feature, to replace special characters with | ||
custom DOM nodes. This feature is designed for single character | ||
replacements, not tags like 'firstName'. So we replace each tag | ||
with an unused character from the Unicode private use area, and | ||
tell CM to replace that with a DOM node display the tag label with | ||
the pill box effect. | ||
Is this evil? Perhaps a little, but delete, undo, redo, cut, copy | ||
and paste of the tag pill boxes just work because CM treats them as | ||
atomic single characters, and it's not much code on our part. | ||
*/ | ||
@@ -42,2 +42,3 @@ module.exports = React.createClass({ | ||
componentDidMount: function componentDidMount() { | ||
console.log('------- pt2'); | ||
this.createEditor(); | ||
@@ -52,22 +53,2 @@ }, | ||
} | ||
// If they just typed in a tag like {{firstName}} we have to replace it | ||
if (this.state.codeMirrorMode && this.codeMirror.getValue().match(/\{\{.+\}\}/)) { | ||
// avoid recursive update cycle | ||
this.updatingCodeMirror = true; | ||
// get new encoded value for CodeMirror | ||
var cmValue = this.codeMirror.getValue(); | ||
var decodedValue = this.state.translator.decodeValue(cmValue); | ||
var encodedValue = this.state.translator.encodeValue(decodedValue); | ||
// Grab the cursor so we can reset it. | ||
// The new length of the CM value will be shorter after replacing a tag like {{firstName}} | ||
// with a single special char, so adjust cursor position accordingly. | ||
var cursor = this.codeMirror.getCursor(); | ||
cursor.ch -= cmValue.length - encodedValue.length; | ||
this.codeMirror.setValue(encodedValue); | ||
this.codeMirror.setCursor(cursor); | ||
} | ||
}, | ||
@@ -97,7 +78,6 @@ | ||
var nextState = { | ||
replaceChoices: replaceChoices | ||
replaceChoices: replaceChoices, | ||
translator: TagTranslator(replaceChoices, this.props.config.humanize) | ||
}; | ||
this.state.translator.addChoices(replaceChoices); | ||
if (this.state.value !== nextProps.field.value && nextProps.field.value) { | ||
@@ -111,9 +91,13 @@ nextState.value = nextProps.field.value; | ||
handleChoiceSelection: function handleChoiceSelection(key) { | ||
this.setState({ isChoicesOpen: false }); | ||
var pos = this.state.selectedTagPos; | ||
var tag = '{{' + key + '}}'; | ||
var char = this.state.translator.encodeTag(key); | ||
if (pos) { | ||
this.codeMirror.replaceRange(tag, { line: pos.line, ch: pos.start }, { line: pos.line, ch: pos.stop }); | ||
} else { | ||
this.codeMirror.replaceSelection(tag, 'end'); | ||
} | ||
this.codeMirror.focus(); | ||
// put the cursor at the end of the inserted tag. | ||
this.codeMirror.replaceSelection(char, 'end'); | ||
this.codeMirror.focus(); | ||
this.setState({ isChoicesOpen: false, selectedTagPos: null }); | ||
}, | ||
@@ -130,7 +114,9 @@ | ||
var tabIndex = field.tabIndex; | ||
var textBoxClasses = cx(_.extend({}, this.props.classes, { 'pretty-text-box': true })); | ||
var textBox = this.createTextBoxNode(); | ||
var insertBtn = config.createElement('insert-button', { ref: 'toggle', onClick: this.onToggleChoices }, 'Insert...'); | ||
var onInsertClick = function onInsertClick() { | ||
this.setState({ selectedTagPos: null }); | ||
this.onToggleChoices(); | ||
}; | ||
var insertBtn = config.createElement('insert-button', { ref: 'toggle', onClick: onInsertClick.bind(this) }, 'Insert...'); | ||
@@ -146,4 +132,3 @@ var choices = config.createElement('choices', { | ||
// Render read-only version. We are using pure HTML via dangerouslySetInnerHTML, to avoid | ||
// the cost of the react nodes. This is probably a premature optimization. | ||
// Render read-only version. | ||
var element = React.createElement( | ||
@@ -155,3 +140,3 @@ 'div', | ||
{ className: textBoxClasses, tabIndex: tabIndex, onFocus: this.onFocusAction, onBlur: this.onBlurAction }, | ||
textBox | ||
React.createElement('div', { ref: 'textBox', className: 'internal-text-wrapper' }) | ||
), | ||
@@ -185,11 +170,2 @@ insertBtn, | ||
createTextBoxNode: function createTextBoxNode() { | ||
if (this.state.codeMirrorMode) { | ||
return React.createElement('div', { ref: 'textBox', className: 'internal-text-wrapper' }); | ||
} else { | ||
var html = this.state.translator.toHtml(this.state.value); | ||
return React.createElement('div', { ref: 'textBox', className: 'internal-text-wrapper', dangerouslySetInnerHTML: { __html: html } }); | ||
} | ||
}, | ||
createEditor: function createEditor() { | ||
@@ -204,10 +180,6 @@ if (this.state.codeMirrorMode) { | ||
createCodeMirrorEditor: function createCodeMirrorEditor() { | ||
var cmValue = this.state.translator.encodeValue(this.state.value); | ||
var options = { | ||
lineWrapping: true, | ||
tabindex: this.props.tabIndex, | ||
value: cmValue, | ||
specialChars: this.state.translator.specialCharsRegexp, | ||
specialCharPlaceholder: this.createTagNode, | ||
value: this.state.value, | ||
extraKeys: { | ||
@@ -223,4 +195,20 @@ Tab: false | ||
this.codeMirror.on('change', this.onCodeMirrorChange); | ||
this.tagCodeMirror(); | ||
}, | ||
tagCodeMirror: function tagCodeMirror() { | ||
var positions = this.state.translator.getTagPositions(this.codeMirror.getValue()); | ||
var self = this; | ||
var tagOps = function tagOps() { | ||
positions.forEach(function (pos) { | ||
var node = self.createTagNode(pos); | ||
self.codeMirror.markText({ line: pos.line, ch: pos.start }, { line: pos.line, ch: pos.stop }, { replacedWith: node, handleMouseEvents: true }); | ||
}); | ||
}; | ||
this.codeMirror.operation(tagOps); | ||
}, | ||
onCodeMirrorChange: function onCodeMirrorChange() { | ||
@@ -233,10 +221,41 @@ if (this.updatingCodeMirror) { | ||
var newValue = this.state.translator.decodeValue(this.codeMirror.getValue()); | ||
var newValue = this.codeMirror.getValue(); | ||
this.onChangeValue(newValue); | ||
this.setState({ value: newValue }); | ||
this.tagCodeMirror(); | ||
}, | ||
getTagClasses: function getTagClasses(tag) { | ||
var choice = _.find(this.state.replaceChoices, function (c) { | ||
return c.value === tag; | ||
}); | ||
return choice && choice.tagClasses || {}; | ||
}, | ||
createReadonlyEditor: function createReadonlyEditor() { | ||
var textBoxNode = this.refs.textBox.getDOMNode(); | ||
textBoxNode.innerHTML = this.state.translator.toHtml(this.state.value); | ||
var tokens = this.state.translator.tokenize(this.state.value); | ||
var self = this; | ||
var nodes = tokens.map(function (part) { | ||
if (part.type === 'tag') { | ||
var tagClasses = self.getTagClasses(part.value); | ||
return React.createElement( | ||
PrettyTag, | ||
{ classes: tagClasses }, | ||
self.state.translator.getLabel(part.value) | ||
); | ||
} | ||
return React.createElement( | ||
'span', | ||
null, | ||
part.value | ||
); | ||
}); | ||
React.render(React.createElement( | ||
'span', | ||
null, | ||
nodes | ||
), textBoxNode); | ||
}, | ||
@@ -257,11 +276,15 @@ | ||
// Create pill box style for display inside CM. For example | ||
// '\ue000' becomes '<span class="tag>First Name</span>' | ||
createTagNode: function createTagNode(char) { | ||
createTagNode: function createTagNode(pos) { | ||
var node = document.createElement('span'); | ||
var label = this.state.translator.decodeChar(char); | ||
var label = this.state.translator.getLabel(pos.tag); | ||
var tagClasses = this.getTagClasses(pos.tag); | ||
var onTagClick = function onTagClick() { | ||
this.setState({ selectedTagPos: pos }); | ||
this.onToggleChoices(); | ||
}; | ||
React.render(React.createElement( | ||
'span', | ||
{ className: 'pretty-part', onClick: this.onToggleChoices }, | ||
PrettyTag, | ||
{ classes: tagClasses, onClick: onTagClick.bind(this) }, | ||
label | ||
@@ -268,0 +291,0 @@ ), node); |
'use strict'; | ||
// Constant for first unused special use character. | ||
// See IMPLEMENTATION NOTE in pretty-text2.js. | ||
var FIRST_SPECIAL_CHAR = 57344; | ||
// regexp used to grep out tags like {{firstName}} | ||
var TAGS_REGEXP = /\{\{(.+?)\}\}/g; | ||
function buildChoicesMap(replaceChoices) { | ||
@@ -22,95 +15,9 @@ var choices = {}; | ||
an encoded representation suitable for use in CodeMirror. | ||
See IMPLEMENTATION NOTE in pretty-text2.js. | ||
*/ | ||
function TagTranslator(replaceChoices, humanize) { | ||
var nextCharCode = FIRST_SPECIAL_CHAR; | ||
// Map of tag to label 'firstName' --> 'First Name' | ||
var choices = {}; | ||
var choices = buildChoicesMap(replaceChoices); | ||
// To help translate to and from the CM representation with the special | ||
// characters, build two maps: | ||
// - charToTagMap: special char to tag - i.e. { '\ue000': 'firstName' } | ||
// - tagToCharMap: tag to special char, i.e. { firstName: '\ue000' } | ||
var charToTagMap = {}; | ||
var tagToCharMap = {}; | ||
function addChoices(choicesArray) { | ||
choices = buildChoicesMap(choicesArray); | ||
Object.keys(choices).sort().forEach(function (tag) { | ||
if (tagToCharMap[tag]) { | ||
return; // we already have this tag mapped | ||
} | ||
var char = String.fromCharCode(nextCharCode++); | ||
charToTagMap[char] = tag; | ||
tagToCharMap[tag] = char; | ||
}); | ||
} | ||
addChoices(replaceChoices); | ||
return { | ||
specialCharsRegexp: /[\ue000-\uefff]/g, | ||
addChoices: addChoices, | ||
/* | ||
Convert tag to encoded character. For example | ||
'firstName' becomes '\ue000'. | ||
*/ | ||
encodeTag: function encodeTag(tag) { | ||
if (!tagToCharMap[tag]) { | ||
var char = String.fromCharCode(nextCharCode++); | ||
tagToCharMap[tag] = char; | ||
charToTagMap[char] = tag; | ||
} | ||
return tagToCharMap[tag]; | ||
}, | ||
/* | ||
Convert text value to encoded value for CodeMirror. For example | ||
'hello {{firstName}}' becomes 'hello \ue000' | ||
*/ | ||
encodeValue: function encodeValue(value) { | ||
return String(value).replace(TAGS_REGEXP, (function (m, tag) { | ||
return this.encodeTag(tag); | ||
}).bind(this)); | ||
}, | ||
/* | ||
Convert encoded text used in CM to tagged text. For example | ||
'hello \ue000' becomes 'hello {{firstName}}' | ||
*/ | ||
decodeValue: function decodeValue(encodedValue) { | ||
return String(encodedValue).replace(this.specialCharsRegexp, function (c) { | ||
var tag = charToTagMap[c]; | ||
return '{{' + tag + '}}'; | ||
}); | ||
}, | ||
/* | ||
Convert encoded character to label. For example | ||
'\ue000' becomes 'Last Name'. | ||
*/ | ||
decodeChar: function decodeChar(char) { | ||
var tag = charToTagMap[char]; | ||
return this.getLabel(tag); | ||
}, | ||
/* | ||
Convert tagged value to HTML. For example | ||
'hello {{firstName}}' becomes 'hello <span class="tag">First Name</span>' | ||
*/ | ||
toHtml: function toHtml(value) { | ||
return String(value).replace(TAGS_REGEXP, (function (m, mustache) { | ||
var tag = mustache.replace('{{', '').replace('}}', ''); | ||
var label = this.getLabel(tag); | ||
return '<span class="pretty-part">' + label + '</span>'; | ||
}).bind(this)); | ||
}, | ||
/* | ||
Get label for tag. For example 'firstName' becomes 'First Name'. | ||
@@ -127,2 +34,42 @@ Returns a humanized version of the tag if we don't have a label for the tag. | ||
return label; | ||
}, | ||
tokenize: function tokenize(text) { | ||
var regexp = /(\{\{|\}\})/; | ||
var parts = text.split(regexp); | ||
var tokens = []; | ||
var inTag = false; | ||
parts.forEach(function (part) { | ||
if (part === '{{') { | ||
inTag = true; | ||
} else if (part === '}}') { | ||
inTag = false; | ||
} else if (inTag) { | ||
tokens.push({ type: 'tag', value: part }); | ||
} else { | ||
tokens.push({ type: 'string', value: part }); | ||
} | ||
}); | ||
return tokens; | ||
}, | ||
getTagPositions: function getTagPositions(text) { | ||
var lines = text.split('\n'); | ||
var re = /\{\{.+?\}\}/g; | ||
var positions = []; | ||
var m; | ||
for (var i = 0; i < lines.length; i++) { | ||
while ((m = re.exec(lines[i])) !== null) { | ||
var tag = m[0].substring(2, m[0].length - 2); | ||
positions.push({ | ||
line: i, | ||
start: m.index, | ||
stop: m.index + m[0].length, | ||
tag: tag | ||
}); | ||
} | ||
} | ||
return positions; | ||
} | ||
@@ -129,0 +76,0 @@ }; |
{ | ||
"name": "formatic", | ||
"version": "0.2.31", | ||
"version": "0.2.32", | ||
"description": "Automatic, pluggable form generation", | ||
@@ -5,0 +5,0 @@ "main": "./build/lib/formatic", |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
735702
10024