(function ($) {
// Use underscore.js html escaper
// http://underscorejs.org/#escape
var _escape;
(function () {
var entityMap = {
escape: {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": '''
}
};
var escapeKeys = [];
for (var k in entityMap.escape) {
if (entityMap.escape.hasOwnProperty(k)) {
escapeKeys.push(k);
}
}
escapeKeys = escapeKeys.join('');
var entityRegexes = {
escape: new RegExp('[' + escapeKeys + ']', 'g')
};
_escape = function (string) {
if (string == null) { return ''; }
return ('' + string).replace(entityRegexes['escape'], function (match) {
return entityMap['escape'][match];
});
};
})();
var _defaultOptions = {
linkAttributes: {
title: 'Click outside of link to edit',
contenteditable: 'false'
},
parseLinks: true,
newlines: true,
editable: true
};
var _options = null;
var _methods = {
destroy: function () {
// remove listeners
this.off('change keydown keypress input', _placeholderUpdate);
this.off('blur', this.data('onBlurListener'));
this.off('focus', this.data('onFocusListener'));
this.find('a').off('mousedown', this.data('onMouseDownListener'));
},
value: function (val) {
if (typeof val === 'string') {
return _methods.setValue.call(this, val);
}
return _methods.getValue.call(this);
},
getValue: function () {
var text, inner;
var textEl = this.clone();
var children = textEl.children('div');
textEl.find('br').replaceWith('\n');
if (children) {
children.detach();
text = textEl.text();
children.each(function (indx, child) {
inner = $(child).text();
if (inner === '\n') {
text += inner;
} else {
text += ('\n' + inner);
}
});
} else {
text = textEl.text();
}
return text;
},
setValue: function (val) {
if (_options && _options.parseLinks) {
this.html(_parseLinks(val));
} else {
this.html(val);
}
_placeholderUpdate.call(this);
return this;
}
};
var _parseLinks = function (text, options) {
if (!text) { return ''; }
options || (options = {});
var linkMaps = [];
var escaped = [];
// use String.prototype.replace because its a crossbrowser way to iterate over links
text.replace(_linkDetectionRegex, function () {
var url = arguments[0];
var index = arguments[arguments.length - 2];
// Map the positions of link and non-link text
linkMaps.push({
linkTag: _makeLink(url, options.linkAttributes),
linkStart: index,
linkEnd: index + url.length
});
// don't actually change the text yet
return url;
});
var lastI = 0;
// Go though each link, and escape the text between it and the last link
for (var i = 0; i < linkMaps.length; i++) {
escaped.push(_escape(text.slice(lastI, linkMaps[i].linkStart)));
escaped.push(linkMaps[i].linkTag);
lastI = linkMaps[i].linkEnd;
};
// Escape everything after the last link
if (lastI) {
escaped.push(_escape(text.slice(lastI)));
} else {
// or if there where no links just escape the whole string
escaped.push(_escape(text));
}
escaped = escaped.join('');
if (options.newlines) { escaped = escaped.replace(/\n/g, '
'); }
return escaped;
};
var _makeLink = function (url, attrs) {
var linkStart = "";
var linkAttributes = '';
for (var a in attrs) {
if (attrs.hasOwnProperty(a) && a !== 'href' && a !== 'src') {
linkAttributes += (a + "='" + attrs[a] + "' ");
}
}
return linkStart + linkAttributes + linkEnd;
};
var _linkDetectionRegex = /(([a-z]+:\/\/)?(([a-z0-9\-]+\.)+([a-z]{2}|aero|arpa|biz|com|coop|edu|gov|info|int|jobs|mil|museum|name|nato|net|org|pro|travel|local|internal))(:[0-9]{1,5})?(\/[a-z0-9_\-\.~]+)*(\/([a-z0-9_\-\.]*)(\?[a-z0-9+_\-\.%=&]*)?)?(#[a-zA-Z0-9!$&'()*+.=-_~:@/?]*)?)(?=(\)|\(|\<|\>|\s|$))/gi;
// emulate placeholder text in contenteditable HTML
// this was inspired by https://github.com/sprucemedia/jQuery.divPlaceholder.js
var _placeholderUpdate = function () {
$(this).each(function () {
if (this.textContent) {
this.setAttribute('data-div-placeholder-content', 'true');
}
else {
this.removeAttribute('data-div-placeholder-content');
}
});
};
var _smarttext = function ($el, options) {
if (options.parseLinks) {
$el.html(_parseLinks(_methods['value'].call($el), options));
} else {
$el.html(_methods['value'].call($el));
}
// This ensures (cross-browser) that if the user clicks the link before
// the element gets focus we follow the hyperlink as usual
$el.find('a').one('mousedown', $el.data('onMouseDownListener'));
return $el;
};
$.fn.smarttext = function () {
var args = Array.prototype.slice.apply(arguments);
var isMethod = typeof args[0] === 'string';
if (isMethod) {
return _methods[args[0]].apply(this, args.slice(1));
}
_options = $.extend(true, {}, _defaultOptions, args[0]);
return this.each(function (indx, el) {
var $el = $(el);
$el.attr('contenteditable', _options.editable);
var onFocusListener = function () {
if ($el.data('follow-link')) { return; }
$el.find('a').attr('contenteditable', _options.editable);
};
var onBlurListener = function () {
_smarttext($el, _options);
$el.find('a').attr('contenteditable', _options.linkAttributes.contenteditable);
$el.data('follow-link', false);
};
var onMouseDownListener = function () {
// we are relying on the mousedown event firing before focus
if ($(this).attr('contenteditable') === 'false') {
$el.data('follow-link', true);
}
}
$el.data('onFocusListener', onFocusListener);
$el.data('onBlurListener', onBlurListener);
$el.data('onMouseDownListener', onMouseDownListener);
_smarttext($el, _options);
_placeholderUpdate.call($el);
$el.on('focus', onFocusListener).on('blur', onBlurListener);
$el.on('change keydown keypress input', _placeholderUpdate);
});
};
})(jQuery);