textAngular.js 126 KB

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