| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465 |
- /**
- * Formatter.js
- *
- * Copyright, Moxiecode Systems AB
- * Released under LGPL License.
- *
- * License: http://www.tinymce.com/license
- * Contributing: http://www.tinymce.com/contributing
- */
- /**
- * Text formatter engine class. This class is used to apply formats like bold, italic, font size
- * etc to the current selection or specific nodes. This engine was build to replace the browsers
- * default formatting logic for execCommand due to it's inconsistent and buggy behavior.
- *
- * @class tinymce.Formatter
- * @example
- * tinymce.activeEditor.formatter.register('mycustomformat', {
- * inline: 'span',
- * styles: {color: '#ff0000'}
- * });
- *
- * tinymce.activeEditor.formatter.apply('mycustomformat');
- */
- define("tinymce/Formatter", [
- "tinymce/dom/TreeWalker",
- "tinymce/dom/RangeUtils",
- "tinymce/util/Tools",
- "tinymce/fmt/Preview"
- ], function(TreeWalker, RangeUtils, Tools, Preview) {
- /**
- * Constructs a new formatter instance.
- *
- * @constructor Formatter
- * @param {tinymce.Editor} ed Editor instance to construct the formatter engine to.
- */
- return function(ed) {
- var formats = {},
- dom = ed.dom,
- selection = ed.selection,
- rangeUtils = new RangeUtils(dom),
- isValid = ed.schema.isValidChild,
- isBlock = dom.isBlock,
- forcedRootBlock = ed.settings.forced_root_block,
- nodeIndex = dom.nodeIndex,
- INVISIBLE_CHAR = '\uFEFF',
- MCE_ATTR_RE = /^(src|href|style)$/,
- FALSE = false,
- TRUE = true,
- formatChangeData,
- undef,
- getContentEditable = dom.getContentEditable,
- disableCaretContainer,
- markCaretContainersBogus;
- var each = Tools.each,
- grep = Tools.grep,
- walk = Tools.walk,
- extend = Tools.extend;
- function isTextBlock(name) {
- if (name.nodeType) {
- name = name.nodeName;
- }
- return !!ed.schema.getTextBlockElements()[name.toLowerCase()];
- }
- function getParents(node, selector) {
- return dom.getParents(node, selector, dom.getRoot());
- }
- function isCaretNode(node) {
- return node.nodeType === 1 && node.id === '_mce_caret';
- }
- function defaultFormats() {
- register({
-
- valigntop: [
- {selector: 'td,th', styles: {'verticalAlign': 'top'}}
- ],
- valignmiddle: [
- {selector: 'td,th', styles: {'verticalAlign': 'middle'}}
- ],
- valignbottom: [
- {selector: 'td,th', styles: {'verticalAlign': 'bottom'}}
- ],
- alignleft: [
- {selector: 'figure,p,h1,h2,h3,h4,h5,h6,td,th,tr,div,ul,ol,li', styles: {textAlign: 'left'}, defaultBlock: 'div'},
- {selector: 'img,table', collapsed: false, styles: {'float': 'left'}}
- ],
- aligncenter: [
- {selector: 'figure,p,h1,h2,h3,h4,h5,h6,td,th,tr,div,ul,ol,li', styles: {textAlign: 'center'}, defaultBlock: 'div'},
- {selector: 'img', collapsed: false, styles: {display: 'block', marginLeft: 'auto', marginRight: 'auto'}},
- {selector: 'table', collapsed: false, styles: {marginLeft: 'auto', marginRight: 'auto'}}
- ],
- alignright: [
- {selector: 'figure,p,h1,h2,h3,h4,h5,h6,td,th,tr,div,ul,ol,li', styles: {textAlign: 'right'}, defaultBlock: 'div'},
- {selector: 'img,table', collapsed: false, styles: {'float': 'right'}}
- ],
- alignjustify: [
- {selector: 'figure,p,h1,h2,h3,h4,h5,h6,td,th,tr,div,ul,ol,li', styles: {textAlign: 'justify'}, defaultBlock: 'div'}
- ],
- bold: [
- {inline: 'strong', remove: 'all'},
- {inline: 'span', styles: {fontWeight: 'bold'}},
- {inline: 'b', remove: 'all'}
- ],
- italic: [
- {inline: 'em', remove: 'all'},
- {inline: 'span', styles: {fontStyle: 'italic'}},
- {inline: 'i', remove: 'all'}
- ],
- underline: [
- {inline: 'span', styles: {textDecoration: 'underline'}, exact: true},
- {inline: 'u', remove: 'all'}
- ],
- strikethrough: [
- {inline: 'span', styles: {textDecoration: 'line-through'}, exact: true},
- {inline: 'strike', remove: 'all'}
- ],
- forecolor: {inline: 'span', styles: {color: '%value'}, wrap_links: false},
- hilitecolor: {inline: 'span', styles: {backgroundColor: '%value'}, wrap_links: false},
- fontname: {inline: 'span', styles: {fontFamily: '%value'}},
- fontsize: {inline: 'span', styles: {fontSize: '%value'}},
- fontsize_class: {inline: 'span', attributes: {'class': '%value'}},
- blockquote: {block: 'blockquote', wrapper: 1, remove: 'all'},
- subscript: {inline: 'sub'},
- superscript: {inline: 'sup'},
- code: {inline: 'code'},
- link: {inline: 'a', selector: 'a', remove: 'all', split: true, deep: true,
- onmatch: function() {
- return true;
- },
- onformat: function(elm, fmt, vars) {
- each(vars, function(value, key) {
- dom.setAttrib(elm, key, value);
- });
- }
- },
- removeformat: [
- {
- selector: 'b,strong,em,i,font,u,strike,sub,sup,dfn,code,samp,kbd,var,cite,mark,q',
- remove: 'all',
- split: true,
- expand: false,
- block_expand: true,
- deep: true
- },
- {selector: 'span', attributes: ['style', 'class'], remove: 'empty', split: true, expand: false, deep: true},
- {selector: '*', attributes: ['style', 'class'], split: false, expand: false, deep: true}
- ]
- });
- // Register default block formats
- each('p h1 h2 h3 h4 h5 h6 div address pre div dt dd samp'.split(/\s/), function(name) {
- register(name, {block: name, remove: 'all'});
- });
- // Register user defined formats
- register(ed.settings.formats);
- }
- function addKeyboardShortcuts() {
- // Add some inline shortcuts
- ed.addShortcut('ctrl+b', 'bold_desc', 'Bold');
- ed.addShortcut('ctrl+i', 'italic_desc', 'Italic');
- ed.addShortcut('ctrl+u', 'underline_desc', 'Underline');
- // BlockFormat shortcuts keys
- for (var i = 1; i <= 6; i++) {
- ed.addShortcut('ctrl+' + i, '', ['FormatBlock', false, 'h' + i]);
- }
- ed.addShortcut('ctrl+7', '', ['FormatBlock', false, 'p']);
- ed.addShortcut('ctrl+8', '', ['FormatBlock', false, 'div']);
- ed.addShortcut('ctrl+9', '', ['FormatBlock', false, 'address']);
- }
- // Public functions
- /**
- * Returns the format by name or all formats if no name is specified.
- *
- * @method get
- * @param {String} name Optional name to retrive by.
- * @return {Array/Object} Array/Object with all registred formats or a specific format.
- */
- function get(name) {
- return name ? formats[name] : formats;
- }
- /**
- * Registers a specific format by name.
- *
- * @method register
- * @param {Object/String} name Name of the format for example "bold".
- * @param {Object/Array} format Optional format object or array of format variants
- * can only be omitted if the first arg is an object.
- */
- function register(name, format) {
- if (name) {
- if (typeof(name) !== 'string') {
- each(name, function(format, name) {
- register(name, format);
- });
- } else {
- // Force format into array and add it to internal collection
- format = format.length ? format : [format];
- each(format, function(format) {
- // Set deep to false by default on selector formats this to avoid removing
- // alignment on images inside paragraphs when alignment is changed on paragraphs
- if (format.deep === undef) {
- format.deep = !format.selector;
- }
- // Default to true
- if (format.split === undef) {
- format.split = !format.selector || format.inline;
- }
- // Default to true
- if (format.remove === undef && format.selector && !format.inline) {
- format.remove = 'none';
- }
- // Mark format as a mixed format inline + block level
- if (format.selector && format.inline) {
- format.mixed = true;
- format.block_expand = true;
- }
- // Split classes if needed
- if (typeof(format.classes) === 'string') {
- format.classes = format.classes.split(/\s+/);
- }
- });
- formats[name] = format;
- }
- }
- }
- function getTextDecoration(node) {
- var decoration;
- ed.dom.getParent(node, function(n) {
- decoration = ed.dom.getStyle(n, 'text-decoration');
- return decoration && decoration !== 'none';
- });
- return decoration;
- }
- function processUnderlineAndColor(node) {
- var textDecoration;
- if (node.nodeType === 1 && node.parentNode && node.parentNode.nodeType === 1) {
- textDecoration = getTextDecoration(node.parentNode);
- if (ed.dom.getStyle(node, 'color') && textDecoration) {
- ed.dom.setStyle(node, 'text-decoration', textDecoration);
- } else if (ed.dom.getStyle(node, 'textdecoration') === textDecoration) {
- ed.dom.setStyle(node, 'text-decoration', null);
- }
- }
- }
- /**
- * Applies the specified format to the current selection or specified node.
- *
- * @method apply
- * @param {String} name Name of format to apply.
- * @param {Object} vars Optional list of variables to replace within format before applying it.
- * @param {Node} node Optional node to apply the format to defaults to current selection.
- */
- function apply(name, vars, node) {
- var formatList = get(name), format = formatList[0], bookmark, rng, isCollapsed = !node && selection.isCollapsed();
- function setElementFormat(elm, fmt) {
- fmt = fmt || format;
- if (elm) {
- if (fmt.onformat) {
- fmt.onformat(elm, fmt, vars, node);
- }
- each(fmt.styles, function(value, name) {
- dom.setStyle(elm, name, replaceVars(value, vars));
- });
- // Needed for the WebKit span spam bug
- // TODO: Remove this once WebKit/Blink fixes this
- if (fmt.styles) {
- var styleVal = dom.getAttrib(elm, 'style');
- if (styleVal) {
- elm.setAttribute('data-mce-style', styleVal);
- }
- }
- each(fmt.attributes, function(value, name) {
- dom.setAttrib(elm, name, replaceVars(value, vars));
- });
- each(fmt.classes, function(value) {
- value = replaceVars(value, vars);
- if (!dom.hasClass(elm, value)) {
- dom.addClass(elm, value);
- }
- });
- }
- }
- function adjustSelectionToVisibleSelection() {
- function findSelectionEnd(start, end) {
- var walker = new TreeWalker(end);
- for (node = walker.current(); node; node = walker.prev()) {
- if (node.childNodes.length > 1 || node == start || node.tagName == 'BR') {
- return node;
- }
- }
- }
- // Adjust selection so that a end container with a end offset of zero is not included in the selection
- // as this isn't visible to the user.
- var rng = ed.selection.getRng();
- var start = rng.startContainer;
- var end = rng.endContainer;
- if (start != end && rng.endOffset === 0) {
- var newEnd = findSelectionEnd(start, end);
- var endOffset = newEnd.nodeType == 3 ? newEnd.length : newEnd.childNodes.length;
- rng.setEnd(newEnd, endOffset);
- }
- return rng;
- }
- function applyStyleToList(node, bookmark, wrapElm, newWrappers, process){
- var nodes = [], listIndex = -1, list, startIndex = -1, endIndex = -1, currentWrapElm;
- // find the index of the first child list.
- each(node.childNodes, function(n, index) {
- if (n.nodeName === "UL" || n.nodeName === "OL") {
- listIndex = index;
- list = n;
- return false;
- }
- });
- // get the index of the bookmarks
- each(node.childNodes, function(n, index) {
- if (n.nodeName === "SPAN" && dom.getAttrib(n, "data-mce-type") == "bookmark") {
- if (n.id == bookmark.id + "_start") {
- startIndex = index;
- } else if (n.id == bookmark.id + "_end") {
- endIndex = index;
- }
- }
- });
- // if the selection spans across an embedded list, or there isn't an embedded list - handle processing normally
- if (listIndex <= 0 || (startIndex < listIndex && endIndex > listIndex)) {
- each(grep(node.childNodes), process);
- return 0;
- } else {
- currentWrapElm = dom.clone(wrapElm, FALSE);
- // create a list of the nodes on the same side of the list as the selection
- each(grep(node.childNodes), function(n, index) {
- if ((startIndex < listIndex && index < listIndex) || (startIndex > listIndex && index > listIndex)) {
- nodes.push(n);
- n.parentNode.removeChild(n);
- }
- });
- // insert the wrapping element either before or after the list.
- if (startIndex < listIndex) {
- node.insertBefore(currentWrapElm, list);
- } else if (startIndex > listIndex) {
- node.insertBefore(currentWrapElm, list.nextSibling);
- }
- // add the new nodes to the list.
- newWrappers.push(currentWrapElm);
- each(nodes, function(node) {
- currentWrapElm.appendChild(node);
- });
- return currentWrapElm;
- }
- }
- function applyRngStyle(rng, bookmark, node_specific) {
- var newWrappers = [], wrapName, wrapElm, contentEditable = true;
- // Setup wrapper element
- wrapName = format.inline || format.block;
- wrapElm = dom.create(wrapName);
- setElementFormat(wrapElm);
- rangeUtils.walk(rng, function(nodes) {
- var currentWrapElm;
- /**
- * Process a list of nodes wrap them.
- */
- function process(node) {
- var nodeName, parentName, found, hasContentEditableState, lastContentEditable;
- lastContentEditable = contentEditable;
- nodeName = node.nodeName.toLowerCase();
- parentName = node.parentNode.nodeName.toLowerCase();
- // Node has a contentEditable value
- if (node.nodeType === 1 && getContentEditable(node)) {
- lastContentEditable = contentEditable;
- contentEditable = getContentEditable(node) === "true";
- hasContentEditableState = true; // We don't want to wrap the container only it's children
- }
- // Stop wrapping on br elements
- if (isEq(nodeName, 'br')) {
- currentWrapElm = 0;
- // Remove any br elements when we wrap things
- if (format.block) {
- dom.remove(node);
- }
- return;
- }
- // If node is wrapper type
- if (format.wrapper && matchNode(node, name, vars)) {
- currentWrapElm = 0;
- return;
- }
- // Can we rename the block
- // TODO: Break this if up, too complex
- if (contentEditable && !hasContentEditableState && format.block &&
- !format.wrapper && isTextBlock(nodeName) && isValid(parentName, wrapName)) {
- node = dom.rename(node, wrapName);
- setElementFormat(node);
- newWrappers.push(node);
- currentWrapElm = 0;
- return;
- }
- // Handle selector patterns
- if (format.selector) {
- // Look for matching formats
- each(formatList, function(format) {
- // Check collapsed state if it exists
- if ('collapsed' in format && format.collapsed !== isCollapsed) {
- return;
- }
- if (dom.is(node, format.selector) && !isCaretNode(node)) {
- setElementFormat(node, format);
- found = true;
- }
- });
- // Continue processing if a selector match wasn't found and a inline element is defined
- if (!format.inline || found) {
- currentWrapElm = 0;
- return;
- }
- }
- // Is it valid to wrap this item
- // TODO: Break this if up, too complex
- if (contentEditable && !hasContentEditableState && isValid(wrapName, nodeName) && isValid(parentName, wrapName) &&
- !(!node_specific && node.nodeType === 3 &&
- node.nodeValue.length === 1 &&
- node.nodeValue.charCodeAt(0) === 65279) &&
- !isCaretNode(node) &&
- (!format.inline || !isBlock(node))) {
- // Start wrapping
- if (!currentWrapElm) {
- // Wrap the node
- currentWrapElm = dom.clone(wrapElm, FALSE);
- node.parentNode.insertBefore(currentWrapElm, node);
- newWrappers.push(currentWrapElm);
- }
- currentWrapElm.appendChild(node);
- } else if (nodeName == 'li' && bookmark) {
- // Start wrapping - if we are in a list node and have a bookmark, then
- // we will always begin by wrapping in a new element.
- currentWrapElm = applyStyleToList(node, bookmark, wrapElm, newWrappers, process);
- } else {
- // Start a new wrapper for possible children
- currentWrapElm = 0;
- each(grep(node.childNodes), process);
- if (hasContentEditableState) {
- contentEditable = lastContentEditable; // Restore last contentEditable state from stack
- }
- // End the last wrapper
- currentWrapElm = 0;
- }
- }
- // Process siblings from range
- each(nodes, process);
- });
- // Wrap links inside as well, for example color inside a link when the wrapper is around the link
- if (format.wrap_links === false) {
- each(newWrappers, function(node) {
- function process(node) {
- var i, currentWrapElm, children;
- if (node.nodeName === 'A') {
- currentWrapElm = dom.clone(wrapElm, FALSE);
- newWrappers.push(currentWrapElm);
- children = grep(node.childNodes);
- for (i = 0; i < children.length; i++) {
- currentWrapElm.appendChild(children[i]);
- }
- node.appendChild(currentWrapElm);
- }
- each(grep(node.childNodes), process);
- }
- process(node);
- });
- }
- // Cleanup
- each(newWrappers, function(node) {
- var childCount;
- function getChildCount(node) {
- var count = 0;
- each(node.childNodes, function(node) {
- if (!isWhiteSpaceNode(node) && !isBookmarkNode(node)) {
- count++;
- }
- });
- return count;
- }
- function mergeStyles(node) {
- var child, clone;
- each(node.childNodes, function(node) {
- if (node.nodeType == 1 && !isBookmarkNode(node) && !isCaretNode(node)) {
- child = node;
- return FALSE; // break loop
- }
- });
- // If child was found and of the same type as the current node
- if (child && !isBookmarkNode(child) && matchName(child, format)) {
- clone = dom.clone(child, FALSE);
- setElementFormat(clone);
- dom.replace(clone, node, TRUE);
- dom.remove(child, 1);
- }
- return clone || node;
- }
- childCount = getChildCount(node);
- // Remove empty nodes but only if there is multiple wrappers and they are not block
- // elements so never remove single <h1></h1> since that would remove the
- // currrent empty block element where the caret is at
- if ((newWrappers.length > 1 || !isBlock(node)) && childCount === 0) {
- dom.remove(node, 1);
- return;
- }
- if (format.inline || format.wrapper) {
- // Merges the current node with it's children of similar type to reduce the number of elements
- if (!format.exact && childCount === 1) {
- node = mergeStyles(node);
- }
- // Remove/merge children
- each(formatList, function(format) {
- // Merge all children of similar type will move styles from child to parent
- // this: <span style="color:red"><b><span style="color:red; font-size:10px">text</span></b></span>
- // will become: <span style="color:red"><b><span style="font-size:10px">text</span></b></span>
- each(dom.select(format.inline, node), function(child) {
- var parent;
- if (isBookmarkNode(child)) {
- return;
- }
- // When wrap_links is set to false we don't want
- // to remove the format on children within links
- if (format.wrap_links === false) {
- parent = child.parentNode;
- do {
- if (parent.nodeName === 'A') {
- return;
- }
- } while ((parent = parent.parentNode));
- }
- removeFormat(format, vars, child, format.exact ? child : null);
- });
- });
- // Remove child if direct parent is of same type
- if (matchNode(node.parentNode, name, vars)) {
- dom.remove(node, 1);
- node = 0;
- return TRUE;
- }
- // Look for parent with similar style format
- if (format.merge_with_parents) {
- dom.getParent(node.parentNode, function(parent) {
- if (matchNode(parent, name, vars)) {
- dom.remove(node, 1);
- node = 0;
- return TRUE;
- }
- });
- }
- // Merge next and previous siblings if they are similar <b>text</b><b>text</b> becomes <b>texttext</b>
- if (node && format.merge_siblings !== false) {
- node = mergeSiblings(getNonWhiteSpaceSibling(node), node);
- node = mergeSiblings(node, getNonWhiteSpaceSibling(node, TRUE));
- }
- }
- });
- }
- if (format) {
- if (node) {
- if (node.nodeType) {
- rng = dom.createRng();
- rng.setStartBefore(node);
- rng.setEndAfter(node);
- applyRngStyle(expandRng(rng, formatList), null, true);
- } else {
- applyRngStyle(node, null, true);
- }
- } else {
- if (!isCollapsed || !format.inline || dom.select('td.mce-item-selected,th.mce-item-selected').length) {
- // Obtain selection node before selection is unselected by applyRngStyle()
- var curSelNode = ed.selection.getNode();
- // If the formats have a default block and we can't find a parent block then
- // start wrapping it with a DIV this is for forced_root_blocks: false
- // It's kind of a hack but people should be using the default block type P since all desktop editors work that way
- if (!forcedRootBlock && formatList[0].defaultBlock && !dom.getParent(curSelNode, dom.isBlock)) {
- apply(formatList[0].defaultBlock);
- }
- // Apply formatting to selection
- ed.selection.setRng(adjustSelectionToVisibleSelection());
- bookmark = selection.getBookmark();
- applyRngStyle(expandRng(selection.getRng(TRUE), formatList), bookmark);
- // Colored nodes should be underlined so that the color of the underline matches the text color.
- if (format.styles && (format.styles.color || format.styles.textDecoration)) {
- walk(curSelNode, processUnderlineAndColor, 'childNodes');
- processUnderlineAndColor(curSelNode);
- }
- selection.moveToBookmark(bookmark);
- moveStart(selection.getRng(TRUE));
- ed.nodeChanged();
- } else {
- performCaretAction('apply', name, vars);
- }
- }
- }
- }
- /**
- * Removes the specified format from the current selection or specified node.
- *
- * @method remove
- * @param {String} name Name of format to remove.
- * @param {Object} vars Optional list of variables to replace within format before removing it.
- * @param {Node/Range} node Optional node or DOM range to remove the format from defaults to current selection.
- */
- function remove(name, vars, node) {
- var formatList = get(name), format = formatList[0], bookmark, rng, contentEditable = true;
- // Merges the styles for each node
- function process(node) {
- var children, i, l, lastContentEditable, hasContentEditableState;
- // Node has a contentEditable value
- if (node.nodeType === 1 && getContentEditable(node)) {
- lastContentEditable = contentEditable;
- contentEditable = getContentEditable(node) === "true";
- hasContentEditableState = true; // We don't want to wrap the container only it's children
- }
- // Grab the children first since the nodelist might be changed
- children = grep(node.childNodes);
- // Process current node
- if (contentEditable && !hasContentEditableState) {
- for (i = 0, l = formatList.length; i < l; i++) {
- if (removeFormat(formatList[i], vars, node, node)) {
- break;
- }
- }
- }
- // Process the children
- if (format.deep) {
- if (children.length) {
- for (i = 0, l = children.length; i < l; i++) {
- process(children[i]);
- }
- if (hasContentEditableState) {
- contentEditable = lastContentEditable; // Restore last contentEditable state from stack
- }
- }
- }
- }
- function findFormatRoot(container) {
- var formatRoot;
- // Find format root
- each(getParents(container.parentNode).reverse(), function(parent) {
- var format;
- // Find format root element
- if (!formatRoot && parent.id != '_start' && parent.id != '_end') {
- // Is the node matching the format we are looking for
- format = matchNode(parent, name, vars);
- if (format && format.split !== false) {
- formatRoot = parent;
- }
- }
- });
- return formatRoot;
- }
- function wrapAndSplit(format_root, container, target, split) {
- var parent, clone, lastClone, firstClone, i, formatRootParent;
- // Format root found then clone formats and split it
- if (format_root) {
- formatRootParent = format_root.parentNode;
- for (parent = container.parentNode; parent && parent != formatRootParent; parent = parent.parentNode) {
- clone = dom.clone(parent, FALSE);
- for (i = 0; i < formatList.length; i++) {
- if (removeFormat(formatList[i], vars, clone, clone)) {
- clone = 0;
- break;
- }
- }
- // Build wrapper node
- if (clone) {
- if (lastClone) {
- clone.appendChild(lastClone);
- }
- if (!firstClone) {
- firstClone = clone;
- }
- lastClone = clone;
- }
- }
- // Never split block elements if the format is mixed
- if (split && (!format.mixed || !isBlock(format_root))) {
- container = dom.split(format_root, container);
- }
- // Wrap container in cloned formats
- if (lastClone) {
- target.parentNode.insertBefore(lastClone, target);
- firstClone.appendChild(target);
- }
- }
- return container;
- }
- function splitToFormatRoot(container) {
- return wrapAndSplit(findFormatRoot(container), container, container, true);
- }
- function unwrap(start) {
- var node = dom.get(start ? '_start' : '_end'),
- out = node[start ? 'firstChild' : 'lastChild'];
- // If the end is placed within the start the result will be removed
- // So this checks if the out node is a bookmark node if it is it
- // checks for another more suitable node
- if (isBookmarkNode(out)) {
- out = out[start ? 'firstChild' : 'lastChild'];
- }
- dom.remove(node, true);
- return out;
- }
- function removeRngStyle(rng) {
- var startContainer, endContainer;
- var commonAncestorContainer = rng.commonAncestorContainer;
- rng = expandRng(rng, formatList, TRUE);
- if (format.split) {
- startContainer = getContainer(rng, TRUE);
- endContainer = getContainer(rng);
- if (startContainer != endContainer) {
- // WebKit will render the table incorrectly if we wrap a TH or TD in a SPAN
- // so let's see if we can use the first child instead
- // This will happen if you triple click a table cell and use remove formatting
- if (/^(TR|TH|TD)$/.test(startContainer.nodeName) && startContainer.firstChild) {
- if (startContainer.nodeName == "TR") {
- startContainer = startContainer.firstChild.firstChild || startContainer;
- } else {
- startContainer = startContainer.firstChild || startContainer;
- }
- }
- // Try to adjust endContainer as well if cells on the same row were selected - bug #6410
- if (commonAncestorContainer &&
- /^T(HEAD|BODY|FOOT|R)$/.test(commonAncestorContainer.nodeName) &&
- /^(TH|TD)$/.test(endContainer.nodeName) && endContainer.firstChild) {
- endContainer = endContainer.firstChild || endContainer;
- }
- // Wrap start/end nodes in span element since these might be cloned/moved
- startContainer = wrap(startContainer, 'span', {id: '_start', 'data-mce-type': 'bookmark'});
- endContainer = wrap(endContainer, 'span', {id: '_end', 'data-mce-type': 'bookmark'});
- // Split start/end
- splitToFormatRoot(startContainer);
- splitToFormatRoot(endContainer);
- // Unwrap start/end to get real elements again
- startContainer = unwrap(TRUE);
- endContainer = unwrap();
- } else {
- startContainer = endContainer = splitToFormatRoot(startContainer);
- }
- // Update range positions since they might have changed after the split operations
- rng.startContainer = startContainer.parentNode;
- rng.startOffset = nodeIndex(startContainer);
- rng.endContainer = endContainer.parentNode;
- rng.endOffset = nodeIndex(endContainer) + 1;
- }
- // Remove items between start/end
- rangeUtils.walk(rng, function(nodes) {
- each(nodes, function(node) {
- process(node);
- // Remove parent span if it only contains text-decoration: underline, yet a parent node is also underlined.
- if (node.nodeType === 1 && ed.dom.getStyle(node, 'text-decoration') === 'underline' &&
- node.parentNode && getTextDecoration(node.parentNode) === 'underline') {
- removeFormat({
- 'deep': false,
- 'exact': true,
- 'inline': 'span',
- 'styles': {
- 'textDecoration': 'underline'
- }
- }, null, node);
- }
- });
- });
- }
- // Handle node
- if (node) {
- if (node.nodeType) {
- rng = dom.createRng();
- rng.setStartBefore(node);
- rng.setEndAfter(node);
- removeRngStyle(rng);
- } else {
- removeRngStyle(node);
- }
- return;
- }
- if (!selection.isCollapsed() || !format.inline || dom.select('td.mce-item-selected,th.mce-item-selected').length) {
- bookmark = selection.getBookmark();
- removeRngStyle(selection.getRng(TRUE));
- selection.moveToBookmark(bookmark);
- // Check if start element still has formatting then we are at: "<b>text|</b>text"
- // and need to move the start into the next text node
- if (format.inline && match(name, vars, selection.getStart())) {
- moveStart(selection.getRng(true));
- }
- ed.nodeChanged();
- } else {
- performCaretAction('remove', name, vars);
- }
- }
- /**
- * Toggles the specified format on/off.
- *
- * @method toggle
- * @param {String} name Name of format to apply/remove.
- * @param {Object} vars Optional list of variables to replace within format before applying/removing it.
- * @param {Node} node Optional node to apply the format to or remove from. Defaults to current selection.
- */
- function toggle(name, vars, node) {
- var fmt = get(name);
- if (match(name, vars, node) && (!('toggle' in fmt[0]) || fmt[0].toggle)) {
- remove(name, vars, node);
- } else {
- apply(name, vars, node);
- }
- }
- /**
- * Return true/false if the specified node has the specified format.
- *
- * @method matchNode
- * @param {Node} node Node to check the format on.
- * @param {String} name Format name to check.
- * @param {Object} vars Optional list of variables to replace before checking it.
- * @param {Boolean} similar Match format that has similar properties.
- * @return {Object} Returns the format object it matches or undefined if it doesn't match.
- */
- function matchNode(node, name, vars, similar) {
- var formatList = get(name), format, i, classes;
- function matchItems(node, format, item_name) {
- var key, value, items = format[item_name], i;
- // Custom match
- if (format.onmatch) {
- return format.onmatch(node, format, item_name);
- }
- // Check all items
- if (items) {
- // Non indexed object
- if (items.length === undef) {
- for (key in items) {
- if (items.hasOwnProperty(key)) {
- if (item_name === 'attributes') {
- value = dom.getAttrib(node, key);
- } else {
- value = getStyle(node, key);
- }
- if (similar && !value && !format.exact) {
- return;
- }
- if ((!similar || format.exact) && !isEq(value, normalizeStyleValue(replaceVars(items[key], vars), key))) {
- return;
- }
- }
- }
- } else {
- // Only one match needed for indexed arrays
- for (i = 0; i < items.length; i++) {
- if (item_name === 'attributes' ? dom.getAttrib(node, items[i]) : getStyle(node, items[i])) {
- return format;
- }
- }
- }
- }
- return format;
- }
- if (formatList && node) {
- // Check each format in list
- for (i = 0; i < formatList.length; i++) {
- format = formatList[i];
- // Name name, attributes, styles and classes
- if (matchName(node, format) && matchItems(node, format, 'attributes') && matchItems(node, format, 'styles')) {
- // Match classes
- if ((classes = format.classes)) {
- for (i = 0; i < classes.length; i++) {
- if (!dom.hasClass(node, classes[i])) {
- return;
- }
- }
- }
- return format;
- }
- }
- }
- }
- /**
- * Matches the current selection or specified node against the specified format name.
- *
- * @method match
- * @param {String} name Name of format to match.
- * @param {Object} vars Optional list of variables to replace before checking it.
- * @param {Node} node Optional node to check.
- * @return {boolean} true/false if the specified selection/node matches the format.
- */
- function match(name, vars, node) {
- var startNode;
- function matchParents(node) {
- var root = dom.getRoot();
- if (node === root) {
- return false;
- }
- // Find first node with similar format settings
- node = dom.getParent(node, function(node) {
- return node.parentNode === root || !!matchNode(node, name, vars, true);
- });
- // Do an exact check on the similar format element
- return matchNode(node, name, vars);
- }
- // Check specified node
- if (node) {
- return matchParents(node);
- }
- // Check selected node
- node = selection.getNode();
- if (matchParents(node)) {
- return TRUE;
- }
- // Check start node if it's different
- startNode = selection.getStart();
- if (startNode != node) {
- if (matchParents(startNode)) {
- return TRUE;
- }
- }
- return FALSE;
- }
- /**
- * Matches the current selection against the array of formats and returns a new array with matching formats.
- *
- * @method matchAll
- * @param {Array} names Name of format to match.
- * @param {Object} vars Optional list of variables to replace before checking it.
- * @return {Array} Array with matched formats.
- */
- function matchAll(names, vars) {
- var startElement, matchedFormatNames = [], checkedMap = {};
- // Check start of selection for formats
- startElement = selection.getStart();
- dom.getParent(startElement, function(node) {
- var i, name;
- for (i = 0; i < names.length; i++) {
- name = names[i];
- if (!checkedMap[name] && matchNode(node, name, vars)) {
- checkedMap[name] = true;
- matchedFormatNames.push(name);
- }
- }
- }, dom.getRoot());
- return matchedFormatNames;
- }
- /**
- * Returns true/false if the specified format can be applied to the current selection or not. It
- * will currently only check the state for selector formats, it returns true on all other format types.
- *
- * @method canApply
- * @param {String} name Name of format to check.
- * @return {boolean} true/false if the specified format can be applied to the current selection/node.
- */
- function canApply(name) {
- var formatList = get(name), startNode, parents, i, x, selector;
- if (formatList) {
- startNode = selection.getStart();
- parents = getParents(startNode);
- for (x = formatList.length - 1; x >= 0; x--) {
- selector = formatList[x].selector;
- // Format is not selector based then always return TRUE
- // Is it has a defaultBlock then it's likely it can be applied for example align on a non block element line
- if (!selector || formatList[x].defaultBlock) {
- return TRUE;
- }
- for (i = parents.length - 1; i >= 0; i--) {
- if (dom.is(parents[i], selector)) {
- return TRUE;
- }
- }
- }
- }
- return FALSE;
- }
- /**
- * Executes the specified callback when the current selection matches the formats or not.
- *
- * @method formatChanged
- * @param {String} formats Comma separated list of formats to check for.
- * @param {function} callback Callback with state and args when the format is changed/toggled on/off.
- * @param {Boolean} similar True/false state if the match should handle similar or exact formats.
- */
- function formatChanged(formats, callback, similar) {
- var currentFormats;
- // Setup format node change logic
- if (!formatChangeData) {
- formatChangeData = {};
- currentFormats = {};
- ed.on('NodeChange', function(e) {
- var parents = getParents(e.element), matchedFormats = {};
- // Check for new formats
- each(formatChangeData, function(callbacks, format) {
- each(parents, function(node) {
- if (matchNode(node, format, {}, callbacks.similar)) {
- if (!currentFormats[format]) {
- // Execute callbacks
- each(callbacks, function(callback) {
- callback(true, {node: node, format: format, parents: parents});
- });
- currentFormats[format] = callbacks;
- }
- matchedFormats[format] = callbacks;
- return false;
- }
- });
- });
- // Check if current formats still match
- each(currentFormats, function(callbacks, format) {
- if (!matchedFormats[format]) {
- delete currentFormats[format];
- each(callbacks, function(callback) {
- callback(false, {node: e.element, format: format, parents: parents});
- });
- }
- });
- });
- }
- // Add format listeners
- each(formats.split(','), function(format) {
- if (!formatChangeData[format]) {
- formatChangeData[format] = [];
- formatChangeData[format].similar = similar;
- }
- formatChangeData[format].push(callback);
- });
- return this;
- }
- /**
- * Returns a preview css text for the specified format.
- *
- * @method getCssText
- * @param {String/Object} format Format to generate preview css text for.
- * @return {String} Css text for the specified format.
- * @example
- * var cssText1 = editor.formatter.getCssText('bold');
- * var cssText2 = editor.formatter.getCssText({inline: 'b'});
- */
- function getCssText(format) {
- return Preview.getCssText(ed, format);
- }
- // Expose to public
- extend(this, {
- get: get,
- register: register,
- apply: apply,
- remove: remove,
- toggle: toggle,
- match: match,
- matchAll: matchAll,
- matchNode: matchNode,
- canApply: canApply,
- formatChanged: formatChanged,
- getCssText: getCssText
- });
- // Initialize
- defaultFormats();
- addKeyboardShortcuts();
- ed.on('BeforeGetContent', function() {
- if (markCaretContainersBogus) {
- markCaretContainersBogus();
- }
- });
- ed.on('mouseup keydown', function(e) {
- if (disableCaretContainer) {
- disableCaretContainer(e);
- }
- });
- // Private functions
- /**
- * Checks if the specified nodes name matches the format inline/block or selector.
- *
- * @private
- * @param {Node} node Node to match against the specified format.
- * @param {Object} format Format object o match with.
- * @return {boolean} true/false if the format matches.
- */
- function matchName(node, format) {
- // Check for inline match
- if (isEq(node, format.inline)) {
- return TRUE;
- }
- // Check for block match
- if (isEq(node, format.block)) {
- return TRUE;
- }
- // Check for selector match
- if (format.selector) {
- return node.nodeType == 1 && dom.is(node, format.selector);
- }
- }
- /**
- * Compares two string/nodes regardless of their case.
- *
- * @private
- * @param {String/Node} Node or string to compare.
- * @param {String/Node} Node or string to compare.
- * @return {boolean} True/false if they match.
- */
- function isEq(str1, str2) {
- str1 = str1 || '';
- str2 = str2 || '';
- str1 = '' + (str1.nodeName || str1);
- str2 = '' + (str2.nodeName || str2);
- return str1.toLowerCase() == str2.toLowerCase();
- }
- /**
- * Returns the style by name on the specified node. This method modifies the style
- * contents to make it more easy to match. This will resolve a few browser issues.
- *
- * @private
- * @param {Node} node to get style from.
- * @param {String} name Style name to get.
- * @return {String} Style item value.
- */
- function getStyle(node, name) {
- return normalizeStyleValue(dom.getStyle(node, name), name);
- }
- /**
- * Normalize style value by name. This method modifies the style contents
- * to make it more easy to match. This will resolve a few browser issues.
- *
- * @private
- * @param {Node} node to get style from.
- * @param {String} name Style name to get.
- * @return {String} Style item value.
- */
- function normalizeStyleValue(value, name) {
- // Force the format to hex
- if (name == 'color' || name == 'backgroundColor') {
- value = dom.toHex(value);
- }
- // Opera will return bold as 700
- if (name == 'fontWeight' && value == 700) {
- value = 'bold';
- }
- // Normalize fontFamily so "'Font name', Font" becomes: "Font name,Font"
- if (name == 'fontFamily') {
- value = value.replace(/[\'\"]/g, '').replace(/,\s+/g, ',');
- }
- return '' + value;
- }
- /**
- * Replaces variables in the value. The variable format is %var.
- *
- * @private
- * @param {String} value Value to replace variables in.
- * @param {Object} vars Name/value array with variables to replace.
- * @return {String} New value with replaced variables.
- */
- function replaceVars(value, vars) {
- if (typeof(value) != "string") {
- value = value(vars);
- } else if (vars) {
- value = value.replace(/%(\w+)/g, function(str, name) {
- return vars[name] || str;
- });
- }
- return value;
- }
- function isWhiteSpaceNode(node) {
- return node && node.nodeType === 3 && /^([\t \r\n]+|)$/.test(node.nodeValue);
- }
- function wrap(node, name, attrs) {
- var wrapper = dom.create(name, attrs);
- node.parentNode.insertBefore(wrapper, node);
- wrapper.appendChild(node);
- return wrapper;
- }
- /**
- * Expands the specified range like object to depending on format.
- *
- * For example on block formats it will move the start/end position
- * to the beginning of the current block.
- *
- * @private
- * @param {Object} rng Range like object.
- * @param {Array} formats Array with formats to expand by.
- * @return {Object} Expanded range like object.
- */
- function expandRng(rng, format, remove) {
- var lastIdx, leaf, endPoint,
- startContainer = rng.startContainer,
- startOffset = rng.startOffset,
- endContainer = rng.endContainer,
- endOffset = rng.endOffset;
- // This function walks up the tree if there is no siblings before/after the node
- function findParentContainer(start) {
- var container, parent, sibling, siblingName, root;
- container = parent = start ? startContainer : endContainer;
- siblingName = start ? 'previousSibling' : 'nextSibling';
- root = dom.getRoot();
- function isBogusBr(node) {
- return node.nodeName == "BR" && node.getAttribute('data-mce-bogus') && !node.nextSibling;
- }
- // If it's a text node and the offset is inside the text
- if (container.nodeType == 3 && !isWhiteSpaceNode(container)) {
- if (start ? startOffset > 0 : endOffset < container.nodeValue.length) {
- return container;
- }
- }
- /*eslint no-constant-condition:0 */
- while (true) {
- // Stop expanding on block elements
- if (!format[0].block_expand && isBlock(parent)) {
- return parent;
- }
- // Walk left/right
- for (sibling = parent[siblingName]; sibling; sibling = sibling[siblingName]) {
- if (!isBookmarkNode(sibling) && !isWhiteSpaceNode(sibling) && !isBogusBr(sibling)) {
- return parent;
- }
- }
- // Check if we can move up are we at root level or body level
- if (parent.parentNode == root) {
- container = parent;
- break;
- }
- parent = parent.parentNode;
- }
- return container;
- }
- // This function walks down the tree to find the leaf at the selection.
- // The offset is also returned as if node initially a leaf, the offset may be in the middle of the text node.
- function findLeaf(node, offset) {
- if (offset === undef) {
- offset = node.nodeType === 3 ? node.length : node.childNodes.length;
- }
- while (node && node.hasChildNodes()) {
- node = node.childNodes[offset];
- if (node) {
- offset = node.nodeType === 3 ? node.length : node.childNodes.length;
- }
- }
- return { node: node, offset: offset };
- }
- // If index based start position then resolve it
- if (startContainer.nodeType == 1 && startContainer.hasChildNodes()) {
- lastIdx = startContainer.childNodes.length - 1;
- startContainer = startContainer.childNodes[startOffset > lastIdx ? lastIdx : startOffset];
- if (startContainer.nodeType == 3) {
- startOffset = 0;
- }
- }
- // If index based end position then resolve it
- if (endContainer.nodeType == 1 && endContainer.hasChildNodes()) {
- lastIdx = endContainer.childNodes.length - 1;
- endContainer = endContainer.childNodes[endOffset > lastIdx ? lastIdx : endOffset - 1];
- if (endContainer.nodeType == 3) {
- endOffset = endContainer.nodeValue.length;
- }
- }
- // Expands the node to the closes contentEditable false element if it exists
- function findParentContentEditable(node) {
- var parent = node;
- while (parent) {
- if (parent.nodeType === 1 && getContentEditable(parent)) {
- return getContentEditable(parent) === "false" ? parent : node;
- }
- parent = parent.parentNode;
- }
- return node;
- }
- function findWordEndPoint(container, offset, start) {
- var walker, node, pos, lastTextNode;
- function findSpace(node, offset) {
- var pos, pos2, str = node.nodeValue;
- if (typeof(offset) == "undefined") {
- offset = start ? str.length : 0;
- }
- if (start) {
- pos = str.lastIndexOf(' ', offset);
- pos2 = str.lastIndexOf('\u00a0', offset);
- pos = pos > pos2 ? pos : pos2;
- // Include the space on remove to avoid tag soup
- if (pos !== -1 && !remove) {
- pos++;
- }
- } else {
- pos = str.indexOf(' ', offset);
- pos2 = str.indexOf('\u00a0', offset);
- pos = pos !== -1 && (pos2 === -1 || pos < pos2) ? pos : pos2;
- }
- return pos;
- }
- if (container.nodeType === 3) {
- pos = findSpace(container, offset);
- if (pos !== -1) {
- return {container: container, offset: pos};
- }
- lastTextNode = container;
- }
- // Walk the nodes inside the block
- walker = new TreeWalker(container, dom.getParent(container, isBlock) || ed.getBody());
- while ((node = walker[start ? 'prev' : 'next']())) {
- if (node.nodeType === 3) {
- lastTextNode = node;
- pos = findSpace(node);
- if (pos !== -1) {
- return {container: node, offset: pos};
- }
- } else if (isBlock(node)) {
- break;
- }
- }
- if (lastTextNode) {
- if (start) {
- offset = 0;
- } else {
- offset = lastTextNode.length;
- }
- return {container: lastTextNode, offset: offset};
- }
- }
- function findSelectorEndPoint(container, sibling_name) {
- var parents, i, y, curFormat;
- if (container.nodeType == 3 && container.nodeValue.length === 0 && container[sibling_name]) {
- container = container[sibling_name];
- }
- parents = getParents(container);
- for (i = 0; i < parents.length; i++) {
- for (y = 0; y < format.length; y++) {
- curFormat = format[y];
- // If collapsed state is set then skip formats that doesn't match that
- if ("collapsed" in curFormat && curFormat.collapsed !== rng.collapsed) {
- continue;
- }
- if (dom.is(parents[i], curFormat.selector)) {
- return parents[i];
- }
- }
- }
- return container;
- }
- function findBlockEndPoint(container, sibling_name) {
- var node, root = dom.getRoot();
- // Expand to block of similar type
- if (!format[0].wrapper) {
- node = dom.getParent(container, format[0].block, root);
- }
- // Expand to first wrappable block element or any block element
- if (!node) {
- node = dom.getParent(container.nodeType == 3 ? container.parentNode : container, function(node) {
- // Fixes #6183 where it would expand to editable parent element in inline mode
- return node != root && isTextBlock(node);
- });
- }
- // Exclude inner lists from wrapping
- if (node && format[0].wrapper) {
- node = getParents(node, 'ul,ol').reverse()[0] || node;
- }
- // Didn't find a block element look for first/last wrappable element
- if (!node) {
- node = container;
- while (node[sibling_name] && !isBlock(node[sibling_name])) {
- node = node[sibling_name];
- // Break on BR but include it will be removed later on
- // we can't remove it now since we need to check if it can be wrapped
- if (isEq(node, 'br')) {
- break;
- }
- }
- }
- return node || container;
- }
- // Expand to closest contentEditable element
- startContainer = findParentContentEditable(startContainer);
- endContainer = findParentContentEditable(endContainer);
- // Exclude bookmark nodes if possible
- if (isBookmarkNode(startContainer.parentNode) || isBookmarkNode(startContainer)) {
- startContainer = isBookmarkNode(startContainer) ? startContainer : startContainer.parentNode;
- startContainer = startContainer.nextSibling || startContainer;
- if (startContainer.nodeType == 3) {
- startOffset = 0;
- }
- }
- if (isBookmarkNode(endContainer.parentNode) || isBookmarkNode(endContainer)) {
- endContainer = isBookmarkNode(endContainer) ? endContainer : endContainer.parentNode;
- endContainer = endContainer.previousSibling || endContainer;
- if (endContainer.nodeType == 3) {
- endOffset = endContainer.length;
- }
- }
- if (format[0].inline) {
- if (rng.collapsed) {
- // Expand left to closest word boundary
- endPoint = findWordEndPoint(startContainer, startOffset, true);
- if (endPoint) {
- startContainer = endPoint.container;
- startOffset = endPoint.offset;
- }
- // Expand right to closest word boundary
- endPoint = findWordEndPoint(endContainer, endOffset);
- if (endPoint) {
- endContainer = endPoint.container;
- endOffset = endPoint.offset;
- }
- }
- // Avoid applying formatting to a trailing space.
- leaf = findLeaf(endContainer, endOffset);
- if (leaf.node) {
- while (leaf.node && leaf.offset === 0 && leaf.node.previousSibling) {
- leaf = findLeaf(leaf.node.previousSibling);
- }
- if (leaf.node && leaf.offset > 0 && leaf.node.nodeType === 3 &&
- leaf.node.nodeValue.charAt(leaf.offset - 1) === ' ') {
- if (leaf.offset > 1) {
- endContainer = leaf.node;
- endContainer.splitText(leaf.offset - 1);
- }
- }
- }
- }
- // Move start/end point up the tree if the leaves are sharp and if we are in different containers
- // Example * becomes !: !<p><b><i>*text</i><i>text*</i></b></p>!
- // This will reduce the number of wrapper elements that needs to be created
- // Move start point up the tree
- if (format[0].inline || format[0].block_expand) {
- if (!format[0].inline || (startContainer.nodeType != 3 || startOffset === 0)) {
- startContainer = findParentContainer(true);
- }
- if (!format[0].inline || (endContainer.nodeType != 3 || endOffset === endContainer.nodeValue.length)) {
- endContainer = findParentContainer();
- }
- }
- // Expand start/end container to matching selector
- if (format[0].selector && format[0].expand !== FALSE && !format[0].inline) {
- // Find new startContainer/endContainer if there is better one
- startContainer = findSelectorEndPoint(startContainer, 'previousSibling');
- endContainer = findSelectorEndPoint(endContainer, 'nextSibling');
- }
- // Expand start/end container to matching block element or text node
- if (format[0].block || format[0].selector) {
- // Find new startContainer/endContainer if there is better one
- startContainer = findBlockEndPoint(startContainer, 'previousSibling');
- endContainer = findBlockEndPoint(endContainer, 'nextSibling');
- // Non block element then try to expand up the leaf
- if (format[0].block) {
- if (!isBlock(startContainer)) {
- startContainer = findParentContainer(true);
- }
- if (!isBlock(endContainer)) {
- endContainer = findParentContainer();
- }
- }
- }
- // Setup index for startContainer
- if (startContainer.nodeType == 1) {
- startOffset = nodeIndex(startContainer);
- startContainer = startContainer.parentNode;
- }
- // Setup index for endContainer
- if (endContainer.nodeType == 1) {
- endOffset = nodeIndex(endContainer) + 1;
- endContainer = endContainer.parentNode;
- }
- // Return new range like object
- return {
- startContainer: startContainer,
- startOffset: startOffset,
- endContainer: endContainer,
- endOffset: endOffset
- };
- }
- /**
- * Removes the specified format for the specified node. It will also remove the node if it doesn't have
- * any attributes if the format specifies it to do so.
- *
- * @private
- * @param {Object} format Format object with items to remove from node.
- * @param {Object} vars Name/value object with variables to apply to format.
- * @param {Node} node Node to remove the format styles on.
- * @param {Node} compare_node Optional compare node, if specified the styles will be compared to that node.
- * @return {Boolean} True/false if the node was removed or not.
- */
- function removeFormat(format, vars, node, compare_node) {
- var i, attrs, stylesModified;
- // Check if node matches format
- if (!matchName(node, format)) {
- return FALSE;
- }
- // Should we compare with format attribs and styles
- if (format.remove != 'all') {
- // Remove styles
- each(format.styles, function(value, name) {
- value = normalizeStyleValue(replaceVars(value, vars), name);
- // Indexed array
- if (typeof(name) === 'number') {
- name = value;
- compare_node = 0;
- }
- if (!compare_node || isEq(getStyle(compare_node, name), value)) {
- dom.setStyle(node, name, '');
- }
- stylesModified = 1;
- });
- // Remove style attribute if it's empty
- if (stylesModified && dom.getAttrib(node, 'style') === '') {
- node.removeAttribute('style');
- node.removeAttribute('data-mce-style');
- }
- // Remove attributes
- each(format.attributes, function(value, name) {
- var valueOut;
- value = replaceVars(value, vars);
- // Indexed array
- if (typeof(name) === 'number') {
- name = value;
- compare_node = 0;
- }
- if (!compare_node || isEq(dom.getAttrib(compare_node, name), value)) {
- // Keep internal classes
- if (name == 'class') {
- value = dom.getAttrib(node, name);
- if (value) {
- // Build new class value where everything is removed except the internal prefixed classes
- valueOut = '';
- each(value.split(/\s+/), function(cls) {
- if (/mce\w+/.test(cls)) {
- valueOut += (valueOut ? ' ' : '') + cls;
- }
- });
- // We got some internal classes left
- if (valueOut) {
- dom.setAttrib(node, name, valueOut);
- return;
- }
- }
- }
- // IE6 has a bug where the attribute doesn't get removed correctly
- if (name == "class") {
- node.removeAttribute('className');
- }
- // Remove mce prefixed attributes
- if (MCE_ATTR_RE.test(name)) {
- node.removeAttribute('data-mce-' + name);
- }
- node.removeAttribute(name);
- }
- });
- // Remove classes
- each(format.classes, function(value) {
- value = replaceVars(value, vars);
- if (!compare_node || dom.hasClass(compare_node, value)) {
- dom.removeClass(node, value);
- }
- });
- // Check for non internal attributes
- attrs = dom.getAttribs(node);
- for (i = 0; i < attrs.length; i++) {
- if (attrs[i].nodeName.indexOf('_') !== 0) {
- return FALSE;
- }
- }
- }
- // Remove the inline child if it's empty for example <b> or <span>
- if (format.remove != 'none') {
- removeNode(node, format);
- return TRUE;
- }
- }
- /**
- * Removes the node and wrap it's children in paragraphs before doing so or
- * appends BR elements to the beginning/end of the block element if forcedRootBlocks is disabled.
- *
- * If the div in the node below gets removed:
- * text<div>text</div>text
- *
- * Output becomes:
- * text<div><br />text<br /></div>text
- *
- * So when the div is removed the result is:
- * text<br />text<br />text
- *
- * @private
- * @param {Node} node Node to remove + apply BR/P elements to.
- * @param {Object} format Format rule.
- * @return {Node} Input node.
- */
- function removeNode(node, format) {
- var parentNode = node.parentNode, rootBlockElm;
- function find(node, next, inc) {
- node = getNonWhiteSpaceSibling(node, next, inc);
- return !node || (node.nodeName == 'BR' || isBlock(node));
- }
- if (format.block) {
- if (!forcedRootBlock) {
- // Append BR elements if needed before we remove the block
- if (isBlock(node) && !isBlock(parentNode)) {
- if (!find(node, FALSE) && !find(node.firstChild, TRUE, 1)) {
- node.insertBefore(dom.create('br'), node.firstChild);
- }
- if (!find(node, TRUE) && !find(node.lastChild, FALSE, 1)) {
- node.appendChild(dom.create('br'));
- }
- }
- } else {
- // Wrap the block in a forcedRootBlock if we are at the root of document
- if (parentNode == dom.getRoot()) {
- if (!format.list_block || !isEq(node, format.list_block)) {
- each(grep(node.childNodes), function(node) {
- if (isValid(forcedRootBlock, node.nodeName.toLowerCase())) {
- if (!rootBlockElm) {
- rootBlockElm = wrap(node, forcedRootBlock);
- dom.setAttribs(rootBlockElm, ed.settings.forced_root_block_attrs);
- } else {
- rootBlockElm.appendChild(node);
- }
- } else {
- rootBlockElm = 0;
- }
- });
- }
- }
- }
- }
- // Never remove nodes that isn't the specified inline element if a selector is specified too
- if (format.selector && format.inline && !isEq(format.inline, node)) {
- return;
- }
- dom.remove(node, 1);
- }
- /**
- * Returns the next/previous non whitespace node.
- *
- * @private
- * @param {Node} node Node to start at.
- * @param {boolean} next (Optional) Include next or previous node defaults to previous.
- * @param {boolean} inc (Optional) Include the current node in checking. Defaults to false.
- * @return {Node} Next or previous node or undefined if it wasn't found.
- */
- function getNonWhiteSpaceSibling(node, next, inc) {
- if (node) {
- next = next ? 'nextSibling' : 'previousSibling';
- for (node = inc ? node : node[next]; node; node = node[next]) {
- if (node.nodeType == 1 || !isWhiteSpaceNode(node)) {
- return node;
- }
- }
- }
- }
- /**
- * Checks if the specified node is a bookmark node or not.
- *
- * @private
- * @param {Node} node Node to check if it's a bookmark node or not.
- * @return {Boolean} true/false if the node is a bookmark node.
- */
- function isBookmarkNode(node) {
- return node && node.nodeType == 1 && node.getAttribute('data-mce-type') == 'bookmark';
- }
- /**
- * Merges the next/previous sibling element if they match.
- *
- * @private
- * @param {Node} prev Previous node to compare/merge.
- * @param {Node} next Next node to compare/merge.
- * @return {Node} Next node if we didn't merge and prev node if we did.
- */
- function mergeSiblings(prev, next) {
- var sibling, tmpSibling;
- /**
- * Compares two nodes and checks if it's attributes and styles matches.
- * This doesn't compare classes as items since their order is significant.
- *
- * @private
- * @param {Node} node1 First node to compare with.
- * @param {Node} node2 Second node to compare with.
- * @return {boolean} True/false if the nodes are the same or not.
- */
- function compareElements(node1, node2) {
- // Not the same name
- if (node1.nodeName != node2.nodeName) {
- return FALSE;
- }
- /**
- * Returns all the nodes attributes excluding internal ones, styles and classes.
- *
- * @private
- * @param {Node} node Node to get attributes from.
- * @return {Object} Name/value object with attributes and attribute values.
- */
- function getAttribs(node) {
- var attribs = {};
- each(dom.getAttribs(node), function(attr) {
- var name = attr.nodeName.toLowerCase();
- // Don't compare internal attributes or style
- if (name.indexOf('_') !== 0 && name !== 'style' && name !== 'data-mce-style') {
- attribs[name] = dom.getAttrib(node, name);
- }
- });
- return attribs;
- }
- /**
- * Compares two objects checks if it's key + value exists in the other one.
- *
- * @private
- * @param {Object} obj1 First object to compare.
- * @param {Object} obj2 Second object to compare.
- * @return {boolean} True/false if the objects matches or not.
- */
- function compareObjects(obj1, obj2) {
- var value, name;
- for (name in obj1) {
- // Obj1 has item obj2 doesn't have
- if (obj1.hasOwnProperty(name)) {
- value = obj2[name];
- // Obj2 doesn't have obj1 item
- if (value === undef) {
- return FALSE;
- }
- // Obj2 item has a different value
- if (obj1[name] != value) {
- return FALSE;
- }
- // Delete similar value
- delete obj2[name];
- }
- }
- // Check if obj 2 has something obj 1 doesn't have
- for (name in obj2) {
- // Obj2 has item obj1 doesn't have
- if (obj2.hasOwnProperty(name)) {
- return FALSE;
- }
- }
- return TRUE;
- }
- // Attribs are not the same
- if (!compareObjects(getAttribs(node1), getAttribs(node2))) {
- return FALSE;
- }
- // Styles are not the same
- if (!compareObjects(dom.parseStyle(dom.getAttrib(node1, 'style')), dom.parseStyle(dom.getAttrib(node2, 'style')))) {
- return FALSE;
- }
- return !isBookmarkNode(node1) && !isBookmarkNode(node2);
- }
- function findElementSibling(node, sibling_name) {
- for (sibling = node; sibling; sibling = sibling[sibling_name]) {
- if (sibling.nodeType == 3 && sibling.nodeValue.length !== 0) {
- return node;
- }
- if (sibling.nodeType == 1 && !isBookmarkNode(sibling)) {
- return sibling;
- }
- }
- return node;
- }
- // Check if next/prev exists and that they are elements
- if (prev && next) {
- // If previous sibling is empty then jump over it
- prev = findElementSibling(prev, 'previousSibling');
- next = findElementSibling(next, 'nextSibling');
- // Compare next and previous nodes
- if (compareElements(prev, next)) {
- // Append nodes between
- for (sibling = prev.nextSibling; sibling && sibling != next;) {
- tmpSibling = sibling;
- sibling = sibling.nextSibling;
- prev.appendChild(tmpSibling);
- }
- // Remove next node
- dom.remove(next);
- // Move children into prev node
- each(grep(next.childNodes), function(node) {
- prev.appendChild(node);
- });
- return prev;
- }
- }
- return next;
- }
- function getContainer(rng, start) {
- var container, offset, lastIdx;
- container = rng[start ? 'startContainer' : 'endContainer'];
- offset = rng[start ? 'startOffset' : 'endOffset'];
- if (container.nodeType == 1) {
- lastIdx = container.childNodes.length - 1;
- if (!start && offset) {
- offset--;
- }
- container = container.childNodes[offset > lastIdx ? lastIdx : offset];
- }
- // If start text node is excluded then walk to the next node
- if (container.nodeType === 3 && start && offset >= container.nodeValue.length) {
- container = new TreeWalker(container, ed.getBody()).next() || container;
- }
- // If end text node is excluded then walk to the previous node
- if (container.nodeType === 3 && !start && offset === 0) {
- container = new TreeWalker(container, ed.getBody()).prev() || container;
- }
- return container;
- }
- function performCaretAction(type, name, vars) {
- var caretContainerId = '_mce_caret', debug = ed.settings.caret_debug;
- // Creates a caret container bogus element
- function createCaretContainer(fill) {
- var caretContainer = dom.create('span', {id: caretContainerId, 'data-mce-bogus': true, style: debug ? 'color:red' : ''});
- if (fill) {
- caretContainer.appendChild(ed.getDoc().createTextNode(INVISIBLE_CHAR));
- }
- return caretContainer;
- }
- function isCaretContainerEmpty(node, nodes) {
- while (node) {
- if ((node.nodeType === 3 && node.nodeValue !== INVISIBLE_CHAR) || node.childNodes.length > 1) {
- return false;
- }
- // Collect nodes
- if (nodes && node.nodeType === 1) {
- nodes.push(node);
- }
- node = node.firstChild;
- }
- return true;
- }
- // Returns any parent caret container element
- function getParentCaretContainer(node) {
- while (node) {
- if (node.id === caretContainerId) {
- return node;
- }
- node = node.parentNode;
- }
- }
- // Finds the first text node in the specified node
- function findFirstTextNode(node) {
- var walker;
- if (node) {
- walker = new TreeWalker(node, node);
- for (node = walker.current(); node; node = walker.next()) {
- if (node.nodeType === 3) {
- return node;
- }
- }
- }
- }
- // Removes the caret container for the specified node or all on the current document
- function removeCaretContainer(node, move_caret) {
- var child, rng;
- if (!node) {
- node = getParentCaretContainer(selection.getStart());
- if (!node) {
- while ((node = dom.get(caretContainerId))) {
- removeCaretContainer(node, false);
- }
- }
- } else {
- rng = selection.getRng(true);
- if (isCaretContainerEmpty(node)) {
- if (move_caret !== false) {
- rng.setStartBefore(node);
- rng.setEndBefore(node);
- }
- dom.remove(node);
- } else {
- child = findFirstTextNode(node);
- if (child.nodeValue.charAt(0) === INVISIBLE_CHAR) {
- child = child.deleteData(0, 1);
- }
- dom.remove(node, 1);
- }
- selection.setRng(rng);
- }
- }
- // Applies formatting to the caret postion
- function applyCaretFormat() {
- var rng, caretContainer, textNode, offset, bookmark, container, text;
- rng = selection.getRng(true);
- offset = rng.startOffset;
- container = rng.startContainer;
- text = container.nodeValue;
- caretContainer = getParentCaretContainer(selection.getStart());
- if (caretContainer) {
- textNode = findFirstTextNode(caretContainer);
- }
- // Expand to word is caret is in the middle of a text node and the char before/after is a alpha numeric character
- if (text && offset > 0 && offset < text.length && /\w/.test(text.charAt(offset)) && /\w/.test(text.charAt(offset - 1))) {
- // Get bookmark of caret position
- bookmark = selection.getBookmark();
- // Collapse bookmark range (WebKit)
- rng.collapse(true);
- // Expand the range to the closest word and split it at those points
- rng = expandRng(rng, get(name));
- rng = rangeUtils.split(rng);
- // Apply the format to the range
- apply(name, vars, rng);
- // Move selection back to caret position
- selection.moveToBookmark(bookmark);
- } else {
- if (!caretContainer || textNode.nodeValue !== INVISIBLE_CHAR) {
- caretContainer = createCaretContainer(true);
- textNode = caretContainer.firstChild;
- rng.insertNode(caretContainer);
- offset = 1;
- apply(name, vars, caretContainer);
- } else {
- apply(name, vars, caretContainer);
- }
- // Move selection to text node
- selection.setCursorLocation(textNode, offset);
- }
- }
- function removeCaretFormat() {
- var rng = selection.getRng(true), container, offset, bookmark,
- hasContentAfter, node, formatNode, parents = [], i, caretContainer;
- container = rng.startContainer;
- offset = rng.startOffset;
- node = container;
- if (container.nodeType == 3) {
- if (offset != container.nodeValue.length || container.nodeValue === INVISIBLE_CHAR) {
- hasContentAfter = true;
- }
- node = node.parentNode;
- }
- while (node) {
- if (matchNode(node, name, vars)) {
- formatNode = node;
- break;
- }
- if (node.nextSibling) {
- hasContentAfter = true;
- }
- parents.push(node);
- node = node.parentNode;
- }
- // Node doesn't have the specified format
- if (!formatNode) {
- return;
- }
- // Is there contents after the caret then remove the format on the element
- if (hasContentAfter) {
- // Get bookmark of caret position
- bookmark = selection.getBookmark();
- // Collapse bookmark range (WebKit)
- rng.collapse(true);
- // Expand the range to the closest word and split it at those points
- rng = expandRng(rng, get(name), true);
- rng = rangeUtils.split(rng);
- // Remove the format from the range
- remove(name, vars, rng);
- // Move selection back to caret position
- selection.moveToBookmark(bookmark);
- } else {
- caretContainer = createCaretContainer();
- node = caretContainer;
- for (i = parents.length - 1; i >= 0; i--) {
- node.appendChild(dom.clone(parents[i], false));
- node = node.firstChild;
- }
- // Insert invisible character into inner most format element
- node.appendChild(dom.doc.createTextNode(INVISIBLE_CHAR));
- node = node.firstChild;
- var block = dom.getParent(formatNode, isTextBlock);
- if (block && dom.isEmpty(block)) {
- // Replace formatNode with caretContainer when removing format from empty block like <p><b>|</b></p>
- formatNode.parentNode.replaceChild(caretContainer, formatNode);
- } else {
- // Insert caret container after the formated node
- dom.insertAfter(caretContainer, formatNode);
- }
- // Move selection to text node
- selection.setCursorLocation(node, 1);
- // If the formatNode is empty, we can remove it safely.
- if (dom.isEmpty(formatNode)) {
- dom.remove(formatNode);
- }
- }
- }
- // Checks if the parent caret container node isn't empty if that is the case it
- // will remove the bogus state on all children that isn't empty
- function unmarkBogusCaretParents() {
- var caretContainer;
- caretContainer = getParentCaretContainer(selection.getStart());
- if (caretContainer && !dom.isEmpty(caretContainer)) {
- walk(caretContainer, function(node) {
- if (node.nodeType == 1 && node.id !== caretContainerId && !dom.isEmpty(node)) {
- dom.setAttrib(node, 'data-mce-bogus', null);
- }
- }, 'childNodes');
- }
- }
- // Only bind the caret events once
- if (!ed._hasCaretEvents) {
- // Mark current caret container elements as bogus when getting the contents so we don't end up with empty elements
- markCaretContainersBogus = function() {
- var nodes = [], i;
- if (isCaretContainerEmpty(getParentCaretContainer(selection.getStart()), nodes)) {
- // Mark children
- i = nodes.length;
- while (i--) {
- dom.setAttrib(nodes[i], 'data-mce-bogus', '1');
- }
- }
- };
- disableCaretContainer = function(e) {
- var keyCode = e.keyCode;
- removeCaretContainer();
- // Remove caret container on keydown and it's a backspace, enter or left/right arrow keys
- if (keyCode == 8 || keyCode == 37 || keyCode == 39) {
- removeCaretContainer(getParentCaretContainer(selection.getStart()));
- }
- unmarkBogusCaretParents();
- };
- // Remove bogus state if they got filled by contents using editor.selection.setContent
- ed.on('SetContent', function(e) {
- if (e.selection) {
- unmarkBogusCaretParents();
- }
- });
- ed._hasCaretEvents = true;
- }
- // Do apply or remove caret format
- if (type == "apply") {
- applyCaretFormat();
- } else {
- removeCaretFormat();
- }
- }
- /**
- * Moves the start to the first suitable text node.
- */
- function moveStart(rng) {
- var container = rng.startContainer,
- offset = rng.startOffset, isAtEndOfText,
- walker, node, nodes, tmpNode;
- // Convert text node into index if possible
- if (container.nodeType == 3 && offset >= container.nodeValue.length) {
- // Get the parent container location and walk from there
- offset = nodeIndex(container);
- container = container.parentNode;
- isAtEndOfText = true;
- }
- // Move startContainer/startOffset in to a suitable node
- if (container.nodeType == 1) {
- nodes = container.childNodes;
- container = nodes[Math.min(offset, nodes.length - 1)];
- walker = new TreeWalker(container, dom.getParent(container, dom.isBlock));
- // If offset is at end of the parent node walk to the next one
- if (offset > nodes.length - 1 || isAtEndOfText) {
- walker.next();
- }
- for (node = walker.current(); node; node = walker.next()) {
- if (node.nodeType == 3 && !isWhiteSpaceNode(node)) {
- // IE has a "neat" feature where it moves the start node into the closest element
- // we can avoid this by inserting an element before it and then remove it after we set the selection
- tmpNode = dom.create('a', null, INVISIBLE_CHAR);
- node.parentNode.insertBefore(tmpNode, node);
- // Set selection and remove tmpNode
- rng.setStart(node, 0);
- selection.setRng(rng);
- dom.remove(tmpNode);
- return;
- }
- }
- }
- }
- };
- });
|