3069 wiersze
126 KiB
JavaScript
3069 wiersze
126 KiB
JavaScript
/*
|
||
@license textAngular
|
||
Author : Austin Anderson
|
||
License : 2013 MIT
|
||
Version 1.5.1
|
||
|
||
See README.md or https://github.com/fraywing/textAngular/wiki for requirements and use.
|
||
*/
|
||
|
||
/*
|
||
Commonjs package manager support (eg componentjs).
|
||
*/
|
||
|
||
|
||
"use strict";
|
||
// IE version detection - http://stackoverflow.com/questions/4169160/javascript-ie-detection-why-not-use-simple-conditional-comments
|
||
// We need this as IE sometimes plays funny tricks with the contenteditable.
|
||
// ----------------------------------------------------------
|
||
// If you're not in IE (or IE version is less than 5) then:
|
||
// ie === undefined
|
||
// If you're in IE (>=5) then you can determine which version:
|
||
// ie === 7; // IE7
|
||
// Thus, to detect IE:
|
||
// if (ie) {}
|
||
// And to detect the version:
|
||
// ie === 6 // IE6
|
||
// ie > 7 // IE8, IE9, IE10 ...
|
||
// ie < 9 // Anything less than IE9
|
||
// ----------------------------------------------------------
|
||
/* istanbul ignore next: untestable browser check */
|
||
var _browserDetect = {
|
||
ie: (function(){
|
||
var undef,
|
||
v = 3,
|
||
div = document.createElement('div'),
|
||
all = div.getElementsByTagName('i');
|
||
|
||
while (
|
||
div.innerHTML = '<!--[if gt IE ' + (++v) + ']><i></i><![endif]-->',
|
||
all[0]
|
||
);
|
||
|
||
return v > 4 ? v : undef;
|
||
}()),
|
||
webkit: /AppleWebKit\/([\d.]+)/i.test(navigator.userAgent)
|
||
};
|
||
|
||
// fix a webkit bug, see: https://gist.github.com/shimondoodkin/1081133
|
||
// this is set true when a blur occurs as the blur of the ta-bind triggers before the click
|
||
var globalContentEditableBlur = false;
|
||
/* istanbul ignore next: Browser Un-Focus fix for webkit */
|
||
if(_browserDetect.webkit) {
|
||
document.addEventListener("mousedown", function(_event){
|
||
var e = _event || window.event;
|
||
var curelement = e.target;
|
||
if(globalContentEditableBlur && curelement !== null){
|
||
var isEditable = false;
|
||
var tempEl = curelement;
|
||
while(tempEl !== null && tempEl.tagName.toLowerCase() !== 'html' && !isEditable){
|
||
isEditable = tempEl.contentEditable === 'true';
|
||
tempEl = tempEl.parentNode;
|
||
}
|
||
if(!isEditable){
|
||
document.getElementById('textAngular-editableFix-010203040506070809').setSelectionRange(0, 0); // set caret focus to an element that handles caret focus correctly.
|
||
curelement.focus(); // focus the wanted element.
|
||
if (curelement.select) {
|
||
curelement.select(); // use select to place cursor for input elements.
|
||
}
|
||
}
|
||
}
|
||
globalContentEditableBlur = false;
|
||
}, false); // add global click handler
|
||
angular.element(document).ready(function () {
|
||
angular.element(document.body).append(angular.element('<input id="textAngular-editableFix-010203040506070809" class="ta-hidden-input" aria-hidden="true" unselectable="on" tabIndex="-1">'));
|
||
});
|
||
}
|
||
|
||
// Gloabl to textAngular REGEXP vars for block and list elements.
|
||
|
||
var BLOCKELEMENTS = /^(address|article|aside|audio|blockquote|canvas|dd|div|dl|fieldset|figcaption|figure|footer|form|h1|h2|h3|h4|h5|h6|header|hgroup|hr|noscript|ol|output|p|pre|section|table|tfoot|ul|video)$/i;
|
||
var LISTELEMENTS = /^(ul|li|ol)$/i;
|
||
var VALIDELEMENTS = /^(address|article|aside|audio|blockquote|canvas|dd|div|dl|fieldset|figcaption|figure|footer|form|h1|h2|h3|h4|h5|h6|header|hgroup|hr|noscript|ol|output|p|pre|section|table|tfoot|ul|video|li)$/i;
|
||
|
||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/Trim#Compatibility
|
||
/* istanbul ignore next: trim shim for older browsers */
|
||
if (!String.prototype.trim) {
|
||
String.prototype.trim = function () {
|
||
return this.replace(/^\s+|\s+$/g, '');
|
||
};
|
||
}
|
||
|
||
/*
|
||
Custom stylesheet for the placeholders rules.
|
||
Credit to: http://davidwalsh.name/add-rules-stylesheets
|
||
*/
|
||
var sheet, addCSSRule, removeCSSRule, _addCSSRule, _removeCSSRule, _getRuleIndex;
|
||
/* istanbul ignore else: IE <8 test*/
|
||
if(_browserDetect.ie > 8 || _browserDetect.ie === undefined){
|
||
var _sheets = document.styleSheets;
|
||
/* istanbul ignore next: preference for stylesheet loaded externally */
|
||
for(var i = 0; i < _sheets.length; i++){
|
||
if(_sheets[i].media.length === 0 || _sheets[i].media.mediaText.match(/(all|screen)/ig)){
|
||
if(_sheets[i].href){
|
||
if(_sheets[i].href.match(/textangular\.(min\.|)css/ig)){
|
||
sheet = _sheets[i];
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
/* istanbul ignore next: preference for stylesheet loaded externally */
|
||
if(!sheet){
|
||
// this sheet is used for the placeholders later on.
|
||
sheet = (function() {
|
||
// Create the <style> tag
|
||
var style = document.createElement("style");
|
||
/* istanbul ignore else : WebKit hack :( */
|
||
if(_browserDetect.webkit) style.appendChild(document.createTextNode(""));
|
||
|
||
// Add the <style> element to the page, add as first so the styles can be overridden by custom stylesheets
|
||
document.getElementsByTagName('head')[0].appendChild(style);
|
||
|
||
return style.sheet;
|
||
})();
|
||
}
|
||
|
||
// use as: addCSSRule("header", "float: left");
|
||
addCSSRule = function(selector, rules) {
|
||
return _addCSSRule(sheet, selector, rules);
|
||
};
|
||
_addCSSRule = function(_sheet, selector, rules){
|
||
var insertIndex;
|
||
var insertedRule;
|
||
// This order is important as IE 11 has both cssRules and rules but they have different lengths - cssRules is correct, rules gives an error in IE 11
|
||
/* istanbul ignore next: browser catches */
|
||
if(_sheet.cssRules) insertIndex = Math.max(_sheet.cssRules.length - 1, 0);
|
||
else if(_sheet.rules) insertIndex = Math.max(_sheet.rules.length - 1, 0);
|
||
|
||
/* istanbul ignore else: untestable IE option */
|
||
if(_sheet.insertRule) {
|
||
_sheet.insertRule(selector + "{" + rules + "}", insertIndex);
|
||
}
|
||
else {
|
||
_sheet.addRule(selector, rules, insertIndex);
|
||
}
|
||
/* istanbul ignore next: browser catches */
|
||
if(sheet.rules) insertedRule = sheet.rules[insertIndex];
|
||
else if(sheet.cssRules) insertedRule = sheet.cssRules[insertIndex];
|
||
// return the inserted stylesheet rule
|
||
return insertedRule;
|
||
};
|
||
|
||
_getRuleIndex = function(rule, rules) {
|
||
var i, ruleIndex;
|
||
for (i=0; i < rules.length; i++) {
|
||
/* istanbul ignore else: check for correct rule */
|
||
if (rules[i].cssText === rule.cssText) {
|
||
ruleIndex = i;
|
||
break;
|
||
}
|
||
}
|
||
return ruleIndex;
|
||
};
|
||
|
||
removeCSSRule = function(rule){
|
||
_removeCSSRule(sheet, rule);
|
||
};
|
||
/* istanbul ignore next: tests are browser specific */
|
||
_removeCSSRule = function(sheet, rule){
|
||
var rules = sheet.cssRules || sheet.rules;
|
||
if(!rules || rules.length === 0) return;
|
||
var ruleIndex = _getRuleIndex(rule, rules);
|
||
if(sheet.removeRule){
|
||
sheet.removeRule(ruleIndex);
|
||
}else{
|
||
sheet.deleteRule(ruleIndex);
|
||
}
|
||
};
|
||
}
|
||
|
||
angular.module('textAngular.factories', [])
|
||
.factory('taBrowserTag', [function(){
|
||
return function(tag){
|
||
/* istanbul ignore next: ie specific test */
|
||
if(!tag) return (_browserDetect.ie <= 8)? 'P' : 'p';
|
||
else if(tag === '') return (_browserDetect.ie === undefined)? 'div' : (_browserDetect.ie <= 8)? 'P' : 'p';
|
||
else return (_browserDetect.ie <= 8)? tag.toUpperCase() : tag;
|
||
};
|
||
}]).factory('taApplyCustomRenderers', ['taCustomRenderers', 'taDOM', function(taCustomRenderers, taDOM){
|
||
return function(val){
|
||
var element = angular.element('<div></div>');
|
||
element[0].innerHTML = val;
|
||
|
||
angular.forEach(taCustomRenderers, function(renderer){
|
||
var elements = [];
|
||
// get elements based on what is defined. If both defined do secondary filter in the forEach after using selector string
|
||
if(renderer.selector && renderer.selector !== '')
|
||
elements = element.find(renderer.selector);
|
||
/* istanbul ignore else: shouldn't fire, if it does we're ignoring everything */
|
||
else if(renderer.customAttribute && renderer.customAttribute !== '')
|
||
elements = taDOM.getByAttribute(element, renderer.customAttribute);
|
||
// process elements if any found
|
||
angular.forEach(elements, function(_element){
|
||
_element = angular.element(_element);
|
||
if(renderer.selector && renderer.selector !== '' && renderer.customAttribute && renderer.customAttribute !== ''){
|
||
if(_element.attr(renderer.customAttribute) !== undefined) renderer.renderLogic(_element);
|
||
} else renderer.renderLogic(_element);
|
||
});
|
||
});
|
||
|
||
return element[0].innerHTML;
|
||
};
|
||
}]).factory('taFixChrome', function(){
|
||
// get whaterever rubbish is inserted in chrome
|
||
// should be passed an html string, returns an html string
|
||
var taFixChrome = function(html){
|
||
if(!html || !angular.isString(html) || html.length <= 0) return html;
|
||
// grab all elements with a style attibute
|
||
var spanMatch = /<([^>\/]+?)style=("([^"]+)"|'([^']+)')([^>]*)>/ig;
|
||
var match, styleVal, newTag, finalHtml = '', lastIndex = 0;
|
||
while(match = spanMatch.exec(html)){
|
||
// one of the quoted values ' or "
|
||
/* istanbul ignore next: quotations match */
|
||
styleVal = match[3] || match[4];
|
||
// test for chrome inserted junk
|
||
if(styleVal && styleVal.match(/line-height: 1.[0-9]{3,12};|color: inherit; line-height: 1.1;/i)){
|
||
// replace original tag with new tag
|
||
styleVal = styleVal.replace(/( |)font-family: inherit;|( |)line-height: 1.[0-9]{3,12};|( |)color: inherit;/ig, '');
|
||
newTag = '<' + match[1].trim();
|
||
if(styleVal.trim().length > 0) newTag += ' style=' + match[2].substring(0,1) + styleVal + match[2].substring(0,1);
|
||
newTag += match[5].trim() + ">";
|
||
finalHtml += html.substring(lastIndex, match.index) + newTag;
|
||
lastIndex = match.index + match[0].length;
|
||
}
|
||
}
|
||
finalHtml += html.substring(lastIndex);
|
||
// only replace when something has changed, else we get focus problems on inserting lists
|
||
if(lastIndex > 0){
|
||
// replace all empty strings
|
||
return finalHtml.replace(/<span\s?>(.*?)<\/span>(<br(\/|)>|)/ig, '$1');
|
||
} else return html;
|
||
};
|
||
return taFixChrome;
|
||
}).factory('taSanitize', ['$sanitize', function taSanitizeFactory($sanitize){
|
||
|
||
var convert_infos = [
|
||
{
|
||
property: 'font-weight',
|
||
values: [ 'bold' ],
|
||
tag: 'b'
|
||
},
|
||
{
|
||
property: 'font-style',
|
||
values: [ 'italic' ],
|
||
tag: 'i'
|
||
}
|
||
];
|
||
|
||
var styleMatch = [];
|
||
for(var i = 0; i < convert_infos.length; i++){
|
||
var _partialStyle = '(' + convert_infos[i].property + ':\\s*(';
|
||
for(var j = 0; j < convert_infos[i].values.length; j++){
|
||
/* istanbul ignore next: not needed to be tested yet */
|
||
if(j > 0) _partialStyle += '|';
|
||
_partialStyle += convert_infos[i].values[j];
|
||
}
|
||
_partialStyle += ');)';
|
||
styleMatch.push(_partialStyle);
|
||
}
|
||
var styleRegexString = '(' + styleMatch.join('|') + ')';
|
||
|
||
function wrapNested(html, wrapTag) {
|
||
var depth = 0;
|
||
var lastIndex = 0;
|
||
var match;
|
||
var tagRegex = /<[^>]*>/ig;
|
||
while(match = tagRegex.exec(html)){
|
||
lastIndex = match.index;
|
||
if(match[0].substr(1, 1) === '/'){
|
||
if(depth === 0) break;
|
||
else depth--;
|
||
}else depth++;
|
||
}
|
||
return wrapTag +
|
||
html.substring(0, lastIndex) +
|
||
// get the start tags reversed - this is safe as we construct the strings with no content except the tags
|
||
angular.element(wrapTag)[0].outerHTML.substring(wrapTag.length) +
|
||
html.substring(lastIndex);
|
||
}
|
||
|
||
function transformLegacyStyles(html){
|
||
if(!html || !angular.isString(html) || html.length <= 0) return html;
|
||
var i;
|
||
var styleElementMatch = /<([^>\/]+?)style=("([^"]+)"|'([^']+)')([^>]*)>/ig;
|
||
var match, subMatch, styleVal, newTag, lastNewTag = '', newHtml, finalHtml = '', lastIndex = 0;
|
||
while(match = styleElementMatch.exec(html)){
|
||
// one of the quoted values ' or "
|
||
/* istanbul ignore next: quotations match */
|
||
styleVal = match[3] || match[4];
|
||
var styleRegex = new RegExp(styleRegexString, 'i');
|
||
// test for style values to change
|
||
if(angular.isString(styleVal) && styleRegex.test(styleVal)){
|
||
// remove build tag list
|
||
newTag = '';
|
||
// init regex here for exec
|
||
var styleRegexExec = new RegExp(styleRegexString, 'ig');
|
||
// find relevand tags and build a string of them
|
||
while(subMatch = styleRegexExec.exec(styleVal)){
|
||
for(i = 0; i < convert_infos.length; i++){
|
||
if(!!subMatch[(i*2) + 2]){
|
||
newTag += '<' + convert_infos[i].tag + '>';
|
||
}
|
||
}
|
||
}
|
||
// recursively find more legacy styles in html before this tag and after the previous match (if any)
|
||
newHtml = transformLegacyStyles(html.substring(lastIndex, match.index));
|
||
// build up html
|
||
if(lastNewTag.length > 0){
|
||
finalHtml += wrapNested(newHtml, lastNewTag);
|
||
}else finalHtml += newHtml;
|
||
// grab the style val without the transformed values
|
||
styleVal = styleVal.replace(new RegExp(styleRegexString, 'ig'), '');
|
||
// build the html tag
|
||
finalHtml += '<' + match[1].trim();
|
||
if(styleVal.length > 0) finalHtml += ' style="' + styleVal + '"';
|
||
finalHtml += match[5] + '>';
|
||
// update the start index to after this tag
|
||
lastIndex = match.index + match[0].length;
|
||
lastNewTag = newTag;
|
||
}
|
||
}
|
||
if(lastNewTag.length > 0){
|
||
finalHtml += wrapNested(html.substring(lastIndex), lastNewTag);
|
||
}
|
||
else finalHtml += html.substring(lastIndex);
|
||
return finalHtml;
|
||
}
|
||
|
||
function transformLegacyAttributes(html){
|
||
if(!html || !angular.isString(html) || html.length <= 0) return html;
|
||
// replace all align='...' tags with text-align attributes
|
||
var attrElementMatch = /<([^>\/]+?)align=("([^"]+)"|'([^']+)')([^>]*)>/ig;
|
||
var match, finalHtml = '', lastIndex = 0;
|
||
// match all attr tags
|
||
while(match = attrElementMatch.exec(html)){
|
||
// add all html before this tag
|
||
finalHtml += html.substring(lastIndex, match.index);
|
||
// record last index after this tag
|
||
lastIndex = match.index + match[0].length;
|
||
// construct tag without the align attribute
|
||
var newTag = '<' + match[1] + match[5];
|
||
// add the style attribute
|
||
if(/style=("([^"]+)"|'([^']+)')/ig.test(newTag)){
|
||
/* istanbul ignore next: quotations match */
|
||
newTag = newTag.replace(/style=("([^"]+)"|'([^']+)')/i, 'style="$2$3 text-align:' + (match[3] || match[4]) + ';"');
|
||
}else{
|
||
/* istanbul ignore next: quotations match */
|
||
newTag += ' style="text-align:' + (match[3] || match[4]) + ';"';
|
||
}
|
||
newTag += '>';
|
||
// add to html
|
||
finalHtml += newTag;
|
||
}
|
||
// return with remaining html
|
||
return finalHtml + html.substring(lastIndex);
|
||
}
|
||
|
||
return function taSanitize(unsafe, oldsafe, ignore){
|
||
// unsafe html should NEVER built into a DOM object via angular.element. This allows XSS to be inserted and run.
|
||
if ( !ignore ) {
|
||
try {
|
||
unsafe = transformLegacyStyles(unsafe);
|
||
} catch (e) {
|
||
}
|
||
}
|
||
|
||
// unsafe and oldsafe should be valid HTML strings
|
||
// any exceptions (lets say, color for example) should be made here but with great care
|
||
// setup unsafe element for modification
|
||
unsafe = transformLegacyAttributes(unsafe);
|
||
|
||
var safe;
|
||
try {
|
||
safe = $sanitize(unsafe);
|
||
// do this afterwards, then the $sanitizer should still throw for bad markup
|
||
if(ignore) safe = unsafe;
|
||
} catch (e){
|
||
safe = oldsafe || '';
|
||
}
|
||
|
||
// Do processing for <pre> tags, removing tabs and return carriages outside of them
|
||
|
||
var _preTags = safe.match(/(<pre[^>]*>.*?<\/pre[^>]*>)/ig);
|
||
var processedSafe = safe.replace(/(&#(9|10);)*/ig, '');
|
||
var re = /<pre[^>]*>.*?<\/pre[^>]*>/ig;
|
||
var index = 0;
|
||
var lastIndex = 0;
|
||
var origTag;
|
||
safe = '';
|
||
while((origTag = re.exec(processedSafe)) !== null && index < _preTags.length){
|
||
safe += processedSafe.substring(lastIndex, origTag.index) + _preTags[index];
|
||
lastIndex = origTag.index + origTag[0].length;
|
||
index++;
|
||
}
|
||
return safe + processedSafe.substring(lastIndex);
|
||
};
|
||
}]).factory('taToolExecuteAction', ['$q', '$log', function($q, $log){
|
||
// this must be called on a toolScope or instance
|
||
return function(editor){
|
||
if(editor !== undefined) this.$editor = function(){ return editor; };
|
||
var deferred = $q.defer(),
|
||
promise = deferred.promise,
|
||
_editor = this.$editor();
|
||
// pass into the action the deferred function and also the function to reload the current selection if rangy available
|
||
var result;
|
||
try{
|
||
result = this.action(deferred, _editor.startAction());
|
||
// We set the .finally callback here to make sure it doesn't get executed before any other .then callback.
|
||
promise['finally'](function(){
|
||
_editor.endAction.call(_editor);
|
||
});
|
||
}catch(exc){
|
||
$log.error(exc);
|
||
}
|
||
if(result || result === undefined){
|
||
// if true or undefined is returned then the action has finished. Otherwise the deferred action will be resolved manually.
|
||
deferred.resolve();
|
||
}
|
||
};
|
||
}]);
|
||
angular.module('textAngular.DOM', ['textAngular.factories'])
|
||
.factory('taExecCommand', ['taSelection', 'taBrowserTag', '$document', function(taSelection, taBrowserTag, $document){
|
||
var listToDefault = function(listElement, defaultWrap){
|
||
var $target, i;
|
||
// if all selected then we should remove the list
|
||
// grab all li elements and convert to taDefaultWrap tags
|
||
var children = listElement.find('li');
|
||
for(i = children.length - 1; i >= 0; i--){
|
||
$target = angular.element('<' + defaultWrap + '>' + children[i].innerHTML + '</' + defaultWrap + '>');
|
||
listElement.after($target);
|
||
}
|
||
listElement.remove();
|
||
taSelection.setSelectionToElementEnd($target[0]);
|
||
};
|
||
var selectLi = function(liElement){
|
||
if(/(<br(|\/)>)$/i.test(liElement.innerHTML.trim())) taSelection.setSelectionBeforeElement(angular.element(liElement).find("br")[0]);
|
||
else taSelection.setSelectionToElementEnd(liElement);
|
||
};
|
||
var listToList = function(listElement, newListTag){
|
||
var $target = angular.element('<' + newListTag + '>' + listElement[0].innerHTML + '</' + newListTag + '>');
|
||
listElement.after($target);
|
||
listElement.remove();
|
||
selectLi($target.find('li')[0]);
|
||
};
|
||
var childElementsToList = function(elements, listElement, newListTag){
|
||
var html = '';
|
||
for(var i = 0; i < elements.length; i++){
|
||
html += '<' + taBrowserTag('li') + '>' + elements[i].innerHTML + '</' + taBrowserTag('li') + '>';
|
||
}
|
||
var $target = angular.element('<' + newListTag + '>' + html + '</' + newListTag + '>');
|
||
listElement.after($target);
|
||
listElement.remove();
|
||
selectLi($target.find('li')[0]);
|
||
};
|
||
return function(taDefaultWrap, topNode){
|
||
taDefaultWrap = taBrowserTag(taDefaultWrap);
|
||
return function(command, showUI, options, defaultTagAttributes){
|
||
var i, $target, html, _nodes, next, optionsTagName, selectedElement;
|
||
var defaultWrapper = angular.element('<' + taDefaultWrap + '>');
|
||
try{
|
||
selectedElement = taSelection.getSelectionElement();
|
||
}catch(e){}
|
||
var $selected = angular.element(selectedElement);
|
||
if(selectedElement !== undefined){
|
||
var tagName = selectedElement.tagName.toLowerCase();
|
||
if(command.toLowerCase() === 'insertorderedlist' || command.toLowerCase() === 'insertunorderedlist'){
|
||
var selfTag = taBrowserTag((command.toLowerCase() === 'insertorderedlist')? 'ol' : 'ul');
|
||
if(tagName === selfTag){
|
||
// if all selected then we should remove the list
|
||
// grab all li elements and convert to taDefaultWrap tags
|
||
return listToDefault($selected, taDefaultWrap);
|
||
}else if(tagName === 'li' && $selected.parent()[0].tagName.toLowerCase() === selfTag && $selected.parent().children().length === 1){
|
||
// catch for the previous statement if only one li exists
|
||
return listToDefault($selected.parent(), taDefaultWrap);
|
||
}else if(tagName === 'li' && $selected.parent()[0].tagName.toLowerCase() !== selfTag && $selected.parent().children().length === 1){
|
||
// catch for the previous statement if only one li exists
|
||
return listToList($selected.parent(), selfTag);
|
||
}else if(tagName.match(BLOCKELEMENTS) && !$selected.hasClass('ta-bind')){
|
||
// if it's one of those block elements we have to change the contents
|
||
// if it's a ol/ul we are changing from one to the other
|
||
if(tagName === 'ol' || tagName === 'ul'){
|
||
return listToList($selected, selfTag);
|
||
}else{
|
||
var childBlockElements = false;
|
||
angular.forEach($selected.children(), function(elem){
|
||
if(elem.tagName.match(BLOCKELEMENTS)) {
|
||
childBlockElements = true;
|
||
}
|
||
});
|
||
if(childBlockElements){
|
||
return childElementsToList($selected.children(), $selected, selfTag);
|
||
}else{
|
||
return childElementsToList([angular.element('<div>' + selectedElement.innerHTML + '</div>')[0]], $selected, selfTag);
|
||
}
|
||
}
|
||
}else if(tagName.match(BLOCKELEMENTS)){
|
||
// if we get here then all the contents of the ta-bind are selected
|
||
_nodes = taSelection.getOnlySelectedElements();
|
||
if(_nodes.length === 0){
|
||
// here is if there is only text in ta-bind ie <div ta-bind>test content</div>
|
||
$target = angular.element('<' + selfTag + '><li>' + selectedElement.innerHTML + '</li></' + selfTag + '>');
|
||
$selected.html('');
|
||
$selected.append($target);
|
||
}else if(_nodes.length === 1 && (_nodes[0].tagName.toLowerCase() === 'ol' || _nodes[0].tagName.toLowerCase() === 'ul')){
|
||
if(_nodes[0].tagName.toLowerCase() === selfTag){
|
||
// remove
|
||
return listToDefault(angular.element(_nodes[0]), taDefaultWrap);
|
||
}else{
|
||
return listToList(angular.element(_nodes[0]), selfTag);
|
||
}
|
||
}else{
|
||
html = '';
|
||
var $nodes = [];
|
||
for(i = 0; i < _nodes.length; i++){
|
||
/* istanbul ignore else: catch for real-world can't make it occur in testing */
|
||
if(_nodes[i].nodeType !== 3){
|
||
var $n = angular.element(_nodes[i]);
|
||
/* istanbul ignore if: browser check only, phantomjs doesn't return children nodes but chrome at least does */
|
||
if(_nodes[i].tagName.toLowerCase() === 'li') continue;
|
||
else if(_nodes[i].tagName.toLowerCase() === 'ol' || _nodes[i].tagName.toLowerCase() === 'ul'){
|
||
html += $n[0].innerHTML; // if it's a list, add all it's children
|
||
}else if(_nodes[i].tagName.toLowerCase() === 'span' && (_nodes[i].childNodes[0].tagName.toLowerCase() === 'ol' || _nodes[i].childNodes[0].tagName.toLowerCase() === 'ul')){
|
||
html += $n[0].childNodes[0].innerHTML; // if it's a list, add all it's children
|
||
}else{
|
||
html += '<' + taBrowserTag('li') + '>' + $n[0].innerHTML + '</' + taBrowserTag('li') + '>';
|
||
}
|
||
$nodes.unshift($n);
|
||
}
|
||
}
|
||
$target = angular.element('<' + selfTag + '>' + html + '</' + selfTag + '>');
|
||
$nodes.pop().replaceWith($target);
|
||
angular.forEach($nodes, function($node){ $node.remove(); });
|
||
}
|
||
taSelection.setSelectionToElementEnd($target[0]);
|
||
return;
|
||
}
|
||
}else if(command.toLowerCase() === 'formatblock'){
|
||
optionsTagName = options.toLowerCase().replace(/[<>]/ig, '');
|
||
if(optionsTagName.trim() === 'default') {
|
||
optionsTagName = taDefaultWrap;
|
||
options = '<' + taDefaultWrap + '>';
|
||
}
|
||
if(tagName === 'li') $target = $selected.parent();
|
||
else $target = $selected;
|
||
// find the first blockElement
|
||
while(!$target[0].tagName || !$target[0].tagName.match(BLOCKELEMENTS) && !$target.parent().attr('contenteditable')){
|
||
$target = $target.parent();
|
||
/* istanbul ignore next */
|
||
tagName = ($target[0].tagName || '').toLowerCase();
|
||
}
|
||
if(tagName === optionsTagName){
|
||
// $target is wrap element
|
||
_nodes = $target.children();
|
||
var hasBlock = false;
|
||
for(i = 0; i < _nodes.length; i++){
|
||
hasBlock = hasBlock || _nodes[i].tagName.match(BLOCKELEMENTS);
|
||
}
|
||
if(hasBlock){
|
||
$target.after(_nodes);
|
||
next = $target.next();
|
||
$target.remove();
|
||
$target = next;
|
||
}else{
|
||
defaultWrapper.append($target[0].childNodes);
|
||
$target.after(defaultWrapper);
|
||
$target.remove();
|
||
$target = defaultWrapper;
|
||
}
|
||
}else if($target.parent()[0].tagName.toLowerCase() === optionsTagName && !$target.parent().hasClass('ta-bind')){
|
||
//unwrap logic for parent
|
||
var blockElement = $target.parent();
|
||
var contents = blockElement.contents();
|
||
for(i = 0; i < contents.length; i ++){
|
||
/* istanbul ignore next: can't test - some wierd thing with how phantomjs works */
|
||
if(blockElement.parent().hasClass('ta-bind') && contents[i].nodeType === 3){
|
||
defaultWrapper = angular.element('<' + taDefaultWrap + '>');
|
||
defaultWrapper[0].innerHTML = contents[i].outerHTML;
|
||
contents[i] = defaultWrapper[0];
|
||
}
|
||
blockElement.parent()[0].insertBefore(contents[i], blockElement[0]);
|
||
}
|
||
blockElement.remove();
|
||
}else if(tagName.match(LISTELEMENTS)){
|
||
// wrapping a list element
|
||
$target.wrap(options);
|
||
}else{
|
||
// default wrap behaviour
|
||
_nodes = taSelection.getOnlySelectedElements();
|
||
if(_nodes.length === 0) _nodes = [$target[0]];
|
||
// find the parent block element if any of the nodes are inline or text
|
||
for(i = 0; i < _nodes.length; i++){
|
||
if(_nodes[i].nodeType === 3 || !_nodes[i].tagName.match(BLOCKELEMENTS)){
|
||
while(_nodes[i].nodeType === 3 || !_nodes[i].tagName || !_nodes[i].tagName.match(BLOCKELEMENTS)){
|
||
_nodes[i] = _nodes[i].parentNode;
|
||
}
|
||
}
|
||
}
|
||
if(angular.element(_nodes[0]).hasClass('ta-bind')){
|
||
$target = angular.element(options);
|
||
$target[0].innerHTML = _nodes[0].innerHTML;
|
||
_nodes[0].innerHTML = $target[0].outerHTML;
|
||
}else if(optionsTagName === 'blockquote'){
|
||
// blockquotes wrap other block elements
|
||
html = '';
|
||
for(i = 0; i < _nodes.length; i++){
|
||
html += _nodes[i].outerHTML;
|
||
}
|
||
$target = angular.element(options);
|
||
$target[0].innerHTML = html;
|
||
_nodes[0].parentNode.insertBefore($target[0],_nodes[0]);
|
||
for(i = _nodes.length - 1; i >= 0; i--){
|
||
/* istanbul ignore else: */
|
||
if(_nodes[i].parentNode) _nodes[i].parentNode.removeChild(_nodes[i]);
|
||
}
|
||
}
|
||
else {
|
||
// regular block elements replace other block elements
|
||
for(i = 0; i < _nodes.length; i++){
|
||
$target = angular.element(options);
|
||
$target[0].innerHTML = _nodes[i].innerHTML;
|
||
_nodes[i].parentNode.insertBefore($target[0],_nodes[i]);
|
||
_nodes[i].parentNode.removeChild(_nodes[i]);
|
||
}
|
||
}
|
||
}
|
||
taSelection.setSelectionToElementEnd($target[0]);
|
||
return;
|
||
}else if(command.toLowerCase() === 'createlink'){
|
||
var tagBegin = '<a href="' + options + '" target="' +
|
||
(defaultTagAttributes.a.target ? defaultTagAttributes.a.target : '') +
|
||
'">',
|
||
tagEnd = '</a>',
|
||
_selection = taSelection.getSelection();
|
||
if(_selection.collapsed){
|
||
// insert text at selection, then select then just let normal exec-command run
|
||
taSelection.insertHtml(tagBegin + options + tagEnd, topNode);
|
||
}else if(rangy.getSelection().getRangeAt(0).canSurroundContents()){
|
||
var node = angular.element(tagBegin + tagEnd)[0];
|
||
rangy.getSelection().getRangeAt(0).surroundContents(node);
|
||
}
|
||
return;
|
||
}else if(command.toLowerCase() === 'inserthtml'){
|
||
taSelection.insertHtml(options, topNode);
|
||
return;
|
||
}
|
||
}
|
||
try{
|
||
$document[0].execCommand(command, showUI, options);
|
||
}catch(e){}
|
||
};
|
||
};
|
||
}]).service('taSelection', ['$document', 'taDOM',
|
||
/* istanbul ignore next: all browser specifics and PhantomJS dosen't seem to support half of it */
|
||
function($document, taDOM){
|
||
// need to dereference the document else the calls don't work correctly
|
||
var _document = $document[0];
|
||
var brException = function (element, offset) {
|
||
/* check if selection is a BR element at the beginning of a container. If so, get
|
||
* the parentNode instead.
|
||
* offset should be zero in this case. Otherwise, return the original
|
||
* element.
|
||
*/
|
||
if (element.tagName && element.tagName.match(/^br$/i) && offset === 0 && !element.previousSibling) {
|
||
return {
|
||
element: element.parentNode,
|
||
offset: 0
|
||
};
|
||
} else {
|
||
return {
|
||
element: element,
|
||
offset: offset
|
||
};
|
||
}
|
||
};
|
||
var api = {
|
||
getSelection: function(){
|
||
var range = rangy.getSelection().getRangeAt(0);
|
||
var container = range.commonAncestorContainer;
|
||
var selection = {
|
||
start: brException(range.startContainer, range.startOffset),
|
||
end: brException(range.endContainer, range.endOffset),
|
||
collapsed: range.collapsed
|
||
};
|
||
// Check if the container is a text node and return its parent if so
|
||
container = container.nodeType === 3 ? container.parentNode : container;
|
||
if (container.parentNode === selection.start.element ||
|
||
container.parentNode === selection.end.element) {
|
||
selection.container = container.parentNode;
|
||
} else {
|
||
selection.container = container;
|
||
}
|
||
return selection;
|
||
},
|
||
getOnlySelectedElements: function(){
|
||
var range = rangy.getSelection().getRangeAt(0);
|
||
var container = range.commonAncestorContainer;
|
||
// Check if the container is a text node and return its parent if so
|
||
container = container.nodeType === 3 ? container.parentNode : container;
|
||
return range.getNodes([1], function(node){
|
||
return node.parentNode === container;
|
||
});
|
||
},
|
||
// Some basic selection functions
|
||
getSelectionElement: function () {
|
||
return api.getSelection().container;
|
||
},
|
||
setSelection: function(el, start, end){
|
||
var range = rangy.createRange();
|
||
|
||
range.setStart(el, start);
|
||
range.setEnd(el, end);
|
||
|
||
rangy.getSelection().setSingleRange(range);
|
||
},
|
||
setSelectionBeforeElement: function (el){
|
||
var range = rangy.createRange();
|
||
|
||
range.selectNode(el);
|
||
range.collapse(true);
|
||
|
||
rangy.getSelection().setSingleRange(range);
|
||
},
|
||
setSelectionAfterElement: function (el){
|
||
var range = rangy.createRange();
|
||
|
||
range.selectNode(el);
|
||
range.collapse(false);
|
||
|
||
rangy.getSelection().setSingleRange(range);
|
||
},
|
||
setSelectionToElementStart: function (el){
|
||
var range = rangy.createRange();
|
||
|
||
range.selectNodeContents(el);
|
||
range.collapse(true);
|
||
|
||
rangy.getSelection().setSingleRange(range);
|
||
},
|
||
setSelectionToElementEnd: function (el){
|
||
var range = rangy.createRange();
|
||
|
||
range.selectNodeContents(el);
|
||
range.collapse(false);
|
||
if(el.childNodes && el.childNodes[el.childNodes.length - 1] && el.childNodes[el.childNodes.length - 1].nodeName === 'br'){
|
||
range.startOffset = range.endOffset = range.startOffset - 1;
|
||
}
|
||
rangy.getSelection().setSingleRange(range);
|
||
},
|
||
// from http://stackoverflow.com/questions/6690752/insert-html-at-caret-in-a-contenteditable-div
|
||
// topNode is the contenteditable normally, all manipulation MUST be inside this.
|
||
insertHtml: function(html, topNode){
|
||
var parent, secondParent, _childI, nodes, i, lastNode, _tempFrag;
|
||
var element = angular.element("<div>" + html + "</div>");
|
||
var range = rangy.getSelection().getRangeAt(0);
|
||
var frag = _document.createDocumentFragment();
|
||
var children = element[0].childNodes;
|
||
var isInline = true;
|
||
|
||
if(children.length > 0){
|
||
// NOTE!! We need to do the following:
|
||
// check for blockelements - if they exist then we have to split the current element in half (and all others up to the closest block element) and insert all children in-between.
|
||
// If there are no block elements, or there is a mixture we need to create textNodes for the non wrapped text (we don't want them spans messing up the picture).
|
||
nodes = [];
|
||
for(_childI = 0; _childI < children.length; _childI++){
|
||
if(!(
|
||
(children[_childI].nodeName.toLowerCase() === 'p' && children[_childI].innerHTML.trim() === '') || // empty p element
|
||
(children[_childI].nodeType === 3 && children[_childI].nodeValue.trim() === '') // empty text node
|
||
)){
|
||
isInline = isInline && !BLOCKELEMENTS.test(children[_childI].nodeName);
|
||
nodes.push(children[_childI]);
|
||
}
|
||
}
|
||
for(var _n = 0; _n < nodes.length; _n++) lastNode = frag.appendChild(nodes[_n]);
|
||
if(!isInline && range.collapsed && /^(|<br(|\/)>)$/i.test(range.startContainer.innerHTML)) range.selectNode(range.startContainer);
|
||
}else{
|
||
isInline = true;
|
||
// paste text of some sort
|
||
lastNode = frag = _document.createTextNode(html);
|
||
}
|
||
|
||
// Other Edge case - selected data spans multiple blocks.
|
||
if(isInline){
|
||
range.deleteContents();
|
||
}else{ // not inline insert
|
||
if(range.collapsed && range.startContainer !== topNode){
|
||
if(range.startContainer.innerHTML && range.startContainer.innerHTML.match(/^<[^>]*>$/i)){
|
||
// this log is to catch when innerHTML is something like `<img ...>`
|
||
parent = range.startContainer;
|
||
if(range.startOffset === 1){
|
||
// before single tag
|
||
range.setStartAfter(parent);
|
||
range.setEndAfter(parent);
|
||
}else{
|
||
// after single tag
|
||
range.setStartBefore(parent);
|
||
range.setEndBefore(parent);
|
||
}
|
||
}else{
|
||
// split element into 2 and insert block element in middle
|
||
if(range.startContainer.nodeType === 3 && range.startContainer.parentNode !== topNode){ // if text node
|
||
parent = range.startContainer.parentNode;
|
||
secondParent = parent.cloneNode();
|
||
// split the nodes into two lists - before and after, splitting the node with the selection into 2 text nodes.
|
||
taDOM.splitNodes(parent.childNodes, parent, secondParent, range.startContainer, range.startOffset);
|
||
|
||
// Escape out of the inline tags like b
|
||
while(!VALIDELEMENTS.test(parent.nodeName)){
|
||
angular.element(parent).after(secondParent);
|
||
parent = parent.parentNode;
|
||
var _lastSecondParent = secondParent;
|
||
secondParent = parent.cloneNode();
|
||
// split the nodes into two lists - before and after, splitting the node with the selection into 2 text nodes.
|
||
taDOM.splitNodes(parent.childNodes, parent, secondParent, _lastSecondParent);
|
||
}
|
||
}else{
|
||
parent = range.startContainer;
|
||
secondParent = parent.cloneNode();
|
||
taDOM.splitNodes(parent.childNodes, parent, secondParent, undefined, undefined, range.startOffset);
|
||
}
|
||
|
||
angular.element(parent).after(secondParent);
|
||
// put cursor to end of inserted content
|
||
range.setStartAfter(parent);
|
||
range.setEndAfter(parent);
|
||
|
||
if(/^(|<br(|\/)>)$/i.test(parent.innerHTML.trim())){
|
||
range.setStartBefore(parent);
|
||
range.setEndBefore(parent);
|
||
angular.element(parent).remove();
|
||
}
|
||
if(/^(|<br(|\/)>)$/i.test(secondParent.innerHTML.trim())) angular.element(secondParent).remove();
|
||
if(parent.nodeName.toLowerCase() === 'li'){
|
||
_tempFrag = _document.createDocumentFragment();
|
||
for(i = 0; i < frag.childNodes.length; i++){
|
||
element = angular.element('<li>');
|
||
taDOM.transferChildNodes(frag.childNodes[i], element[0]);
|
||
taDOM.transferNodeAttributes(frag.childNodes[i], element[0]);
|
||
_tempFrag.appendChild(element[0]);
|
||
}
|
||
frag = _tempFrag;
|
||
if(lastNode){
|
||
lastNode = frag.childNodes[frag.childNodes.length - 1];
|
||
lastNode = lastNode.childNodes[lastNode.childNodes.length - 1];
|
||
}
|
||
}
|
||
}
|
||
}else{
|
||
range.deleteContents();
|
||
}
|
||
}
|
||
|
||
range.insertNode(frag);
|
||
if(lastNode){
|
||
api.setSelectionToElementEnd(lastNode);
|
||
}
|
||
}
|
||
};
|
||
return api;
|
||
}]).service('taDOM', function(){
|
||
var taDOM = {
|
||
// recursive function that returns an array of angular.elements that have the passed attribute set on them
|
||
getByAttribute: function(element, attribute){
|
||
var resultingElements = [];
|
||
var childNodes = element.children();
|
||
if(childNodes.length){
|
||
angular.forEach(childNodes, function(child){
|
||
resultingElements = resultingElements.concat(taDOM.getByAttribute(angular.element(child), attribute));
|
||
});
|
||
}
|
||
if(element.attr(attribute) !== undefined) resultingElements.push(element);
|
||
return resultingElements;
|
||
},
|
||
|
||
transferChildNodes: function(source, target){
|
||
// clear out target
|
||
target.innerHTML = '';
|
||
while(source.childNodes.length > 0) target.appendChild(source.childNodes[0]);
|
||
return target;
|
||
},
|
||
|
||
splitNodes: function(nodes, target1, target2, splitNode, subSplitIndex, splitIndex){
|
||
if(!splitNode && isNaN(splitIndex)) throw new Error('taDOM.splitNodes requires a splitNode or splitIndex');
|
||
var startNodes = document.createDocumentFragment();
|
||
var endNodes = document.createDocumentFragment();
|
||
var index = 0;
|
||
|
||
while(nodes.length > 0 && (isNaN(splitIndex) || splitIndex !== index) && nodes[0] !== splitNode){
|
||
startNodes.appendChild(nodes[0]); // this removes from the nodes array (if proper childNodes object.
|
||
index++;
|
||
}
|
||
|
||
if(!isNaN(subSplitIndex) && subSplitIndex >= 0 && nodes[0]){
|
||
startNodes.appendChild(document.createTextNode(nodes[0].nodeValue.substring(0, subSplitIndex)));
|
||
nodes[0].nodeValue = nodes[0].nodeValue.substring(subSplitIndex);
|
||
}
|
||
while(nodes.length > 0) endNodes.appendChild(nodes[0]);
|
||
|
||
taDOM.transferChildNodes(startNodes, target1);
|
||
taDOM.transferChildNodes(endNodes, target2);
|
||
},
|
||
|
||
transferNodeAttributes: function(source, target){
|
||
for(var i = 0; i < source.attributes.length; i++) target.setAttribute(source.attributes[i].name, source.attributes[i].value);
|
||
return target;
|
||
}
|
||
};
|
||
return taDOM;
|
||
});
|
||
angular.module('textAngular.validators', [])
|
||
.directive('taMaxText', function(){
|
||
return {
|
||
restrict: 'A',
|
||
require: 'ngModel',
|
||
link: function(scope, elem, attrs, ctrl){
|
||
var max = parseInt(scope.$eval(attrs.taMaxText));
|
||
if (isNaN(max)){
|
||
throw('Max text must be an integer');
|
||
}
|
||
attrs.$observe('taMaxText', function(value){
|
||
max = parseInt(value);
|
||
if (isNaN(max)){
|
||
throw('Max text must be an integer');
|
||
}
|
||
if (ctrl.$dirty){
|
||
ctrl.$validate();
|
||
}
|
||
});
|
||
ctrl.$validators.taMaxText = function(viewValue){
|
||
var source = angular.element('<div/>');
|
||
source.html(viewValue);
|
||
return source.text().length <= max;
|
||
};
|
||
}
|
||
};
|
||
}).directive('taMinText', function(){
|
||
return {
|
||
restrict: 'A',
|
||
require: 'ngModel',
|
||
link: function(scope, elem, attrs, ctrl){
|
||
var min = parseInt(scope.$eval(attrs.taMinText));
|
||
if (isNaN(min)){
|
||
throw('Min text must be an integer');
|
||
}
|
||
attrs.$observe('taMinText', function(value){
|
||
min = parseInt(value);
|
||
if (isNaN(min)){
|
||
throw('Min text must be an integer');
|
||
}
|
||
if (ctrl.$dirty){
|
||
ctrl.$validate();
|
||
}
|
||
});
|
||
ctrl.$validators.taMinText = function(viewValue){
|
||
var source = angular.element('<div/>');
|
||
source.html(viewValue);
|
||
return !source.text().length || source.text().length >= min;
|
||
};
|
||
}
|
||
};
|
||
});
|
||
angular.module('textAngular.taBind', ['textAngular.factories', 'textAngular.DOM'])
|
||
.service('_taBlankTest', [function(){
|
||
var INLINETAGS_NONBLANK = /<(a|abbr|acronym|bdi|bdo|big|cite|code|del|dfn|img|ins|kbd|label|map|mark|q|ruby|rp|rt|s|samp|time|tt|var)[^>]*(>|$)/i;
|
||
return function(_defaultTest){
|
||
return function(_blankVal){
|
||
if(!_blankVal) return true;
|
||
// find first non-tag match - ie start of string or after tag that is not whitespace
|
||
var _firstMatch = /(^[^<]|>)[^<]/i.exec(_blankVal);
|
||
var _firstTagIndex;
|
||
if(!_firstMatch){
|
||
// find the end of the first tag removing all the
|
||
// Don't do a global replace as that would be waaayy too long, just replace the first 4 occurences should be enough
|
||
_blankVal = _blankVal.toString().replace(/="[^"]*"/i, '').replace(/="[^"]*"/i, '').replace(/="[^"]*"/i, '').replace(/="[^"]*"/i, '');
|
||
_firstTagIndex = _blankVal.indexOf('>');
|
||
}else{
|
||
_firstTagIndex = _firstMatch.index;
|
||
}
|
||
_blankVal = _blankVal.trim().substring(_firstTagIndex, _firstTagIndex + 100);
|
||
// check for no tags entry
|
||
if(/^[^<>]+$/i.test(_blankVal)) return false;
|
||
// this regex is to match any number of whitespace only between two tags
|
||
if (_blankVal.length === 0 || _blankVal === _defaultTest || /^>(\s| )*<\/[^>]+>$/ig.test(_blankVal)) return true;
|
||
// this regex tests if there is a tag followed by some optional whitespace and some text after that
|
||
else if (/>\s*[^\s<]/i.test(_blankVal) || INLINETAGS_NONBLANK.test(_blankVal)) return false;
|
||
else return true;
|
||
};
|
||
};
|
||
}])
|
||
.directive('taButton', [function(){
|
||
return {
|
||
link: function(scope, element, attrs){
|
||
element.attr('unselectable', 'on');
|
||
element.on('mousedown', function(e, eventData){
|
||
/* istanbul ignore else: this is for catching the jqLite testing*/
|
||
if(eventData) angular.extend(e, eventData);
|
||
// this prevents focusout from firing on the editor when clicking toolbar buttons
|
||
e.preventDefault();
|
||
return false;
|
||
});
|
||
}
|
||
};
|
||
}])
|
||
.directive('taBind', [
|
||
'taSanitize', '$timeout', '$document', 'taFixChrome', 'taBrowserTag',
|
||
'taSelection', 'taSelectableElements', 'taApplyCustomRenderers', 'taOptions',
|
||
'_taBlankTest', '$parse', 'taDOM', 'textAngularManager',
|
||
function(
|
||
taSanitize, $timeout, $document, taFixChrome, taBrowserTag,
|
||
taSelection, taSelectableElements, taApplyCustomRenderers, taOptions,
|
||
_taBlankTest, $parse, taDOM, textAngularManager){
|
||
// Uses for this are textarea or input with ng-model and ta-bind='text'
|
||
// OR any non-form element with contenteditable="contenteditable" ta-bind="html|text" ng-model
|
||
return {
|
||
priority: 2, // So we override validators correctly
|
||
require: ['ngModel','?ngModelOptions'],
|
||
link: function(scope, element, attrs, controller){
|
||
var ngModel = controller[0];
|
||
var ngModelOptions = controller[1] || {};
|
||
// the option to use taBind on an input or textarea is required as it will sanitize all input into it correctly.
|
||
var _isContentEditable = element.attr('contenteditable') !== undefined && element.attr('contenteditable');
|
||
var _isInputFriendly = _isContentEditable || element[0].tagName.toLowerCase() === 'textarea' || element[0].tagName.toLowerCase() === 'input';
|
||
var _isReadonly = false;
|
||
var _focussed = false;
|
||
var _skipRender = false;
|
||
var _disableSanitizer = attrs.taUnsafeSanitizer || taOptions.disableSanitizer;
|
||
var _lastKey;
|
||
// see http://www.javascripter.net/faq/keycodes.htm for good information
|
||
// NOTE Mute On|Off 173 (Opera MSIE Safari Chrome) 181 (Firefox)
|
||
// BLOCKED_KEYS are special keys...
|
||
// Tab, pause/break, CapsLock, Esc, Page Up, End, Home,
|
||
// Left arrow, Up arrow, Right arrow, Down arrow, Insert, Delete,
|
||
// f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12
|
||
// NumLock, ScrollLock
|
||
var BLOCKED_KEYS = /^(9|19|20|27|33|34|35|36|37|38|39|40|45|112|113|114|115|116|117|118|119|120|121|122|123|144|145)$/i;
|
||
// UNDO_TRIGGER_KEYS - spaces, enter, delete, backspace, all punctuation
|
||
// Backspace, Enter, Space, Delete, (; :) (Firefox), (= +) (Firefox),
|
||
// Numpad +, Numpad -, (; :), (= +),
|
||
// (, <), (- _), (. >), (/ ?), (` ~), ([ {), (\ |), (] }), (' ")
|
||
// NOTE - Firefox: 173 = (- _) -- adding this to UNDO_TRIGGER_KEYS
|
||
var UNDO_TRIGGER_KEYS = /^(8|13|32|46|59|61|107|109|173|186|187|188|189|190|191|192|219|220|221|222)$/i;
|
||
var _pasteHandler;
|
||
|
||
// defaults to the paragraph element, but we need the line-break or it doesn't allow you to type into the empty element
|
||
// non IE is '<p><br/></p>', ie is '<p></p>' as for once IE gets it correct...
|
||
var _defaultVal, _defaultTest;
|
||
|
||
var _CTRL_KEY = 0x0001;
|
||
var _META_KEY = 0x0002;
|
||
var _ALT_KEY = 0x0004;
|
||
var _SHIFT_KEY = 0x0008;
|
||
// map events to special keys...
|
||
// mappings is an array of maps from events to specialKeys as declared in textAngularSetup
|
||
var _keyMappings = [
|
||
// ctrl/command + z
|
||
{
|
||
specialKey: 'UndoKey',
|
||
forbiddenModifiers: _ALT_KEY + _SHIFT_KEY,
|
||
mustHaveModifiers: [_META_KEY + _CTRL_KEY],
|
||
keyCode: 90
|
||
},
|
||
// ctrl/command + shift + z
|
||
{
|
||
specialKey: 'RedoKey',
|
||
forbiddenModifiers: _ALT_KEY,
|
||
mustHaveModifiers: [_META_KEY + _CTRL_KEY, _SHIFT_KEY],
|
||
keyCode: 90
|
||
},
|
||
// ctrl/command + y
|
||
{
|
||
specialKey: 'RedoKey',
|
||
forbiddenModifiers: _ALT_KEY + _SHIFT_KEY,
|
||
mustHaveModifiers: [_META_KEY + _CTRL_KEY],
|
||
keyCode: 89
|
||
},
|
||
// TabKey
|
||
{
|
||
specialKey: 'TabKey',
|
||
forbiddenModifiers: _META_KEY + _SHIFT_KEY + _ALT_KEY + _CTRL_KEY,
|
||
mustHaveModifiers: [],
|
||
keyCode: 9
|
||
},
|
||
// shift + TabKey
|
||
{
|
||
specialKey: 'ShiftTabKey',
|
||
forbiddenModifiers: _META_KEY + _ALT_KEY + _CTRL_KEY,
|
||
mustHaveModifiers: [_SHIFT_KEY],
|
||
keyCode: 9
|
||
}
|
||
];
|
||
function _mapKeys(event) {
|
||
var specialKey;
|
||
_keyMappings.forEach(function (map){
|
||
if (map.keyCode === event.keyCode) {
|
||
var netModifiers = (event.metaKey ? _META_KEY: 0) +
|
||
(event.ctrlKey ? _CTRL_KEY: 0) +
|
||
(event.shiftKey ? _SHIFT_KEY: 0) +
|
||
(event.altKey ? _ALT_KEY: 0);
|
||
if (map.forbiddenModifiers & netModifiers) return;
|
||
if (map.mustHaveModifiers.every(function (modifier) { return netModifiers & modifier; })){
|
||
specialKey = map.specialKey;
|
||
}
|
||
}
|
||
});
|
||
return specialKey;
|
||
}
|
||
|
||
// set the default to be a paragraph value
|
||
if(attrs.taDefaultWrap === undefined) attrs.taDefaultWrap = 'p';
|
||
/* istanbul ignore next: ie specific test */
|
||
if(attrs.taDefaultWrap === ''){
|
||
_defaultVal = '';
|
||
_defaultTest = (_browserDetect.ie === undefined)? '<div><br></div>' : (_browserDetect.ie >= 11)? '<p><br></p>' : (_browserDetect.ie <= 8)? '<P> </P>' : '<p> </p>';
|
||
}else{
|
||
_defaultVal = (_browserDetect.ie === undefined || _browserDetect.ie >= 11)?
|
||
'<' + attrs.taDefaultWrap + '><br></' + attrs.taDefaultWrap + '>' :
|
||
(_browserDetect.ie <= 8)?
|
||
'<' + attrs.taDefaultWrap.toUpperCase() + '></' + attrs.taDefaultWrap.toUpperCase() + '>' :
|
||
'<' + attrs.taDefaultWrap + '></' + attrs.taDefaultWrap + '>';
|
||
_defaultTest = (_browserDetect.ie === undefined || _browserDetect.ie >= 11)?
|
||
'<' + attrs.taDefaultWrap + '><br></' + attrs.taDefaultWrap + '>' :
|
||
(_browserDetect.ie <= 8)?
|
||
'<' + attrs.taDefaultWrap.toUpperCase() + '> </' + attrs.taDefaultWrap.toUpperCase() + '>' :
|
||
'<' + attrs.taDefaultWrap + '> </' + attrs.taDefaultWrap + '>';
|
||
}
|
||
|
||
/* istanbul ignore else */
|
||
if(!ngModelOptions.$options) ngModelOptions.$options = {}; // ng-model-options support
|
||
|
||
var _blankTest = _taBlankTest(_defaultTest);
|
||
|
||
var _ensureContentWrapped = function(value) {
|
||
if (_blankTest(value)) return value;
|
||
var domTest = angular.element("<div>" + value + "</div>");
|
||
//console.log('domTest.children().length():', domTest.children().length);
|
||
if (domTest.children().length === 0) {
|
||
value = "<" + attrs.taDefaultWrap + ">" + value + "</" + attrs.taDefaultWrap + ">";
|
||
} else {
|
||
var _children = domTest[0].childNodes;
|
||
var i;
|
||
var _foundBlockElement = false;
|
||
for (i = 0; i < _children.length; i++) {
|
||
if (_foundBlockElement = _children[i].nodeName.toLowerCase().match(BLOCKELEMENTS)) break;
|
||
}
|
||
if (!_foundBlockElement) {
|
||
value = "<" + attrs.taDefaultWrap + ">" + value + "</" + attrs.taDefaultWrap + ">";
|
||
}
|
||
else{
|
||
value = "";
|
||
for(i = 0; i < _children.length; i++){
|
||
var node = _children[i];
|
||
var nodeName = node.nodeName.toLowerCase();
|
||
//console.log(nodeName);
|
||
if(nodeName === '#comment') {
|
||
value += '<!--' + node.nodeValue + '-->';
|
||
} else if(nodeName === '#text') {
|
||
// determine if this is all whitespace, if so, we will leave it as it is.
|
||
// otherwise, we will wrap it as it is
|
||
var text = node.textContent;
|
||
if (!text.trim()) {
|
||
// just whitespace
|
||
value += text;
|
||
} else {
|
||
// not pure white space so wrap in <p>...</p> or whatever attrs.taDefaultWrap is set to.
|
||
value += "<" + attrs.taDefaultWrap + ">" + text + "</" + attrs.taDefaultWrap + ">";
|
||
}
|
||
} else if(!nodeName.match(BLOCKELEMENTS)){
|
||
/* istanbul ignore next: Doesn't seem to trigger on tests */
|
||
var _subVal = (node.outerHTML || node.nodeValue);
|
||
/* istanbul ignore else: Doesn't seem to trigger on tests, is tested though */
|
||
if(_subVal.trim() !== '')
|
||
value += "<" + attrs.taDefaultWrap + ">" + _subVal + "</" + attrs.taDefaultWrap + ">";
|
||
else value += _subVal;
|
||
} else {
|
||
value += node.outerHTML;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
//console.log(value);
|
||
return value;
|
||
};
|
||
|
||
if(attrs.taPaste) _pasteHandler = $parse(attrs.taPaste);
|
||
|
||
element.addClass('ta-bind');
|
||
|
||
var _undoKeyupTimeout;
|
||
|
||
scope['$undoManager' + (attrs.id || '')] = ngModel.$undoManager = {
|
||
_stack: [],
|
||
_index: 0,
|
||
_max: 1000,
|
||
push: function(value){
|
||
if((typeof value === "undefined" || value === null) ||
|
||
((typeof this.current() !== "undefined" && this.current() !== null) && value === this.current())) return value;
|
||
if(this._index < this._stack.length - 1){
|
||
this._stack = this._stack.slice(0,this._index+1);
|
||
}
|
||
this._stack.push(value);
|
||
if(_undoKeyupTimeout) $timeout.cancel(_undoKeyupTimeout);
|
||
if(this._stack.length > this._max) this._stack.shift();
|
||
this._index = this._stack.length - 1;
|
||
return value;
|
||
},
|
||
undo: function(){
|
||
return this.setToIndex(this._index-1);
|
||
},
|
||
redo: function(){
|
||
return this.setToIndex(this._index+1);
|
||
},
|
||
setToIndex: function(index){
|
||
if(index < 0 || index > this._stack.length - 1){
|
||
return undefined;
|
||
}
|
||
this._index = index;
|
||
return this.current();
|
||
},
|
||
current: function(){
|
||
return this._stack[this._index];
|
||
}
|
||
};
|
||
|
||
var _redoUndoTimeout;
|
||
var _undo = scope['$undoTaBind' + (attrs.id || '')] = function(){
|
||
/* istanbul ignore else: can't really test it due to all changes being ignored as well in readonly */
|
||
if(!_isReadonly && _isContentEditable){
|
||
var content = ngModel.$undoManager.undo();
|
||
if(typeof content !== "undefined" && content !== null){
|
||
_setInnerHTML(content);
|
||
_setViewValue(content, false);
|
||
if(_redoUndoTimeout) $timeout.cancel(_redoUndoTimeout);
|
||
_redoUndoTimeout = $timeout(function(){
|
||
element[0].focus();
|
||
taSelection.setSelectionToElementEnd(element[0]);
|
||
}, 1);
|
||
}
|
||
}
|
||
};
|
||
|
||
var _redo = scope['$redoTaBind' + (attrs.id || '')] = function(){
|
||
/* istanbul ignore else: can't really test it due to all changes being ignored as well in readonly */
|
||
if(!_isReadonly && _isContentEditable){
|
||
var content = ngModel.$undoManager.redo();
|
||
if(typeof content !== "undefined" && content !== null){
|
||
_setInnerHTML(content);
|
||
_setViewValue(content, false);
|
||
/* istanbul ignore next */
|
||
if(_redoUndoTimeout) $timeout.cancel(_redoUndoTimeout);
|
||
_redoUndoTimeout = $timeout(function(){
|
||
element[0].focus();
|
||
taSelection.setSelectionToElementEnd(element[0]);
|
||
}, 1);
|
||
}
|
||
}
|
||
};
|
||
|
||
// in here we are undoing the converts used elsewhere to prevent the < > and & being displayed when they shouldn't in the code.
|
||
var _compileHtml = function(){
|
||
if(_isContentEditable) return element[0].innerHTML;
|
||
if(_isInputFriendly) return element.val();
|
||
throw ('textAngular Error: attempting to update non-editable taBind');
|
||
};
|
||
|
||
var _setViewValue = function(_val, triggerUndo, skipRender){
|
||
_skipRender = skipRender || false;
|
||
if(typeof triggerUndo === "undefined" || triggerUndo === null) triggerUndo = true && _isContentEditable; // if not contentEditable then the native undo/redo is fine
|
||
if(typeof _val === "undefined" || _val === null) _val = _compileHtml();
|
||
if(_blankTest(_val)){
|
||
// this avoids us from tripping the ng-pristine flag if we click in and out with out typing
|
||
if(ngModel.$viewValue !== '') ngModel.$setViewValue('');
|
||
if(triggerUndo && ngModel.$undoManager.current() !== '') ngModel.$undoManager.push('');
|
||
}else{
|
||
_reApplyOnSelectorHandlers();
|
||
if(ngModel.$viewValue !== _val){
|
||
ngModel.$setViewValue(_val);
|
||
if(triggerUndo) ngModel.$undoManager.push(_val);
|
||
}
|
||
}
|
||
ngModel.$render();
|
||
};
|
||
|
||
//used for updating when inserting wrapped elements
|
||
scope['updateTaBind' + (attrs.id || '')] = function(){
|
||
if(!_isReadonly) _setViewValue(undefined, undefined, true);
|
||
};
|
||
|
||
// catch DOM XSS via taSanitize
|
||
// Sanitizing both ways is identical
|
||
var _sanitize = function(unsafe){
|
||
return (ngModel.$oldViewValue = taSanitize(taFixChrome(unsafe), ngModel.$oldViewValue, _disableSanitizer));
|
||
};
|
||
|
||
// trigger the validation calls
|
||
if(element.attr('required')) ngModel.$validators.required = function(modelValue, viewValue) {
|
||
return !_blankTest(modelValue || viewValue);
|
||
};
|
||
// parsers trigger from the above keyup function or any other time that the viewValue is updated and parses it for storage in the ngModel
|
||
ngModel.$parsers.push(_sanitize);
|
||
ngModel.$parsers.unshift(_ensureContentWrapped);
|
||
// because textAngular is bi-directional (which is awesome) we need to also sanitize values going in from the server
|
||
ngModel.$formatters.push(_sanitize);
|
||
ngModel.$formatters.unshift(_ensureContentWrapped);
|
||
ngModel.$formatters.unshift(function(value){
|
||
return ngModel.$undoManager.push(value || '');
|
||
});
|
||
|
||
//this code is used to update the models when data is entered/deleted
|
||
if(_isInputFriendly){
|
||
scope.events = {};
|
||
if(!_isContentEditable){
|
||
// if a textarea or input just add in change and blur handlers, everything else is done by angulars input directive
|
||
element.on('change blur', scope.events.change = scope.events.blur = function(){
|
||
if(!_isReadonly) ngModel.$setViewValue(_compileHtml());
|
||
});
|
||
|
||
element.on('keydown', scope.events.keydown = function(event, eventData){
|
||
/* istanbul ignore else: this is for catching the jqLite testing*/
|
||
if(eventData) angular.extend(event, eventData);
|
||
// Reference to http://stackoverflow.com/questions/6140632/how-to-handle-tab-in-textarea
|
||
/* istanbul ignore else: otherwise normal functionality */
|
||
if(event.keyCode === 9){ // tab was pressed
|
||
// get caret position/selection
|
||
var start = this.selectionStart;
|
||
var end = this.selectionEnd;
|
||
|
||
var value = element.val();
|
||
if(event.shiftKey){
|
||
// find \t
|
||
var _linebreak = value.lastIndexOf('\n', start), _tab = value.lastIndexOf('\t', start);
|
||
if(_tab !== -1 && _tab >= _linebreak){
|
||
// set textarea value to: text before caret + tab + text after caret
|
||
element.val(value.substring(0, _tab) + value.substring(_tab + 1));
|
||
|
||
// put caret at right position again (add one for the tab)
|
||
this.selectionStart = this.selectionEnd = start - 1;
|
||
}
|
||
}else{
|
||
// set textarea value to: text before caret + tab + text after caret
|
||
element.val(value.substring(0, start) + "\t" + value.substring(end));
|
||
|
||
// put caret at right position again (add one for the tab)
|
||
this.selectionStart = this.selectionEnd = start + 1;
|
||
}
|
||
// prevent the focus lose
|
||
event.preventDefault();
|
||
}
|
||
});
|
||
|
||
var _repeat = function(string, n){
|
||
var result = '';
|
||
for(var _n = 0; _n < n; _n++) result += string;
|
||
return result;
|
||
};
|
||
|
||
// add a forEach function that will work on a NodeList, etc..
|
||
var forEach = function (array, callback, scope) {
|
||
for (var i= 0; i<array.length; i++) {
|
||
callback.call(scope, i, array[i]);
|
||
}
|
||
};
|
||
|
||
// handle <ul> or <ol> nodes
|
||
var recursiveListFormat = function(listNode, tablevel){
|
||
var _html = '';
|
||
var _subnodes = listNode.childNodes;
|
||
tablevel++;
|
||
// tab out and add the <ul> or <ol> html piece
|
||
_html += _repeat('\t', tablevel-1) + listNode.outerHTML.substring(0, 4);
|
||
forEach(_subnodes, function (index, node) {
|
||
/* istanbul ignore next: browser catch */
|
||
var nodeName = node.nodeName.toLowerCase();
|
||
if (nodeName === '#comment') {
|
||
_html += '<!--' + node.nodeValue + '-->';
|
||
return;
|
||
}
|
||
if (nodeName === '#text') {
|
||
_html += node.textContent;
|
||
return;
|
||
}
|
||
/* istanbul ignore next: not tested, and this was original code -- so not wanting to possibly cause an issue, leaving it... */
|
||
if(!node.outerHTML) {
|
||
// no html to add
|
||
return;
|
||
}
|
||
if(nodeName === 'ul' || nodeName === 'ol') {
|
||
_html += '\n' + recursiveListFormat(node, tablevel);
|
||
}
|
||
else {
|
||
// no reformatting within this subnode, so just do the tabing...
|
||
_html += '\n' + _repeat('\t', tablevel) + node.outerHTML;
|
||
}
|
||
});
|
||
// now add on the </ol> or </ul> piece
|
||
_html += '\n' + _repeat('\t', tablevel-1) + listNode.outerHTML.substring(listNode.outerHTML.lastIndexOf('<'));
|
||
return _html;
|
||
};
|
||
// handle formating of something like:
|
||
// <ol><!--First comment-->
|
||
// <li>Test Line 1<!--comment test list 1--></li>
|
||
// <ul><!--comment ul-->
|
||
// <li>Nested Line 1</li>
|
||
// <!--comment between nested lines--><li>Nested Line 2</li>
|
||
// </ul>
|
||
// <li>Test Line 3</li>
|
||
// </ol>
|
||
ngModel.$formatters.unshift(function(htmlValue){
|
||
// tabulate the HTML so it looks nicer
|
||
//
|
||
// first get a list of the nodes...
|
||
// we do this by using the element parser...
|
||
//
|
||
// doing this -- which is simpiler -- breaks our tests...
|
||
//var _nodes=angular.element(htmlValue);
|
||
var _nodes = angular.element('<div>' + htmlValue + '</div>')[0].childNodes;
|
||
if(_nodes.length > 0){
|
||
// do the reformatting of the layout...
|
||
htmlValue = '';
|
||
forEach(_nodes, function (index, node) {
|
||
var nodeName = node.nodeName.toLowerCase();
|
||
if (nodeName === '#comment') {
|
||
htmlValue += '<!--' + node.nodeValue + '-->';
|
||
return;
|
||
}
|
||
if (nodeName === '#text') {
|
||
htmlValue += node.textContent;
|
||
return;
|
||
}
|
||
/* istanbul ignore next: not tested, and this was original code -- so not wanting to possibly cause an issue, leaving it... */
|
||
if(!node.outerHTML)
|
||
{
|
||
// nothing to format!
|
||
return;
|
||
}
|
||
if(htmlValue.length > 0) {
|
||
// we aready have some content, so drop to a new line
|
||
htmlValue += '\n';
|
||
}
|
||
if(nodeName === 'ul' || nodeName === 'ol') {
|
||
// okay a set of list stuff we want to reformat in a nested way
|
||
htmlValue += '' + recursiveListFormat(node, 0);
|
||
}
|
||
else {
|
||
// just use the original without any additional formating
|
||
htmlValue += '' + node.outerHTML;
|
||
}
|
||
});
|
||
}
|
||
return htmlValue;
|
||
});
|
||
}else{
|
||
// all the code specific to contenteditable divs
|
||
var _processingPaste = false;
|
||
/* istanbul ignore next: phantom js cannot test this for some reason */
|
||
var processpaste = function(text) {
|
||
var _isOneNote = text.match(/content=["']*OneNote.File/i);
|
||
/* istanbul ignore else: don't care if nothing pasted */
|
||
//console.log(text);
|
||
if(text && text.trim().length){
|
||
// test paste from word/microsoft product
|
||
if(text.match(/class=["']*Mso(Normal|List)/i) || text.match(/content=["']*Word.Document/i) || text.match(/content=["']*OneNote.File/i)){
|
||
var textFragment = text.match(/<!--StartFragment-->([\s\S]*?)<!--EndFragment-->/i);
|
||
if(!textFragment) textFragment = text;
|
||
else textFragment = textFragment[1];
|
||
textFragment = textFragment.replace(/<o:p>[\s\S]*?<\/o:p>/ig, '').replace(/class=(["']|)MsoNormal(["']|)/ig, '');
|
||
var dom = angular.element("<div>" + textFragment + "</div>");
|
||
var targetDom = angular.element("<div></div>");
|
||
var _list = {
|
||
element: null,
|
||
lastIndent: [],
|
||
lastLi: null,
|
||
isUl: false
|
||
};
|
||
_list.lastIndent.peek = function(){
|
||
var n = this.length;
|
||
if (n>0) return this[n-1];
|
||
};
|
||
var _resetList = function(isUl){
|
||
_list.isUl = isUl;
|
||
_list.element = angular.element(isUl ? "<ul>" : "<ol>");
|
||
_list.lastIndent = [];
|
||
_list.lastIndent.peek = function(){
|
||
var n = this.length;
|
||
if (n>0) return this[n-1];
|
||
};
|
||
_list.lastLevelMatch = null;
|
||
};
|
||
for(var i = 0; i <= dom[0].childNodes.length; i++){
|
||
if(!dom[0].childNodes[i] || dom[0].childNodes[i].nodeName === "#text"){
|
||
continue;
|
||
} else {
|
||
var tagName = dom[0].childNodes[i].tagName.toLowerCase();
|
||
if(tagName !== "p" && tagName !== "h1" && tagName !== "h2" && tagName !== "h3" && tagName !== "h4" && tagName !== "h5" && tagName !== "h6"){
|
||
continue;
|
||
}
|
||
}
|
||
var el = angular.element(dom[0].childNodes[i]);
|
||
var _listMatch = (el.attr('class') || '').match(/MsoList(Bullet|Number|Paragraph)(CxSp(First|Middle|Last)|)/i);
|
||
|
||
if(_listMatch){
|
||
if(el[0].childNodes.length < 2 || el[0].childNodes[1].childNodes.length < 1){
|
||
continue;
|
||
}
|
||
var isUl = _listMatch[1].toLowerCase() === "bullet" || (_listMatch[1].toLowerCase() !== "number" && !(/^[^0-9a-z<]*[0-9a-z]+[^0-9a-z<>]</i.test(el[0].childNodes[1].innerHTML) || /^[^0-9a-z<]*[0-9a-z]+[^0-9a-z<>]</i.test(el[0].childNodes[1].childNodes[0].innerHTML)));
|
||
var _indentMatch = (el.attr('style') || '').match(/margin-left:([\-\.0-9]*)/i);
|
||
var indent = parseFloat((_indentMatch)?_indentMatch[1]:0);
|
||
var _levelMatch = (el.attr('style') || '').match(/mso-list:l([0-9]+) level([0-9]+) lfo[0-9+]($|;)/i);
|
||
// prefers the mso-list syntax
|
||
|
||
if(_levelMatch && _levelMatch[2]) indent = parseInt(_levelMatch[2]);
|
||
|
||
if ((_levelMatch && (!_list.lastLevelMatch || _levelMatch[1] !== _list.lastLevelMatch[1])) || !_listMatch[3] || _listMatch[3].toLowerCase() === "first" || (_list.lastIndent.peek() === null) || (_list.isUl !== isUl && _list.lastIndent.peek() === indent)) {
|
||
_resetList(isUl);
|
||
targetDom.append(_list.element);
|
||
} else if (_list.lastIndent.peek() != null && _list.lastIndent.peek() < indent){
|
||
_list.element = angular.element(isUl ? "<ul>" : "<ol>");
|
||
_list.lastLi.append(_list.element);
|
||
} else if (_list.lastIndent.peek() != null && _list.lastIndent.peek() > indent){
|
||
while(_list.lastIndent.peek() != null && _list.lastIndent.peek() > indent){
|
||
if(_list.element.parent()[0].tagName.toLowerCase() === 'li'){
|
||
_list.element = _list.element.parent();
|
||
continue;
|
||
}else if(/[uo]l/i.test(_list.element.parent()[0].tagName.toLowerCase())){
|
||
_list.element = _list.element.parent();
|
||
}else{ // else it's it should be a sibling
|
||
break;
|
||
}
|
||
_list.lastIndent.pop();
|
||
}
|
||
_list.isUl = _list.element[0].tagName.toLowerCase() === "ul";
|
||
if (isUl !== _list.isUl) {
|
||
_resetList(isUl);
|
||
targetDom.append(_list.element);
|
||
}
|
||
}
|
||
|
||
_list.lastLevelMatch = _levelMatch;
|
||
if(indent !== _list.lastIndent.peek()) _list.lastIndent.push(indent);
|
||
_list.lastLi = angular.element("<li>");
|
||
_list.element.append(_list.lastLi);
|
||
_list.lastLi.html(el.html().replace(/<!(--|)\[if !supportLists\](--|)>[\s\S]*?<!(--|)\[endif\](--|)>/ig, ''));
|
||
el.remove();
|
||
}else{
|
||
_resetList(false);
|
||
targetDom.append(el);
|
||
}
|
||
}
|
||
var _unwrapElement = function(node){
|
||
node = angular.element(node);
|
||
for(var _n = node[0].childNodes.length - 1; _n >= 0; _n--) node.after(node[0].childNodes[_n]);
|
||
node.remove();
|
||
};
|
||
|
||
angular.forEach(targetDom.find('span'), function(node){
|
||
node.removeAttribute('lang');
|
||
if(node.attributes.length <= 0) _unwrapElement(node);
|
||
});
|
||
angular.forEach(targetDom.find('font'), _unwrapElement);
|
||
|
||
text = targetDom.html();
|
||
if(_isOneNote){
|
||
text = targetDom.html() || dom.html();
|
||
}
|
||
}else{
|
||
// remove unnecessary chrome insert
|
||
text = text.replace(/<(|\/)meta[^>]*?>/ig, '');
|
||
if(text.match(/<[^>]*?(ta-bind)[^>]*?>/)){
|
||
// entire text-angular or ta-bind has been pasted, REMOVE AT ONCE!!
|
||
if(text.match(/<[^>]*?(text-angular)[^>]*?>/)){
|
||
var _el = angular.element("<div>" + text + "</div>");
|
||
_el.find('textarea').remove();
|
||
var binds = taDOM.getByAttribute(_el, 'ta-bind');
|
||
for(var _b = 0; _b < binds.length; _b++){
|
||
var _target = binds[_b][0].parentNode.parentNode;
|
||
for(var _c = 0; _c < binds[_b][0].childNodes.length; _c++){
|
||
_target.parentNode.insertBefore(binds[_b][0].childNodes[_c], _target);
|
||
}
|
||
_target.parentNode.removeChild(_target);
|
||
}
|
||
text = _el.html().replace('<br class="Apple-interchange-newline">', '');
|
||
}
|
||
}else if(text.match(/^<span/)){
|
||
// in case of pasting only a span - chrome paste, remove them. THis is just some wierd formatting
|
||
// if we remove the '<span class="Apple-converted-space"> </span>' here we destroy the spacing
|
||
// on paste from even ourselves!
|
||
if (!text.match(/<span class=(\"Apple-converted-space\"|\'Apple-converted-space\')>.<\/span>/ig)) {
|
||
text = text.replace(/<(|\/)span[^>]*?>/ig, '');
|
||
}
|
||
}
|
||
// Webkit on Apple tags
|
||
text = text.replace(/<br class="Apple-interchange-newline"[^>]*?>/ig, '').replace(/<span class="Apple-converted-space">( | )<\/span>/ig, ' ');
|
||
}
|
||
|
||
if (/<li(\s.*)?>/i.test(text) && /(<ul(\s.*)?>|<ol(\s.*)?>).*<li(\s.*)?>/i.test(text) === false) {
|
||
// insert missing parent of li element
|
||
text = text.replace(/<li(\s.*)?>.*<\/li(\s.*)?>/i, '<ul>$&</ul>');
|
||
}
|
||
|
||
// parse whitespace from plaintext input, starting with preceding spaces that get stripped on paste
|
||
text = text.replace(/^[ |\u00A0]+/gm, function (match) {
|
||
var result = '';
|
||
for (var i = 0; i < match.length; i++) {
|
||
result += ' ';
|
||
}
|
||
return result;
|
||
}).replace(/\n|\r\n|\r/g, '<br />').replace(/\t/g, ' ');
|
||
|
||
if(_pasteHandler) text = _pasteHandler(scope, {$html: text}) || text;
|
||
|
||
text = taSanitize(text, '', _disableSanitizer);
|
||
|
||
taSelection.insertHtml(text, element[0]);
|
||
$timeout(function(){
|
||
ngModel.$setViewValue(_compileHtml());
|
||
_processingPaste = false;
|
||
element.removeClass('processing-paste');
|
||
}, 0);
|
||
}else{
|
||
_processingPaste = false;
|
||
element.removeClass('processing-paste');
|
||
}
|
||
};
|
||
|
||
element.on('paste', scope.events.paste = function(e, eventData){
|
||
/* istanbul ignore else: this is for catching the jqLite testing*/
|
||
if(eventData) angular.extend(e, eventData);
|
||
if(_isReadonly || _processingPaste){
|
||
e.stopPropagation();
|
||
e.preventDefault();
|
||
return false;
|
||
}
|
||
|
||
// Code adapted from http://stackoverflow.com/questions/2176861/javascript-get-clipboard-data-on-paste-event-cross-browser/6804718#6804718
|
||
_processingPaste = true;
|
||
element.addClass('processing-paste');
|
||
var pastedContent;
|
||
var clipboardData = (e.originalEvent || e).clipboardData;
|
||
if (clipboardData && clipboardData.getData && clipboardData.types.length > 0) {// Webkit - get data from clipboard, put into editdiv, cleanup, then cancel event
|
||
var _types = "";
|
||
for(var _t = 0; _t < clipboardData.types.length; _t++){
|
||
_types += " " + clipboardData.types[_t];
|
||
}
|
||
/* istanbul ignore next: browser tests */
|
||
if (/text\/html/i.test(_types)) {
|
||
pastedContent = clipboardData.getData('text/html');
|
||
} else if (/text\/plain/i.test(_types)) {
|
||
pastedContent = clipboardData.getData('text/plain');
|
||
}
|
||
|
||
processpaste(pastedContent);
|
||
e.stopPropagation();
|
||
e.preventDefault();
|
||
return false;
|
||
} else {// Everything else - empty editdiv and allow browser to paste content into it, then cleanup
|
||
var _savedSelection = rangy.saveSelection(),
|
||
_tempDiv = angular.element('<div class="ta-hidden-input" contenteditable="true"></div>');
|
||
$document.find('body').append(_tempDiv);
|
||
_tempDiv[0].focus();
|
||
$timeout(function(){
|
||
// restore selection
|
||
rangy.restoreSelection(_savedSelection);
|
||
processpaste(_tempDiv[0].innerHTML);
|
||
element[0].focus();
|
||
_tempDiv.remove();
|
||
}, 0);
|
||
}
|
||
});
|
||
element.on('cut', scope.events.cut = function(e){
|
||
// timeout to next is needed as otherwise the paste/cut event has not finished actually changing the display
|
||
if(!_isReadonly) $timeout(function(){
|
||
ngModel.$setViewValue(_compileHtml());
|
||
}, 0);
|
||
else e.preventDefault();
|
||
});
|
||
|
||
element.on('keydown', scope.events.keydown = function(event, eventData){
|
||
/* istanbul ignore else: this is for catching the jqLite testing*/
|
||
if(eventData) angular.extend(event, eventData);
|
||
event.specialKey = _mapKeys(event);
|
||
var userSpecialKey;
|
||
/* istanbul ignore next: difficult to test */
|
||
taOptions.keyMappings.forEach(function (mapping) {
|
||
if (event.specialKey === mapping.commandKeyCode) {
|
||
// taOptions has remapped this binding... so
|
||
// we disable our own
|
||
event.specialKey = undefined;
|
||
}
|
||
if (mapping.testForKey(event)) {
|
||
userSpecialKey = mapping.commandKeyCode;
|
||
}
|
||
if ((mapping.commandKeyCode === 'UndoKey') || (mapping.commandKeyCode === 'RedoKey')) {
|
||
// this is necessary to fully stop the propagation.
|
||
if (!mapping.enablePropagation) {
|
||
event.preventDefault();
|
||
}
|
||
}
|
||
});
|
||
/* istanbul ignore next: difficult to test */
|
||
if (typeof userSpecialKey !== 'undefined') {
|
||
event.specialKey = userSpecialKey;
|
||
}
|
||
/* istanbul ignore next: difficult to test as can't seem to select */
|
||
if ((typeof event.specialKey !== 'undefined') && (
|
||
event.specialKey !== 'UndoKey' || event.specialKey !== 'RedoKey'
|
||
)) {
|
||
event.preventDefault();
|
||
textAngularManager.sendKeyCommand(scope, event);
|
||
}
|
||
/* istanbul ignore else: readonly check */
|
||
if(!_isReadonly){
|
||
if (event.specialKey==='UndoKey') {
|
||
_undo();
|
||
event.preventDefault();
|
||
}
|
||
if (event.specialKey==='RedoKey') {
|
||
_redo();
|
||
event.preventDefault();
|
||
}
|
||
/* istanbul ignore next: difficult to test as can't seem to select */
|
||
if(event.keyCode === 13 && !event.shiftKey){
|
||
var contains = function(a, obj) {
|
||
for (var i = 0; i < a.length; i++) {
|
||
if (a[i] === obj) {
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
};
|
||
var $selection;
|
||
var selection = taSelection.getSelectionElement();
|
||
if(!selection.tagName.match(VALIDELEMENTS)) return;
|
||
var _new = angular.element(_defaultVal);
|
||
// if we are in the last element of a blockquote, or ul or ol and the element is blank
|
||
// we need to pull the element outside of the said type
|
||
var moveOutsideElements = ['blockquote', 'ul', 'ol'];
|
||
if (contains(moveOutsideElements, selection.parentNode.tagName.toLowerCase())) {
|
||
if (/^<br(|\/)>$/i.test(selection.innerHTML.trim()) && !selection.nextSibling) {
|
||
// if last element is blank, pull element outside.
|
||
$selection = angular.element(selection);
|
||
var _parent = $selection.parent();
|
||
_parent.after(_new);
|
||
$selection.remove();
|
||
if (_parent.children().length === 0) _parent.remove();
|
||
taSelection.setSelectionToElementStart(_new[0]);
|
||
event.preventDefault();
|
||
}
|
||
if (/^<[^>]+><br(|\/)><\/[^>]+>$/i.test(selection.innerHTML.trim())) {
|
||
$selection = angular.element(selection);
|
||
$selection.after(_new);
|
||
$selection.remove();
|
||
taSelection.setSelectionToElementStart(_new[0]);
|
||
event.preventDefault();
|
||
}
|
||
}
|
||
}
|
||
}
|
||
});
|
||
var _keyupTimeout;
|
||
element.on('keyup', scope.events.keyup = function(event, eventData){
|
||
/* istanbul ignore else: this is for catching the jqLite testing*/
|
||
if(eventData) angular.extend(event, eventData);
|
||
/* istanbul ignore next: FF specific bug fix */
|
||
if (event.keyCode === 9) {
|
||
var _selection = taSelection.getSelection();
|
||
if(_selection.start.element === element[0] && element.children().length) taSelection.setSelectionToElementStart(element.children()[0]);
|
||
return;
|
||
}
|
||
if(_undoKeyupTimeout) $timeout.cancel(_undoKeyupTimeout);
|
||
if(!_isReadonly && !BLOCKED_KEYS.test(event.keyCode)){
|
||
// if enter - insert new taDefaultWrap, if shift+enter insert <br/>
|
||
if(_defaultVal !== '' && event.keyCode === 13){
|
||
if(!event.shiftKey){
|
||
// new paragraph, br should be caught correctly
|
||
var selection = taSelection.getSelectionElement();
|
||
while(!selection.tagName.match(VALIDELEMENTS) && selection !== element[0]){
|
||
selection = selection.parentNode;
|
||
}
|
||
|
||
if(selection.tagName.toLowerCase() !== attrs.taDefaultWrap && selection.tagName.toLowerCase() !== 'li' && (selection.innerHTML.trim() === '' || selection.innerHTML.trim() === '<br>')){
|
||
var _new = angular.element(_defaultVal);
|
||
angular.element(selection).replaceWith(_new);
|
||
taSelection.setSelectionToElementStart(_new[0]);
|
||
}
|
||
}
|
||
}
|
||
var val = _compileHtml();
|
||
if(_defaultVal !== '' && val.trim() === ''){
|
||
_setInnerHTML(_defaultVal);
|
||
taSelection.setSelectionToElementStart(element.children()[0]);
|
||
}else if(val.substring(0, 1) !== '<' && attrs.taDefaultWrap !== ''){
|
||
/* we no longer do this, since there can be comments here and white space
|
||
var _savedSelection = rangy.saveSelection();
|
||
val = _compileHtml();
|
||
val = "<" + attrs.taDefaultWrap + ">" + val + "</" + attrs.taDefaultWrap + ">";
|
||
_setInnerHTML(val);
|
||
rangy.restoreSelection(_savedSelection);
|
||
*/
|
||
}
|
||
var triggerUndo = _lastKey !== event.keyCode && UNDO_TRIGGER_KEYS.test(event.keyCode);
|
||
if(_keyupTimeout) $timeout.cancel(_keyupTimeout);
|
||
_keyupTimeout = $timeout(function() {
|
||
_setViewValue(val, triggerUndo, true);
|
||
}, ngModelOptions.$options.debounce || 400);
|
||
if(!triggerUndo) _undoKeyupTimeout = $timeout(function(){ ngModel.$undoManager.push(val); }, 250);
|
||
_lastKey = event.keyCode;
|
||
}
|
||
});
|
||
|
||
element.on('blur', scope.events.blur = function(){
|
||
_focussed = false;
|
||
/* istanbul ignore else: if readonly don't update model */
|
||
if(!_isReadonly){
|
||
_setViewValue(undefined, undefined, true);
|
||
}else{
|
||
_skipRender = true; // don't redo the whole thing, just check the placeholder logic
|
||
ngModel.$render();
|
||
}
|
||
});
|
||
|
||
// Placeholders not supported on ie 8 and below
|
||
if(attrs.placeholder && (_browserDetect.ie > 8 || _browserDetect.ie === undefined)){
|
||
var rule;
|
||
if(attrs.id) rule = addCSSRule('#' + attrs.id + '.placeholder-text:before', 'content: "' + attrs.placeholder + '"');
|
||
else throw('textAngular Error: An unique ID is required for placeholders to work');
|
||
|
||
scope.$on('$destroy', function(){
|
||
removeCSSRule(rule);
|
||
});
|
||
}
|
||
|
||
element.on('focus', scope.events.focus = function(){
|
||
_focussed = true;
|
||
element.removeClass('placeholder-text');
|
||
_reApplyOnSelectorHandlers();
|
||
});
|
||
|
||
element.on('mouseup', scope.events.mouseup = function(){
|
||
var _selection = taSelection.getSelection();
|
||
if(_selection.start.element === element[0] && element.children().length) taSelection.setSelectionToElementStart(element.children()[0]);
|
||
});
|
||
|
||
// prevent propagation on mousedown in editor, see #206
|
||
element.on('mousedown', scope.events.mousedown = function(event, eventData){
|
||
/* istanbul ignore else: this is for catching the jqLite testing*/
|
||
if(eventData) angular.extend(event, eventData);
|
||
event.stopPropagation();
|
||
});
|
||
}
|
||
}
|
||
|
||
var selectorClickHandler = function(event){
|
||
// emit the element-select event, pass the element
|
||
scope.$emit('ta-element-select', this);
|
||
event.preventDefault();
|
||
return false;
|
||
};
|
||
var fileDropHandler = function(event, eventData){
|
||
/* istanbul ignore else: this is for catching the jqLite testing*/
|
||
if(eventData) angular.extend(event, eventData);
|
||
// emit the drop event, pass the element, preventing should be done elsewhere
|
||
if(!dropFired && !_isReadonly){
|
||
dropFired = true;
|
||
var dataTransfer;
|
||
if(event.originalEvent) dataTransfer = event.originalEvent.dataTransfer;
|
||
else dataTransfer = event.dataTransfer;
|
||
scope.$emit('ta-drop-event', this, event, dataTransfer);
|
||
$timeout(function(){
|
||
dropFired = false;
|
||
_setViewValue(undefined, undefined, true);
|
||
}, 100);
|
||
}
|
||
};
|
||
|
||
//used for updating when inserting wrapped elements
|
||
var _reApplyOnSelectorHandlers = scope['reApplyOnSelectorHandlers' + (attrs.id || '')] = function(){
|
||
/* istanbul ignore else */
|
||
if(!_isReadonly) angular.forEach(taSelectableElements, function(selector){
|
||
// check we don't apply the handler twice
|
||
element.find(selector)
|
||
.off('click', selectorClickHandler)
|
||
.on('click', selectorClickHandler);
|
||
});
|
||
};
|
||
|
||
var _setInnerHTML = function(newval){
|
||
element[0].innerHTML = newval;
|
||
};
|
||
var _renderTimeout;
|
||
var _renderInProgress = false;
|
||
// changes to the model variable from outside the html/text inputs
|
||
ngModel.$render = function(){
|
||
/* istanbul ignore if: Catches rogue renders, hard to replicate in tests */
|
||
if(_renderInProgress) return;
|
||
else _renderInProgress = true;
|
||
// catch model being null or undefined
|
||
var val = ngModel.$viewValue || '';
|
||
// if the editor isn't focused it needs to be updated, otherwise it's receiving user input
|
||
if(!_skipRender){
|
||
/* istanbul ignore else: in other cases we don't care */
|
||
if(_isContentEditable && _focussed){
|
||
// update while focussed
|
||
element.removeClass('placeholder-text');
|
||
if(_renderTimeout) $timeout.cancel(_renderTimeout);
|
||
_renderTimeout = $timeout(function(){
|
||
/* istanbul ignore if: Can't be bothered testing this... */
|
||
if(!_focussed){
|
||
element[0].focus();
|
||
taSelection.setSelectionToElementEnd(element.children()[element.children().length - 1]);
|
||
}
|
||
_renderTimeout = undefined;
|
||
}, 1);
|
||
}
|
||
if(_isContentEditable){
|
||
// WYSIWYG Mode
|
||
if(attrs.placeholder){
|
||
if(val === ''){
|
||
// blank
|
||
_setInnerHTML(_defaultVal);
|
||
}else{
|
||
// not-blank
|
||
_setInnerHTML(val);
|
||
}
|
||
}else{
|
||
_setInnerHTML((val === '') ? _defaultVal : val);
|
||
}
|
||
// if in WYSIWYG and readOnly we kill the use of links by clicking
|
||
if(!_isReadonly){
|
||
_reApplyOnSelectorHandlers();
|
||
element.on('drop', fileDropHandler);
|
||
}else{
|
||
element.off('drop', fileDropHandler);
|
||
}
|
||
}else if(element[0].tagName.toLowerCase() !== 'textarea' && element[0].tagName.toLowerCase() !== 'input'){
|
||
// make sure the end user can SEE the html code as a display. This is a read-only display element
|
||
_setInnerHTML(taApplyCustomRenderers(val));
|
||
}else{
|
||
// only for input and textarea inputs
|
||
element.val(val);
|
||
}
|
||
}
|
||
if(_isContentEditable && attrs.placeholder){
|
||
if(val === ''){
|
||
if(_focussed) element.removeClass('placeholder-text');
|
||
else element.addClass('placeholder-text');
|
||
}else{
|
||
element.removeClass('placeholder-text');
|
||
}
|
||
}
|
||
_renderInProgress = _skipRender = false;
|
||
};
|
||
|
||
if(attrs.taReadonly){
|
||
//set initial value
|
||
_isReadonly = scope.$eval(attrs.taReadonly);
|
||
if(_isReadonly){
|
||
element.addClass('ta-readonly');
|
||
// we changed to readOnly mode (taReadonly='true')
|
||
if(element[0].tagName.toLowerCase() === 'textarea' || element[0].tagName.toLowerCase() === 'input'){
|
||
element.attr('disabled', 'disabled');
|
||
}
|
||
if(element.attr('contenteditable') !== undefined && element.attr('contenteditable')){
|
||
element.removeAttr('contenteditable');
|
||
}
|
||
}else{
|
||
element.removeClass('ta-readonly');
|
||
// we changed to NOT readOnly mode (taReadonly='false')
|
||
if(element[0].tagName.toLowerCase() === 'textarea' || element[0].tagName.toLowerCase() === 'input'){
|
||
element.removeAttr('disabled');
|
||
}else if(_isContentEditable){
|
||
element.attr('contenteditable', 'true');
|
||
}
|
||
}
|
||
// taReadonly only has an effect if the taBind element is an input or textarea or has contenteditable='true' on it.
|
||
// Otherwise it is readonly by default
|
||
scope.$watch(attrs.taReadonly, function(newVal, oldVal){
|
||
if(oldVal === newVal) return;
|
||
if(newVal){
|
||
element.addClass('ta-readonly');
|
||
// we changed to readOnly mode (taReadonly='true')
|
||
if(element[0].tagName.toLowerCase() === 'textarea' || element[0].tagName.toLowerCase() === 'input'){
|
||
element.attr('disabled', 'disabled');
|
||
}
|
||
if(element.attr('contenteditable') !== undefined && element.attr('contenteditable')){
|
||
element.removeAttr('contenteditable');
|
||
}
|
||
// turn ON selector click handlers
|
||
angular.forEach(taSelectableElements, function(selector){
|
||
element.find(selector).on('click', selectorClickHandler);
|
||
});
|
||
element.off('drop', fileDropHandler);
|
||
}else{
|
||
element.removeClass('ta-readonly');
|
||
// we changed to NOT readOnly mode (taReadonly='false')
|
||
if(element[0].tagName.toLowerCase() === 'textarea' || element[0].tagName.toLowerCase() === 'input'){
|
||
element.removeAttr('disabled');
|
||
}else if(_isContentEditable){
|
||
element.attr('contenteditable', 'true');
|
||
}
|
||
// remove the selector click handlers
|
||
angular.forEach(taSelectableElements, function(selector){
|
||
element.find(selector).off('click', selectorClickHandler);
|
||
});
|
||
element.on('drop', fileDropHandler);
|
||
}
|
||
_isReadonly = newVal;
|
||
});
|
||
}
|
||
|
||
// Initialise the selectableElements
|
||
// if in WYSIWYG and readOnly we kill the use of links by clicking
|
||
if(_isContentEditable && !_isReadonly){
|
||
angular.forEach(taSelectableElements, function(selector){
|
||
element.find(selector).on('click', selectorClickHandler);
|
||
});
|
||
element.on('drop', fileDropHandler);
|
||
element.on('blur', function(){
|
||
/* istanbul ignore next: webkit fix */
|
||
if(_browserDetect.webkit) { // detect webkit
|
||
globalContentEditableBlur = true;
|
||
}
|
||
});
|
||
}
|
||
}
|
||
};
|
||
}]);
|
||
|
||
// this global var is used to prevent multiple fires of the drop event. Needs to be global to the textAngular file.
|
||
var dropFired = false;
|
||
var textAngular = angular.module("textAngular", ['ngSanitize', 'textAngularSetup', 'textAngular.factories', 'textAngular.DOM', 'textAngular.validators', 'textAngular.taBind']); //This makes ngSanitize required
|
||
|
||
textAngular.config([function(){
|
||
// clear taTools variable. Just catches testing and any other time that this config may run multiple times...
|
||
angular.forEach(taTools, function(value, key){ delete taTools[key]; });
|
||
}]);
|
||
|
||
textAngular.directive("textAngular", [
|
||
'$compile', '$timeout', 'taOptions', 'taSelection', 'taExecCommand',
|
||
'textAngularManager', '$document', '$animate', '$log', '$q', '$parse',
|
||
function($compile, $timeout, taOptions, taSelection, taExecCommand,
|
||
textAngularManager, $document, $animate, $log, $q, $parse){
|
||
return {
|
||
require: '?ngModel',
|
||
scope: {},
|
||
restrict: "EA",
|
||
priority: 2, // So we override validators correctly
|
||
link: function(scope, element, attrs, ngModel){
|
||
// all these vars should not be accessable outside this directive
|
||
var _keydown, _keyup, _keypress, _mouseup, _focusin, _focusout,
|
||
_originalContents, _toolbars,
|
||
_serial = (attrs.serial) ? attrs.serial : Math.floor(Math.random() * 10000000000000000),
|
||
_taExecCommand, _resizeMouseDown, _updateSelectedStylesTimeout;
|
||
|
||
scope._name = (attrs.name) ? attrs.name : 'textAngularEditor' + _serial;
|
||
|
||
var oneEvent = function(_element, event, action){
|
||
$timeout(function(){
|
||
// shim the .one till fixed
|
||
var _func = function(){
|
||
_element.off(event, _func);
|
||
action.apply(this, arguments);
|
||
};
|
||
_element.on(event, _func);
|
||
}, 100);
|
||
};
|
||
_taExecCommand = taExecCommand(attrs.taDefaultWrap);
|
||
// get the settings from the defaults and add our specific functions that need to be on the scope
|
||
angular.extend(scope, angular.copy(taOptions), {
|
||
// wraps the selection in the provided tag / execCommand function. Should only be called in WYSIWYG mode.
|
||
wrapSelection: function(command, opt, isSelectableElementTool){
|
||
if(command.toLowerCase() === "undo"){
|
||
scope['$undoTaBindtaTextElement' + _serial]();
|
||
}else if(command.toLowerCase() === "redo"){
|
||
scope['$redoTaBindtaTextElement' + _serial]();
|
||
}else{
|
||
// catch errors like FF erroring when you try to force an undo with nothing done
|
||
_taExecCommand(command, false, opt, scope.defaultTagAttributes);
|
||
if(isSelectableElementTool){
|
||
// re-apply the selectable tool events
|
||
scope['reApplyOnSelectorHandlerstaTextElement' + _serial]();
|
||
}
|
||
// refocus on the shown display element, this fixes a display bug when using :focus styles to outline the box.
|
||
// You still have focus on the text/html input it just doesn't show up
|
||
scope.displayElements.text[0].focus();
|
||
}
|
||
},
|
||
showHtml: scope.$eval(attrs.taShowHtml) || false
|
||
});
|
||
// setup the options from the optional attributes
|
||
if(attrs.taFocussedClass) scope.classes.focussed = attrs.taFocussedClass;
|
||
if(attrs.taTextEditorClass) scope.classes.textEditor = attrs.taTextEditorClass;
|
||
if(attrs.taHtmlEditorClass) scope.classes.htmlEditor = attrs.taHtmlEditorClass;
|
||
if(attrs.taDefaultTagAttributes){
|
||
try {
|
||
// TODO: This should use angular.merge to enhance functionality once angular 1.4 is required
|
||
angular.extend(scope.defaultTagAttributes, angular.fromJson(attrs.taDefaultTagAttributes));
|
||
} catch (error) {
|
||
$log.error(error);
|
||
}
|
||
}
|
||
// optional setup functions
|
||
if(attrs.taTextEditorSetup) scope.setup.textEditorSetup = scope.$parent.$eval(attrs.taTextEditorSetup);
|
||
if(attrs.taHtmlEditorSetup) scope.setup.htmlEditorSetup = scope.$parent.$eval(attrs.taHtmlEditorSetup);
|
||
// optional fileDropHandler function
|
||
if(attrs.taFileDrop) scope.fileDropHandler = scope.$parent.$eval(attrs.taFileDrop);
|
||
else scope.fileDropHandler = scope.defaultFileDropHandler;
|
||
|
||
_originalContents = element[0].innerHTML;
|
||
// clear the original content
|
||
element[0].innerHTML = '';
|
||
|
||
// Setup the HTML elements as variable references for use later
|
||
scope.displayElements = {
|
||
// we still need the hidden input even with a textarea as the textarea may have invalid/old input in it,
|
||
// wheras the input will ALLWAYS have the correct value.
|
||
forminput: angular.element("<input type='hidden' tabindex='-1' style='display: none;'>"),
|
||
html: angular.element("<textarea></textarea>"),
|
||
text: angular.element("<div></div>"),
|
||
// other toolbased elements
|
||
scrollWindow: angular.element("<div class='ta-scroll-window'></div>"),
|
||
popover: angular.element('<div class="popover fade bottom" style="max-width: none; width: 305px;"></div>'),
|
||
popoverArrow: angular.element('<div class="arrow"></div>'),
|
||
popoverContainer: angular.element('<div class="popover-content"></div>'),
|
||
resize: {
|
||
overlay: angular.element('<div class="ta-resizer-handle-overlay"></div>'),
|
||
background: angular.element('<div class="ta-resizer-handle-background"></div>'),
|
||
anchors: [
|
||
angular.element('<div class="ta-resizer-handle-corner ta-resizer-handle-corner-tl"></div>'),
|
||
angular.element('<div class="ta-resizer-handle-corner ta-resizer-handle-corner-tr"></div>'),
|
||
angular.element('<div class="ta-resizer-handle-corner ta-resizer-handle-corner-bl"></div>'),
|
||
angular.element('<div class="ta-resizer-handle-corner ta-resizer-handle-corner-br"></div>')
|
||
],
|
||
info: angular.element('<div class="ta-resizer-handle-info"></div>')
|
||
}
|
||
};
|
||
|
||
// Setup the popover
|
||
scope.displayElements.popover.append(scope.displayElements.popoverArrow);
|
||
scope.displayElements.popover.append(scope.displayElements.popoverContainer);
|
||
scope.displayElements.scrollWindow.append(scope.displayElements.popover);
|
||
|
||
scope.displayElements.popover.on('mousedown', function(e, eventData){
|
||
/* istanbul ignore else: this is for catching the jqLite testing*/
|
||
if(eventData) angular.extend(e, eventData);
|
||
// this prevents focusout from firing on the editor when clicking anything in the popover
|
||
e.preventDefault();
|
||
return false;
|
||
});
|
||
|
||
// define the popover show and hide functions
|
||
scope.showPopover = function(_el){
|
||
scope.displayElements.popover.css('display', 'block');
|
||
scope.reflowPopover(_el);
|
||
$animate.addClass(scope.displayElements.popover, 'in');
|
||
oneEvent($document.find('body'), 'click keyup', function(){scope.hidePopover();});
|
||
};
|
||
scope.reflowPopover = function(_el){
|
||
/* istanbul ignore if: catches only if near bottom of editor */
|
||
if(scope.displayElements.text[0].offsetHeight - 51 > _el[0].offsetTop){
|
||
scope.displayElements.popover.css('top', _el[0].offsetTop + _el[0].offsetHeight + scope.displayElements.scrollWindow[0].scrollTop + 'px');
|
||
scope.displayElements.popover.removeClass('top').addClass('bottom');
|
||
}else{
|
||
scope.displayElements.popover.css('top', _el[0].offsetTop - 54 + scope.displayElements.scrollWindow[0].scrollTop + 'px');
|
||
scope.displayElements.popover.removeClass('bottom').addClass('top');
|
||
}
|
||
var _maxLeft = scope.displayElements.text[0].offsetWidth - scope.displayElements.popover[0].offsetWidth;
|
||
var _targetLeft = _el[0].offsetLeft + (_el[0].offsetWidth / 2.0) - (scope.displayElements.popover[0].offsetWidth / 2.0);
|
||
scope.displayElements.popover.css('left', Math.max(0, Math.min(_maxLeft, _targetLeft)) + 'px');
|
||
scope.displayElements.popoverArrow.css('margin-left', (Math.min(_targetLeft, (Math.max(0, _targetLeft - _maxLeft))) - 11) + 'px');
|
||
};
|
||
scope.hidePopover = function(){
|
||
scope.displayElements.popover.css('display', '');
|
||
scope.displayElements.popoverContainer.attr('style', '');
|
||
scope.displayElements.popoverContainer.attr('class', 'popover-content');
|
||
scope.displayElements.popover.removeClass('in');
|
||
};
|
||
|
||
// setup the resize overlay
|
||
scope.displayElements.resize.overlay.append(scope.displayElements.resize.background);
|
||
angular.forEach(scope.displayElements.resize.anchors, function(anchor){ scope.displayElements.resize.overlay.append(anchor);});
|
||
scope.displayElements.resize.overlay.append(scope.displayElements.resize.info);
|
||
scope.displayElements.scrollWindow.append(scope.displayElements.resize.overlay);
|
||
|
||
// define the show and hide events
|
||
scope.reflowResizeOverlay = function(_el){
|
||
_el = angular.element(_el)[0];
|
||
scope.displayElements.resize.overlay.css({
|
||
'display': 'block',
|
||
'left': _el.offsetLeft - 5 + 'px',
|
||
'top': _el.offsetTop - 5 + 'px',
|
||
'width': _el.offsetWidth + 10 + 'px',
|
||
'height': _el.offsetHeight + 10 + 'px'
|
||
});
|
||
scope.displayElements.resize.info.text(_el.offsetWidth + ' x ' + _el.offsetHeight);
|
||
};
|
||
/* istanbul ignore next: pretty sure phantomjs won't test this */
|
||
scope.showResizeOverlay = function(_el){
|
||
var _body = $document.find('body');
|
||
_resizeMouseDown = function(event){
|
||
var startPosition = {
|
||
width: parseInt(_el.attr('width')),
|
||
height: parseInt(_el.attr('height')),
|
||
x: event.clientX,
|
||
y: event.clientY
|
||
};
|
||
if(startPosition.width === undefined || isNaN(startPosition.width)) startPosition.width = _el[0].offsetWidth;
|
||
if(startPosition.height === undefined || isNaN(startPosition.height)) startPosition.height = _el[0].offsetHeight;
|
||
scope.hidePopover();
|
||
var ratio = startPosition.height / startPosition.width;
|
||
var mousemove = function(event){
|
||
// calculate new size
|
||
var pos = {
|
||
x: Math.max(0, startPosition.width + (event.clientX - startPosition.x)),
|
||
y: Math.max(0, startPosition.height + (event.clientY - startPosition.y))
|
||
};
|
||
|
||
// DEFAULT: the aspect ratio is not locked unless the Shift key is pressed.
|
||
//
|
||
// attribute: ta-resize-force-aspect-ratio -- locks resize into maintaing the aspect ratio
|
||
var bForceAspectRatio = (attrs.taResizeForceAspectRatio !== undefined);
|
||
// attribute: ta-resize-maintain-aspect-ratio=true causes the space ratio to remain locked
|
||
// unless the Shift key is pressed
|
||
var bFlipKeyBinding = attrs.taResizeMaintainAspectRatio;
|
||
var bKeepRatio = bForceAspectRatio || (bFlipKeyBinding && !event.shiftKey);
|
||
if(bKeepRatio) {
|
||
var newRatio = pos.y / pos.x;
|
||
pos.x = ratio > newRatio ? pos.x : pos.y / ratio;
|
||
pos.y = ratio > newRatio ? pos.x * ratio : pos.y;
|
||
}
|
||
var el = angular.element(_el);
|
||
function roundedMaxVal(val) {
|
||
return Math.round(Math.max(0, val));
|
||
}
|
||
el.css('height', roundedMaxVal(pos.y) + 'px');
|
||
el.css('width', roundedMaxVal(pos.x) + 'px');
|
||
|
||
// reflow the popover tooltip
|
||
scope.reflowResizeOverlay(_el);
|
||
};
|
||
_body.on('mousemove', mousemove);
|
||
oneEvent(_body, 'mouseup', function(event){
|
||
event.preventDefault();
|
||
event.stopPropagation();
|
||
_body.off('mousemove', mousemove);
|
||
// at this point, we need to force the model to update! since the css has changed!
|
||
// this fixes bug: #862 - we now hide the popover -- as this seems more consitent.
|
||
// there are still issues under firefox, the window does not repaint. -- not sure
|
||
// how best to resolve this, but clicking anywhere works.
|
||
scope.$apply(function (){
|
||
scope.hidePopover();
|
||
scope.updateTaBindtaTextElement();
|
||
}, 100);
|
||
});
|
||
event.stopPropagation();
|
||
event.preventDefault();
|
||
};
|
||
|
||
scope.displayElements.resize.anchors[3].off('mousedown');
|
||
scope.displayElements.resize.anchors[3].on('mousedown', _resizeMouseDown);
|
||
|
||
scope.reflowResizeOverlay(_el);
|
||
oneEvent(_body, 'click', function(){scope.hideResizeOverlay();});
|
||
};
|
||
/* istanbul ignore next: pretty sure phantomjs won't test this */
|
||
scope.hideResizeOverlay = function(){
|
||
scope.displayElements.resize.anchors[3].off('mousedown', _resizeMouseDown);
|
||
scope.displayElements.resize.overlay.css('display', '');
|
||
};
|
||
|
||
// allow for insertion of custom directives on the textarea and div
|
||
scope.setup.htmlEditorSetup(scope.displayElements.html);
|
||
scope.setup.textEditorSetup(scope.displayElements.text);
|
||
scope.displayElements.html.attr({
|
||
'id': 'taHtmlElement' + _serial,
|
||
'ng-show': 'showHtml',
|
||
'ta-bind': 'ta-bind',
|
||
'ng-model': 'html',
|
||
'ng-model-options': element.attr('ng-model-options')
|
||
});
|
||
scope.displayElements.text.attr({
|
||
'id': 'taTextElement' + _serial,
|
||
'contentEditable': 'true',
|
||
'ta-bind': 'ta-bind',
|
||
'ng-model': 'html',
|
||
'ng-model-options': element.attr('ng-model-options')
|
||
});
|
||
scope.displayElements.scrollWindow.attr({'ng-hide': 'showHtml'});
|
||
if(attrs.taDefaultWrap) scope.displayElements.text.attr('ta-default-wrap', attrs.taDefaultWrap);
|
||
|
||
if(attrs.taUnsafeSanitizer){
|
||
scope.displayElements.text.attr('ta-unsafe-sanitizer', attrs.taUnsafeSanitizer);
|
||
scope.displayElements.html.attr('ta-unsafe-sanitizer', attrs.taUnsafeSanitizer);
|
||
}
|
||
|
||
// add the main elements to the origional element
|
||
scope.displayElements.scrollWindow.append(scope.displayElements.text);
|
||
element.append(scope.displayElements.scrollWindow);
|
||
element.append(scope.displayElements.html);
|
||
|
||
scope.displayElements.forminput.attr('name', scope._name);
|
||
element.append(scope.displayElements.forminput);
|
||
|
||
if(attrs.tabindex){
|
||
element.removeAttr('tabindex');
|
||
scope.displayElements.text.attr('tabindex', attrs.tabindex);
|
||
scope.displayElements.html.attr('tabindex', attrs.tabindex);
|
||
}
|
||
|
||
if (attrs.placeholder) {
|
||
scope.displayElements.text.attr('placeholder', attrs.placeholder);
|
||
scope.displayElements.html.attr('placeholder', attrs.placeholder);
|
||
}
|
||
|
||
if(attrs.taDisabled){
|
||
scope.displayElements.text.attr('ta-readonly', 'disabled');
|
||
scope.displayElements.html.attr('ta-readonly', 'disabled');
|
||
scope.disabled = scope.$parent.$eval(attrs.taDisabled);
|
||
scope.$parent.$watch(attrs.taDisabled, function(newVal){
|
||
scope.disabled = newVal;
|
||
if(scope.disabled){
|
||
element.addClass(scope.classes.disabled);
|
||
}else{
|
||
element.removeClass(scope.classes.disabled);
|
||
}
|
||
});
|
||
}
|
||
|
||
if(attrs.taPaste){
|
||
scope._pasteHandler = function(_html){
|
||
return $parse(attrs.taPaste)(scope.$parent, {$html: _html});
|
||
};
|
||
scope.displayElements.text.attr('ta-paste', '_pasteHandler($html)');
|
||
}
|
||
|
||
// compile the scope with the text and html elements only - if we do this with the main element it causes a compile loop
|
||
$compile(scope.displayElements.scrollWindow)(scope);
|
||
$compile(scope.displayElements.html)(scope);
|
||
|
||
scope.updateTaBindtaTextElement = scope['updateTaBindtaTextElement' + _serial];
|
||
scope.updateTaBindtaHtmlElement = scope['updateTaBindtaHtmlElement' + _serial];
|
||
|
||
// add the classes manually last
|
||
element.addClass("ta-root");
|
||
scope.displayElements.scrollWindow.addClass("ta-text ta-editor " + scope.classes.textEditor);
|
||
scope.displayElements.html.addClass("ta-html ta-editor " + scope.classes.htmlEditor);
|
||
|
||
// used in the toolbar actions
|
||
scope._actionRunning = false;
|
||
var _savedSelection = false;
|
||
scope.startAction = function(){
|
||
scope._actionRunning = true;
|
||
// if rangy library is loaded return a function to reload the current selection
|
||
_savedSelection = rangy.saveSelection();
|
||
return function(){
|
||
if(_savedSelection) rangy.restoreSelection(_savedSelection);
|
||
};
|
||
};
|
||
scope.endAction = function(){
|
||
scope._actionRunning = false;
|
||
if(_savedSelection){
|
||
if(scope.showHtml){
|
||
scope.displayElements.html[0].focus();
|
||
}else{
|
||
scope.displayElements.text[0].focus();
|
||
}
|
||
// rangy.restoreSelection(_savedSelection);
|
||
rangy.removeMarkers(_savedSelection);
|
||
}
|
||
_savedSelection = false;
|
||
scope.updateSelectedStyles();
|
||
// only update if in text or WYSIWYG mode
|
||
if(!scope.showHtml) scope['updateTaBindtaTextElement' + _serial]();
|
||
};
|
||
|
||
// note that focusout > focusin is called everytime we click a button - except bad support: http://www.quirksmode.org/dom/events/blurfocus.html
|
||
// cascades to displayElements.text and displayElements.html automatically.
|
||
_focusin = function(){
|
||
scope.focussed = true;
|
||
element.addClass(scope.classes.focussed);
|
||
_toolbars.focus();
|
||
element.triggerHandler('focus');
|
||
};
|
||
scope.displayElements.html.on('focus', _focusin);
|
||
scope.displayElements.text.on('focus', _focusin);
|
||
_focusout = function(e){
|
||
// if we are NOT runnig an action and have NOT focussed again on the text etc then fire the blur events
|
||
if(!scope._actionRunning && $document[0].activeElement !== scope.displayElements.html[0] && $document[0].activeElement !== scope.displayElements.text[0]){
|
||
element.removeClass(scope.classes.focussed);
|
||
_toolbars.unfocus();
|
||
// to prevent multiple apply error defer to next seems to work.
|
||
$timeout(function(){
|
||
scope._bUpdateSelectedStyles = false;
|
||
element.triggerHandler('blur');
|
||
scope.focussed = false;
|
||
}, 0);
|
||
}
|
||
e.preventDefault();
|
||
return false;
|
||
};
|
||
scope.displayElements.html.on('blur', _focusout);
|
||
scope.displayElements.text.on('blur', _focusout);
|
||
|
||
scope.displayElements.text.on('paste', function(event){
|
||
element.triggerHandler('paste', event);
|
||
});
|
||
|
||
// Setup the default toolbar tools, this way allows the user to add new tools like plugins.
|
||
// This is on the editor for future proofing if we find a better way to do this.
|
||
scope.queryFormatBlockState = function(command){
|
||
// $document[0].queryCommandValue('formatBlock') errors in Firefox if we call this when focussed on the textarea
|
||
return !scope.showHtml && command.toLowerCase() === $document[0].queryCommandValue('formatBlock').toLowerCase();
|
||
};
|
||
scope.queryCommandState = function(command){
|
||
// $document[0].queryCommandValue('formatBlock') errors in Firefox if we call this when focussed on the textarea
|
||
return (!scope.showHtml) ? $document[0].queryCommandState(command) : '';
|
||
};
|
||
scope.switchView = function(){
|
||
scope.showHtml = !scope.showHtml;
|
||
$animate.enabled(false, scope.displayElements.html);
|
||
$animate.enabled(false, scope.displayElements.text);
|
||
//Show the HTML view
|
||
if(scope.showHtml){
|
||
//defer until the element is visible
|
||
$timeout(function(){
|
||
$animate.enabled(true, scope.displayElements.html);
|
||
$animate.enabled(true, scope.displayElements.text);
|
||
// [0] dereferences the DOM object from the angular.element
|
||
return scope.displayElements.html[0].focus();
|
||
}, 100);
|
||
}else{
|
||
//Show the WYSIWYG view
|
||
//defer until the element is visible
|
||
$timeout(function(){
|
||
$animate.enabled(true, scope.displayElements.html);
|
||
$animate.enabled(true, scope.displayElements.text);
|
||
// [0] dereferences the DOM object from the angular.element
|
||
return scope.displayElements.text[0].focus();
|
||
}, 100);
|
||
}
|
||
};
|
||
|
||
// changes to the model variable from outside the html/text inputs
|
||
// if no ngModel, then the only input is from inside text-angular
|
||
if(attrs.ngModel){
|
||
var _firstRun = true;
|
||
ngModel.$render = function(){
|
||
if(_firstRun){
|
||
// we need this firstRun to set the originalContents otherwise it gets overrided by the setting of ngModel to undefined from NaN
|
||
_firstRun = false;
|
||
// if view value is null or undefined initially and there was original content, set to the original content
|
||
var _initialValue = scope.$parent.$eval(attrs.ngModel);
|
||
if((_initialValue === undefined || _initialValue === null) && (_originalContents && _originalContents !== '')){
|
||
// on passing through to taBind it will be sanitised
|
||
ngModel.$setViewValue(_originalContents);
|
||
}
|
||
}
|
||
scope.displayElements.forminput.val(ngModel.$viewValue);
|
||
// if the editors aren't focused they need to be updated, otherwise they are doing the updating
|
||
scope.html = ngModel.$viewValue || '';
|
||
};
|
||
// trigger the validation calls
|
||
if(element.attr('required')) ngModel.$validators.required = function(modelValue, viewValue) {
|
||
var value = modelValue || viewValue;
|
||
return !(!value || value.trim() === '');
|
||
};
|
||
}else{
|
||
// if no ngModel then update from the contents of the origional html.
|
||
scope.displayElements.forminput.val(_originalContents);
|
||
scope.html = _originalContents;
|
||
}
|
||
|
||
// changes from taBind back up to here
|
||
scope.$watch('html', function(newValue, oldValue){
|
||
if(newValue !== oldValue){
|
||
if(attrs.ngModel && ngModel.$viewValue !== newValue) ngModel.$setViewValue(newValue);
|
||
scope.displayElements.forminput.val(newValue);
|
||
}
|
||
});
|
||
|
||
if(attrs.taTargetToolbars) _toolbars = textAngularManager.registerEditor(scope._name, scope, attrs.taTargetToolbars.split(','));
|
||
else{
|
||
var _toolbar = angular.element('<div text-angular-toolbar name="textAngularToolbar' + _serial + '">');
|
||
// passthrough init of toolbar options
|
||
if(attrs.taToolbar) _toolbar.attr('ta-toolbar', attrs.taToolbar);
|
||
if(attrs.taToolbarClass) _toolbar.attr('ta-toolbar-class', attrs.taToolbarClass);
|
||
if(attrs.taToolbarGroupClass) _toolbar.attr('ta-toolbar-group-class', attrs.taToolbarGroupClass);
|
||
if(attrs.taToolbarButtonClass) _toolbar.attr('ta-toolbar-button-class', attrs.taToolbarButtonClass);
|
||
if(attrs.taToolbarActiveButtonClass) _toolbar.attr('ta-toolbar-active-button-class', attrs.taToolbarActiveButtonClass);
|
||
if(attrs.taFocussedClass) _toolbar.attr('ta-focussed-class', attrs.taFocussedClass);
|
||
|
||
element.prepend(_toolbar);
|
||
$compile(_toolbar)(scope.$parent);
|
||
_toolbars = textAngularManager.registerEditor(scope._name, scope, ['textAngularToolbar' + _serial]);
|
||
}
|
||
|
||
scope.$on('$destroy', function(){
|
||
textAngularManager.unregisterEditor(scope._name);
|
||
angular.element(window).off('blur');
|
||
});
|
||
|
||
// catch element select event and pass to toolbar tools
|
||
scope.$on('ta-element-select', function(event, element){
|
||
if(_toolbars.triggerElementSelect(event, element)){
|
||
scope['reApplyOnSelectorHandlerstaTextElement' + _serial]();
|
||
}
|
||
});
|
||
|
||
scope.$on('ta-drop-event', function(event, element, dropEvent, dataTransfer){
|
||
scope.displayElements.text[0].focus();
|
||
if(dataTransfer && dataTransfer.files && dataTransfer.files.length > 0){
|
||
angular.forEach(dataTransfer.files, function(file){
|
||
// taking advantage of boolean execution, if the fileDropHandler returns true, nothing else after it is executed
|
||
// If it is false then execute the defaultFileDropHandler if the fileDropHandler is NOT the default one
|
||
// Once one of these has been executed wrap the result as a promise, if undefined or variable update the taBind, else we should wait for the promise
|
||
try{
|
||
$q.when(scope.fileDropHandler(file, scope.wrapSelection) ||
|
||
(scope.fileDropHandler !== scope.defaultFileDropHandler &&
|
||
$q.when(scope.defaultFileDropHandler(file, scope.wrapSelection)))).then(function(){
|
||
scope['updateTaBindtaTextElement' + _serial]();
|
||
});
|
||
}catch(error){
|
||
$log.error(error);
|
||
}
|
||
});
|
||
dropEvent.preventDefault();
|
||
dropEvent.stopPropagation();
|
||
/* istanbul ignore else, the updates if moved text */
|
||
}else{
|
||
$timeout(function(){
|
||
scope['updateTaBindtaTextElement' + _serial]();
|
||
}, 0);
|
||
}
|
||
});
|
||
|
||
// the following is for applying the active states to the tools that support it
|
||
scope._bUpdateSelectedStyles = false;
|
||
/* istanbul ignore next: browser window/tab leave check */
|
||
angular.element(window).on('blur', function(){
|
||
scope._bUpdateSelectedStyles = false;
|
||
scope.focussed = false;
|
||
});
|
||
// loop through all the tools polling their activeState function if it exists
|
||
scope.updateSelectedStyles = function(){
|
||
var _selection;
|
||
/* istanbul ignore next: This check is to ensure multiple timeouts don't exist */
|
||
if(_updateSelectedStylesTimeout) $timeout.cancel(_updateSelectedStylesTimeout);
|
||
// test if the common element ISN'T the root ta-text node
|
||
if((_selection = taSelection.getSelectionElement()) !== undefined && _selection.parentNode !== scope.displayElements.text[0]){
|
||
_toolbars.updateSelectedStyles(angular.element(_selection));
|
||
}else _toolbars.updateSelectedStyles();
|
||
// used to update the active state when a key is held down, ie the left arrow
|
||
/* istanbul ignore else: browser only check */
|
||
if(scope._bUpdateSelectedStyles) _updateSelectedStylesTimeout = $timeout(scope.updateSelectedStyles, 200);
|
||
};
|
||
// start updating on keydown
|
||
_keydown = function(){
|
||
/* istanbul ignore next: ie catch */
|
||
if(!scope.focussed){
|
||
scope._bUpdateSelectedStyles = false;
|
||
return;
|
||
}
|
||
/* istanbul ignore else: don't run if already running */
|
||
if(!scope._bUpdateSelectedStyles){
|
||
scope._bUpdateSelectedStyles = true;
|
||
scope.$apply(function(){
|
||
scope.updateSelectedStyles();
|
||
});
|
||
}
|
||
};
|
||
scope.displayElements.html.on('keydown', _keydown);
|
||
scope.displayElements.text.on('keydown', _keydown);
|
||
// stop updating on key up and update the display/model
|
||
_keyup = function(){
|
||
scope._bUpdateSelectedStyles = false;
|
||
};
|
||
scope.displayElements.html.on('keyup', _keyup);
|
||
scope.displayElements.text.on('keyup', _keyup);
|
||
// stop updating on key up and update the display/model
|
||
_keypress = function(event, eventData){
|
||
/* istanbul ignore else: this is for catching the jqLite testing*/
|
||
if(eventData) angular.extend(event, eventData);
|
||
scope.$apply(function(){
|
||
if(_toolbars.sendKeyCommand(event)){
|
||
/* istanbul ignore else: don't run if already running */
|
||
if(!scope._bUpdateSelectedStyles){
|
||
scope.updateSelectedStyles();
|
||
}
|
||
event.preventDefault();
|
||
return false;
|
||
}
|
||
});
|
||
};
|
||
scope.displayElements.html.on('keypress', _keypress);
|
||
scope.displayElements.text.on('keypress', _keypress);
|
||
// update the toolbar active states when we click somewhere in the text/html boxed
|
||
_mouseup = function(){
|
||
// ensure only one execution of updateSelectedStyles()
|
||
scope._bUpdateSelectedStyles = false;
|
||
scope.$apply(function(){
|
||
scope.updateSelectedStyles();
|
||
});
|
||
};
|
||
scope.displayElements.html.on('mouseup', _mouseup);
|
||
scope.displayElements.text.on('mouseup', _mouseup);
|
||
}
|
||
};
|
||
}
|
||
]);
|
||
textAngular.service('textAngularManager', ['taToolExecuteAction', 'taTools', 'taRegisterTool', function(taToolExecuteAction, taTools, taRegisterTool){
|
||
// this service is used to manage all textAngular editors and toolbars.
|
||
// All publicly published functions that modify/need to access the toolbar or editor scopes should be in here
|
||
// these contain references to all the editors and toolbars that have been initialised in this app
|
||
var toolbars = {}, editors = {};
|
||
// when we focus into a toolbar, we need to set the TOOLBAR's $parent to be the toolbars it's linked to.
|
||
// We also need to set the tools to be updated to be the toolbars...
|
||
return {
|
||
// register an editor and the toolbars that it is affected by
|
||
registerEditor: function(name, scope, targetToolbars){
|
||
// targetToolbars are optional, we don't require a toolbar to function
|
||
if(!name || name === '') throw('textAngular Error: An editor requires a name');
|
||
if(!scope) throw('textAngular Error: An editor requires a scope');
|
||
if(editors[name]) throw('textAngular Error: An Editor with name "' + name + '" already exists');
|
||
// _toolbars is an ARRAY of toolbar scopes
|
||
var _toolbars = [];
|
||
angular.forEach(targetToolbars, function(_name){
|
||
if(toolbars[_name]) _toolbars.push(toolbars[_name]);
|
||
// if it doesn't exist it may not have been compiled yet and it will be added later
|
||
});
|
||
editors[name] = {
|
||
scope: scope,
|
||
toolbars: targetToolbars,
|
||
_registerToolbar: function(toolbarScope){
|
||
// add to the list late
|
||
if(this.toolbars.indexOf(toolbarScope.name) >= 0) _toolbars.push(toolbarScope);
|
||
},
|
||
// this is a suite of functions the editor should use to update all it's linked toolbars
|
||
editorFunctions: {
|
||
disable: function(){
|
||
// disable all linked toolbars
|
||
angular.forEach(_toolbars, function(toolbarScope){ toolbarScope.disabled = true; });
|
||
},
|
||
enable: function(){
|
||
// enable all linked toolbars
|
||
angular.forEach(_toolbars, function(toolbarScope){ toolbarScope.disabled = false; });
|
||
},
|
||
focus: function(){
|
||
// this should be called when the editor is focussed
|
||
angular.forEach(_toolbars, function(toolbarScope){
|
||
toolbarScope._parent = scope;
|
||
toolbarScope.disabled = false;
|
||
toolbarScope.focussed = true;
|
||
scope.focussed = true;
|
||
});
|
||
},
|
||
unfocus: function(){
|
||
// this should be called when the editor becomes unfocussed
|
||
angular.forEach(_toolbars, function(toolbarScope){
|
||
toolbarScope.disabled = true;
|
||
toolbarScope.focussed = false;
|
||
});
|
||
scope.focussed = false;
|
||
},
|
||
updateSelectedStyles: function(selectedElement){
|
||
// update the active state of all buttons on liked toolbars
|
||
angular.forEach(_toolbars, function(toolbarScope){
|
||
angular.forEach(toolbarScope.tools, function(toolScope){
|
||
if(toolScope.activeState){
|
||
toolbarScope._parent = scope;
|
||
toolScope.active = toolScope.activeState(selectedElement);
|
||
}
|
||
});
|
||
});
|
||
},
|
||
sendKeyCommand: function(event){
|
||
// we return true if we applied an action, false otherwise
|
||
var result = false;
|
||
if(event.ctrlKey || event.metaKey || event.specialKey) angular.forEach(taTools, function(tool, name){
|
||
if(tool.commandKeyCode && (tool.commandKeyCode === event.which || tool.commandKeyCode === event.specialKey)){
|
||
for(var _t = 0; _t < _toolbars.length; _t++){
|
||
if(_toolbars[_t].tools[name] !== undefined){
|
||
taToolExecuteAction.call(_toolbars[_t].tools[name], scope);
|
||
result = true;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
});
|
||
return result;
|
||
},
|
||
triggerElementSelect: function(event, element){
|
||
// search through the taTools to see if a match for the tag is made.
|
||
// if there is, see if the tool is on a registered toolbar and not disabled.
|
||
// NOTE: This can trigger on MULTIPLE tools simultaneously.
|
||
var elementHasAttrs = function(_element, attrs){
|
||
var result = true;
|
||
for(var i = 0; i < attrs.length; i++) result = result && _element.attr(attrs[i]);
|
||
return result;
|
||
};
|
||
var workerTools = [];
|
||
var unfilteredTools = {};
|
||
var result = false;
|
||
element = angular.element(element);
|
||
// get all valid tools by element name, keep track if one matches the
|
||
var onlyWithAttrsFilter = false;
|
||
angular.forEach(taTools, function(tool, name){
|
||
if(
|
||
tool.onElementSelect &&
|
||
tool.onElementSelect.element &&
|
||
tool.onElementSelect.element.toLowerCase() === element[0].tagName.toLowerCase() &&
|
||
(!tool.onElementSelect.filter || tool.onElementSelect.filter(element))
|
||
){
|
||
// this should only end up true if the element matches the only attributes
|
||
onlyWithAttrsFilter = onlyWithAttrsFilter ||
|
||
(angular.isArray(tool.onElementSelect.onlyWithAttrs) && elementHasAttrs(element, tool.onElementSelect.onlyWithAttrs));
|
||
if(!tool.onElementSelect.onlyWithAttrs || elementHasAttrs(element, tool.onElementSelect.onlyWithAttrs)) unfilteredTools[name] = tool;
|
||
}
|
||
});
|
||
// if we matched attributes to filter on, then filter, else continue
|
||
if(onlyWithAttrsFilter){
|
||
angular.forEach(unfilteredTools, function(tool, name){
|
||
if(tool.onElementSelect.onlyWithAttrs && elementHasAttrs(element, tool.onElementSelect.onlyWithAttrs)) workerTools.push({'name': name, 'tool': tool});
|
||
});
|
||
// sort most specific (most attrs to find) first
|
||
workerTools.sort(function(a,b){
|
||
return b.tool.onElementSelect.onlyWithAttrs.length - a.tool.onElementSelect.onlyWithAttrs.length;
|
||
});
|
||
}else{
|
||
angular.forEach(unfilteredTools, function(tool, name){
|
||
workerTools.push({'name': name, 'tool': tool});
|
||
});
|
||
}
|
||
// Run the actions on the first visible filtered tool only
|
||
if(workerTools.length > 0){
|
||
for(var _i = 0; _i < workerTools.length; _i++){
|
||
var tool = workerTools[_i].tool;
|
||
var name = workerTools[_i].name;
|
||
for(var _t = 0; _t < _toolbars.length; _t++){
|
||
if(_toolbars[_t].tools[name] !== undefined){
|
||
tool.onElementSelect.action.call(_toolbars[_t].tools[name], event, element, scope);
|
||
result = true;
|
||
break;
|
||
}
|
||
}
|
||
if(result) break;
|
||
}
|
||
}
|
||
return result;
|
||
}
|
||
}
|
||
};
|
||
return editors[name].editorFunctions;
|
||
},
|
||
// retrieve editor by name, largely used by testing suites only
|
||
retrieveEditor: function(name){
|
||
return editors[name];
|
||
},
|
||
unregisterEditor: function(name){
|
||
delete editors[name];
|
||
},
|
||
// registers a toolbar such that it can be linked to editors
|
||
registerToolbar: function(scope){
|
||
if(!scope) throw('textAngular Error: A toolbar requires a scope');
|
||
if(!scope.name || scope.name === '') throw('textAngular Error: A toolbar requires a name');
|
||
if(toolbars[scope.name]) throw('textAngular Error: A toolbar with name "' + scope.name + '" already exists');
|
||
toolbars[scope.name] = scope;
|
||
angular.forEach(editors, function(_editor){
|
||
_editor._registerToolbar(scope);
|
||
});
|
||
},
|
||
// retrieve toolbar by name, largely used by testing suites only
|
||
retrieveToolbar: function(name){
|
||
return toolbars[name];
|
||
},
|
||
// retrieve toolbars by editor name, largely used by testing suites only
|
||
retrieveToolbarsViaEditor: function(name){
|
||
var result = [], _this = this;
|
||
angular.forEach(this.retrieveEditor(name).toolbars, function(name){
|
||
result.push(_this.retrieveToolbar(name));
|
||
});
|
||
return result;
|
||
},
|
||
unregisterToolbar: function(name){
|
||
delete toolbars[name];
|
||
},
|
||
// functions for updating the toolbar buttons display
|
||
updateToolsDisplay: function(newTaTools){
|
||
// pass a partial struct of the taTools, this allows us to update the tools on the fly, will not change the defaults.
|
||
var _this = this;
|
||
angular.forEach(newTaTools, function(_newTool, key){
|
||
_this.updateToolDisplay(key, _newTool);
|
||
});
|
||
},
|
||
// this function resets all toolbars to their default tool definitions
|
||
resetToolsDisplay: function(){
|
||
var _this = this;
|
||
angular.forEach(taTools, function(_newTool, key){
|
||
_this.resetToolDisplay(key);
|
||
});
|
||
},
|
||
// update a tool on all toolbars
|
||
updateToolDisplay: function(toolKey, _newTool){
|
||
var _this = this;
|
||
angular.forEach(toolbars, function(toolbarScope, toolbarKey){
|
||
_this.updateToolbarToolDisplay(toolbarKey, toolKey, _newTool);
|
||
});
|
||
},
|
||
// resets a tool to the default/starting state on all toolbars
|
||
resetToolDisplay: function(toolKey){
|
||
var _this = this;
|
||
angular.forEach(toolbars, function(toolbarScope, toolbarKey){
|
||
_this.resetToolbarToolDisplay(toolbarKey, toolKey);
|
||
});
|
||
},
|
||
// update a tool on a specific toolbar
|
||
updateToolbarToolDisplay: function(toolbarKey, toolKey, _newTool){
|
||
if(toolbars[toolbarKey]) toolbars[toolbarKey].updateToolDisplay(toolKey, _newTool);
|
||
else throw('textAngular Error: No Toolbar with name "' + toolbarKey + '" exists');
|
||
},
|
||
// reset a tool on a specific toolbar to it's default starting value
|
||
resetToolbarToolDisplay: function(toolbarKey, toolKey){
|
||
if(toolbars[toolbarKey]) toolbars[toolbarKey].updateToolDisplay(toolKey, taTools[toolKey], true);
|
||
else throw('textAngular Error: No Toolbar with name "' + toolbarKey + '" exists');
|
||
},
|
||
// removes a tool from all toolbars and it's definition
|
||
removeTool: function(toolKey){
|
||
delete taTools[toolKey];
|
||
angular.forEach(toolbars, function(toolbarScope){
|
||
delete toolbarScope.tools[toolKey];
|
||
for(var i = 0; i < toolbarScope.toolbar.length; i++){
|
||
var toolbarIndex;
|
||
for(var j = 0; j < toolbarScope.toolbar[i].length; j++){
|
||
if(toolbarScope.toolbar[i][j] === toolKey){
|
||
toolbarIndex = {
|
||
group: i,
|
||
index: j
|
||
};
|
||
break;
|
||
}
|
||
if(toolbarIndex !== undefined) break;
|
||
}
|
||
if(toolbarIndex !== undefined){
|
||
toolbarScope.toolbar[toolbarIndex.group].slice(toolbarIndex.index, 1);
|
||
toolbarScope._$element.children().eq(toolbarIndex.group).children().eq(toolbarIndex.index).remove();
|
||
}
|
||
}
|
||
});
|
||
},
|
||
// toolkey, toolDefinition are required. If group is not specified will pick the last group, if index isnt defined will append to group
|
||
addTool: function(toolKey, toolDefinition, group, index){
|
||
taRegisterTool(toolKey, toolDefinition);
|
||
angular.forEach(toolbars, function(toolbarScope){
|
||
toolbarScope.addTool(toolKey, toolDefinition, group, index);
|
||
});
|
||
},
|
||
// adds a Tool but only to one toolbar not all
|
||
addToolToToolbar: function(toolKey, toolDefinition, toolbarKey, group, index){
|
||
taRegisterTool(toolKey, toolDefinition);
|
||
toolbars[toolbarKey].addTool(toolKey, toolDefinition, group, index);
|
||
},
|
||
// this is used when externally the html of an editor has been changed and textAngular needs to be notified to update the model.
|
||
// this will call a $digest if not already happening
|
||
refreshEditor: function(name){
|
||
if(editors[name]){
|
||
editors[name].scope.updateTaBindtaTextElement();
|
||
/* istanbul ignore else: phase catch */
|
||
if(!editors[name].scope.$$phase) editors[name].scope.$digest();
|
||
}else throw('textAngular Error: No Editor with name "' + name + '" exists');
|
||
},
|
||
// this is used by taBind to send a key command in response to a special key event
|
||
sendKeyCommand: function(scope, event){
|
||
var _editor = editors[scope._name];
|
||
/* istanbul ignore else: if nothing to do, do nothing */
|
||
if (_editor && _editor.editorFunctions.sendKeyCommand(event)) {
|
||
/* istanbul ignore else: don't run if already running */
|
||
if(!scope._bUpdateSelectedStyles){
|
||
scope.updateSelectedStyles();
|
||
}
|
||
event.preventDefault();
|
||
return false;
|
||
}
|
||
}
|
||
};
|
||
}]);
|
||
textAngular.directive('textAngularToolbar', [
|
||
'$compile', 'textAngularManager', 'taOptions', 'taTools', 'taToolExecuteAction', '$window',
|
||
function($compile, textAngularManager, taOptions, taTools, taToolExecuteAction, $window){
|
||
return {
|
||
scope: {
|
||
name: '@' // a name IS required
|
||
},
|
||
restrict: "EA",
|
||
link: function(scope, element, attrs){
|
||
if(!scope.name || scope.name === '') throw('textAngular Error: A toolbar requires a name');
|
||
angular.extend(scope, angular.copy(taOptions));
|
||
if(attrs.taToolbar) scope.toolbar = scope.$parent.$eval(attrs.taToolbar);
|
||
if(attrs.taToolbarClass) scope.classes.toolbar = attrs.taToolbarClass;
|
||
if(attrs.taToolbarGroupClass) scope.classes.toolbarGroup = attrs.taToolbarGroupClass;
|
||
if(attrs.taToolbarButtonClass) scope.classes.toolbarButton = attrs.taToolbarButtonClass;
|
||
if(attrs.taToolbarActiveButtonClass) scope.classes.toolbarButtonActive = attrs.taToolbarActiveButtonClass;
|
||
if(attrs.taFocussedClass) scope.classes.focussed = attrs.taFocussedClass;
|
||
|
||
scope.disabled = true;
|
||
scope.focussed = false;
|
||
scope._$element = element;
|
||
element[0].innerHTML = '';
|
||
element.addClass("ta-toolbar " + scope.classes.toolbar);
|
||
|
||
scope.$watch('focussed', function(){
|
||
if(scope.focussed) element.addClass(scope.classes.focussed);
|
||
else element.removeClass(scope.classes.focussed);
|
||
});
|
||
|
||
var setupToolElement = function(toolDefinition, toolScope){
|
||
var toolElement;
|
||
if(toolDefinition && toolDefinition.display){
|
||
toolElement = angular.element(toolDefinition.display);
|
||
}
|
||
else toolElement = angular.element("<button type='button'>");
|
||
|
||
if(toolDefinition && toolDefinition["class"]) toolElement.addClass(toolDefinition["class"]);
|
||
else toolElement.addClass(scope.classes.toolbarButton);
|
||
|
||
toolElement.attr('name', toolScope.name);
|
||
// important to not take focus from the main text/html entry
|
||
toolElement.attr('ta-button', 'ta-button');
|
||
toolElement.attr('ng-disabled', 'isDisabled()');
|
||
toolElement.attr('tabindex', '-1');
|
||
toolElement.attr('ng-click', 'executeAction()');
|
||
toolElement.attr('ng-class', 'displayActiveToolClass(active)');
|
||
|
||
if (toolDefinition && toolDefinition.tooltiptext) {
|
||
toolElement.attr('title', toolDefinition.tooltiptext);
|
||
}
|
||
if(toolDefinition && !toolDefinition.display && !toolScope._display){
|
||
// first clear out the current contents if any
|
||
toolElement[0].innerHTML = '';
|
||
// add the buttonText
|
||
if(toolDefinition.buttontext) toolElement[0].innerHTML = toolDefinition.buttontext;
|
||
// add the icon to the front of the button if there is content
|
||
if(toolDefinition.iconclass){
|
||
var icon = angular.element('<i>'), content = toolElement[0].innerHTML;
|
||
icon.addClass(toolDefinition.iconclass);
|
||
toolElement[0].innerHTML = '';
|
||
toolElement.append(icon);
|
||
if(content && content !== '') toolElement.append(' ' + content);
|
||
}
|
||
}
|
||
|
||
toolScope._lastToolDefinition = angular.copy(toolDefinition);
|
||
|
||
return $compile(toolElement)(toolScope);
|
||
};
|
||
|
||
// Keep a reference for updating the active states later
|
||
scope.tools = {};
|
||
// create the tools in the toolbar
|
||
// default functions and values to prevent errors in testing and on init
|
||
scope._parent = {
|
||
disabled: true,
|
||
showHtml: false,
|
||
queryFormatBlockState: function(){ return false; },
|
||
queryCommandState: function(){ return false; }
|
||
};
|
||
var defaultChildScope = {
|
||
$window: $window,
|
||
$editor: function(){
|
||
// dynamically gets the editor as it is set
|
||
return scope._parent;
|
||
},
|
||
isDisabled: function(){
|
||
// to set your own disabled logic set a function or boolean on the tool called 'disabled'
|
||
return ( // this bracket is important as without it it just returns the first bracket and ignores the rest
|
||
// when the button's disabled function/value evaluates to true
|
||
(typeof this.$eval('disabled') !== 'function' && this.$eval('disabled')) || this.$eval('disabled()') ||
|
||
// all buttons except the HTML Switch button should be disabled in the showHtml (RAW html) mode
|
||
(this.name !== 'html' && this.$editor().showHtml) ||
|
||
// if the toolbar is disabled
|
||
this.$parent.disabled ||
|
||
// if the current editor is disabled
|
||
this.$editor().disabled
|
||
);
|
||
},
|
||
displayActiveToolClass: function(active){
|
||
return (active)? scope.classes.toolbarButtonActive : '';
|
||
},
|
||
executeAction: taToolExecuteAction
|
||
};
|
||
|
||
angular.forEach(scope.toolbar, function(group){
|
||
// setup the toolbar group
|
||
var groupElement = angular.element("<div>");
|
||
groupElement.addClass(scope.classes.toolbarGroup);
|
||
angular.forEach(group, function(tool){
|
||
// init and add the tools to the group
|
||
// a tool name (key name from taTools struct)
|
||
//creates a child scope of the main angularText scope and then extends the childScope with the functions of this particular tool
|
||
// reference to the scope and element kept
|
||
scope.tools[tool] = angular.extend(scope.$new(true), taTools[tool], defaultChildScope, {name: tool});
|
||
scope.tools[tool].$element = setupToolElement(taTools[tool], scope.tools[tool]);
|
||
// append the tool compiled with the childScope to the group element
|
||
groupElement.append(scope.tools[tool].$element);
|
||
});
|
||
// append the group to the toolbar
|
||
element.append(groupElement);
|
||
});
|
||
|
||
// update a tool
|
||
// if a value is set to null, remove from the display
|
||
// when forceNew is set to true it will ignore all previous settings, used to reset to taTools definition
|
||
// to reset to defaults pass in taTools[key] as _newTool and forceNew as true, ie `updateToolDisplay(key, taTools[key], true);`
|
||
scope.updateToolDisplay = function(key, _newTool, forceNew){
|
||
var toolInstance = scope.tools[key];
|
||
if(toolInstance){
|
||
// get the last toolDefinition, then override with the new definition
|
||
if(toolInstance._lastToolDefinition && !forceNew) _newTool = angular.extend({}, toolInstance._lastToolDefinition, _newTool);
|
||
if(_newTool.buttontext === null && _newTool.iconclass === null && _newTool.display === null)
|
||
throw('textAngular Error: Tool Definition for updating "' + key + '" does not have a valid display/iconclass/buttontext value');
|
||
|
||
// if tool is defined on this toolbar, update/redo the tool
|
||
if(_newTool.buttontext === null){
|
||
delete _newTool.buttontext;
|
||
}
|
||
if(_newTool.iconclass === null){
|
||
delete _newTool.iconclass;
|
||
}
|
||
if(_newTool.display === null){
|
||
delete _newTool.display;
|
||
}
|
||
|
||
var toolElement = setupToolElement(_newTool, toolInstance);
|
||
toolInstance.$element.replaceWith(toolElement);
|
||
toolInstance.$element = toolElement;
|
||
}
|
||
};
|
||
|
||
// we assume here that all values passed are valid and correct
|
||
scope.addTool = function(key, _newTool, groupIndex, index){
|
||
scope.tools[key] = angular.extend(scope.$new(true), taTools[key], defaultChildScope, {name: key});
|
||
scope.tools[key].$element = setupToolElement(taTools[key], scope.tools[key]);
|
||
var group;
|
||
if(groupIndex === undefined) groupIndex = scope.toolbar.length - 1;
|
||
group = angular.element(element.children()[groupIndex]);
|
||
|
||
if(index === undefined){
|
||
group.append(scope.tools[key].$element);
|
||
scope.toolbar[groupIndex][scope.toolbar[groupIndex].length - 1] = key;
|
||
}else{
|
||
group.children().eq(index).after(scope.tools[key].$element);
|
||
scope.toolbar[groupIndex][index] = key;
|
||
}
|
||
};
|
||
|
||
textAngularManager.registerToolbar(scope);
|
||
|
||
scope.$on('$destroy', function(){
|
||
textAngularManager.unregisterToolbar(scope.name);
|
||
});
|
||
}
|
||
};
|
||
}
|
||
]);
|