textAngular.js 126 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068
  1. /*
  2. @license textAngular
  3. Author : Austin Anderson
  4. License : 2013 MIT
  5. Version 1.4.6
  6. See README.md or https://github.com/fraywing/textAngular/wiki for requirements and use.
  7. */
  8. /*
  9. Commonjs package manager support (eg componentjs).
  10. */
  11. /* istanbul ignore next: */
  12. 'undefined'!=typeof module&&'undefined'!=typeof exports&&module.exports===exports&&(module.exports='textAngular');
  13. (function(){ // encapsulate all variables so they don't become global vars
  14. "use strict";
  15. // IE version detection - http://stackoverflow.com/questions/4169160/javascript-ie-detection-why-not-use-simple-conditional-comments
  16. // We need this as IE sometimes plays funny tricks with the contenteditable.
  17. // ----------------------------------------------------------
  18. // If you're not in IE (or IE version is less than 5) then:
  19. // ie === undefined
  20. // If you're in IE (>=5) then you can determine which version:
  21. // ie === 7; // IE7
  22. // Thus, to detect IE:
  23. // if (ie) {}
  24. // And to detect the version:
  25. // ie === 6 // IE6
  26. // ie > 7 // IE8, IE9, IE10 ...
  27. // ie < 9 // Anything less than IE9
  28. // ----------------------------------------------------------
  29. /* istanbul ignore next: untestable browser check */
  30. var _browserDetect = {
  31. ie: (function(){
  32. var undef,
  33. v = 3,
  34. div = document.createElement('div'),
  35. all = div.getElementsByTagName('i');
  36. while (
  37. div.innerHTML = '<!--[if gt IE ' + (++v) + ']><i></i><![endif]-->',
  38. all[0]
  39. );
  40. return v > 4 ? v : undef;
  41. }()),
  42. webkit: /AppleWebKit\/([\d.]+)/i.test(navigator.userAgent)
  43. };
  44. // fix a webkit bug, see: https://gist.github.com/shimondoodkin/1081133
  45. // this is set true when a blur occurs as the blur of the ta-bind triggers before the click
  46. var globalContentEditableBlur = false;
  47. /* istanbul ignore next: Browser Un-Focus fix for webkit */
  48. if(_browserDetect.webkit) {
  49. document.addEventListener("mousedown", function(_event){
  50. var e = _event || window.event;
  51. var curelement = e.target;
  52. if(globalContentEditableBlur && curelement !== null){
  53. var isEditable = false;
  54. var tempEl = curelement;
  55. while(tempEl !== null && tempEl.tagName.toLowerCase() !== 'html' && !isEditable){
  56. isEditable = tempEl.contentEditable === 'true';
  57. tempEl = tempEl.parentNode;
  58. }
  59. if(!isEditable){
  60. document.getElementById('textAngular-editableFix-010203040506070809').setSelectionRange(0, 0); // set caret focus to an element that handles caret focus correctly.
  61. curelement.focus(); // focus the wanted element.
  62. if (curelement.select) {
  63. curelement.select(); // use select to place cursor for input elements.
  64. }
  65. }
  66. }
  67. globalContentEditableBlur = false;
  68. }, false); // add global click handler
  69. angular.element(document).ready(function () {
  70. angular.element(document.body).append(angular.element('<input id="textAngular-editableFix-010203040506070809" class="ta-hidden-input" aria-hidden="true" unselectable="on" tabIndex="-1">'));
  71. });
  72. }
  73. // Gloabl to textAngular REGEXP vars for block and list elements.
  74. 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;
  75. var LISTELEMENTS = /^(ul|li|ol)$/i;
  76. 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;
  77. // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/Trim#Compatibility
  78. /* istanbul ignore next: trim shim for older browsers */
  79. if (!String.prototype.trim) {
  80. String.prototype.trim = function () {
  81. return this.replace(/^\s+|\s+$/g, '');
  82. };
  83. }
  84. /*
  85. Custom stylesheet for the placeholders rules.
  86. Credit to: http://davidwalsh.name/add-rules-stylesheets
  87. */
  88. var sheet, addCSSRule, removeCSSRule, _addCSSRule, _removeCSSRule, _getRuleIndex;
  89. /* istanbul ignore else: IE <8 test*/
  90. if(_browserDetect.ie > 8 || _browserDetect.ie === undefined){
  91. var _sheets = document.styleSheets;
  92. /* istanbul ignore next: preference for stylesheet loaded externally */
  93. for(var i = 0; i < _sheets.length; i++){
  94. if(_sheets[i].media.length === 0 || _sheets[i].media.mediaText.match(/(all|screen)/ig)){
  95. if(_sheets[i].href){
  96. if(_sheets[i].href.match(/textangular\.(min\.|)css/ig)){
  97. sheet = _sheets[i];
  98. break;
  99. }
  100. }
  101. }
  102. }
  103. /* istanbul ignore next: preference for stylesheet loaded externally */
  104. if(!sheet){
  105. // this sheet is used for the placeholders later on.
  106. sheet = (function() {
  107. // Create the <style> tag
  108. var style = document.createElement("style");
  109. /* istanbul ignore else : WebKit hack :( */
  110. if(_browserDetect.webkit) style.appendChild(document.createTextNode(""));
  111. // Add the <style> element to the page, add as first so the styles can be overridden by custom stylesheets
  112. document.getElementsByTagName('head')[0].appendChild(style);
  113. return style.sheet;
  114. })();
  115. }
  116. // use as: addCSSRule("header", "float: left");
  117. addCSSRule = function(selector, rules) {
  118. return _addCSSRule(sheet, selector, rules);
  119. };
  120. _addCSSRule = function(_sheet, selector, rules){
  121. var insertIndex;
  122. var insertedRule;
  123. // 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
  124. /* istanbul ignore next: browser catches */
  125. if(_sheet.cssRules) insertIndex = Math.max(_sheet.cssRules.length - 1, 0);
  126. else if(_sheet.rules) insertIndex = Math.max(_sheet.rules.length - 1, 0);
  127. /* istanbul ignore else: untestable IE option */
  128. if(_sheet.insertRule) {
  129. _sheet.insertRule(selector + "{" + rules + "}", insertIndex);
  130. }
  131. else {
  132. _sheet.addRule(selector, rules, insertIndex);
  133. }
  134. /* istanbul ignore next: browser catches */
  135. if(sheet.rules) insertedRule = sheet.rules[insertIndex];
  136. else if(sheet.cssRules) insertedRule = sheet.cssRules[insertIndex];
  137. // return the inserted stylesheet rule
  138. return insertedRule;
  139. };
  140. _getRuleIndex = function(rule, rules) {
  141. var i, ruleIndex;
  142. for (i=0; i < rules.length; i++) {
  143. /* istanbul ignore else: check for correct rule */
  144. if (rules[i].cssText === rule.cssText) {
  145. ruleIndex = i;
  146. break;
  147. }
  148. }
  149. return ruleIndex;
  150. };
  151. removeCSSRule = function(rule){
  152. _removeCSSRule(sheet, rule);
  153. };
  154. /* istanbul ignore next: tests are browser specific */
  155. _removeCSSRule = function(sheet, rule){
  156. var rules = sheet.cssRules || sheet.rules;
  157. if(!rules || rules.length === 0) return;
  158. var ruleIndex = _getRuleIndex(rule, rules);
  159. if(sheet.removeRule){
  160. sheet.removeRule(ruleIndex);
  161. }else{
  162. sheet.deleteRule(ruleIndex);
  163. }
  164. };
  165. }
  166. angular.module('textAngular.factories', [])
  167. .factory('taBrowserTag', [function(){
  168. return function(tag){
  169. /* istanbul ignore next: ie specific test */
  170. if(!tag) return (_browserDetect.ie <= 8)? 'P' : 'p';
  171. else if(tag === '') return (_browserDetect.ie === undefined)? 'div' : (_browserDetect.ie <= 8)? 'P' : 'p';
  172. else return (_browserDetect.ie <= 8)? tag.toUpperCase() : tag;
  173. };
  174. }]).factory('taApplyCustomRenderers', ['taCustomRenderers', 'taDOM', function(taCustomRenderers, taDOM){
  175. return function(val){
  176. var element = angular.element('<div></div>');
  177. element[0].innerHTML = val;
  178. angular.forEach(taCustomRenderers, function(renderer){
  179. var elements = [];
  180. // get elements based on what is defined. If both defined do secondary filter in the forEach after using selector string
  181. if(renderer.selector && renderer.selector !== '')
  182. elements = element.find(renderer.selector);
  183. /* istanbul ignore else: shouldn't fire, if it does we're ignoring everything */
  184. else if(renderer.customAttribute && renderer.customAttribute !== '')
  185. elements = taDOM.getByAttribute(element, renderer.customAttribute);
  186. // process elements if any found
  187. angular.forEach(elements, function(_element){
  188. _element = angular.element(_element);
  189. if(renderer.selector && renderer.selector !== '' && renderer.customAttribute && renderer.customAttribute !== ''){
  190. if(_element.attr(renderer.customAttribute) !== undefined) renderer.renderLogic(_element);
  191. } else renderer.renderLogic(_element);
  192. });
  193. });
  194. return element[0].innerHTML;
  195. };
  196. }]).factory('taFixChrome', function(){
  197. // get whaterever rubbish is inserted in chrome
  198. // should be passed an html string, returns an html string
  199. var taFixChrome = function(html){
  200. if(!html || !angular.isString(html) || html.length <= 0) return html;
  201. // grab all elements with a style attibute
  202. var spanMatch = /<([^>\/]+?)style=("([^"]+)"|'([^']+)')([^>]*)>/ig;
  203. var match, styleVal, newTag, finalHtml = '', lastIndex = 0;
  204. while(match = spanMatch.exec(html)){
  205. // one of the quoted values ' or "
  206. /* istanbul ignore next: quotations match */
  207. styleVal = match[3] || match[4];
  208. // test for chrome inserted junk
  209. if(styleVal && styleVal.match(/line-height: 1.[0-9]{3,12};|color: inherit; line-height: 1.1;/i)){
  210. // replace original tag with new tag
  211. styleVal = styleVal.replace(/( |)font-family: inherit;|( |)line-height: 1.[0-9]{3,12};|( |)color: inherit;/ig, '');
  212. newTag = '<' + match[1].trim();
  213. if(styleVal.trim().length > 0) newTag += ' style=' + match[2].substring(0,1) + styleVal + match[2].substring(0,1);
  214. newTag += match[5].trim() + ">";
  215. finalHtml += html.substring(lastIndex, match.index) + newTag;
  216. lastIndex = match.index + match[0].length;
  217. }
  218. }
  219. finalHtml += html.substring(lastIndex);
  220. // only replace when something has changed, else we get focus problems on inserting lists
  221. if(lastIndex > 0){
  222. // replace all empty strings
  223. return finalHtml.replace(/<span\s?>(.*?)<\/span>(<br(\/|)>|)/ig, '$1');
  224. } else return html;
  225. };
  226. return taFixChrome;
  227. }).factory('taSanitize', ['$sanitize', function taSanitizeFactory($sanitize){
  228. var convert_infos = [
  229. {
  230. property: 'font-weight',
  231. values: [ 'bold' ],
  232. tag: 'b'
  233. },
  234. {
  235. property: 'font-style',
  236. values: [ 'italic' ],
  237. tag: 'i'
  238. }
  239. ];
  240. var styleMatch = [];
  241. for(var i = 0; i < convert_infos.length; i++){
  242. var _partialStyle = '(' + convert_infos[i].property + ':\\s*(';
  243. for(var j = 0; j < convert_infos[i].values.length; j++){
  244. /* istanbul ignore next: not needed to be tested yet */
  245. if(j > 0) _partialStyle += '|';
  246. _partialStyle += convert_infos[i].values[j];
  247. }
  248. _partialStyle += ');)';
  249. styleMatch.push(_partialStyle);
  250. }
  251. var styleRegexString = '(' + styleMatch.join('|') + ')';
  252. function wrapNested(html, wrapTag) {
  253. var depth = 0;
  254. var lastIndex = 0;
  255. var match;
  256. var tagRegex = /<[^>]*>/ig;
  257. while(match = tagRegex.exec(html)){
  258. lastIndex = match.index;
  259. if(match[0].substr(1, 1) === '/'){
  260. if(depth === 0) break;
  261. else depth--;
  262. }else depth++;
  263. }
  264. return wrapTag +
  265. html.substring(0, lastIndex) +
  266. // get the start tags reversed - this is safe as we construct the strings with no content except the tags
  267. angular.element(wrapTag)[0].outerHTML.substring(wrapTag.length) +
  268. html.substring(lastIndex);
  269. }
  270. function transformLegacyStyles(html){
  271. if(!html || !angular.isString(html) || html.length <= 0) return html;
  272. var i;
  273. var styleElementMatch = /<([^>\/]+?)style=("([^"]+)"|'([^']+)')([^>]*)>/ig;
  274. var match, subMatch, styleVal, newTag, lastNewTag = '', newHtml, finalHtml = '', lastIndex = 0;
  275. while(match = styleElementMatch.exec(html)){
  276. // one of the quoted values ' or "
  277. /* istanbul ignore next: quotations match */
  278. styleVal = match[3] || match[4];
  279. var styleRegex = new RegExp(styleRegexString, 'i');
  280. // test for style values to change
  281. if(angular.isString(styleVal) && styleRegex.test(styleVal)){
  282. // remove build tag list
  283. newTag = '';
  284. // init regex here for exec
  285. var styleRegexExec = new RegExp(styleRegexString, 'ig');
  286. // find relevand tags and build a string of them
  287. while(subMatch = styleRegexExec.exec(styleVal)){
  288. for(i = 0; i < convert_infos.length; i++){
  289. if(!!subMatch[(i*2) + 2]){
  290. newTag += '<' + convert_infos[i].tag + '>';
  291. }
  292. }
  293. }
  294. // recursively find more legacy styles in html before this tag and after the previous match (if any)
  295. newHtml = transformLegacyStyles(html.substring(lastIndex, match.index));
  296. // build up html
  297. if(lastNewTag.length > 0){
  298. finalHtml += wrapNested(newHtml, lastNewTag);
  299. }else finalHtml += newHtml;
  300. // grab the style val without the transformed values
  301. styleVal = styleVal.replace(new RegExp(styleRegexString, 'ig'), '');
  302. // build the html tag
  303. finalHtml += '<' + match[1].trim();
  304. if(styleVal.length > 0) finalHtml += ' style="' + styleVal + '"';
  305. finalHtml += match[5] + '>';
  306. // update the start index to after this tag
  307. lastIndex = match.index + match[0].length;
  308. lastNewTag = newTag;
  309. }
  310. }
  311. if(lastNewTag.length > 0){
  312. finalHtml += wrapNested(html.substring(lastIndex), lastNewTag);
  313. }
  314. else finalHtml += html.substring(lastIndex);
  315. return finalHtml;
  316. }
  317. function transformLegacyAttributes(html){
  318. if(!html || !angular.isString(html) || html.length <= 0) return html;
  319. // replace all align='...' tags with text-align attributes
  320. var attrElementMatch = /<([^>\/]+?)align=("([^"]+)"|'([^']+)')([^>]*)>/ig;
  321. var match, finalHtml = '', lastIndex = 0;
  322. // match all attr tags
  323. while(match = attrElementMatch.exec(html)){
  324. // add all html before this tag
  325. finalHtml += html.substring(lastIndex, match.index);
  326. // record last index after this tag
  327. lastIndex = match.index + match[0].length;
  328. // construct tag without the align attribute
  329. var newTag = '<' + match[1] + match[5];
  330. // add the style attribute
  331. if(/style=("([^"]+)"|'([^']+)')/ig.test(newTag)){
  332. /* istanbul ignore next: quotations match */
  333. newTag = newTag.replace(/style=("([^"]+)"|'([^']+)')/i, 'style="$2$3 text-align:' + (match[3] || match[4]) + ';"');
  334. }else{
  335. /* istanbul ignore next: quotations match */
  336. newTag += ' style="text-align:' + (match[3] || match[4]) + ';"';
  337. }
  338. newTag += '>';
  339. // add to html
  340. finalHtml += newTag;
  341. }
  342. // return with remaining html
  343. return finalHtml + html.substring(lastIndex);
  344. }
  345. return function taSanitize(unsafe, oldsafe, ignore){
  346. // unsafe html should NEVER built into a DOM object via angular.element. This allows XSS to be inserted and run.
  347. if ( !ignore ) {
  348. try {
  349. unsafe = transformLegacyStyles(unsafe);
  350. } catch (e) {
  351. }
  352. }
  353. // unsafe and oldsafe should be valid HTML strings
  354. // any exceptions (lets say, color for example) should be made here but with great care
  355. // setup unsafe element for modification
  356. unsafe = transformLegacyAttributes(unsafe);
  357. var safe;
  358. try {
  359. safe = $sanitize(unsafe);
  360. // do this afterwards, then the $sanitizer should still throw for bad markup
  361. if(ignore) safe = unsafe;
  362. } catch (e){
  363. safe = oldsafe || '';
  364. }
  365. // Do processing for <pre> tags, removing tabs and return carriages outside of them
  366. var _preTags = safe.match(/(<pre[^>]*>.*?<\/pre[^>]*>)/ig);
  367. var processedSafe = safe.replace(/(&#(9|10);)*/ig, '');
  368. var re = /<pre[^>]*>.*?<\/pre[^>]*>/ig;
  369. var index = 0;
  370. var lastIndex = 0;
  371. var origTag;
  372. safe = '';
  373. while((origTag = re.exec(processedSafe)) !== null && index < _preTags.length){
  374. safe += processedSafe.substring(lastIndex, origTag.index) + _preTags[index];
  375. lastIndex = origTag.index + origTag[0].length;
  376. index++;
  377. }
  378. return safe + processedSafe.substring(lastIndex);
  379. };
  380. }]).factory('taToolExecuteAction', ['$q', '$log', function($q, $log){
  381. // this must be called on a toolScope or instance
  382. return function(editor){
  383. if(editor !== undefined) this.$editor = function(){ return editor; };
  384. var deferred = $q.defer(),
  385. promise = deferred.promise,
  386. _editor = this.$editor();
  387. // pass into the action the deferred function and also the function to reload the current selection if rangy available
  388. var result;
  389. try{
  390. result = this.action(deferred, _editor.startAction());
  391. // We set the .finally callback here to make sure it doesn't get executed before any other .then callback.
  392. promise['finally'](function(){
  393. _editor.endAction.call(_editor);
  394. });
  395. }catch(exc){
  396. $log.error(exc);
  397. }
  398. if(result || result === undefined){
  399. // if true or undefined is returned then the action has finished. Otherwise the deferred action will be resolved manually.
  400. deferred.resolve();
  401. }
  402. };
  403. }]);
  404. angular.module('textAngular.DOM', ['textAngular.factories'])
  405. .factory('taExecCommand', ['taSelection', 'taBrowserTag', '$document', function(taSelection, taBrowserTag, $document){
  406. var listToDefault = function(listElement, defaultWrap){
  407. var $target, i;
  408. // if all selected then we should remove the list
  409. // grab all li elements and convert to taDefaultWrap tags
  410. var children = listElement.find('li');
  411. for(i = children.length - 1; i >= 0; i--){
  412. $target = angular.element('<' + defaultWrap + '>' + children[i].innerHTML + '</' + defaultWrap + '>');
  413. listElement.after($target);
  414. }
  415. listElement.remove();
  416. taSelection.setSelectionToElementEnd($target[0]);
  417. };
  418. var selectLi = function(liElement){
  419. if(/(<br(|\/)>)$/i.test(liElement.innerHTML.trim())) taSelection.setSelectionBeforeElement(angular.element(liElement).find("br")[0]);
  420. else taSelection.setSelectionToElementEnd(liElement);
  421. };
  422. var listToList = function(listElement, newListTag){
  423. var $target = angular.element('<' + newListTag + '>' + listElement[0].innerHTML + '</' + newListTag + '>');
  424. listElement.after($target);
  425. listElement.remove();
  426. selectLi($target.find('li')[0]);
  427. };
  428. var childElementsToList = function(elements, listElement, newListTag){
  429. var html = '';
  430. for(var i = 0; i < elements.length; i++){
  431. html += '<' + taBrowserTag('li') + '>' + elements[i].innerHTML + '</' + taBrowserTag('li') + '>';
  432. }
  433. var $target = angular.element('<' + newListTag + '>' + html + '</' + newListTag + '>');
  434. listElement.after($target);
  435. listElement.remove();
  436. selectLi($target.find('li')[0]);
  437. };
  438. return function(taDefaultWrap, topNode){
  439. taDefaultWrap = taBrowserTag(taDefaultWrap);
  440. return function(command, showUI, options, defaultTagAttributes){
  441. var i, $target, html, _nodes, next, optionsTagName, selectedElement;
  442. var defaultWrapper = angular.element('<' + taDefaultWrap + '>');
  443. try{
  444. selectedElement = taSelection.getSelectionElement();
  445. }catch(e){}
  446. var $selected = angular.element(selectedElement);
  447. if(selectedElement !== undefined){
  448. var tagName = selectedElement.tagName.toLowerCase();
  449. if(command.toLowerCase() === 'insertorderedlist' || command.toLowerCase() === 'insertunorderedlist'){
  450. var selfTag = taBrowserTag((command.toLowerCase() === 'insertorderedlist')? 'ol' : 'ul');
  451. if(tagName === selfTag){
  452. // if all selected then we should remove the list
  453. // grab all li elements and convert to taDefaultWrap tags
  454. return listToDefault($selected, taDefaultWrap);
  455. }else if(tagName === 'li' && $selected.parent()[0].tagName.toLowerCase() === selfTag && $selected.parent().children().length === 1){
  456. // catch for the previous statement if only one li exists
  457. return listToDefault($selected.parent(), taDefaultWrap);
  458. }else if(tagName === 'li' && $selected.parent()[0].tagName.toLowerCase() !== selfTag && $selected.parent().children().length === 1){
  459. // catch for the previous statement if only one li exists
  460. return listToList($selected.parent(), selfTag);
  461. }else if(tagName.match(BLOCKELEMENTS) && !$selected.hasClass('ta-bind')){
  462. // if it's one of those block elements we have to change the contents
  463. // if it's a ol/ul we are changing from one to the other
  464. if(tagName === 'ol' || tagName === 'ul'){
  465. return listToList($selected, selfTag);
  466. }else{
  467. var childBlockElements = false;
  468. angular.forEach($selected.children(), function(elem){
  469. if(elem.tagName.match(BLOCKELEMENTS)) {
  470. childBlockElements = true;
  471. }
  472. });
  473. if(childBlockElements){
  474. return childElementsToList($selected.children(), $selected, selfTag);
  475. }else{
  476. return childElementsToList([angular.element('<div>' + selectedElement.innerHTML + '</div>')[0]], $selected, selfTag);
  477. }
  478. }
  479. }else if(tagName.match(BLOCKELEMENTS)){
  480. // if we get here then all the contents of the ta-bind are selected
  481. _nodes = taSelection.getOnlySelectedElements();
  482. if(_nodes.length === 0){
  483. // here is if there is only text in ta-bind ie <div ta-bind>test content</div>
  484. $target = angular.element('<' + selfTag + '><li>' + selectedElement.innerHTML + '</li></' + selfTag + '>');
  485. $selected.html('');
  486. $selected.append($target);
  487. }else if(_nodes.length === 1 && (_nodes[0].tagName.toLowerCase() === 'ol' || _nodes[0].tagName.toLowerCase() === 'ul')){
  488. if(_nodes[0].tagName.toLowerCase() === selfTag){
  489. // remove
  490. return listToDefault(angular.element(_nodes[0]), taDefaultWrap);
  491. }else{
  492. return listToList(angular.element(_nodes[0]), selfTag);
  493. }
  494. }else{
  495. html = '';
  496. var $nodes = [];
  497. for(i = 0; i < _nodes.length; i++){
  498. /* istanbul ignore else: catch for real-world can't make it occur in testing */
  499. if(_nodes[i].nodeType !== 3){
  500. var $n = angular.element(_nodes[i]);
  501. /* istanbul ignore if: browser check only, phantomjs doesn't return children nodes but chrome at least does */
  502. if(_nodes[i].tagName.toLowerCase() === 'li') continue;
  503. else if(_nodes[i].tagName.toLowerCase() === 'ol' || _nodes[i].tagName.toLowerCase() === 'ul'){
  504. html += $n[0].innerHTML; // if it's a list, add all it's children
  505. }else if(_nodes[i].tagName.toLowerCase() === 'span' && (_nodes[i].childNodes[0].tagName.toLowerCase() === 'ol' || _nodes[i].childNodes[0].tagName.toLowerCase() === 'ul')){
  506. html += $n[0].childNodes[0].innerHTML; // if it's a list, add all it's children
  507. }else{
  508. html += '<' + taBrowserTag('li') + '>' + $n[0].innerHTML + '</' + taBrowserTag('li') + '>';
  509. }
  510. $nodes.unshift($n);
  511. }
  512. }
  513. $target = angular.element('<' + selfTag + '>' + html + '</' + selfTag + '>');
  514. $nodes.pop().replaceWith($target);
  515. angular.forEach($nodes, function($node){ $node.remove(); });
  516. }
  517. taSelection.setSelectionToElementEnd($target[0]);
  518. return;
  519. }
  520. }else if(command.toLowerCase() === 'formatblock'){
  521. optionsTagName = options.toLowerCase().replace(/[<>]/ig, '');
  522. if(optionsTagName.trim() === 'default') {
  523. optionsTagName = taDefaultWrap;
  524. options = '<' + taDefaultWrap + '>';
  525. }
  526. if(tagName === 'li') $target = $selected.parent();
  527. else $target = $selected;
  528. // find the first blockElement
  529. while(!$target[0].tagName || !$target[0].tagName.match(BLOCKELEMENTS) && !$target.parent().attr('contenteditable')){
  530. $target = $target.parent();
  531. /* istanbul ignore next */
  532. tagName = ($target[0].tagName || '').toLowerCase();
  533. }
  534. if(tagName === optionsTagName){
  535. // $target is wrap element
  536. _nodes = $target.children();
  537. var hasBlock = false;
  538. for(i = 0; i < _nodes.length; i++){
  539. hasBlock = hasBlock || _nodes[i].tagName.match(BLOCKELEMENTS);
  540. }
  541. if(hasBlock){
  542. $target.after(_nodes);
  543. next = $target.next();
  544. $target.remove();
  545. $target = next;
  546. }else{
  547. defaultWrapper.append($target[0].childNodes);
  548. $target.after(defaultWrapper);
  549. $target.remove();
  550. $target = defaultWrapper;
  551. }
  552. }else if($target.parent()[0].tagName.toLowerCase() === optionsTagName && !$target.parent().hasClass('ta-bind')){
  553. //unwrap logic for parent
  554. var blockElement = $target.parent();
  555. var contents = blockElement.contents();
  556. for(i = 0; i < contents.length; i ++){
  557. /* istanbul ignore next: can't test - some wierd thing with how phantomjs works */
  558. if(blockElement.parent().hasClass('ta-bind') && contents[i].nodeType === 3){
  559. defaultWrapper = angular.element('<' + taDefaultWrap + '>');
  560. defaultWrapper[0].innerHTML = contents[i].outerHTML;
  561. contents[i] = defaultWrapper[0];
  562. }
  563. blockElement.parent()[0].insertBefore(contents[i], blockElement[0]);
  564. }
  565. blockElement.remove();
  566. }else if(tagName.match(LISTELEMENTS)){
  567. // wrapping a list element
  568. $target.wrap(options);
  569. }else{
  570. // default wrap behaviour
  571. _nodes = taSelection.getOnlySelectedElements();
  572. if(_nodes.length === 0) _nodes = [$target[0]];
  573. // find the parent block element if any of the nodes are inline or text
  574. for(i = 0; i < _nodes.length; i++){
  575. if(_nodes[i].nodeType === 3 || !_nodes[i].tagName.match(BLOCKELEMENTS)){
  576. while(_nodes[i].nodeType === 3 || !_nodes[i].tagName || !_nodes[i].tagName.match(BLOCKELEMENTS)){
  577. _nodes[i] = _nodes[i].parentNode;
  578. }
  579. }
  580. }
  581. if(angular.element(_nodes[0]).hasClass('ta-bind')){
  582. $target = angular.element(options);
  583. $target[0].innerHTML = _nodes[0].innerHTML;
  584. _nodes[0].innerHTML = $target[0].outerHTML;
  585. }else if(optionsTagName === 'blockquote'){
  586. // blockquotes wrap other block elements
  587. html = '';
  588. for(i = 0; i < _nodes.length; i++){
  589. html += _nodes[i].outerHTML;
  590. }
  591. $target = angular.element(options);
  592. $target[0].innerHTML = html;
  593. _nodes[0].parentNode.insertBefore($target[0],_nodes[0]);
  594. for(i = _nodes.length - 1; i >= 0; i--){
  595. /* istanbul ignore else: */
  596. if(_nodes[i].parentNode) _nodes[i].parentNode.removeChild(_nodes[i]);
  597. }
  598. }
  599. else {
  600. // regular block elements replace other block elements
  601. for(i = 0; i < _nodes.length; i++){
  602. $target = angular.element(options);
  603. $target[0].innerHTML = _nodes[i].innerHTML;
  604. _nodes[i].parentNode.insertBefore($target[0],_nodes[i]);
  605. _nodes[i].parentNode.removeChild(_nodes[i]);
  606. }
  607. }
  608. }
  609. taSelection.setSelectionToElementEnd($target[0]);
  610. return;
  611. }else if(command.toLowerCase() === 'createlink'){
  612. var tagBegin = '<a href="' + options + '" target="' +
  613. (defaultTagAttributes.a.target ? defaultTagAttributes.a.target : '') +
  614. '">',
  615. tagEnd = '</a>',
  616. _selection = taSelection.getSelection();
  617. if(_selection.collapsed){
  618. // insert text at selection, then select then just let normal exec-command run
  619. taSelection.insertHtml(tagBegin + options + tagEnd, topNode);
  620. }else if(rangy.getSelection().getRangeAt(0).canSurroundContents()){
  621. var node = angular.element(tagBegin + tagEnd)[0];
  622. rangy.getSelection().getRangeAt(0).surroundContents(node);
  623. }
  624. return;
  625. }else if(command.toLowerCase() === 'inserthtml'){
  626. taSelection.insertHtml(options, topNode);
  627. return;
  628. }
  629. }
  630. try{
  631. $document[0].execCommand(command, showUI, options);
  632. }catch(e){}
  633. };
  634. };
  635. }]).service('taSelection', ['$window', '$document', 'taDOM',
  636. /* istanbul ignore next: all browser specifics and PhantomJS dosen't seem to support half of it */
  637. function($window, $document, taDOM){
  638. // need to dereference the document else the calls don't work correctly
  639. var _document = $document[0];
  640. var rangy = $window.rangy;
  641. var brException = function (element, offset) {
  642. /* check if selection is a BR element at the beginning of a container. If so, get
  643. * the parentNode instead.
  644. * offset should be zero in this case. Otherwise, return the original
  645. * element.
  646. */
  647. if (element.tagName && element.tagName.match(/^br$/i) && offset === 0 && !element.previousSibling) {
  648. return {
  649. element: element.parentNode,
  650. offset: 0
  651. };
  652. } else {
  653. return {
  654. element: element,
  655. offset: offset
  656. };
  657. }
  658. };
  659. var api = {
  660. getSelection: function(){
  661. var range = rangy.getSelection().getRangeAt(0);
  662. var container = range.commonAncestorContainer;
  663. var selection = {
  664. start: brException(range.startContainer, range.startOffset),
  665. end: brException(range.endContainer, range.endOffset),
  666. collapsed: range.collapsed
  667. };
  668. // Check if the container is a text node and return its parent if so
  669. container = container.nodeType === 3 ? container.parentNode : container;
  670. if (container.parentNode === selection.start.element ||
  671. container.parentNode === selection.end.element) {
  672. selection.container = container.parentNode;
  673. } else {
  674. selection.container = container;
  675. }
  676. return selection;
  677. },
  678. getOnlySelectedElements: function(){
  679. var range = rangy.getSelection().getRangeAt(0);
  680. var container = range.commonAncestorContainer;
  681. // Check if the container is a text node and return its parent if so
  682. container = container.nodeType === 3 ? container.parentNode : container;
  683. return range.getNodes([1], function(node){
  684. return node.parentNode === container;
  685. });
  686. },
  687. // Some basic selection functions
  688. getSelectionElement: function () {
  689. return api.getSelection().container;
  690. },
  691. setSelection: function(el, start, end){
  692. var range = rangy.createRange();
  693. range.setStart(el, start);
  694. range.setEnd(el, end);
  695. rangy.getSelection().setSingleRange(range);
  696. },
  697. setSelectionBeforeElement: function (el){
  698. var range = rangy.createRange();
  699. range.selectNode(el);
  700. range.collapse(true);
  701. rangy.getSelection().setSingleRange(range);
  702. },
  703. setSelectionAfterElement: function (el){
  704. var range = rangy.createRange();
  705. range.selectNode(el);
  706. range.collapse(false);
  707. rangy.getSelection().setSingleRange(range);
  708. },
  709. setSelectionToElementStart: function (el){
  710. var range = rangy.createRange();
  711. range.selectNodeContents(el);
  712. range.collapse(true);
  713. rangy.getSelection().setSingleRange(range);
  714. },
  715. setSelectionToElementEnd: function (el){
  716. var range = rangy.createRange();
  717. range.selectNodeContents(el);
  718. range.collapse(false);
  719. if(el.childNodes && el.childNodes[el.childNodes.length - 1] && el.childNodes[el.childNodes.length - 1].nodeName === 'br'){
  720. range.startOffset = range.endOffset = range.startOffset - 1;
  721. }
  722. rangy.getSelection().setSingleRange(range);
  723. },
  724. // from http://stackoverflow.com/questions/6690752/insert-html-at-caret-in-a-contenteditable-div
  725. // topNode is the contenteditable normally, all manipulation MUST be inside this.
  726. insertHtml: function(html, topNode){
  727. var parent, secondParent, _childI, nodes, i, lastNode, _tempFrag;
  728. var element = angular.element("<div>" + html + "</div>");
  729. var range = rangy.getSelection().getRangeAt(0);
  730. var frag = _document.createDocumentFragment();
  731. var children = element[0].childNodes;
  732. var isInline = true;
  733. if(children.length > 0){
  734. // NOTE!! We need to do the following:
  735. // 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.
  736. // 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).
  737. nodes = [];
  738. for(_childI = 0; _childI < children.length; _childI++){
  739. if(!(
  740. (children[_childI].nodeName.toLowerCase() === 'p' && children[_childI].innerHTML.trim() === '') || // empty p element
  741. (children[_childI].nodeType === 3 && children[_childI].nodeValue.trim() === '') // empty text node
  742. )){
  743. isInline = isInline && !BLOCKELEMENTS.test(children[_childI].nodeName);
  744. nodes.push(children[_childI]);
  745. }
  746. }
  747. for(var _n = 0; _n < nodes.length; _n++) lastNode = frag.appendChild(nodes[_n]);
  748. if(!isInline && range.collapsed && /^(|<br(|\/)>)$/i.test(range.startContainer.innerHTML)) range.selectNode(range.startContainer);
  749. }else{
  750. isInline = true;
  751. // paste text of some sort
  752. lastNode = frag = _document.createTextNode(html);
  753. }
  754. // Other Edge case - selected data spans multiple blocks.
  755. if(isInline){
  756. range.deleteContents();
  757. }else{ // not inline insert
  758. if(range.collapsed && range.startContainer !== topNode){
  759. if(range.startContainer.innerHTML && range.startContainer.innerHTML.match(/^<[^>]*>$/i)){
  760. // this log is to catch when innerHTML is something like `<img ...>`
  761. parent = range.startContainer;
  762. if(range.startOffset === 1){
  763. // before single tag
  764. range.setStartAfter(parent);
  765. range.setEndAfter(parent);
  766. }else{
  767. // after single tag
  768. range.setStartBefore(parent);
  769. range.setEndBefore(parent);
  770. }
  771. }else{
  772. // split element into 2 and insert block element in middle
  773. if(range.startContainer.nodeType === 3 && range.startContainer.parentNode !== topNode){ // if text node
  774. parent = range.startContainer.parentNode;
  775. secondParent = parent.cloneNode();
  776. // split the nodes into two lists - before and after, splitting the node with the selection into 2 text nodes.
  777. taDOM.splitNodes(parent.childNodes, parent, secondParent, range.startContainer, range.startOffset);
  778. // Escape out of the inline tags like b
  779. while(!VALIDELEMENTS.test(parent.nodeName)){
  780. angular.element(parent).after(secondParent);
  781. parent = parent.parentNode;
  782. var _lastSecondParent = secondParent;
  783. secondParent = parent.cloneNode();
  784. // split the nodes into two lists - before and after, splitting the node with the selection into 2 text nodes.
  785. taDOM.splitNodes(parent.childNodes, parent, secondParent, _lastSecondParent);
  786. }
  787. }else{
  788. parent = range.startContainer;
  789. secondParent = parent.cloneNode();
  790. taDOM.splitNodes(parent.childNodes, parent, secondParent, undefined, undefined, range.startOffset);
  791. }
  792. angular.element(parent).after(secondParent);
  793. // put cursor to end of inserted content
  794. range.setStartAfter(parent);
  795. range.setEndAfter(parent);
  796. if(/^(|<br(|\/)>)$/i.test(parent.innerHTML.trim())){
  797. range.setStartBefore(parent);
  798. range.setEndBefore(parent);
  799. angular.element(parent).remove();
  800. }
  801. if(/^(|<br(|\/)>)$/i.test(secondParent.innerHTML.trim())) angular.element(secondParent).remove();
  802. if(parent.nodeName.toLowerCase() === 'li'){
  803. _tempFrag = _document.createDocumentFragment();
  804. for(i = 0; i < frag.childNodes.length; i++){
  805. element = angular.element('<li>');
  806. taDOM.transferChildNodes(frag.childNodes[i], element[0]);
  807. taDOM.transferNodeAttributes(frag.childNodes[i], element[0]);
  808. _tempFrag.appendChild(element[0]);
  809. }
  810. frag = _tempFrag;
  811. if(lastNode){
  812. lastNode = frag.childNodes[frag.childNodes.length - 1];
  813. lastNode = lastNode.childNodes[lastNode.childNodes.length - 1];
  814. }
  815. }
  816. }
  817. }else{
  818. range.deleteContents();
  819. }
  820. }
  821. range.insertNode(frag);
  822. if(lastNode){
  823. api.setSelectionToElementEnd(lastNode);
  824. }
  825. }
  826. };
  827. return api;
  828. }]).service('taDOM', function(){
  829. var taDOM = {
  830. // recursive function that returns an array of angular.elements that have the passed attribute set on them
  831. getByAttribute: function(element, attribute){
  832. var resultingElements = [];
  833. var childNodes = element.children();
  834. if(childNodes.length){
  835. angular.forEach(childNodes, function(child){
  836. resultingElements = resultingElements.concat(taDOM.getByAttribute(angular.element(child), attribute));
  837. });
  838. }
  839. if(element.attr(attribute) !== undefined) resultingElements.push(element);
  840. return resultingElements;
  841. },
  842. transferChildNodes: function(source, target){
  843. // clear out target
  844. target.innerHTML = '';
  845. while(source.childNodes.length > 0) target.appendChild(source.childNodes[0]);
  846. return target;
  847. },
  848. splitNodes: function(nodes, target1, target2, splitNode, subSplitIndex, splitIndex){
  849. if(!splitNode && isNaN(splitIndex)) throw new Error('taDOM.splitNodes requires a splitNode or splitIndex');
  850. var startNodes = document.createDocumentFragment();
  851. var endNodes = document.createDocumentFragment();
  852. var index = 0;
  853. while(nodes.length > 0 && (isNaN(splitIndex) || splitIndex !== index) && nodes[0] !== splitNode){
  854. startNodes.appendChild(nodes[0]); // this removes from the nodes array (if proper childNodes object.
  855. index++;
  856. }
  857. if(!isNaN(subSplitIndex) && subSplitIndex >= 0 && nodes[0]){
  858. startNodes.appendChild(document.createTextNode(nodes[0].nodeValue.substring(0, subSplitIndex)));
  859. nodes[0].nodeValue = nodes[0].nodeValue.substring(subSplitIndex);
  860. }
  861. while(nodes.length > 0) endNodes.appendChild(nodes[0]);
  862. taDOM.transferChildNodes(startNodes, target1);
  863. taDOM.transferChildNodes(endNodes, target2);
  864. },
  865. transferNodeAttributes: function(source, target){
  866. for(var i = 0; i < source.attributes.length; i++) target.setAttribute(source.attributes[i].name, source.attributes[i].value);
  867. return target;
  868. }
  869. };
  870. return taDOM;
  871. });
  872. angular.module('textAngular.validators', [])
  873. .directive('taMaxText', function(){
  874. return {
  875. restrict: 'A',
  876. require: 'ngModel',
  877. link: function(scope, elem, attrs, ctrl){
  878. var max = parseInt(scope.$eval(attrs.taMaxText));
  879. if (isNaN(max)){
  880. throw('Max text must be an integer');
  881. }
  882. attrs.$observe('taMaxText', function(value){
  883. max = parseInt(value);
  884. if (isNaN(max)){
  885. throw('Max text must be an integer');
  886. }
  887. if (ctrl.$dirty){
  888. ctrl.$validate();
  889. }
  890. });
  891. ctrl.$validators.taMaxText = function(viewValue){
  892. var source = angular.element('<div/>');
  893. source.html(viewValue);
  894. return source.text().length <= max;
  895. };
  896. }
  897. };
  898. }).directive('taMinText', function(){
  899. return {
  900. restrict: 'A',
  901. require: 'ngModel',
  902. link: function(scope, elem, attrs, ctrl){
  903. var min = parseInt(scope.$eval(attrs.taMinText));
  904. if (isNaN(min)){
  905. throw('Min text must be an integer');
  906. }
  907. attrs.$observe('taMinText', function(value){
  908. min = parseInt(value);
  909. if (isNaN(min)){
  910. throw('Min text must be an integer');
  911. }
  912. if (ctrl.$dirty){
  913. ctrl.$validate();
  914. }
  915. });
  916. ctrl.$validators.taMinText = function(viewValue){
  917. var source = angular.element('<div/>');
  918. source.html(viewValue);
  919. return !source.text().length || source.text().length >= min;
  920. };
  921. }
  922. };
  923. });
  924. angular.module('textAngular.taBind', ['textAngular.factories', 'textAngular.DOM'])
  925. .service('_taBlankTest', [function(){
  926. 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;
  927. return function(_defaultTest){
  928. return function(_blankVal){
  929. if(!_blankVal) return true;
  930. // find first non-tag match - ie start of string or after tag that is not whitespace
  931. var _firstMatch = /(^[^<]|>)[^<]/i.exec(_blankVal);
  932. var _firstTagIndex;
  933. if(!_firstMatch){
  934. // find the end of the first tag removing all the
  935. // Don't do a global replace as that would be waaayy too long, just replace the first 4 occurences should be enough
  936. _blankVal = _blankVal.toString().replace(/="[^"]*"/i, '').replace(/="[^"]*"/i, '').replace(/="[^"]*"/i, '').replace(/="[^"]*"/i, '');
  937. _firstTagIndex = _blankVal.indexOf('>');
  938. }else{
  939. _firstTagIndex = _firstMatch.index;
  940. }
  941. _blankVal = _blankVal.trim().substring(_firstTagIndex, _firstTagIndex + 100);
  942. // check for no tags entry
  943. if(/^[^<>]+$/i.test(_blankVal)) return false;
  944. // this regex is to match any number of whitespace only between two tags
  945. if (_blankVal.length === 0 || _blankVal === _defaultTest || /^>(\s|&nbsp;)*<\/[^>]+>$/ig.test(_blankVal)) return true;
  946. // this regex tests if there is a tag followed by some optional whitespace and some text after that
  947. else if (/>\s*[^\s<]/i.test(_blankVal) || INLINETAGS_NONBLANK.test(_blankVal)) return false;
  948. else return true;
  949. };
  950. };
  951. }])
  952. .directive('taButton', [function(){
  953. return {
  954. link: function(scope, element, attrs){
  955. element.attr('unselectable', 'on');
  956. element.on('mousedown', function(e, eventData){
  957. /* istanbul ignore else: this is for catching the jqLite testing*/
  958. if(eventData) angular.extend(e, eventData);
  959. // this prevents focusout from firing on the editor when clicking toolbar buttons
  960. e.preventDefault();
  961. return false;
  962. });
  963. }
  964. };
  965. }])
  966. .directive('taBind', [
  967. 'taSanitize', '$timeout', '$window', '$document', 'taFixChrome', 'taBrowserTag',
  968. 'taSelection', 'taSelectableElements', 'taApplyCustomRenderers', 'taOptions',
  969. '_taBlankTest', '$parse', 'taDOM', 'textAngularManager',
  970. function(
  971. taSanitize, $timeout, $window, $document, taFixChrome, taBrowserTag,
  972. taSelection, taSelectableElements, taApplyCustomRenderers, taOptions,
  973. _taBlankTest, $parse, taDOM, textAngularManager){
  974. // Uses for this are textarea or input with ng-model and ta-bind='text'
  975. // OR any non-form element with contenteditable="contenteditable" ta-bind="html|text" ng-model
  976. return {
  977. priority: 2, // So we override validators correctly
  978. require: ['ngModel','?ngModelOptions'],
  979. link: function(scope, element, attrs, controller){
  980. var ngModel = controller[0];
  981. var ngModelOptions = controller[1] || {};
  982. // the option to use taBind on an input or textarea is required as it will sanitize all input into it correctly.
  983. var _isContentEditable = element.attr('contenteditable') !== undefined && element.attr('contenteditable');
  984. var _isInputFriendly = _isContentEditable || element[0].tagName.toLowerCase() === 'textarea' || element[0].tagName.toLowerCase() === 'input';
  985. var _isReadonly = false;
  986. var _focussed = false;
  987. var _skipRender = false;
  988. var _disableSanitizer = attrs.taUnsafeSanitizer || taOptions.disableSanitizer;
  989. var _lastKey;
  990. // see http://www.javascripter.net/faq/keycodes.htm for good information
  991. // NOTE Mute On|Off 173 (Opera MSIE Safari Chrome) 181 (Firefox)
  992. // BLOCKED_KEYS are special keys...
  993. // Tab, pause/break, CapsLock, Esc, Page Up, End, Home,
  994. // Left arrow, Up arrow, Right arrow, Down arrow, Insert, Delete,
  995. // f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12
  996. // NumLock, ScrollLock
  997. 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;
  998. // UNDO_TRIGGER_KEYS - spaces, enter, delete, backspace, all punctuation
  999. // Backspace, Enter, Space, Delete, (; :) (Firefox), (= +) (Firefox),
  1000. // Numpad +, Numpad -, (; :), (= +),
  1001. // (, <), (- _), (. >), (/ ?), (` ~), ([ {), (\ |), (] }), (' ")
  1002. // NOTE - Firefox: 173 = (- _) -- adding this to UNDO_TRIGGER_KEYS
  1003. 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;
  1004. var _pasteHandler;
  1005. // defaults to the paragraph element, but we need the line-break or it doesn't allow you to type into the empty element
  1006. // non IE is '<p><br/></p>', ie is '<p></p>' as for once IE gets it correct...
  1007. var _defaultVal, _defaultTest;
  1008. var _CTRL_KEY = 0x0001;
  1009. var _META_KEY = 0x0002;
  1010. var _ALT_KEY = 0x0004;
  1011. var _SHIFT_KEY = 0x0008;
  1012. // map events to special keys...
  1013. // mappings is an array of maps from events to specialKeys as declared in textAngularSetup
  1014. var _keyMappings = [
  1015. // ctrl/command + z
  1016. {
  1017. specialKey: 'UndoKey',
  1018. forbiddenModifiers: _ALT_KEY + _SHIFT_KEY,
  1019. mustHaveModifiers: [_META_KEY + _CTRL_KEY],
  1020. keyCode: 90
  1021. },
  1022. // ctrl/command + shift + z
  1023. {
  1024. specialKey: 'RedoKey',
  1025. forbiddenModifiers: _ALT_KEY,
  1026. mustHaveModifiers: [_META_KEY + _CTRL_KEY, _SHIFT_KEY],
  1027. keyCode: 90
  1028. },
  1029. // ctrl/command + y
  1030. {
  1031. specialKey: 'RedoKey',
  1032. forbiddenModifiers: _ALT_KEY + _SHIFT_KEY,
  1033. mustHaveModifiers: [_META_KEY + _CTRL_KEY],
  1034. keyCode: 89
  1035. },
  1036. // TabKey
  1037. {
  1038. specialKey: 'TabKey',
  1039. forbiddenModifiers: _META_KEY + _SHIFT_KEY + _ALT_KEY + _CTRL_KEY,
  1040. mustHaveModifiers: [],
  1041. keyCode: 9
  1042. },
  1043. // shift + TabKey
  1044. {
  1045. specialKey: 'ShiftTabKey',
  1046. forbiddenModifiers: _META_KEY + _ALT_KEY + _CTRL_KEY,
  1047. mustHaveModifiers: [_SHIFT_KEY],
  1048. keyCode: 9
  1049. }
  1050. ];
  1051. function _mapKeys(event) {
  1052. var specialKey;
  1053. _keyMappings.forEach(function (map){
  1054. if (map.keyCode === event.keyCode) {
  1055. var netModifiers = (event.metaKey ? _META_KEY: 0) +
  1056. (event.ctrlKey ? _CTRL_KEY: 0) +
  1057. (event.shiftKey ? _SHIFT_KEY: 0) +
  1058. (event.altKey ? _ALT_KEY: 0);
  1059. if (map.forbiddenModifiers & netModifiers) return;
  1060. if (map.mustHaveModifiers.every(function (modifier) { return netModifiers & modifier; })){
  1061. specialKey = map.specialKey;
  1062. }
  1063. }
  1064. });
  1065. return specialKey;
  1066. }
  1067. // set the default to be a paragraph value
  1068. if(attrs.taDefaultWrap === undefined) attrs.taDefaultWrap = 'p';
  1069. /* istanbul ignore next: ie specific test */
  1070. if(attrs.taDefaultWrap === ''){
  1071. _defaultVal = '';
  1072. _defaultTest = (_browserDetect.ie === undefined)? '<div><br></div>' : (_browserDetect.ie >= 11)? '<p><br></p>' : (_browserDetect.ie <= 8)? '<P>&nbsp;</P>' : '<p>&nbsp;</p>';
  1073. }else{
  1074. _defaultVal = (_browserDetect.ie === undefined || _browserDetect.ie >= 11)?
  1075. '<' + attrs.taDefaultWrap + '><br></' + attrs.taDefaultWrap + '>' :
  1076. (_browserDetect.ie <= 8)?
  1077. '<' + attrs.taDefaultWrap.toUpperCase() + '></' + attrs.taDefaultWrap.toUpperCase() + '>' :
  1078. '<' + attrs.taDefaultWrap + '></' + attrs.taDefaultWrap + '>';
  1079. _defaultTest = (_browserDetect.ie === undefined || _browserDetect.ie >= 11)?
  1080. '<' + attrs.taDefaultWrap + '><br></' + attrs.taDefaultWrap + '>' :
  1081. (_browserDetect.ie <= 8)?
  1082. '<' + attrs.taDefaultWrap.toUpperCase() + '>&nbsp;</' + attrs.taDefaultWrap.toUpperCase() + '>' :
  1083. '<' + attrs.taDefaultWrap + '>&nbsp;</' + attrs.taDefaultWrap + '>';
  1084. }
  1085. /* istanbul ignore else */
  1086. if(!ngModelOptions.$options) ngModelOptions.$options = {}; // ng-model-options support
  1087. var _blankTest = _taBlankTest(_defaultTest);
  1088. var _ensureContentWrapped = function(value) {
  1089. if (_blankTest(value)) return value;
  1090. var domTest = angular.element("<div>" + value + "</div>");
  1091. //console.log('domTest.children().length():', domTest.children().length);
  1092. if (domTest.children().length === 0) {
  1093. value = "<" + attrs.taDefaultWrap + ">" + value + "</" + attrs.taDefaultWrap + ">";
  1094. } else {
  1095. var _children = domTest[0].childNodes;
  1096. var i;
  1097. var _foundBlockElement = false;
  1098. for (i = 0; i < _children.length; i++) {
  1099. if (_foundBlockElement = _children[i].nodeName.toLowerCase().match(BLOCKELEMENTS)) break;
  1100. }
  1101. if (!_foundBlockElement) {
  1102. value = "<" + attrs.taDefaultWrap + ">" + value + "</" + attrs.taDefaultWrap + ">";
  1103. }
  1104. else{
  1105. value = "";
  1106. for(i = 0; i < _children.length; i++){
  1107. var node = _children[i];
  1108. var nodeName = node.nodeName.toLowerCase();
  1109. //console.log(nodeName);
  1110. if(nodeName === '#comment') {
  1111. value += '<!--' + node.nodeValue + '-->';
  1112. } else if(nodeName === '#text') {
  1113. // determine if this is all whitespace, if so, we will leave it as it is.
  1114. // otherwise, we will wrap it as it is
  1115. var text = node.textContent;
  1116. if (!text.trim()) {
  1117. // just whitespace
  1118. value += text;
  1119. } else {
  1120. // not pure white space so wrap in <p>...</p> or whatever attrs.taDefaultWrap is set to.
  1121. value += "<" + attrs.taDefaultWrap + ">" + text + "</" + attrs.taDefaultWrap + ">";
  1122. }
  1123. } else if(!nodeName.match(BLOCKELEMENTS)){
  1124. /* istanbul ignore next: Doesn't seem to trigger on tests */
  1125. var _subVal = (node.outerHTML || node.nodeValue);
  1126. /* istanbul ignore else: Doesn't seem to trigger on tests, is tested though */
  1127. if(_subVal.trim() !== '')
  1128. value += "<" + attrs.taDefaultWrap + ">" + _subVal + "</" + attrs.taDefaultWrap + ">";
  1129. else value += _subVal;
  1130. } else {
  1131. value += node.outerHTML;
  1132. }
  1133. }
  1134. }
  1135. }
  1136. //console.log(value);
  1137. return value;
  1138. };
  1139. if(attrs.taPaste) _pasteHandler = $parse(attrs.taPaste);
  1140. element.addClass('ta-bind');
  1141. var _undoKeyupTimeout;
  1142. scope['$undoManager' + (attrs.id || '')] = ngModel.$undoManager = {
  1143. _stack: [],
  1144. _index: 0,
  1145. _max: 1000,
  1146. push: function(value){
  1147. if((typeof value === "undefined" || value === null) ||
  1148. ((typeof this.current() !== "undefined" && this.current() !== null) && value === this.current())) return value;
  1149. if(this._index < this._stack.length - 1){
  1150. this._stack = this._stack.slice(0,this._index+1);
  1151. }
  1152. this._stack.push(value);
  1153. if(_undoKeyupTimeout) $timeout.cancel(_undoKeyupTimeout);
  1154. if(this._stack.length > this._max) this._stack.shift();
  1155. this._index = this._stack.length - 1;
  1156. return value;
  1157. },
  1158. undo: function(){
  1159. return this.setToIndex(this._index-1);
  1160. },
  1161. redo: function(){
  1162. return this.setToIndex(this._index+1);
  1163. },
  1164. setToIndex: function(index){
  1165. if(index < 0 || index > this._stack.length - 1){
  1166. return undefined;
  1167. }
  1168. this._index = index;
  1169. return this.current();
  1170. },
  1171. current: function(){
  1172. return this._stack[this._index];
  1173. }
  1174. };
  1175. var _redoUndoTimeout;
  1176. var _undo = scope['$undoTaBind' + (attrs.id || '')] = function(){
  1177. /* istanbul ignore else: can't really test it due to all changes being ignored as well in readonly */
  1178. if(!_isReadonly && _isContentEditable){
  1179. var content = ngModel.$undoManager.undo();
  1180. if(typeof content !== "undefined" && content !== null){
  1181. _setInnerHTML(content);
  1182. _setViewValue(content, false);
  1183. if(_redoUndoTimeout) $timeout.cancel(_redoUndoTimeout);
  1184. _redoUndoTimeout = $timeout(function(){
  1185. element[0].focus();
  1186. taSelection.setSelectionToElementEnd(element[0]);
  1187. }, 1);
  1188. }
  1189. }
  1190. };
  1191. var _redo = scope['$redoTaBind' + (attrs.id || '')] = function(){
  1192. /* istanbul ignore else: can't really test it due to all changes being ignored as well in readonly */
  1193. if(!_isReadonly && _isContentEditable){
  1194. var content = ngModel.$undoManager.redo();
  1195. if(typeof content !== "undefined" && content !== null){
  1196. _setInnerHTML(content);
  1197. _setViewValue(content, false);
  1198. /* istanbul ignore next */
  1199. if(_redoUndoTimeout) $timeout.cancel(_redoUndoTimeout);
  1200. _redoUndoTimeout = $timeout(function(){
  1201. element[0].focus();
  1202. taSelection.setSelectionToElementEnd(element[0]);
  1203. }, 1);
  1204. }
  1205. }
  1206. };
  1207. // in here we are undoing the converts used elsewhere to prevent the < > and & being displayed when they shouldn't in the code.
  1208. var _compileHtml = function(){
  1209. if(_isContentEditable) return element[0].innerHTML;
  1210. if(_isInputFriendly) return element.val();
  1211. throw ('textAngular Error: attempting to update non-editable taBind');
  1212. };
  1213. var _setViewValue = function(_val, triggerUndo, skipRender){
  1214. _skipRender = skipRender || false;
  1215. if(typeof triggerUndo === "undefined" || triggerUndo === null) triggerUndo = true && _isContentEditable; // if not contentEditable then the native undo/redo is fine
  1216. if(typeof _val === "undefined" || _val === null) _val = _compileHtml();
  1217. if(_blankTest(_val)){
  1218. // this avoids us from tripping the ng-pristine flag if we click in and out with out typing
  1219. if(ngModel.$viewValue !== '') ngModel.$setViewValue('');
  1220. if(triggerUndo && ngModel.$undoManager.current() !== '') ngModel.$undoManager.push('');
  1221. }else{
  1222. _reApplyOnSelectorHandlers();
  1223. if(ngModel.$viewValue !== _val){
  1224. ngModel.$setViewValue(_val);
  1225. if(triggerUndo) ngModel.$undoManager.push(_val);
  1226. }
  1227. }
  1228. ngModel.$render();
  1229. };
  1230. //used for updating when inserting wrapped elements
  1231. scope['updateTaBind' + (attrs.id || '')] = function(){
  1232. if(!_isReadonly) _setViewValue(undefined, undefined, true);
  1233. };
  1234. // catch DOM XSS via taSanitize
  1235. // Sanitizing both ways is identical
  1236. var _sanitize = function(unsafe){
  1237. return (ngModel.$oldViewValue = taSanitize(taFixChrome(unsafe), ngModel.$oldViewValue, _disableSanitizer));
  1238. };
  1239. // trigger the validation calls
  1240. if(element.attr('required')) ngModel.$validators.required = function(modelValue, viewValue) {
  1241. return !_blankTest(modelValue || viewValue);
  1242. };
  1243. // parsers trigger from the above keyup function or any other time that the viewValue is updated and parses it for storage in the ngModel
  1244. ngModel.$parsers.push(_sanitize);
  1245. ngModel.$parsers.unshift(_ensureContentWrapped);
  1246. // because textAngular is bi-directional (which is awesome) we need to also sanitize values going in from the server
  1247. ngModel.$formatters.push(_sanitize);
  1248. ngModel.$formatters.unshift(_ensureContentWrapped);
  1249. ngModel.$formatters.unshift(function(value){
  1250. return ngModel.$undoManager.push(value || '');
  1251. });
  1252. //this code is used to update the models when data is entered/deleted
  1253. if(_isInputFriendly){
  1254. scope.events = {};
  1255. if(!_isContentEditable){
  1256. // if a textarea or input just add in change and blur handlers, everything else is done by angulars input directive
  1257. element.on('change blur', scope.events.change = scope.events.blur = function(){
  1258. if(!_isReadonly) ngModel.$setViewValue(_compileHtml());
  1259. });
  1260. element.on('keydown', scope.events.keydown = function(event, eventData){
  1261. /* istanbul ignore else: this is for catching the jqLite testing*/
  1262. if(eventData) angular.extend(event, eventData);
  1263. // Reference to http://stackoverflow.com/questions/6140632/how-to-handle-tab-in-textarea
  1264. /* istanbul ignore else: otherwise normal functionality */
  1265. if(event.keyCode === 9){ // tab was pressed
  1266. // get caret position/selection
  1267. var start = this.selectionStart;
  1268. var end = this.selectionEnd;
  1269. var value = element.val();
  1270. if(event.shiftKey){
  1271. // find \t
  1272. var _linebreak = value.lastIndexOf('\n', start), _tab = value.lastIndexOf('\t', start);
  1273. if(_tab !== -1 && _tab >= _linebreak){
  1274. // set textarea value to: text before caret + tab + text after caret
  1275. element.val(value.substring(0, _tab) + value.substring(_tab + 1));
  1276. // put caret at right position again (add one for the tab)
  1277. this.selectionStart = this.selectionEnd = start - 1;
  1278. }
  1279. }else{
  1280. // set textarea value to: text before caret + tab + text after caret
  1281. element.val(value.substring(0, start) + "\t" + value.substring(end));
  1282. // put caret at right position again (add one for the tab)
  1283. this.selectionStart = this.selectionEnd = start + 1;
  1284. }
  1285. // prevent the focus lose
  1286. event.preventDefault();
  1287. }
  1288. });
  1289. var _repeat = function(string, n){
  1290. var result = '';
  1291. for(var _n = 0; _n < n; _n++) result += string;
  1292. return result;
  1293. };
  1294. // add a forEach function that will work on a NodeList, etc..
  1295. var forEach = function (array, callback, scope) {
  1296. for (var i= 0; i<array.length; i++) {
  1297. callback.call(scope, i, array[i]);
  1298. }
  1299. };
  1300. // handle <ul> or <ol> nodes
  1301. var recursiveListFormat = function(listNode, tablevel){
  1302. var _html = '';
  1303. var _subnodes = listNode.childNodes;
  1304. tablevel++;
  1305. // tab out and add the <ul> or <ol> html piece
  1306. _html += _repeat('\t', tablevel-1) + listNode.outerHTML.substring(0, 4);
  1307. forEach(_subnodes, function (index, node) {
  1308. /* istanbul ignore next: browser catch */
  1309. var nodeName = node.nodeName.toLowerCase();
  1310. if (nodeName === '#comment') {
  1311. _html += '<!--' + node.nodeValue + '-->';
  1312. return;
  1313. }
  1314. if (nodeName === '#text') {
  1315. _html += node.textContent;
  1316. return;
  1317. }
  1318. /* istanbul ignore next: not tested, and this was original code -- so not wanting to possibly cause an issue, leaving it... */
  1319. if(!node.outerHTML) {
  1320. // no html to add
  1321. return;
  1322. }
  1323. if(nodeName === 'ul' || nodeName === 'ol') {
  1324. _html += '\n' + recursiveListFormat(node, tablevel);
  1325. }
  1326. else {
  1327. // no reformatting within this subnode, so just do the tabing...
  1328. _html += '\n' + _repeat('\t', tablevel) + node.outerHTML;
  1329. }
  1330. });
  1331. // now add on the </ol> or </ul> piece
  1332. _html += '\n' + _repeat('\t', tablevel-1) + listNode.outerHTML.substring(listNode.outerHTML.lastIndexOf('<'));
  1333. return _html;
  1334. };
  1335. // handle formating of something like:
  1336. // <ol><!--First comment-->
  1337. // <li>Test Line 1<!--comment test list 1--></li>
  1338. // <ul><!--comment ul-->
  1339. // <li>Nested Line 1</li>
  1340. // <!--comment between nested lines--><li>Nested Line 2</li>
  1341. // </ul>
  1342. // <li>Test Line 3</li>
  1343. // </ol>
  1344. ngModel.$formatters.unshift(function(htmlValue){
  1345. // tabulate the HTML so it looks nicer
  1346. //
  1347. // first get a list of the nodes...
  1348. // we do this by using the element parser...
  1349. //
  1350. // doing this -- which is simpiler -- breaks our tests...
  1351. //var _nodes=angular.element(htmlValue);
  1352. var _nodes = angular.element('<div>' + htmlValue + '</div>')[0].childNodes;
  1353. if(_nodes.length > 0){
  1354. // do the reformatting of the layout...
  1355. htmlValue = '';
  1356. forEach(_nodes, function (index, node) {
  1357. var nodeName = node.nodeName.toLowerCase();
  1358. if (nodeName === '#comment') {
  1359. htmlValue += '<!--' + node.nodeValue + '-->';
  1360. return;
  1361. }
  1362. if (nodeName === '#text') {
  1363. htmlValue += node.textContent;
  1364. return;
  1365. }
  1366. /* istanbul ignore next: not tested, and this was original code -- so not wanting to possibly cause an issue, leaving it... */
  1367. if(!node.outerHTML)
  1368. {
  1369. // nothing to format!
  1370. return;
  1371. }
  1372. if(htmlValue.length > 0) {
  1373. // we aready have some content, so drop to a new line
  1374. htmlValue += '\n';
  1375. }
  1376. if(nodeName === 'ul' || nodeName === 'ol') {
  1377. // okay a set of list stuff we want to reformat in a nested way
  1378. htmlValue += '' + recursiveListFormat(node, 0);
  1379. }
  1380. else {
  1381. // just use the original without any additional formating
  1382. htmlValue += '' + node.outerHTML;
  1383. }
  1384. });
  1385. }
  1386. return htmlValue;
  1387. });
  1388. }else{
  1389. // all the code specific to contenteditable divs
  1390. var _processingPaste = false;
  1391. /* istanbul ignore next: phantom js cannot test this for some reason */
  1392. var processpaste = function(text) {
  1393. /* istanbul ignore else: don't care if nothing pasted */
  1394. if(text && text.trim().length){
  1395. // test paste from word/microsoft product
  1396. if(text.match(/class=["']*Mso(Normal|List)/i)){
  1397. var textFragment = text.match(/<!--StartFragment-->([\s\S]*?)<!--EndFragment-->/i);
  1398. if(!textFragment) textFragment = text;
  1399. else textFragment = textFragment[1];
  1400. textFragment = textFragment.replace(/<o:p>[\s\S]*?<\/o:p>/ig, '').replace(/class=(["']|)MsoNormal(["']|)/ig, '');
  1401. var dom = angular.element("<div>" + textFragment + "</div>");
  1402. var targetDom = angular.element("<div></div>");
  1403. var _list = {
  1404. element: null,
  1405. lastIndent: [],
  1406. lastLi: null,
  1407. isUl: false
  1408. };
  1409. _list.lastIndent.peek = function(){
  1410. var n = this.length;
  1411. if (n>0) return this[n-1];
  1412. };
  1413. var _resetList = function(isUl){
  1414. _list.isUl = isUl;
  1415. _list.element = angular.element(isUl ? "<ul>" : "<ol>");
  1416. _list.lastIndent = [];
  1417. _list.lastIndent.peek = function(){
  1418. var n = this.length;
  1419. if (n>0) return this[n-1];
  1420. };
  1421. _list.lastLevelMatch = null;
  1422. };
  1423. for(var i = 0; i <= dom[0].childNodes.length; i++){
  1424. if(!dom[0].childNodes[i] || dom[0].childNodes[i].nodeName === "#text" || dom[0].childNodes[i].tagName.toLowerCase() !== "p") continue;
  1425. var el = angular.element(dom[0].childNodes[i]);
  1426. var _listMatch = (el.attr('class') || '').match(/MsoList(Bullet|Number|Paragraph)(CxSp(First|Middle|Last)|)/i);
  1427. if(_listMatch){
  1428. if(el[0].childNodes.length < 2 || el[0].childNodes[1].childNodes.length < 1){
  1429. continue;
  1430. }
  1431. 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)));
  1432. var _indentMatch = (el.attr('style') || '').match(/margin-left:([\-\.0-9]*)/i);
  1433. var indent = parseFloat((_indentMatch)?_indentMatch[1]:0);
  1434. var _levelMatch = (el.attr('style') || '').match(/mso-list:l([0-9]+) level([0-9]+) lfo[0-9+]($|;)/i);
  1435. // prefers the mso-list syntax
  1436. if(_levelMatch && _levelMatch[2]) indent = parseInt(_levelMatch[2]);
  1437. 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)) {
  1438. _resetList(isUl);
  1439. targetDom.append(_list.element);
  1440. } else if (_list.lastIndent.peek() != null && _list.lastIndent.peek() < indent){
  1441. _list.element = angular.element(isUl ? "<ul>" : "<ol>");
  1442. _list.lastLi.append(_list.element);
  1443. } else if (_list.lastIndent.peek() != null && _list.lastIndent.peek() > indent){
  1444. while(_list.lastIndent.peek() != null && _list.lastIndent.peek() > indent){
  1445. if(_list.element.parent()[0].tagName.toLowerCase() === 'li'){
  1446. _list.element = _list.element.parent();
  1447. continue;
  1448. }else if(/[uo]l/i.test(_list.element.parent()[0].tagName.toLowerCase())){
  1449. _list.element = _list.element.parent();
  1450. }else{ // else it's it should be a sibling
  1451. break;
  1452. }
  1453. _list.lastIndent.pop();
  1454. }
  1455. _list.isUl = _list.element[0].tagName.toLowerCase() === "ul";
  1456. if (isUl !== _list.isUl) {
  1457. _resetList(isUl);
  1458. targetDom.append(_list.element);
  1459. }
  1460. }
  1461. _list.lastLevelMatch = _levelMatch;
  1462. if(indent !== _list.lastIndent.peek()) _list.lastIndent.push(indent);
  1463. _list.lastLi = angular.element("<li>");
  1464. _list.element.append(_list.lastLi);
  1465. _list.lastLi.html(el.html().replace(/<!(--|)\[if !supportLists\](--|)>[\s\S]*?<!(--|)\[endif\](--|)>/ig, ''));
  1466. el.remove();
  1467. }else{
  1468. _resetList(false);
  1469. targetDom.append(el);
  1470. }
  1471. }
  1472. var _unwrapElement = function(node){
  1473. node = angular.element(node);
  1474. for(var _n = node[0].childNodes.length - 1; _n >= 0; _n--) node.after(node[0].childNodes[_n]);
  1475. node.remove();
  1476. };
  1477. angular.forEach(targetDom.find('span'), function(node){
  1478. node.removeAttribute('lang');
  1479. if(node.attributes.length <= 0) _unwrapElement(node);
  1480. });
  1481. angular.forEach(targetDom.find('font'), _unwrapElement);
  1482. text = targetDom.html();
  1483. }else{
  1484. // remove unnecessary chrome insert
  1485. text = text.replace(/<(|\/)meta[^>]*?>/ig, '');
  1486. if(text.match(/<[^>]*?(ta-bind)[^>]*?>/)){
  1487. // entire text-angular or ta-bind has been pasted, REMOVE AT ONCE!!
  1488. if(text.match(/<[^>]*?(text-angular)[^>]*?>/)){
  1489. var _el = angular.element("<div>" + text + "</div>");
  1490. _el.find('textarea').remove();
  1491. var binds = taDOM.getByAttribute(_el, 'ta-bind');
  1492. for(var _b = 0; _b < binds.length; _b++){
  1493. var _target = binds[_b][0].parentNode.parentNode;
  1494. for(var _c = 0; _c < binds[_b][0].childNodes.length; _c++){
  1495. _target.parentNode.insertBefore(binds[_b][0].childNodes[_c], _target);
  1496. }
  1497. _target.parentNode.removeChild(_target);
  1498. }
  1499. text = _el.html().replace('<br class="Apple-interchange-newline">', '');
  1500. }
  1501. }else if(text.match(/^<span/)){
  1502. // in case of pasting only a span - chrome paste, remove them. THis is just some wierd formatting
  1503. // if we remove the '<span class="Apple-converted-space"> </span>' here we destroy the spacing
  1504. // on paste from even ourselves!
  1505. if (!text.match(/<span class=(\"Apple-converted-space\"|\'Apple-converted-space\')>.<\/span>/ig)) {
  1506. text = text.replace(/<(|\/)span[^>]*?>/ig, '');
  1507. }
  1508. }
  1509. // Webkit on Apple tags
  1510. text = text.replace(/<br class="Apple-interchange-newline"[^>]*?>/ig, '').replace(/<span class="Apple-converted-space">( |&nbsp;)<\/span>/ig, '&nbsp;');
  1511. }
  1512. if (/<li(\s.*)?>/i.test(text) && /(<ul(\s.*)?>|<ol(\s.*)?>).*<li(\s.*)?>/i.test(text) === false) {
  1513. // insert missing parent of li element
  1514. text = text.replace(/<li(\s.*)?>.*<\/li(\s.*)?>/i, '<ul>$&</ul>');
  1515. }
  1516. // parse whitespace from plaintext input, starting with preceding spaces that get stripped on paste
  1517. text = text.replace(/^[ |\u00A0]+/gm, function (match) {
  1518. var result = '';
  1519. for (var i = 0; i < match.length; i++) {
  1520. result += '&nbsp;';
  1521. }
  1522. return result;
  1523. }).replace(/\n|\r\n|\r/g, '<br />').replace(/\t/g, '&nbsp;&nbsp;&nbsp;&nbsp;');
  1524. if(_pasteHandler) text = _pasteHandler(scope, {$html: text}) || text;
  1525. text = taSanitize(text, '', _disableSanitizer);
  1526. taSelection.insertHtml(text, element[0]);
  1527. $timeout(function(){
  1528. ngModel.$setViewValue(_compileHtml());
  1529. _processingPaste = false;
  1530. element.removeClass('processing-paste');
  1531. }, 0);
  1532. }else{
  1533. _processingPaste = false;
  1534. element.removeClass('processing-paste');
  1535. }
  1536. };
  1537. element.on('paste', scope.events.paste = function(e, eventData){
  1538. /* istanbul ignore else: this is for catching the jqLite testing*/
  1539. if(eventData) angular.extend(e, eventData);
  1540. if(_isReadonly || _processingPaste){
  1541. e.stopPropagation();
  1542. e.preventDefault();
  1543. return false;
  1544. }
  1545. // Code adapted from http://stackoverflow.com/questions/2176861/javascript-get-clipboard-data-on-paste-event-cross-browser/6804718#6804718
  1546. _processingPaste = true;
  1547. element.addClass('processing-paste');
  1548. var pastedContent;
  1549. var clipboardData = (e.originalEvent || e).clipboardData;
  1550. if (clipboardData && clipboardData.getData && clipboardData.types.length > 0) {// Webkit - get data from clipboard, put into editdiv, cleanup, then cancel event
  1551. var _types = "";
  1552. for(var _t = 0; _t < clipboardData.types.length; _t++){
  1553. _types += " " + clipboardData.types[_t];
  1554. }
  1555. /* istanbul ignore next: browser tests */
  1556. if (/text\/html/i.test(_types)) {
  1557. pastedContent = clipboardData.getData('text/html');
  1558. } else if (/text\/plain/i.test(_types)) {
  1559. pastedContent = clipboardData.getData('text/plain');
  1560. }
  1561. processpaste(pastedContent);
  1562. e.stopPropagation();
  1563. e.preventDefault();
  1564. return false;
  1565. } else {// Everything else - empty editdiv and allow browser to paste content into it, then cleanup
  1566. var _savedSelection = $window.rangy.saveSelection(),
  1567. _tempDiv = angular.element('<div class="ta-hidden-input" contenteditable="true"></div>');
  1568. $document.find('body').append(_tempDiv);
  1569. _tempDiv[0].focus();
  1570. $timeout(function(){
  1571. // restore selection
  1572. $window.rangy.restoreSelection(_savedSelection);
  1573. processpaste(_tempDiv[0].innerHTML);
  1574. element[0].focus();
  1575. _tempDiv.remove();
  1576. }, 0);
  1577. }
  1578. });
  1579. element.on('cut', scope.events.cut = function(e){
  1580. // timeout to next is needed as otherwise the paste/cut event has not finished actually changing the display
  1581. if(!_isReadonly) $timeout(function(){
  1582. ngModel.$setViewValue(_compileHtml());
  1583. }, 0);
  1584. else e.preventDefault();
  1585. });
  1586. element.on('keydown', scope.events.keydown = function(event, eventData){
  1587. /* istanbul ignore else: this is for catching the jqLite testing*/
  1588. if(eventData) angular.extend(event, eventData);
  1589. event.specialKey = _mapKeys(event);
  1590. var userSpecialKey;
  1591. /* istanbul ignore next: difficult to test */
  1592. taOptions.keyMappings.forEach(function (mapping) {
  1593. if (event.specialKey === mapping.commandKeyCode) {
  1594. // taOptions has remapped this binding... so
  1595. // we disable our own
  1596. event.specialKey = undefined;
  1597. }
  1598. if (mapping.testForKey(event)) {
  1599. userSpecialKey = mapping.commandKeyCode;
  1600. }
  1601. if ((mapping.commandKeyCode === 'UndoKey') || (mapping.commandKeyCode === 'RedoKey')) {
  1602. // this is necessary to fully stop the propagation.
  1603. if (!mapping.enablePropagation) {
  1604. event.preventDefault();
  1605. }
  1606. }
  1607. });
  1608. /* istanbul ignore next: difficult to test */
  1609. if (typeof userSpecialKey !== 'undefined') {
  1610. event.specialKey = userSpecialKey;
  1611. }
  1612. /* istanbul ignore next: difficult to test as can't seem to select */
  1613. if ((typeof event.specialKey !== 'undefined') && (
  1614. event.specialKey !== 'UndoKey' || event.specialKey !== 'RedoKey'
  1615. )) {
  1616. event.preventDefault();
  1617. textAngularManager.sendKeyCommand(scope, event);
  1618. }
  1619. /* istanbul ignore else: readonly check */
  1620. if(!_isReadonly){
  1621. if (event.specialKey==='UndoKey') {
  1622. _undo();
  1623. event.preventDefault();
  1624. }
  1625. if (event.specialKey==='RedoKey') {
  1626. _redo();
  1627. event.preventDefault();
  1628. }
  1629. /* istanbul ignore next: difficult to test as can't seem to select */
  1630. if(event.keyCode === 13 && !event.shiftKey){
  1631. var $selection;
  1632. var selection = taSelection.getSelectionElement();
  1633. if(!selection.tagName.match(VALIDELEMENTS)) return;
  1634. var _new = angular.element(_defaultVal);
  1635. if (/^<br(|\/)>$/i.test(selection.innerHTML.trim()) && selection.parentNode.tagName.toLowerCase() === 'blockquote' && !selection.nextSibling) {
  1636. // if last element in blockquote and element is blank, pull element outside of blockquote.
  1637. $selection = angular.element(selection);
  1638. var _parent = $selection.parent();
  1639. _parent.after(_new);
  1640. $selection.remove();
  1641. if(_parent.children().length === 0) _parent.remove();
  1642. taSelection.setSelectionToElementStart(_new[0]);
  1643. event.preventDefault();
  1644. }else if (/^<[^>]+><br(|\/)><\/[^>]+>$/i.test(selection.innerHTML.trim()) && selection.tagName.toLowerCase() === 'blockquote'){
  1645. $selection = angular.element(selection);
  1646. $selection.after(_new);
  1647. $selection.remove();
  1648. taSelection.setSelectionToElementStart(_new[0]);
  1649. event.preventDefault();
  1650. }
  1651. }
  1652. }
  1653. });
  1654. var _keyupTimeout;
  1655. element.on('keyup', scope.events.keyup = function(event, eventData){
  1656. /* istanbul ignore else: this is for catching the jqLite testing*/
  1657. if(eventData) angular.extend(event, eventData);
  1658. /* istanbul ignore next: FF specific bug fix */
  1659. if (event.keyCode === 9) {
  1660. var _selection = taSelection.getSelection();
  1661. if(_selection.start.element === element[0] && element.children().length) taSelection.setSelectionToElementStart(element.children()[0]);
  1662. return;
  1663. }
  1664. if(_undoKeyupTimeout) $timeout.cancel(_undoKeyupTimeout);
  1665. if(!_isReadonly && !BLOCKED_KEYS.test(event.keyCode)){
  1666. // if enter - insert new taDefaultWrap, if shift+enter insert <br/>
  1667. if(_defaultVal !== '' && event.keyCode === 13){
  1668. if(!event.shiftKey){
  1669. // new paragraph, br should be caught correctly
  1670. var selection = taSelection.getSelectionElement();
  1671. while(!selection.tagName.match(VALIDELEMENTS) && selection !== element[0]){
  1672. selection = selection.parentNode;
  1673. }
  1674. if(selection.tagName.toLowerCase() !== attrs.taDefaultWrap && selection.tagName.toLowerCase() !== 'li' && (selection.innerHTML.trim() === '' || selection.innerHTML.trim() === '<br>')){
  1675. var _new = angular.element(_defaultVal);
  1676. angular.element(selection).replaceWith(_new);
  1677. taSelection.setSelectionToElementStart(_new[0]);
  1678. }
  1679. }
  1680. }
  1681. var val = _compileHtml();
  1682. if(_defaultVal !== '' && val.trim() === ''){
  1683. _setInnerHTML(_defaultVal);
  1684. taSelection.setSelectionToElementStart(element.children()[0]);
  1685. }else if(val.substring(0, 1) !== '<' && attrs.taDefaultWrap !== ''){
  1686. /* we no longer do this, since there can be comments here and white space
  1687. var _savedSelection = $window.rangy.saveSelection();
  1688. val = _compileHtml();
  1689. val = "<" + attrs.taDefaultWrap + ">" + val + "</" + attrs.taDefaultWrap + ">";
  1690. _setInnerHTML(val);
  1691. $window.rangy.restoreSelection(_savedSelection);
  1692. */
  1693. }
  1694. var triggerUndo = _lastKey !== event.keyCode && UNDO_TRIGGER_KEYS.test(event.keyCode);
  1695. if(_keyupTimeout) $timeout.cancel(_keyupTimeout);
  1696. _keyupTimeout = $timeout(function() {
  1697. _setViewValue(val, triggerUndo, true);
  1698. }, ngModelOptions.$options.debounce || 400);
  1699. if(!triggerUndo) _undoKeyupTimeout = $timeout(function(){ ngModel.$undoManager.push(val); }, 250);
  1700. _lastKey = event.keyCode;
  1701. }
  1702. });
  1703. element.on('blur', scope.events.blur = function(){
  1704. _focussed = false;
  1705. /* istanbul ignore else: if readonly don't update model */
  1706. if(!_isReadonly){
  1707. _setViewValue(undefined, undefined, true);
  1708. }else{
  1709. _skipRender = true; // don't redo the whole thing, just check the placeholder logic
  1710. ngModel.$render();
  1711. }
  1712. });
  1713. // Placeholders not supported on ie 8 and below
  1714. if(attrs.placeholder && (_browserDetect.ie > 8 || _browserDetect.ie === undefined)){
  1715. var rule;
  1716. if(attrs.id) rule = addCSSRule('#' + attrs.id + '.placeholder-text:before', 'content: "' + attrs.placeholder + '"');
  1717. else throw('textAngular Error: An unique ID is required for placeholders to work');
  1718. scope.$on('$destroy', function(){
  1719. removeCSSRule(rule);
  1720. });
  1721. }
  1722. element.on('focus', scope.events.focus = function(){
  1723. _focussed = true;
  1724. element.removeClass('placeholder-text');
  1725. _reApplyOnSelectorHandlers();
  1726. });
  1727. element.on('mouseup', scope.events.mouseup = function(){
  1728. var _selection = taSelection.getSelection();
  1729. if(_selection.start.element === element[0] && element.children().length) taSelection.setSelectionToElementStart(element.children()[0]);
  1730. });
  1731. // prevent propagation on mousedown in editor, see #206
  1732. element.on('mousedown', scope.events.mousedown = function(event, eventData){
  1733. /* istanbul ignore else: this is for catching the jqLite testing*/
  1734. if(eventData) angular.extend(event, eventData);
  1735. event.stopPropagation();
  1736. });
  1737. }
  1738. }
  1739. var selectorClickHandler = function(event){
  1740. // emit the element-select event, pass the element
  1741. scope.$emit('ta-element-select', this);
  1742. event.preventDefault();
  1743. return false;
  1744. };
  1745. var fileDropHandler = function(event, eventData){
  1746. /* istanbul ignore else: this is for catching the jqLite testing*/
  1747. if(eventData) angular.extend(event, eventData);
  1748. // emit the drop event, pass the element, preventing should be done elsewhere
  1749. if(!dropFired && !_isReadonly){
  1750. dropFired = true;
  1751. var dataTransfer;
  1752. if(event.originalEvent) dataTransfer = event.originalEvent.dataTransfer;
  1753. else dataTransfer = event.dataTransfer;
  1754. scope.$emit('ta-drop-event', this, event, dataTransfer);
  1755. $timeout(function(){
  1756. dropFired = false;
  1757. _setViewValue(undefined, undefined, true);
  1758. }, 100);
  1759. }
  1760. };
  1761. //used for updating when inserting wrapped elements
  1762. var _reApplyOnSelectorHandlers = scope['reApplyOnSelectorHandlers' + (attrs.id || '')] = function(){
  1763. /* istanbul ignore else */
  1764. if(!_isReadonly) angular.forEach(taSelectableElements, function(selector){
  1765. // check we don't apply the handler twice
  1766. element.find(selector)
  1767. .off('click', selectorClickHandler)
  1768. .on('click', selectorClickHandler);
  1769. });
  1770. };
  1771. var _setInnerHTML = function(newval){
  1772. element[0].innerHTML = newval;
  1773. };
  1774. var _renderTimeout;
  1775. var _renderInProgress = false;
  1776. // changes to the model variable from outside the html/text inputs
  1777. ngModel.$render = function(){
  1778. /* istanbul ignore if: Catches rogue renders, hard to replicate in tests */
  1779. if(_renderInProgress) return;
  1780. else _renderInProgress = true;
  1781. // catch model being null or undefined
  1782. var val = ngModel.$viewValue || '';
  1783. // if the editor isn't focused it needs to be updated, otherwise it's receiving user input
  1784. if(!_skipRender){
  1785. /* istanbul ignore else: in other cases we don't care */
  1786. if(_isContentEditable && _focussed){
  1787. // update while focussed
  1788. element.removeClass('placeholder-text');
  1789. if(_renderTimeout) $timeout.cancel(_renderTimeout);
  1790. _renderTimeout = $timeout(function(){
  1791. /* istanbul ignore if: Can't be bothered testing this... */
  1792. if(!_focussed){
  1793. element[0].focus();
  1794. taSelection.setSelectionToElementEnd(element.children()[element.children().length - 1]);
  1795. }
  1796. _renderTimeout = undefined;
  1797. }, 1);
  1798. }
  1799. if(_isContentEditable){
  1800. // WYSIWYG Mode
  1801. if(attrs.placeholder){
  1802. if(val === ''){
  1803. // blank
  1804. _setInnerHTML(_defaultVal);
  1805. }else{
  1806. // not-blank
  1807. _setInnerHTML(val);
  1808. }
  1809. }else{
  1810. _setInnerHTML((val === '') ? _defaultVal : val);
  1811. }
  1812. // if in WYSIWYG and readOnly we kill the use of links by clicking
  1813. if(!_isReadonly){
  1814. _reApplyOnSelectorHandlers();
  1815. element.on('drop', fileDropHandler);
  1816. }else{
  1817. element.off('drop', fileDropHandler);
  1818. }
  1819. }else if(element[0].tagName.toLowerCase() !== 'textarea' && element[0].tagName.toLowerCase() !== 'input'){
  1820. // make sure the end user can SEE the html code as a display. This is a read-only display element
  1821. _setInnerHTML(taApplyCustomRenderers(val));
  1822. }else{
  1823. // only for input and textarea inputs
  1824. element.val(val);
  1825. }
  1826. }
  1827. if(_isContentEditable && attrs.placeholder){
  1828. if(val === ''){
  1829. if(_focussed) element.removeClass('placeholder-text');
  1830. else element.addClass('placeholder-text');
  1831. }else{
  1832. element.removeClass('placeholder-text');
  1833. }
  1834. }
  1835. _renderInProgress = _skipRender = false;
  1836. };
  1837. if(attrs.taReadonly){
  1838. //set initial value
  1839. _isReadonly = scope.$eval(attrs.taReadonly);
  1840. if(_isReadonly){
  1841. element.addClass('ta-readonly');
  1842. // we changed to readOnly mode (taReadonly='true')
  1843. if(element[0].tagName.toLowerCase() === 'textarea' || element[0].tagName.toLowerCase() === 'input'){
  1844. element.attr('disabled', 'disabled');
  1845. }
  1846. if(element.attr('contenteditable') !== undefined && element.attr('contenteditable')){
  1847. element.removeAttr('contenteditable');
  1848. }
  1849. }else{
  1850. element.removeClass('ta-readonly');
  1851. // we changed to NOT readOnly mode (taReadonly='false')
  1852. if(element[0].tagName.toLowerCase() === 'textarea' || element[0].tagName.toLowerCase() === 'input'){
  1853. element.removeAttr('disabled');
  1854. }else if(_isContentEditable){
  1855. element.attr('contenteditable', 'true');
  1856. }
  1857. }
  1858. // taReadonly only has an effect if the taBind element is an input or textarea or has contenteditable='true' on it.
  1859. // Otherwise it is readonly by default
  1860. scope.$watch(attrs.taReadonly, function(newVal, oldVal){
  1861. if(oldVal === newVal) return;
  1862. if(newVal){
  1863. element.addClass('ta-readonly');
  1864. // we changed to readOnly mode (taReadonly='true')
  1865. if(element[0].tagName.toLowerCase() === 'textarea' || element[0].tagName.toLowerCase() === 'input'){
  1866. element.attr('disabled', 'disabled');
  1867. }
  1868. if(element.attr('contenteditable') !== undefined && element.attr('contenteditable')){
  1869. element.removeAttr('contenteditable');
  1870. }
  1871. // turn ON selector click handlers
  1872. angular.forEach(taSelectableElements, function(selector){
  1873. element.find(selector).on('click', selectorClickHandler);
  1874. });
  1875. element.off('drop', fileDropHandler);
  1876. }else{
  1877. element.removeClass('ta-readonly');
  1878. // we changed to NOT readOnly mode (taReadonly='false')
  1879. if(element[0].tagName.toLowerCase() === 'textarea' || element[0].tagName.toLowerCase() === 'input'){
  1880. element.removeAttr('disabled');
  1881. }else if(_isContentEditable){
  1882. element.attr('contenteditable', 'true');
  1883. }
  1884. // remove the selector click handlers
  1885. angular.forEach(taSelectableElements, function(selector){
  1886. element.find(selector).off('click', selectorClickHandler);
  1887. });
  1888. element.on('drop', fileDropHandler);
  1889. }
  1890. _isReadonly = newVal;
  1891. });
  1892. }
  1893. // Initialise the selectableElements
  1894. // if in WYSIWYG and readOnly we kill the use of links by clicking
  1895. if(_isContentEditable && !_isReadonly){
  1896. angular.forEach(taSelectableElements, function(selector){
  1897. element.find(selector).on('click', selectorClickHandler);
  1898. });
  1899. element.on('drop', fileDropHandler);
  1900. element.on('blur', function(){
  1901. /* istanbul ignore next: webkit fix */
  1902. if(_browserDetect.webkit) { // detect webkit
  1903. globalContentEditableBlur = true;
  1904. }
  1905. });
  1906. }
  1907. }
  1908. };
  1909. }]);
  1910. // this global var is used to prevent multiple fires of the drop event. Needs to be global to the textAngular file.
  1911. var dropFired = false;
  1912. var textAngular = angular.module("textAngular", ['ngSanitize', 'textAngularSetup', 'textAngular.factories', 'textAngular.DOM', 'textAngular.validators', 'textAngular.taBind']); //This makes ngSanitize required
  1913. textAngular.config([function(){
  1914. // clear taTools variable. Just catches testing and any other time that this config may run multiple times...
  1915. angular.forEach(taTools, function(value, key){ delete taTools[key]; });
  1916. }]);
  1917. textAngular.run([function(){
  1918. /* istanbul ignore next: not sure how to test this */
  1919. // Require Rangy and rangy savedSelection module.
  1920. if (typeof define === 'function' && define.amd) {
  1921. // AMD. Register as an anonymous module.
  1922. define(function(require) {
  1923. window.rangy = require('rangy');
  1924. window.rangy.saveSelection = require('rangy/lib/rangy-selectionsaverestore');
  1925. });
  1926. } else if (typeof require ==='function' && typeof module !== 'undefined' && typeof exports === 'object') {
  1927. // Node/CommonJS style
  1928. window.rangy = require('rangy');
  1929. window.rangy.saveSelection = require('rangy/lib/rangy-selectionsaverestore');
  1930. } else {
  1931. // Ensure that rangy and rangy.saveSelection exists on the window (global scope).
  1932. // TODO: Refactor so that the global scope is no longer used.
  1933. if(!window.rangy){
  1934. throw("rangy-core.js and rangy-selectionsaverestore.js are required for textAngular to work correctly, rangy-core is not yet loaded.");
  1935. }else{
  1936. window.rangy.init();
  1937. if(!window.rangy.saveSelection){
  1938. throw("rangy-selectionsaverestore.js is required for textAngular to work correctly.");
  1939. }
  1940. }
  1941. }
  1942. }]);
  1943. textAngular.directive("textAngular", [
  1944. '$compile', '$timeout', 'taOptions', 'taSelection', 'taExecCommand',
  1945. 'textAngularManager', '$window', '$document', '$animate', '$log', '$q', '$parse',
  1946. function($compile, $timeout, taOptions, taSelection, taExecCommand,
  1947. textAngularManager, $window, $document, $animate, $log, $q, $parse){
  1948. return {
  1949. require: '?ngModel',
  1950. scope: {},
  1951. restrict: "EA",
  1952. priority: 2, // So we override validators correctly
  1953. link: function(scope, element, attrs, ngModel){
  1954. // all these vars should not be accessable outside this directive
  1955. var _keydown, _keyup, _keypress, _mouseup, _focusin, _focusout,
  1956. _originalContents, _toolbars,
  1957. _serial = (attrs.serial) ? attrs.serial : Math.floor(Math.random() * 10000000000000000),
  1958. _taExecCommand, _resizeMouseDown, _updateSelectedStylesTimeout;
  1959. scope._name = (attrs.name) ? attrs.name : 'textAngularEditor' + _serial;
  1960. var oneEvent = function(_element, event, action){
  1961. $timeout(function(){
  1962. // shim the .one till fixed
  1963. var _func = function(){
  1964. _element.off(event, _func);
  1965. action.apply(this, arguments);
  1966. };
  1967. _element.on(event, _func);
  1968. }, 100);
  1969. };
  1970. _taExecCommand = taExecCommand(attrs.taDefaultWrap);
  1971. // get the settings from the defaults and add our specific functions that need to be on the scope
  1972. angular.extend(scope, angular.copy(taOptions), {
  1973. // wraps the selection in the provided tag / execCommand function. Should only be called in WYSIWYG mode.
  1974. wrapSelection: function(command, opt, isSelectableElementTool){
  1975. if(command.toLowerCase() === "undo"){
  1976. scope['$undoTaBindtaTextElement' + _serial]();
  1977. }else if(command.toLowerCase() === "redo"){
  1978. scope['$redoTaBindtaTextElement' + _serial]();
  1979. }else{
  1980. // catch errors like FF erroring when you try to force an undo with nothing done
  1981. _taExecCommand(command, false, opt, scope.defaultTagAttributes);
  1982. if(isSelectableElementTool){
  1983. // re-apply the selectable tool events
  1984. scope['reApplyOnSelectorHandlerstaTextElement' + _serial]();
  1985. }
  1986. // refocus on the shown display element, this fixes a display bug when using :focus styles to outline the box.
  1987. // You still have focus on the text/html input it just doesn't show up
  1988. scope.displayElements.text[0].focus();
  1989. }
  1990. },
  1991. showHtml: scope.$eval(attrs.taShowHtml) || false
  1992. });
  1993. // setup the options from the optional attributes
  1994. if(attrs.taFocussedClass) scope.classes.focussed = attrs.taFocussedClass;
  1995. if(attrs.taTextEditorClass) scope.classes.textEditor = attrs.taTextEditorClass;
  1996. if(attrs.taHtmlEditorClass) scope.classes.htmlEditor = attrs.taHtmlEditorClass;
  1997. if(attrs.taDefaultTagAttributes){
  1998. try {
  1999. // TODO: This should use angular.merge to enhance functionality once angular 1.4 is required
  2000. angular.extend(scope.defaultTagAttributes, angular.fromJson(attrs.taDefaultTagAttributes));
  2001. } catch (error) {
  2002. $log.error(error);
  2003. }
  2004. }
  2005. // optional setup functions
  2006. if(attrs.taTextEditorSetup) scope.setup.textEditorSetup = scope.$parent.$eval(attrs.taTextEditorSetup);
  2007. if(attrs.taHtmlEditorSetup) scope.setup.htmlEditorSetup = scope.$parent.$eval(attrs.taHtmlEditorSetup);
  2008. // optional fileDropHandler function
  2009. if(attrs.taFileDrop) scope.fileDropHandler = scope.$parent.$eval(attrs.taFileDrop);
  2010. else scope.fileDropHandler = scope.defaultFileDropHandler;
  2011. _originalContents = element[0].innerHTML;
  2012. // clear the original content
  2013. element[0].innerHTML = '';
  2014. // Setup the HTML elements as variable references for use later
  2015. scope.displayElements = {
  2016. // we still need the hidden input even with a textarea as the textarea may have invalid/old input in it,
  2017. // wheras the input will ALLWAYS have the correct value.
  2018. forminput: angular.element("<input type='hidden' tabindex='-1' style='display: none;'>"),
  2019. html: angular.element("<textarea></textarea>"),
  2020. text: angular.element("<div></div>"),
  2021. // other toolbased elements
  2022. scrollWindow: angular.element("<div class='ta-scroll-window'></div>"),
  2023. popover: angular.element('<div class="popover fade bottom" style="max-width: none; width: 305px;"></div>'),
  2024. popoverArrow: angular.element('<div class="arrow"></div>'),
  2025. popoverContainer: angular.element('<div class="popover-content"></div>'),
  2026. resize: {
  2027. overlay: angular.element('<div class="ta-resizer-handle-overlay"></div>'),
  2028. background: angular.element('<div class="ta-resizer-handle-background"></div>'),
  2029. anchors: [
  2030. angular.element('<div class="ta-resizer-handle-corner ta-resizer-handle-corner-tl"></div>'),
  2031. angular.element('<div class="ta-resizer-handle-corner ta-resizer-handle-corner-tr"></div>'),
  2032. angular.element('<div class="ta-resizer-handle-corner ta-resizer-handle-corner-bl"></div>'),
  2033. angular.element('<div class="ta-resizer-handle-corner ta-resizer-handle-corner-br"></div>')
  2034. ],
  2035. info: angular.element('<div class="ta-resizer-handle-info"></div>')
  2036. }
  2037. };
  2038. // Setup the popover
  2039. scope.displayElements.popover.append(scope.displayElements.popoverArrow);
  2040. scope.displayElements.popover.append(scope.displayElements.popoverContainer);
  2041. scope.displayElements.scrollWindow.append(scope.displayElements.popover);
  2042. scope.displayElements.popover.on('mousedown', function(e, eventData){
  2043. /* istanbul ignore else: this is for catching the jqLite testing*/
  2044. if(eventData) angular.extend(e, eventData);
  2045. // this prevents focusout from firing on the editor when clicking anything in the popover
  2046. e.preventDefault();
  2047. return false;
  2048. });
  2049. // define the popover show and hide functions
  2050. scope.showPopover = function(_el){
  2051. scope.displayElements.popover.css('display', 'block');
  2052. scope.reflowPopover(_el);
  2053. $animate.addClass(scope.displayElements.popover, 'in');
  2054. oneEvent($document.find('body'), 'click keyup', function(){scope.hidePopover();});
  2055. };
  2056. scope.reflowPopover = function(_el){
  2057. /* istanbul ignore if: catches only if near bottom of editor */
  2058. if(scope.displayElements.text[0].offsetHeight - 51 > _el[0].offsetTop){
  2059. scope.displayElements.popover.css('top', _el[0].offsetTop + _el[0].offsetHeight + scope.displayElements.scrollWindow[0].scrollTop + 'px');
  2060. scope.displayElements.popover.removeClass('top').addClass('bottom');
  2061. }else{
  2062. scope.displayElements.popover.css('top', _el[0].offsetTop - 54 + scope.displayElements.scrollWindow[0].scrollTop + 'px');
  2063. scope.displayElements.popover.removeClass('bottom').addClass('top');
  2064. }
  2065. var _maxLeft = scope.displayElements.text[0].offsetWidth - scope.displayElements.popover[0].offsetWidth;
  2066. var _targetLeft = _el[0].offsetLeft + (_el[0].offsetWidth / 2.0) - (scope.displayElements.popover[0].offsetWidth / 2.0);
  2067. scope.displayElements.popover.css('left', Math.max(0, Math.min(_maxLeft, _targetLeft)) + 'px');
  2068. scope.displayElements.popoverArrow.css('margin-left', (Math.min(_targetLeft, (Math.max(0, _targetLeft - _maxLeft))) - 11) + 'px');
  2069. };
  2070. scope.hidePopover = function(){
  2071. scope.displayElements.popover.css('display', '');
  2072. scope.displayElements.popoverContainer.attr('style', '');
  2073. scope.displayElements.popoverContainer.attr('class', 'popover-content');
  2074. scope.displayElements.popover.removeClass('in');
  2075. };
  2076. // setup the resize overlay
  2077. scope.displayElements.resize.overlay.append(scope.displayElements.resize.background);
  2078. angular.forEach(scope.displayElements.resize.anchors, function(anchor){ scope.displayElements.resize.overlay.append(anchor);});
  2079. scope.displayElements.resize.overlay.append(scope.displayElements.resize.info);
  2080. scope.displayElements.scrollWindow.append(scope.displayElements.resize.overlay);
  2081. // define the show and hide events
  2082. scope.reflowResizeOverlay = function(_el){
  2083. _el = angular.element(_el)[0];
  2084. scope.displayElements.resize.overlay.css({
  2085. 'display': 'block',
  2086. 'left': _el.offsetLeft - 5 + 'px',
  2087. 'top': _el.offsetTop - 5 + 'px',
  2088. 'width': _el.offsetWidth + 10 + 'px',
  2089. 'height': _el.offsetHeight + 10 + 'px'
  2090. });
  2091. scope.displayElements.resize.info.text(_el.offsetWidth + ' x ' + _el.offsetHeight);
  2092. };
  2093. /* istanbul ignore next: pretty sure phantomjs won't test this */
  2094. scope.showResizeOverlay = function(_el){
  2095. var _body = $document.find('body');
  2096. _resizeMouseDown = function(event){
  2097. var startPosition = {
  2098. width: parseInt(_el.attr('width')),
  2099. height: parseInt(_el.attr('height')),
  2100. x: event.clientX,
  2101. y: event.clientY
  2102. };
  2103. if(startPosition.width === undefined || isNaN(startPosition.width)) startPosition.width = _el[0].offsetWidth;
  2104. if(startPosition.height === undefined || isNaN(startPosition.height)) startPosition.height = _el[0].offsetHeight;
  2105. scope.hidePopover();
  2106. var ratio = startPosition.height / startPosition.width;
  2107. var mousemove = function(event){
  2108. // calculate new size
  2109. var pos = {
  2110. x: Math.max(0, startPosition.width + (event.clientX - startPosition.x)),
  2111. y: Math.max(0, startPosition.height + (event.clientY - startPosition.y))
  2112. };
  2113. // DEFAULT: the aspect ratio is not locked unless the Shift key is pressed.
  2114. //
  2115. // attribute: ta-resize-force-aspect-ratio -- locks resize into maintaing the aspect ratio
  2116. var bForceAspectRatio = (attrs.taResizeForceAspectRatio !== undefined);
  2117. // attribute: ta-resize-maintain-aspect-ratio=true causes the space ratio to remain locked
  2118. // unless the Shift key is pressed
  2119. var bFlipKeyBinding = attrs.taResizeMaintainAspectRatio;
  2120. var bKeepRatio = bForceAspectRatio || (bFlipKeyBinding && !event.shiftKey);
  2121. if(bKeepRatio) {
  2122. var newRatio = pos.y / pos.x;
  2123. pos.x = ratio > newRatio ? pos.x : pos.y / ratio;
  2124. pos.y = ratio > newRatio ? pos.x * ratio : pos.y;
  2125. }
  2126. var el = angular.element(_el);
  2127. function roundedMaxVal(val) {
  2128. return Math.round(Math.max(0, val));
  2129. }
  2130. el.css('height', roundedMaxVal(pos.y) + 'px');
  2131. el.css('width', roundedMaxVal(pos.x) + 'px');
  2132. // reflow the popover tooltip
  2133. scope.reflowResizeOverlay(_el);
  2134. };
  2135. _body.on('mousemove', mousemove);
  2136. oneEvent(_body, 'mouseup', function(event){
  2137. event.preventDefault();
  2138. event.stopPropagation();
  2139. _body.off('mousemove', mousemove);
  2140. scope.showPopover(_el);
  2141. });
  2142. event.stopPropagation();
  2143. event.preventDefault();
  2144. };
  2145. scope.displayElements.resize.anchors[3].off('mousedown');
  2146. scope.displayElements.resize.anchors[3].on('mousedown', _resizeMouseDown);
  2147. scope.reflowResizeOverlay(_el);
  2148. oneEvent(_body, 'click', function(){scope.hideResizeOverlay();});
  2149. };
  2150. /* istanbul ignore next: pretty sure phantomjs won't test this */
  2151. scope.hideResizeOverlay = function(){
  2152. scope.displayElements.resize.anchors[3].off('mousedown', _resizeMouseDown);
  2153. scope.displayElements.resize.overlay.css('display', '');
  2154. };
  2155. // allow for insertion of custom directives on the textarea and div
  2156. scope.setup.htmlEditorSetup(scope.displayElements.html);
  2157. scope.setup.textEditorSetup(scope.displayElements.text);
  2158. scope.displayElements.html.attr({
  2159. 'id': 'taHtmlElement' + _serial,
  2160. 'ng-show': 'showHtml',
  2161. 'ta-bind': 'ta-bind',
  2162. 'ng-model': 'html',
  2163. 'ng-model-options': element.attr('ng-model-options')
  2164. });
  2165. scope.displayElements.text.attr({
  2166. 'id': 'taTextElement' + _serial,
  2167. 'contentEditable': 'true',
  2168. 'ta-bind': 'ta-bind',
  2169. 'ng-model': 'html',
  2170. 'ng-model-options': element.attr('ng-model-options')
  2171. });
  2172. scope.displayElements.scrollWindow.attr({'ng-hide': 'showHtml'});
  2173. if(attrs.taDefaultWrap) scope.displayElements.text.attr('ta-default-wrap', attrs.taDefaultWrap);
  2174. if(attrs.taUnsafeSanitizer){
  2175. scope.displayElements.text.attr('ta-unsafe-sanitizer', attrs.taUnsafeSanitizer);
  2176. scope.displayElements.html.attr('ta-unsafe-sanitizer', attrs.taUnsafeSanitizer);
  2177. }
  2178. // add the main elements to the origional element
  2179. scope.displayElements.scrollWindow.append(scope.displayElements.text);
  2180. element.append(scope.displayElements.scrollWindow);
  2181. element.append(scope.displayElements.html);
  2182. scope.displayElements.forminput.attr('name', scope._name);
  2183. element.append(scope.displayElements.forminput);
  2184. if(attrs.tabindex){
  2185. element.removeAttr('tabindex');
  2186. scope.displayElements.text.attr('tabindex', attrs.tabindex);
  2187. scope.displayElements.html.attr('tabindex', attrs.tabindex);
  2188. }
  2189. if (attrs.placeholder) {
  2190. scope.displayElements.text.attr('placeholder', attrs.placeholder);
  2191. scope.displayElements.html.attr('placeholder', attrs.placeholder);
  2192. }
  2193. if(attrs.taDisabled){
  2194. scope.displayElements.text.attr('ta-readonly', 'disabled');
  2195. scope.displayElements.html.attr('ta-readonly', 'disabled');
  2196. scope.disabled = scope.$parent.$eval(attrs.taDisabled);
  2197. scope.$parent.$watch(attrs.taDisabled, function(newVal){
  2198. scope.disabled = newVal;
  2199. if(scope.disabled){
  2200. element.addClass(scope.classes.disabled);
  2201. }else{
  2202. element.removeClass(scope.classes.disabled);
  2203. }
  2204. });
  2205. }
  2206. if(attrs.taPaste){
  2207. scope._pasteHandler = function(_html){
  2208. return $parse(attrs.taPaste)(scope.$parent, {$html: _html});
  2209. };
  2210. scope.displayElements.text.attr('ta-paste', '_pasteHandler($html)');
  2211. }
  2212. // compile the scope with the text and html elements only - if we do this with the main element it causes a compile loop
  2213. $compile(scope.displayElements.scrollWindow)(scope);
  2214. $compile(scope.displayElements.html)(scope);
  2215. scope.updateTaBindtaTextElement = scope['updateTaBindtaTextElement' + _serial];
  2216. scope.updateTaBindtaHtmlElement = scope['updateTaBindtaHtmlElement' + _serial];
  2217. // add the classes manually last
  2218. element.addClass("ta-root");
  2219. scope.displayElements.scrollWindow.addClass("ta-text ta-editor " + scope.classes.textEditor);
  2220. scope.displayElements.html.addClass("ta-html ta-editor " + scope.classes.htmlEditor);
  2221. // used in the toolbar actions
  2222. scope._actionRunning = false;
  2223. var _savedSelection = false;
  2224. scope.startAction = function(){
  2225. scope._actionRunning = true;
  2226. // if rangy library is loaded return a function to reload the current selection
  2227. _savedSelection = $window.rangy.saveSelection();
  2228. return function(){
  2229. if(_savedSelection) $window.rangy.restoreSelection(_savedSelection);
  2230. };
  2231. };
  2232. scope.endAction = function(){
  2233. scope._actionRunning = false;
  2234. if(_savedSelection){
  2235. if(scope.showHtml){
  2236. scope.displayElements.html[0].focus();
  2237. }else{
  2238. scope.displayElements.text[0].focus();
  2239. }
  2240. // $window.rangy.restoreSelection(_savedSelection);
  2241. $window.rangy.removeMarkers(_savedSelection);
  2242. }
  2243. _savedSelection = false;
  2244. scope.updateSelectedStyles();
  2245. // only update if in text or WYSIWYG mode
  2246. if(!scope.showHtml) scope['updateTaBindtaTextElement' + _serial]();
  2247. };
  2248. // note that focusout > focusin is called everytime we click a button - except bad support: http://www.quirksmode.org/dom/events/blurfocus.html
  2249. // cascades to displayElements.text and displayElements.html automatically.
  2250. _focusin = function(){
  2251. scope.focussed = true;
  2252. element.addClass(scope.classes.focussed);
  2253. _toolbars.focus();
  2254. element.triggerHandler('focus');
  2255. };
  2256. scope.displayElements.html.on('focus', _focusin);
  2257. scope.displayElements.text.on('focus', _focusin);
  2258. _focusout = function(e){
  2259. // if we are NOT runnig an action and have NOT focussed again on the text etc then fire the blur events
  2260. if(!scope._actionRunning && $document[0].activeElement !== scope.displayElements.html[0] && $document[0].activeElement !== scope.displayElements.text[0]){
  2261. element.removeClass(scope.classes.focussed);
  2262. _toolbars.unfocus();
  2263. // to prevent multiple apply error defer to next seems to work.
  2264. $timeout(function(){
  2265. scope._bUpdateSelectedStyles = false;
  2266. element.triggerHandler('blur');
  2267. scope.focussed = false;
  2268. }, 0);
  2269. }
  2270. e.preventDefault();
  2271. return false;
  2272. };
  2273. scope.displayElements.html.on('blur', _focusout);
  2274. scope.displayElements.text.on('blur', _focusout);
  2275. scope.displayElements.text.on('paste', function(event){
  2276. element.triggerHandler('paste', event);
  2277. });
  2278. // Setup the default toolbar tools, this way allows the user to add new tools like plugins.
  2279. // This is on the editor for future proofing if we find a better way to do this.
  2280. scope.queryFormatBlockState = function(command){
  2281. // $document[0].queryCommandValue('formatBlock') errors in Firefox if we call this when focussed on the textarea
  2282. return !scope.showHtml && command.toLowerCase() === $document[0].queryCommandValue('formatBlock').toLowerCase();
  2283. };
  2284. scope.queryCommandState = function(command){
  2285. // $document[0].queryCommandValue('formatBlock') errors in Firefox if we call this when focussed on the textarea
  2286. return (!scope.showHtml) ? $document[0].queryCommandState(command) : '';
  2287. };
  2288. scope.switchView = function(){
  2289. scope.showHtml = !scope.showHtml;
  2290. $animate.enabled(false, scope.displayElements.html);
  2291. $animate.enabled(false, scope.displayElements.text);
  2292. //Show the HTML view
  2293. if(scope.showHtml){
  2294. //defer until the element is visible
  2295. $timeout(function(){
  2296. $animate.enabled(true, scope.displayElements.html);
  2297. $animate.enabled(true, scope.displayElements.text);
  2298. // [0] dereferences the DOM object from the angular.element
  2299. return scope.displayElements.html[0].focus();
  2300. }, 100);
  2301. }else{
  2302. //Show the WYSIWYG view
  2303. //defer until the element is visible
  2304. $timeout(function(){
  2305. $animate.enabled(true, scope.displayElements.html);
  2306. $animate.enabled(true, scope.displayElements.text);
  2307. // [0] dereferences the DOM object from the angular.element
  2308. return scope.displayElements.text[0].focus();
  2309. }, 100);
  2310. }
  2311. };
  2312. // changes to the model variable from outside the html/text inputs
  2313. // if no ngModel, then the only input is from inside text-angular
  2314. if(attrs.ngModel){
  2315. var _firstRun = true;
  2316. ngModel.$render = function(){
  2317. if(_firstRun){
  2318. // we need this firstRun to set the originalContents otherwise it gets overrided by the setting of ngModel to undefined from NaN
  2319. _firstRun = false;
  2320. // if view value is null or undefined initially and there was original content, set to the original content
  2321. var _initialValue = scope.$parent.$eval(attrs.ngModel);
  2322. if((_initialValue === undefined || _initialValue === null) && (_originalContents && _originalContents !== '')){
  2323. // on passing through to taBind it will be sanitised
  2324. ngModel.$setViewValue(_originalContents);
  2325. }
  2326. }
  2327. scope.displayElements.forminput.val(ngModel.$viewValue);
  2328. // if the editors aren't focused they need to be updated, otherwise they are doing the updating
  2329. scope.html = ngModel.$viewValue || '';
  2330. };
  2331. // trigger the validation calls
  2332. if(element.attr('required')) ngModel.$validators.required = function(modelValue, viewValue) {
  2333. var value = modelValue || viewValue;
  2334. return !(!value || value.trim() === '');
  2335. };
  2336. }else{
  2337. // if no ngModel then update from the contents of the origional html.
  2338. scope.displayElements.forminput.val(_originalContents);
  2339. scope.html = _originalContents;
  2340. }
  2341. // changes from taBind back up to here
  2342. scope.$watch('html', function(newValue, oldValue){
  2343. if(newValue !== oldValue){
  2344. if(attrs.ngModel && ngModel.$viewValue !== newValue) ngModel.$setViewValue(newValue);
  2345. scope.displayElements.forminput.val(newValue);
  2346. }
  2347. });
  2348. if(attrs.taTargetToolbars) _toolbars = textAngularManager.registerEditor(scope._name, scope, attrs.taTargetToolbars.split(','));
  2349. else{
  2350. var _toolbar = angular.element('<div text-angular-toolbar name="textAngularToolbar' + _serial + '">');
  2351. // passthrough init of toolbar options
  2352. if(attrs.taToolbar) _toolbar.attr('ta-toolbar', attrs.taToolbar);
  2353. if(attrs.taToolbarClass) _toolbar.attr('ta-toolbar-class', attrs.taToolbarClass);
  2354. if(attrs.taToolbarGroupClass) _toolbar.attr('ta-toolbar-group-class', attrs.taToolbarGroupClass);
  2355. if(attrs.taToolbarButtonClass) _toolbar.attr('ta-toolbar-button-class', attrs.taToolbarButtonClass);
  2356. if(attrs.taToolbarActiveButtonClass) _toolbar.attr('ta-toolbar-active-button-class', attrs.taToolbarActiveButtonClass);
  2357. if(attrs.taFocussedClass) _toolbar.attr('ta-focussed-class', attrs.taFocussedClass);
  2358. element.prepend(_toolbar);
  2359. $compile(_toolbar)(scope.$parent);
  2360. _toolbars = textAngularManager.registerEditor(scope._name, scope, ['textAngularToolbar' + _serial]);
  2361. }
  2362. scope.$on('$destroy', function(){
  2363. textAngularManager.unregisterEditor(scope._name);
  2364. angular.element(window).off('blur');
  2365. });
  2366. // catch element select event and pass to toolbar tools
  2367. scope.$on('ta-element-select', function(event, element){
  2368. if(_toolbars.triggerElementSelect(event, element)){
  2369. scope['reApplyOnSelectorHandlerstaTextElement' + _serial]();
  2370. }
  2371. });
  2372. scope.$on('ta-drop-event', function(event, element, dropEvent, dataTransfer){
  2373. scope.displayElements.text[0].focus();
  2374. if(dataTransfer && dataTransfer.files && dataTransfer.files.length > 0){
  2375. angular.forEach(dataTransfer.files, function(file){
  2376. // taking advantage of boolean execution, if the fileDropHandler returns true, nothing else after it is executed
  2377. // If it is false then execute the defaultFileDropHandler if the fileDropHandler is NOT the default one
  2378. // 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
  2379. try{
  2380. $q.when(scope.fileDropHandler(file, scope.wrapSelection) ||
  2381. (scope.fileDropHandler !== scope.defaultFileDropHandler &&
  2382. $q.when(scope.defaultFileDropHandler(file, scope.wrapSelection)))).then(function(){
  2383. scope['updateTaBindtaTextElement' + _serial]();
  2384. });
  2385. }catch(error){
  2386. $log.error(error);
  2387. }
  2388. });
  2389. dropEvent.preventDefault();
  2390. dropEvent.stopPropagation();
  2391. /* istanbul ignore else, the updates if moved text */
  2392. }else{
  2393. $timeout(function(){
  2394. scope['updateTaBindtaTextElement' + _serial]();
  2395. }, 0);
  2396. }
  2397. });
  2398. // the following is for applying the active states to the tools that support it
  2399. scope._bUpdateSelectedStyles = false;
  2400. /* istanbul ignore next: browser window/tab leave check */
  2401. angular.element(window).on('blur', function(){
  2402. scope._bUpdateSelectedStyles = false;
  2403. scope.focussed = false;
  2404. });
  2405. // loop through all the tools polling their activeState function if it exists
  2406. scope.updateSelectedStyles = function(){
  2407. var _selection;
  2408. /* istanbul ignore next: This check is to ensure multiple timeouts don't exist */
  2409. if(_updateSelectedStylesTimeout) $timeout.cancel(_updateSelectedStylesTimeout);
  2410. // test if the common element ISN'T the root ta-text node
  2411. if((_selection = taSelection.getSelectionElement()) !== undefined && _selection.parentNode !== scope.displayElements.text[0]){
  2412. _toolbars.updateSelectedStyles(angular.element(_selection));
  2413. }else _toolbars.updateSelectedStyles();
  2414. // used to update the active state when a key is held down, ie the left arrow
  2415. /* istanbul ignore else: browser only check */
  2416. if(scope._bUpdateSelectedStyles) _updateSelectedStylesTimeout = $timeout(scope.updateSelectedStyles, 200);
  2417. };
  2418. // start updating on keydown
  2419. _keydown = function(){
  2420. /* istanbul ignore next: ie catch */
  2421. if(!scope.focussed){
  2422. scope._bUpdateSelectedStyles = false;
  2423. return;
  2424. }
  2425. /* istanbul ignore else: don't run if already running */
  2426. if(!scope._bUpdateSelectedStyles){
  2427. scope._bUpdateSelectedStyles = true;
  2428. scope.$apply(function(){
  2429. scope.updateSelectedStyles();
  2430. });
  2431. }
  2432. };
  2433. scope.displayElements.html.on('keydown', _keydown);
  2434. scope.displayElements.text.on('keydown', _keydown);
  2435. // stop updating on key up and update the display/model
  2436. _keyup = function(){
  2437. scope._bUpdateSelectedStyles = false;
  2438. };
  2439. scope.displayElements.html.on('keyup', _keyup);
  2440. scope.displayElements.text.on('keyup', _keyup);
  2441. // stop updating on key up and update the display/model
  2442. _keypress = function(event, eventData){
  2443. /* istanbul ignore else: this is for catching the jqLite testing*/
  2444. if(eventData) angular.extend(event, eventData);
  2445. scope.$apply(function(){
  2446. if(_toolbars.sendKeyCommand(event)){
  2447. /* istanbul ignore else: don't run if already running */
  2448. if(!scope._bUpdateSelectedStyles){
  2449. scope.updateSelectedStyles();
  2450. }
  2451. event.preventDefault();
  2452. return false;
  2453. }
  2454. });
  2455. };
  2456. scope.displayElements.html.on('keypress', _keypress);
  2457. scope.displayElements.text.on('keypress', _keypress);
  2458. // update the toolbar active states when we click somewhere in the text/html boxed
  2459. _mouseup = function(){
  2460. // ensure only one execution of updateSelectedStyles()
  2461. scope._bUpdateSelectedStyles = false;
  2462. scope.$apply(function(){
  2463. scope.updateSelectedStyles();
  2464. });
  2465. };
  2466. scope.displayElements.html.on('mouseup', _mouseup);
  2467. scope.displayElements.text.on('mouseup', _mouseup);
  2468. }
  2469. };
  2470. }
  2471. ]);
  2472. textAngular.service('textAngularManager', ['taToolExecuteAction', 'taTools', 'taRegisterTool', function(taToolExecuteAction, taTools, taRegisterTool){
  2473. // this service is used to manage all textAngular editors and toolbars.
  2474. // All publicly published functions that modify/need to access the toolbar or editor scopes should be in here
  2475. // these contain references to all the editors and toolbars that have been initialised in this app
  2476. var toolbars = {}, editors = {};
  2477. // when we focus into a toolbar, we need to set the TOOLBAR's $parent to be the toolbars it's linked to.
  2478. // We also need to set the tools to be updated to be the toolbars...
  2479. return {
  2480. // register an editor and the toolbars that it is affected by
  2481. registerEditor: function(name, scope, targetToolbars){
  2482. // targetToolbars are optional, we don't require a toolbar to function
  2483. if(!name || name === '') throw('textAngular Error: An editor requires a name');
  2484. if(!scope) throw('textAngular Error: An editor requires a scope');
  2485. if(editors[name]) throw('textAngular Error: An Editor with name "' + name + '" already exists');
  2486. // _toolbars is an ARRAY of toolbar scopes
  2487. var _toolbars = [];
  2488. angular.forEach(targetToolbars, function(_name){
  2489. if(toolbars[_name]) _toolbars.push(toolbars[_name]);
  2490. // if it doesn't exist it may not have been compiled yet and it will be added later
  2491. });
  2492. editors[name] = {
  2493. scope: scope,
  2494. toolbars: targetToolbars,
  2495. _registerToolbar: function(toolbarScope){
  2496. // add to the list late
  2497. if(this.toolbars.indexOf(toolbarScope.name) >= 0) _toolbars.push(toolbarScope);
  2498. },
  2499. // this is a suite of functions the editor should use to update all it's linked toolbars
  2500. editorFunctions: {
  2501. disable: function(){
  2502. // disable all linked toolbars
  2503. angular.forEach(_toolbars, function(toolbarScope){ toolbarScope.disabled = true; });
  2504. },
  2505. enable: function(){
  2506. // enable all linked toolbars
  2507. angular.forEach(_toolbars, function(toolbarScope){ toolbarScope.disabled = false; });
  2508. },
  2509. focus: function(){
  2510. // this should be called when the editor is focussed
  2511. angular.forEach(_toolbars, function(toolbarScope){
  2512. toolbarScope._parent = scope;
  2513. toolbarScope.disabled = false;
  2514. toolbarScope.focussed = true;
  2515. scope.focussed = true;
  2516. });
  2517. },
  2518. unfocus: function(){
  2519. // this should be called when the editor becomes unfocussed
  2520. angular.forEach(_toolbars, function(toolbarScope){
  2521. toolbarScope.disabled = true;
  2522. toolbarScope.focussed = false;
  2523. });
  2524. scope.focussed = false;
  2525. },
  2526. updateSelectedStyles: function(selectedElement){
  2527. // update the active state of all buttons on liked toolbars
  2528. angular.forEach(_toolbars, function(toolbarScope){
  2529. angular.forEach(toolbarScope.tools, function(toolScope){
  2530. if(toolScope.activeState){
  2531. toolbarScope._parent = scope;
  2532. toolScope.active = toolScope.activeState(selectedElement);
  2533. }
  2534. });
  2535. });
  2536. },
  2537. sendKeyCommand: function(event){
  2538. // we return true if we applied an action, false otherwise
  2539. var result = false;
  2540. if(event.ctrlKey || event.metaKey || event.specialKey) angular.forEach(taTools, function(tool, name){
  2541. if(tool.commandKeyCode && (tool.commandKeyCode === event.which || tool.commandKeyCode === event.specialKey)){
  2542. for(var _t = 0; _t < _toolbars.length; _t++){
  2543. if(_toolbars[_t].tools[name] !== undefined){
  2544. taToolExecuteAction.call(_toolbars[_t].tools[name], scope);
  2545. result = true;
  2546. break;
  2547. }
  2548. }
  2549. }
  2550. });
  2551. return result;
  2552. },
  2553. triggerElementSelect: function(event, element){
  2554. // search through the taTools to see if a match for the tag is made.
  2555. // if there is, see if the tool is on a registered toolbar and not disabled.
  2556. // NOTE: This can trigger on MULTIPLE tools simultaneously.
  2557. var elementHasAttrs = function(_element, attrs){
  2558. var result = true;
  2559. for(var i = 0; i < attrs.length; i++) result = result && _element.attr(attrs[i]);
  2560. return result;
  2561. };
  2562. var workerTools = [];
  2563. var unfilteredTools = {};
  2564. var result = false;
  2565. element = angular.element(element);
  2566. // get all valid tools by element name, keep track if one matches the
  2567. var onlyWithAttrsFilter = false;
  2568. angular.forEach(taTools, function(tool, name){
  2569. if(
  2570. tool.onElementSelect &&
  2571. tool.onElementSelect.element &&
  2572. tool.onElementSelect.element.toLowerCase() === element[0].tagName.toLowerCase() &&
  2573. (!tool.onElementSelect.filter || tool.onElementSelect.filter(element))
  2574. ){
  2575. // this should only end up true if the element matches the only attributes
  2576. onlyWithAttrsFilter = onlyWithAttrsFilter ||
  2577. (angular.isArray(tool.onElementSelect.onlyWithAttrs) && elementHasAttrs(element, tool.onElementSelect.onlyWithAttrs));
  2578. if(!tool.onElementSelect.onlyWithAttrs || elementHasAttrs(element, tool.onElementSelect.onlyWithAttrs)) unfilteredTools[name] = tool;
  2579. }
  2580. });
  2581. // if we matched attributes to filter on, then filter, else continue
  2582. if(onlyWithAttrsFilter){
  2583. angular.forEach(unfilteredTools, function(tool, name){
  2584. if(tool.onElementSelect.onlyWithAttrs && elementHasAttrs(element, tool.onElementSelect.onlyWithAttrs)) workerTools.push({'name': name, 'tool': tool});
  2585. });
  2586. // sort most specific (most attrs to find) first
  2587. workerTools.sort(function(a,b){
  2588. return b.tool.onElementSelect.onlyWithAttrs.length - a.tool.onElementSelect.onlyWithAttrs.length;
  2589. });
  2590. }else{
  2591. angular.forEach(unfilteredTools, function(tool, name){
  2592. workerTools.push({'name': name, 'tool': tool});
  2593. });
  2594. }
  2595. // Run the actions on the first visible filtered tool only
  2596. if(workerTools.length > 0){
  2597. for(var _i = 0; _i < workerTools.length; _i++){
  2598. var tool = workerTools[_i].tool;
  2599. var name = workerTools[_i].name;
  2600. for(var _t = 0; _t < _toolbars.length; _t++){
  2601. if(_toolbars[_t].tools[name] !== undefined){
  2602. tool.onElementSelect.action.call(_toolbars[_t].tools[name], event, element, scope);
  2603. result = true;
  2604. break;
  2605. }
  2606. }
  2607. if(result) break;
  2608. }
  2609. }
  2610. return result;
  2611. }
  2612. }
  2613. };
  2614. return editors[name].editorFunctions;
  2615. },
  2616. // retrieve editor by name, largely used by testing suites only
  2617. retrieveEditor: function(name){
  2618. return editors[name];
  2619. },
  2620. unregisterEditor: function(name){
  2621. delete editors[name];
  2622. },
  2623. // registers a toolbar such that it can be linked to editors
  2624. registerToolbar: function(scope){
  2625. if(!scope) throw('textAngular Error: A toolbar requires a scope');
  2626. if(!scope.name || scope.name === '') throw('textAngular Error: A toolbar requires a name');
  2627. if(toolbars[scope.name]) throw('textAngular Error: A toolbar with name "' + scope.name + '" already exists');
  2628. toolbars[scope.name] = scope;
  2629. angular.forEach(editors, function(_editor){
  2630. _editor._registerToolbar(scope);
  2631. });
  2632. },
  2633. // retrieve toolbar by name, largely used by testing suites only
  2634. retrieveToolbar: function(name){
  2635. return toolbars[name];
  2636. },
  2637. // retrieve toolbars by editor name, largely used by testing suites only
  2638. retrieveToolbarsViaEditor: function(name){
  2639. var result = [], _this = this;
  2640. angular.forEach(this.retrieveEditor(name).toolbars, function(name){
  2641. result.push(_this.retrieveToolbar(name));
  2642. });
  2643. return result;
  2644. },
  2645. unregisterToolbar: function(name){
  2646. delete toolbars[name];
  2647. },
  2648. // functions for updating the toolbar buttons display
  2649. updateToolsDisplay: function(newTaTools){
  2650. // pass a partial struct of the taTools, this allows us to update the tools on the fly, will not change the defaults.
  2651. var _this = this;
  2652. angular.forEach(newTaTools, function(_newTool, key){
  2653. _this.updateToolDisplay(key, _newTool);
  2654. });
  2655. },
  2656. // this function resets all toolbars to their default tool definitions
  2657. resetToolsDisplay: function(){
  2658. var _this = this;
  2659. angular.forEach(taTools, function(_newTool, key){
  2660. _this.resetToolDisplay(key);
  2661. });
  2662. },
  2663. // update a tool on all toolbars
  2664. updateToolDisplay: function(toolKey, _newTool){
  2665. var _this = this;
  2666. angular.forEach(toolbars, function(toolbarScope, toolbarKey){
  2667. _this.updateToolbarToolDisplay(toolbarKey, toolKey, _newTool);
  2668. });
  2669. },
  2670. // resets a tool to the default/starting state on all toolbars
  2671. resetToolDisplay: function(toolKey){
  2672. var _this = this;
  2673. angular.forEach(toolbars, function(toolbarScope, toolbarKey){
  2674. _this.resetToolbarToolDisplay(toolbarKey, toolKey);
  2675. });
  2676. },
  2677. // update a tool on a specific toolbar
  2678. updateToolbarToolDisplay: function(toolbarKey, toolKey, _newTool){
  2679. if(toolbars[toolbarKey]) toolbars[toolbarKey].updateToolDisplay(toolKey, _newTool);
  2680. else throw('textAngular Error: No Toolbar with name "' + toolbarKey + '" exists');
  2681. },
  2682. // reset a tool on a specific toolbar to it's default starting value
  2683. resetToolbarToolDisplay: function(toolbarKey, toolKey){
  2684. if(toolbars[toolbarKey]) toolbars[toolbarKey].updateToolDisplay(toolKey, taTools[toolKey], true);
  2685. else throw('textAngular Error: No Toolbar with name "' + toolbarKey + '" exists');
  2686. },
  2687. // removes a tool from all toolbars and it's definition
  2688. removeTool: function(toolKey){
  2689. delete taTools[toolKey];
  2690. angular.forEach(toolbars, function(toolbarScope){
  2691. delete toolbarScope.tools[toolKey];
  2692. for(var i = 0; i < toolbarScope.toolbar.length; i++){
  2693. var toolbarIndex;
  2694. for(var j = 0; j < toolbarScope.toolbar[i].length; j++){
  2695. if(toolbarScope.toolbar[i][j] === toolKey){
  2696. toolbarIndex = {
  2697. group: i,
  2698. index: j
  2699. };
  2700. break;
  2701. }
  2702. if(toolbarIndex !== undefined) break;
  2703. }
  2704. if(toolbarIndex !== undefined){
  2705. toolbarScope.toolbar[toolbarIndex.group].slice(toolbarIndex.index, 1);
  2706. toolbarScope._$element.children().eq(toolbarIndex.group).children().eq(toolbarIndex.index).remove();
  2707. }
  2708. }
  2709. });
  2710. },
  2711. // toolkey, toolDefinition are required. If group is not specified will pick the last group, if index isnt defined will append to group
  2712. addTool: function(toolKey, toolDefinition, group, index){
  2713. taRegisterTool(toolKey, toolDefinition);
  2714. angular.forEach(toolbars, function(toolbarScope){
  2715. toolbarScope.addTool(toolKey, toolDefinition, group, index);
  2716. });
  2717. },
  2718. // adds a Tool but only to one toolbar not all
  2719. addToolToToolbar: function(toolKey, toolDefinition, toolbarKey, group, index){
  2720. taRegisterTool(toolKey, toolDefinition);
  2721. toolbars[toolbarKey].addTool(toolKey, toolDefinition, group, index);
  2722. },
  2723. // this is used when externally the html of an editor has been changed and textAngular needs to be notified to update the model.
  2724. // this will call a $digest if not already happening
  2725. refreshEditor: function(name){
  2726. if(editors[name]){
  2727. editors[name].scope.updateTaBindtaTextElement();
  2728. /* istanbul ignore else: phase catch */
  2729. if(!editors[name].scope.$$phase) editors[name].scope.$digest();
  2730. }else throw('textAngular Error: No Editor with name "' + name + '" exists');
  2731. },
  2732. // this is used by taBind to send a key command in response to a special key event
  2733. sendKeyCommand: function(scope, event){
  2734. angular.forEach(editors, function(_editor){
  2735. /* istanbul ignore else: if nothing to do, do nothing */
  2736. if (_editor.editorFunctions.sendKeyCommand(event)){
  2737. /* istanbul ignore else: don't run if already running */
  2738. if(!scope._bUpdateSelectedStyles){
  2739. scope.updateSelectedStyles();
  2740. }
  2741. event.preventDefault();
  2742. return false;
  2743. }
  2744. });
  2745. }
  2746. };
  2747. }]);
  2748. textAngular.directive('textAngularToolbar', [
  2749. '$compile', 'textAngularManager', 'taOptions', 'taTools', 'taToolExecuteAction', '$window',
  2750. function($compile, textAngularManager, taOptions, taTools, taToolExecuteAction, $window){
  2751. return {
  2752. scope: {
  2753. name: '@' // a name IS required
  2754. },
  2755. restrict: "EA",
  2756. link: function(scope, element, attrs){
  2757. if(!scope.name || scope.name === '') throw('textAngular Error: A toolbar requires a name');
  2758. angular.extend(scope, angular.copy(taOptions));
  2759. if(attrs.taToolbar) scope.toolbar = scope.$parent.$eval(attrs.taToolbar);
  2760. if(attrs.taToolbarClass) scope.classes.toolbar = attrs.taToolbarClass;
  2761. if(attrs.taToolbarGroupClass) scope.classes.toolbarGroup = attrs.taToolbarGroupClass;
  2762. if(attrs.taToolbarButtonClass) scope.classes.toolbarButton = attrs.taToolbarButtonClass;
  2763. if(attrs.taToolbarActiveButtonClass) scope.classes.toolbarButtonActive = attrs.taToolbarActiveButtonClass;
  2764. if(attrs.taFocussedClass) scope.classes.focussed = attrs.taFocussedClass;
  2765. scope.disabled = true;
  2766. scope.focussed = false;
  2767. scope._$element = element;
  2768. element[0].innerHTML = '';
  2769. element.addClass("ta-toolbar " + scope.classes.toolbar);
  2770. scope.$watch('focussed', function(){
  2771. if(scope.focussed) element.addClass(scope.classes.focussed);
  2772. else element.removeClass(scope.classes.focussed);
  2773. });
  2774. var setupToolElement = function(toolDefinition, toolScope){
  2775. var toolElement;
  2776. if(toolDefinition && toolDefinition.display){
  2777. toolElement = angular.element(toolDefinition.display);
  2778. }
  2779. else toolElement = angular.element("<button type='button'>");
  2780. if(toolDefinition && toolDefinition["class"]) toolElement.addClass(toolDefinition["class"]);
  2781. else toolElement.addClass(scope.classes.toolbarButton);
  2782. toolElement.attr('name', toolScope.name);
  2783. // important to not take focus from the main text/html entry
  2784. toolElement.attr('ta-button', 'ta-button');
  2785. toolElement.attr('ng-disabled', 'isDisabled()');
  2786. toolElement.attr('tabindex', '-1');
  2787. toolElement.attr('ng-click', 'executeAction()');
  2788. toolElement.attr('ng-class', 'displayActiveToolClass(active)');
  2789. if (toolDefinition && toolDefinition.tooltiptext) {
  2790. toolElement.attr('title', toolDefinition.tooltiptext);
  2791. }
  2792. if(toolDefinition && !toolDefinition.display && !toolScope._display){
  2793. // first clear out the current contents if any
  2794. toolElement[0].innerHTML = '';
  2795. // add the buttonText
  2796. if(toolDefinition.buttontext) toolElement[0].innerHTML = toolDefinition.buttontext;
  2797. // add the icon to the front of the button if there is content
  2798. if(toolDefinition.iconclass){
  2799. var icon = angular.element('<i>'), content = toolElement[0].innerHTML;
  2800. icon.addClass(toolDefinition.iconclass);
  2801. toolElement[0].innerHTML = '';
  2802. toolElement.append(icon);
  2803. if(content && content !== '') toolElement.append('&nbsp;' + content);
  2804. }
  2805. }
  2806. toolScope._lastToolDefinition = angular.copy(toolDefinition);
  2807. return $compile(toolElement)(toolScope);
  2808. };
  2809. // Keep a reference for updating the active states later
  2810. scope.tools = {};
  2811. // create the tools in the toolbar
  2812. // default functions and values to prevent errors in testing and on init
  2813. scope._parent = {
  2814. disabled: true,
  2815. showHtml: false,
  2816. queryFormatBlockState: function(){ return false; },
  2817. queryCommandState: function(){ return false; }
  2818. };
  2819. var defaultChildScope = {
  2820. $window: $window,
  2821. $editor: function(){
  2822. // dynamically gets the editor as it is set
  2823. return scope._parent;
  2824. },
  2825. isDisabled: function(){
  2826. // to set your own disabled logic set a function or boolean on the tool called 'disabled'
  2827. return ( // this bracket is important as without it it just returns the first bracket and ignores the rest
  2828. // when the button's disabled function/value evaluates to true
  2829. (typeof this.$eval('disabled') !== 'function' && this.$eval('disabled')) || this.$eval('disabled()') ||
  2830. // all buttons except the HTML Switch button should be disabled in the showHtml (RAW html) mode
  2831. (this.name !== 'html' && this.$editor().showHtml) ||
  2832. // if the toolbar is disabled
  2833. this.$parent.disabled ||
  2834. // if the current editor is disabled
  2835. this.$editor().disabled
  2836. );
  2837. },
  2838. displayActiveToolClass: function(active){
  2839. return (active)? scope.classes.toolbarButtonActive : '';
  2840. },
  2841. executeAction: taToolExecuteAction
  2842. };
  2843. angular.forEach(scope.toolbar, function(group){
  2844. // setup the toolbar group
  2845. var groupElement = angular.element("<div>");
  2846. groupElement.addClass(scope.classes.toolbarGroup);
  2847. angular.forEach(group, function(tool){
  2848. // init and add the tools to the group
  2849. // a tool name (key name from taTools struct)
  2850. //creates a child scope of the main angularText scope and then extends the childScope with the functions of this particular tool
  2851. // reference to the scope and element kept
  2852. scope.tools[tool] = angular.extend(scope.$new(true), taTools[tool], defaultChildScope, {name: tool});
  2853. scope.tools[tool].$element = setupToolElement(taTools[tool], scope.tools[tool]);
  2854. // append the tool compiled with the childScope to the group element
  2855. groupElement.append(scope.tools[tool].$element);
  2856. });
  2857. // append the group to the toolbar
  2858. element.append(groupElement);
  2859. });
  2860. // update a tool
  2861. // if a value is set to null, remove from the display
  2862. // when forceNew is set to true it will ignore all previous settings, used to reset to taTools definition
  2863. // to reset to defaults pass in taTools[key] as _newTool and forceNew as true, ie `updateToolDisplay(key, taTools[key], true);`
  2864. scope.updateToolDisplay = function(key, _newTool, forceNew){
  2865. var toolInstance = scope.tools[key];
  2866. if(toolInstance){
  2867. // get the last toolDefinition, then override with the new definition
  2868. if(toolInstance._lastToolDefinition && !forceNew) _newTool = angular.extend({}, toolInstance._lastToolDefinition, _newTool);
  2869. if(_newTool.buttontext === null && _newTool.iconclass === null && _newTool.display === null)
  2870. throw('textAngular Error: Tool Definition for updating "' + key + '" does not have a valid display/iconclass/buttontext value');
  2871. // if tool is defined on this toolbar, update/redo the tool
  2872. if(_newTool.buttontext === null){
  2873. delete _newTool.buttontext;
  2874. }
  2875. if(_newTool.iconclass === null){
  2876. delete _newTool.iconclass;
  2877. }
  2878. if(_newTool.display === null){
  2879. delete _newTool.display;
  2880. }
  2881. var toolElement = setupToolElement(_newTool, toolInstance);
  2882. toolInstance.$element.replaceWith(toolElement);
  2883. toolInstance.$element = toolElement;
  2884. }
  2885. };
  2886. // we assume here that all values passed are valid and correct
  2887. scope.addTool = function(key, _newTool, groupIndex, index){
  2888. scope.tools[key] = angular.extend(scope.$new(true), taTools[key], defaultChildScope, {name: key});
  2889. scope.tools[key].$element = setupToolElement(taTools[key], scope.tools[key]);
  2890. var group;
  2891. if(groupIndex === undefined) groupIndex = scope.toolbar.length - 1;
  2892. group = angular.element(element.children()[groupIndex]);
  2893. if(index === undefined){
  2894. group.append(scope.tools[key].$element);
  2895. scope.toolbar[groupIndex][scope.toolbar[groupIndex].length - 1] = key;
  2896. }else{
  2897. group.children().eq(index).after(scope.tools[key].$element);
  2898. scope.toolbar[groupIndex][index] = key;
  2899. }
  2900. };
  2901. textAngularManager.registerToolbar(scope);
  2902. scope.$on('$destroy', function(){
  2903. textAngularManager.unregisterToolbar(scope.name);
  2904. });
  2905. }
  2906. };
  2907. }
  2908. ]);
  2909. })();