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