1
0

trumbowyg.js 68 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008
  1. /**
  2. * Trumbowyg v2.22.0 - A lightweight WYSIWYG editor
  3. * Trumbowyg core file
  4. * ------------------------
  5. * @link http://alex-d.github.io/Trumbowyg
  6. * @license MIT
  7. * @author Alexandre Demode (Alex-D)
  8. * Twitter : @AlexandreDemode
  9. * Website : alex-d.fr
  10. */
  11. jQuery.trumbowyg = {
  12. langs: {
  13. en: {
  14. viewHTML: 'View HTML',
  15. undo: 'Undo',
  16. redo: 'Redo',
  17. formatting: 'Formatting',
  18. p: 'Paragraph',
  19. blockquote: 'Quote',
  20. code: 'Code',
  21. header: 'Header',
  22. bold: 'Bold',
  23. italic: 'Italic',
  24. strikethrough: 'Strikethrough',
  25. underline: 'Underline',
  26. strong: 'Strong',
  27. em: 'Emphasis',
  28. del: 'Deleted',
  29. superscript: 'Superscript',
  30. subscript: 'Subscript',
  31. unorderedList: 'Unordered list',
  32. orderedList: 'Ordered list',
  33. insertImage: 'Insert Image',
  34. link: 'Link',
  35. createLink: 'Insert link',
  36. unlink: 'Remove link',
  37. justifyLeft: 'Align Left',
  38. justifyCenter: 'Align Center',
  39. justifyRight: 'Align Right',
  40. justifyFull: 'Align Justify',
  41. horizontalRule: 'Insert horizontal rule',
  42. removeformat: 'Remove format',
  43. fullscreen: 'Fullscreen',
  44. close: 'Close',
  45. submit: 'Confirm',
  46. reset: 'Cancel',
  47. required: 'Required',
  48. description: 'Description',
  49. title: 'Title',
  50. text: 'Text',
  51. target: 'Target',
  52. width: 'Width'
  53. }
  54. },
  55. // Plugins
  56. plugins: {},
  57. // SVG Path globally
  58. svgPath: null,
  59. svgAbsoluteUseHref: false,
  60. hideButtonTexts: null
  61. };
  62. // Makes default options read-only
  63. Object.defineProperty(jQuery.trumbowyg, 'defaultOptions', {
  64. value: {
  65. lang: 'en',
  66. fixedBtnPane: false,
  67. fixedFullWidth: false,
  68. autogrow: false,
  69. autogrowOnEnter: false,
  70. imageWidthModalEdit: false,
  71. prefix: 'trumbowyg-',
  72. // classes for inputs
  73. tagClasses:{
  74. h1: null,
  75. h2: null,
  76. h3: null,
  77. h4: null,
  78. p: null,
  79. },
  80. semantic: true,
  81. semanticKeepAttributes: false,
  82. resetCss: false,
  83. removeformatPasted: false,
  84. tabToIndent: false,
  85. tagsToRemove: [],
  86. tagsToKeep: ['hr', 'img', 'embed', 'iframe', 'input'],
  87. btns: [
  88. ['viewHTML'],
  89. ['undo', 'redo'], // Only supported in Blink browsers
  90. ['formatting'],
  91. ['strong', 'em', 'del'],
  92. ['superscript', 'subscript'],
  93. ['link'],
  94. ['insertImage'],
  95. ['justifyLeft', 'justifyCenter', 'justifyRight', 'justifyFull'],
  96. ['unorderedList', 'orderedList'],
  97. ['horizontalRule'],
  98. ['removeformat'],
  99. ['fullscreen']
  100. ],
  101. // For custom button definitions
  102. btnsDef: {},
  103. changeActiveDropdownIcon: false,
  104. inlineElementsSelector: 'a,abbr,acronym,b,caption,cite,code,col,dfn,dir,dt,dd,em,font,hr,i,kbd,li,q,span,strikeout,strong,sub,sup,u',
  105. pasteHandlers: [],
  106. // imgDblClickHandler: default is defined in constructor
  107. plugins: {},
  108. urlProtocol: false,
  109. minimalLinks: false,
  110. defaultLinkTarget: undefined
  111. },
  112. writable: false,
  113. enumerable: true,
  114. configurable: false
  115. });
  116. (function (navigator, window, document, $) {
  117. 'use strict';
  118. var CONFIRM_EVENT = 'tbwconfirm',
  119. CANCEL_EVENT = 'tbwcancel';
  120. $.fn.trumbowyg = function (options, params) {
  121. var trumbowygDataName = 'trumbowyg';
  122. if (options === Object(options) || !options) {
  123. return this.each(function () {
  124. if (!$(this).data(trumbowygDataName)) {
  125. $(this).data(trumbowygDataName, new Trumbowyg(this, options));
  126. }
  127. });
  128. }
  129. if (this.length === 1) {
  130. try {
  131. var t = $(this).data(trumbowygDataName);
  132. switch (options) {
  133. // Exec command
  134. case 'execCmd':
  135. return t.execCmd(params.cmd, params.param, params.forceCss, params.skipTrumbowyg);
  136. // Modal box
  137. case 'openModal':
  138. return t.openModal(params.title, params.content);
  139. case 'closeModal':
  140. return t.closeModal();
  141. case 'openModalInsert':
  142. return t.openModalInsert(params.title, params.fields, params.callback);
  143. // Range
  144. case 'saveRange':
  145. return t.saveRange();
  146. case 'getRange':
  147. return t.range;
  148. case 'getRangeText':
  149. return t.getRangeText();
  150. case 'restoreRange':
  151. return t.restoreRange();
  152. // Enable/disable
  153. case 'enable':
  154. return t.setDisabled(false);
  155. case 'disable':
  156. return t.setDisabled(true);
  157. // Toggle
  158. case 'toggle':
  159. return t.toggle();
  160. // Destroy
  161. case 'destroy':
  162. return t.destroy();
  163. // Empty
  164. case 'empty':
  165. return t.empty();
  166. // HTML
  167. case 'html':
  168. return t.html(params);
  169. }
  170. } catch (c) {
  171. }
  172. }
  173. return false;
  174. };
  175. // @param: editorElem is the DOM element
  176. var Trumbowyg = function (editorElem, options) {
  177. var t = this,
  178. trumbowygIconsId = 'trumbowyg-icons',
  179. $trumbowyg = $.trumbowyg;
  180. // Get the document of the element. It use to makes the plugin
  181. // compatible on iframes.
  182. t.doc = editorElem.ownerDocument || document;
  183. // jQuery object of the editor
  184. t.$ta = $(editorElem); // $ta : Textarea
  185. t.$c = $(editorElem); // $c : creator
  186. options = options || {};
  187. // Localization management
  188. if (options.lang != null || $trumbowyg.langs[options.lang] != null) {
  189. t.lang = $.extend(true, {}, $trumbowyg.langs.en, $trumbowyg.langs[options.lang]);
  190. } else {
  191. t.lang = $trumbowyg.langs.en;
  192. }
  193. t.hideButtonTexts = $trumbowyg.hideButtonTexts != null ? $trumbowyg.hideButtonTexts : options.hideButtonTexts;
  194. // SVG path
  195. var svgPathOption = $trumbowyg.svgPath != null ? $trumbowyg.svgPath : options.svgPath;
  196. t.hasSvg = svgPathOption !== false;
  197. if (svgPathOption !== false && ($trumbowyg.svgAbsoluteUseHref || $('#' + trumbowygIconsId, t.doc).length === 0)) {
  198. if (svgPathOption == null) {
  199. // Hack to get svgPathOption based on trumbowyg.js path
  200. var $scriptElements = $('script[src]');
  201. $scriptElements.each(function (i, scriptElement) {
  202. var source = scriptElement.src;
  203. var matches = source.match('trumbowyg(\.min)?\.js');
  204. if (matches != null) {
  205. svgPathOption = source.substring(0, source.indexOf(matches[0])) + 'ui/icons.svg';
  206. }
  207. })
  208. }
  209. // Do not merge with previous if block: svgPathOption can be redefined in it.
  210. // Here we are checking that we find a match
  211. if (svgPathOption == null) {
  212. console.warn('You must define svgPath: https://goo.gl/CfTY9U'); // jshint ignore:line
  213. } else if (!$trumbowyg.svgAbsoluteUseHref) {
  214. var div = t.doc.createElement('div');
  215. div.id = trumbowygIconsId;
  216. t.doc.body.insertBefore(div, t.doc.body.childNodes[0]);
  217. $.ajax({
  218. async: true,
  219. type: 'GET',
  220. contentType: 'application/x-www-form-urlencoded; charset=UTF-8',
  221. dataType: 'xml',
  222. crossDomain: true,
  223. url: svgPathOption,
  224. data: null,
  225. beforeSend: null,
  226. complete: null,
  227. success: function (data) {
  228. div.innerHTML = new XMLSerializer().serializeToString(data.documentElement);
  229. }
  230. });
  231. }
  232. }
  233. var baseHref = !!t.doc.querySelector('base') ? window.location.href.split(/[?#]/)[0] : '';
  234. t.svgPath = $trumbowyg.svgAbsoluteUseHref ? svgPathOption : baseHref;
  235. /**
  236. * When the button is associated to a empty object
  237. * fn and title attributes are defined from the button key value
  238. *
  239. * For example
  240. * foo: {}
  241. * is equivalent to :
  242. * foo: {
  243. * fn: 'foo',
  244. * title: this.lang.foo
  245. * }
  246. */
  247. var h = t.lang.header, // Header translation
  248. isBlinkFunction = function () {
  249. return (window.chrome || (window.Intl && Intl.v8BreakIterator)) && 'CSS' in window;
  250. };
  251. t.btnsDef = {
  252. viewHTML: {
  253. fn: 'toggle',
  254. class: 'trumbowyg-not-disable',
  255. },
  256. undo: {
  257. isSupported: isBlinkFunction,
  258. key: 'Z'
  259. },
  260. redo: {
  261. isSupported: isBlinkFunction,
  262. key: 'Y'
  263. },
  264. p: {
  265. fn: 'formatBlock'
  266. },
  267. blockquote: {
  268. fn: 'formatBlock'
  269. },
  270. h1: {
  271. fn: 'formatBlock',
  272. title: h + ' 1'
  273. },
  274. h2: {
  275. fn: 'formatBlock',
  276. title: h + ' 2'
  277. },
  278. h3: {
  279. fn: 'formatBlock',
  280. title: h + ' 3'
  281. },
  282. h4: {
  283. fn: 'formatBlock',
  284. title: h + ' 4'
  285. },
  286. h5: {
  287. fn: 'formatBlock',
  288. title: h + ' 5'
  289. },
  290. h6: {
  291. fn: 'formatBlock',
  292. title: h + ' 6'
  293. },
  294. subscript: {
  295. tag: 'sub'
  296. },
  297. superscript: {
  298. tag: 'sup'
  299. },
  300. bold: {
  301. key: 'B',
  302. tag: 'b'
  303. },
  304. italic: {
  305. key: 'I',
  306. tag: 'i'
  307. },
  308. underline: {
  309. tag: 'u'
  310. },
  311. strikethrough: {
  312. tag: 'strike'
  313. },
  314. strong: {
  315. fn: 'bold',
  316. key: 'B'
  317. },
  318. em: {
  319. fn: 'italic',
  320. key: 'I'
  321. },
  322. del: {
  323. fn: 'strikethrough'
  324. },
  325. createLink: {
  326. key: 'K',
  327. tag: 'a'
  328. },
  329. unlink: {},
  330. insertImage: {},
  331. justifyLeft: {
  332. tag: 'left',
  333. forceCss: true
  334. },
  335. justifyCenter: {
  336. tag: 'center',
  337. forceCss: true
  338. },
  339. justifyRight: {
  340. tag: 'right',
  341. forceCss: true
  342. },
  343. justifyFull: {
  344. tag: 'justify',
  345. forceCss: true
  346. },
  347. unorderedList: {
  348. fn: 'insertUnorderedList',
  349. tag: 'ul'
  350. },
  351. orderedList: {
  352. fn: 'insertOrderedList',
  353. tag: 'ol'
  354. },
  355. horizontalRule: {
  356. fn: 'insertHorizontalRule'
  357. },
  358. removeformat: {},
  359. fullscreen: {
  360. class: 'trumbowyg-not-disable'
  361. },
  362. close: {
  363. fn: 'destroy',
  364. class: 'trumbowyg-not-disable'
  365. },
  366. // Dropdowns
  367. formatting: {
  368. dropdown: ['p', 'blockquote', 'h1', 'h2', 'h3', 'h4'],
  369. ico: 'p'
  370. },
  371. link: {
  372. dropdown: ['createLink', 'unlink']
  373. }
  374. };
  375. // Default Options
  376. t.o = $.extend(true, {}, $trumbowyg.defaultOptions, options);
  377. if (!t.o.hasOwnProperty('imgDblClickHandler')) {
  378. t.o.imgDblClickHandler = t.getDefaultImgDblClickHandler();
  379. }
  380. t.urlPrefix = t.setupUrlPrefix();
  381. t.disabled = t.o.disabled || (editorElem.nodeName === 'TEXTAREA' && editorElem.disabled);
  382. if (options.btns) {
  383. t.o.btns = options.btns;
  384. } else if (!t.o.semantic) {
  385. t.o.btns[3] = ['bold', 'italic', 'underline', 'strikethrough'];
  386. }
  387. $.each(t.o.btnsDef, function (btnName, btnDef) {
  388. t.addBtnDef(btnName, btnDef);
  389. });
  390. // put this here in the event it would be merged in with options
  391. t.eventNamespace = 'trumbowyg-event';
  392. // Keyboard shortcuts are load in this array
  393. t.keys = [];
  394. // Tag to button dynamically hydrated
  395. t.tagToButton = {};
  396. t.tagHandlers = [];
  397. // Admit multiple paste handlers
  398. t.pasteHandlers = [].concat(t.o.pasteHandlers);
  399. // Check if browser is IE
  400. t.isIE = navigator.userAgent.indexOf('MSIE') !== -1 || navigator.appVersion.indexOf('Trident/') !== -1;
  401. // Check if we are on macOs
  402. t.isMac = navigator.platform.toUpperCase().indexOf('MAC') !== -1;
  403. t.init();
  404. };
  405. Trumbowyg.prototype = {
  406. DEFAULT_SEMANTIC_MAP: {
  407. 'b': 'strong',
  408. 'i': 'em',
  409. 's': 'del',
  410. 'strike': 'del',
  411. 'div': 'p'
  412. },
  413. init: function () {
  414. var t = this;
  415. t.height = t.$ta.height();
  416. t.initPlugins();
  417. try {
  418. // Disable image resize, try-catch for old IE
  419. t.doc.execCommand('enableObjectResizing', false, false);
  420. t.doc.execCommand('defaultParagraphSeparator', false, 'p');
  421. } catch (e) {
  422. }
  423. t.buildEditor();
  424. t.buildBtnPane();
  425. t.fixedBtnPaneEvents();
  426. t.buildOverlay();
  427. setTimeout(function () {
  428. if (t.disabled) {
  429. t.setDisabled(true);
  430. }
  431. t.$c.trigger('tbwinit');
  432. });
  433. },
  434. addBtnDef: function (btnName, btnDef) {
  435. this.btnsDef[btnName] = $.extend(btnDef, this.btnsDef[btnName] || {});
  436. },
  437. setupUrlPrefix: function () {
  438. var protocol = this.o.urlProtocol;
  439. if (!protocol) {
  440. return;
  441. }
  442. if (typeof (protocol) !== 'string') {
  443. return 'https://';
  444. }
  445. return protocol.replace('://', '') + '://';
  446. },
  447. buildEditor: function () {
  448. var t = this,
  449. prefix = t.o.prefix,
  450. html = '';
  451. t.$box = $('<div/>', {
  452. class: prefix + 'box ' + prefix + 'editor-visible ' + prefix + t.o.lang + ' trumbowyg'
  453. });
  454. // $ta = Textarea
  455. // $ed = Editor
  456. t.isTextarea = t.$ta.is('textarea');
  457. if (t.isTextarea) {
  458. html = t.$ta.val();
  459. t.$ed = $('<div/>');
  460. t.$box
  461. .insertAfter(t.$ta)
  462. .append(t.$ed, t.$ta);
  463. } else {
  464. t.$ed = t.$ta;
  465. html = t.$ed.html();
  466. t.$ta = $('<textarea/>', {
  467. name: t.$ta.attr('id'),
  468. height: t.height
  469. }).val(html);
  470. t.$box
  471. .insertAfter(t.$ed)
  472. .append(t.$ta, t.$ed);
  473. t.syncCode();
  474. }
  475. t.$ta
  476. .addClass(prefix + 'textarea')
  477. .attr('tabindex', -1)
  478. ;
  479. t.$ed
  480. .addClass(prefix + 'editor')
  481. .attr({
  482. contenteditable: true,
  483. dir: t.lang._dir || 'ltr'
  484. })
  485. .html(html)
  486. ;
  487. if (t.o.tabindex) {
  488. t.$ed.attr('tabindex', t.o.tabindex);
  489. }
  490. if (t.$c.is('[placeholder]')) {
  491. t.$ed.attr('placeholder', t.$c.attr('placeholder'));
  492. }
  493. if (t.$c.is('[spellcheck]')) {
  494. t.$ed.attr('spellcheck', t.$c.attr('spellcheck'));
  495. }
  496. if (t.o.resetCss) {
  497. t.$ed.addClass(prefix + 'reset-css');
  498. }
  499. if (!t.o.autogrow) {
  500. t.$ta.add(t.$ed).css({
  501. height: t.height
  502. });
  503. }
  504. t.semanticCode();
  505. if (t.o.autogrowOnEnter) {
  506. t.$ed.addClass(prefix + 'autogrow-on-enter');
  507. }
  508. var ctrl = false,
  509. composition = false,
  510. debounceButtonPaneStatus;
  511. t.$ed
  512. .on('dblclick', 'img', t.o.imgDblClickHandler)
  513. .on('keydown', function (e) {
  514. // append flags to differentiate Chrome spans
  515. var keyCode = e.which;
  516. if (keyCode === 8 || keyCode === 13 || keyCode === 46) {
  517. t.toggleSpan(true);
  518. }
  519. if ((e.ctrlKey || e.metaKey) && !e.altKey) {
  520. ctrl = true;
  521. var key = t.keys[String.fromCharCode(e.which).toUpperCase()];
  522. try {
  523. t.execCmd(key.fn, key.param);
  524. return false;
  525. } catch (c) {
  526. }
  527. } else {
  528. if (t.o.tabToIndent && e.key === 'Tab') {
  529. try {
  530. if (e.shiftKey) {
  531. t.execCmd('outdent', true, null);
  532. } else {
  533. t.execCmd('indent', true, null);
  534. }
  535. return false;
  536. } catch (c) {
  537. }
  538. }
  539. }
  540. })
  541. .on('compositionstart compositionupdate', function () {
  542. composition = true;
  543. })
  544. .on('keyup compositionend', function (e) {
  545. if (e.type === 'compositionend') {
  546. composition = false;
  547. } else if (composition) {
  548. return;
  549. }
  550. var keyCode = e.which;
  551. if (keyCode >= 37 && keyCode <= 40) {
  552. return;
  553. }
  554. // remove Chrome generated span tags
  555. if (keyCode === 8 || keyCode === 13 || keyCode === 46) {
  556. t.toggleSpan();
  557. }
  558. if ((e.ctrlKey || e.metaKey) && (keyCode === 89 || keyCode === 90)) {
  559. t.semanticCode(false, true);
  560. t.$c.trigger('tbwchange');
  561. } else if (!ctrl && keyCode !== 17) {
  562. var compositionEndIE = t.isIE ? e.type === 'compositionend' : true;
  563. t.semanticCode(false, compositionEndIE && keyCode === 13);
  564. t.$c.trigger('tbwchange');
  565. } else if (typeof e.which === 'undefined') {
  566. t.semanticCode(false, false, true);
  567. }
  568. setTimeout(function () {
  569. ctrl = false;
  570. }, 50);
  571. })
  572. .on('mouseup keydown keyup', function (e) {
  573. if ((!e.ctrlKey && !e.metaKey) || e.altKey) {
  574. setTimeout(function () { // "hold on" to the ctrl key for 50ms
  575. ctrl = false;
  576. }, 50);
  577. }
  578. clearTimeout(debounceButtonPaneStatus);
  579. debounceButtonPaneStatus = setTimeout(function () {
  580. t.updateButtonPaneStatus();
  581. }, 50);
  582. })
  583. .on('focus blur', function (e) {
  584. if (e.type === 'blur') {
  585. t.clearButtonPaneStatus();
  586. }
  587. t.$c.trigger('tbw' + e.type);
  588. if (t.o.autogrowOnEnter) {
  589. if (t.autogrowOnEnterDontClose) {
  590. return;
  591. }
  592. if (e.type === 'focus') {
  593. t.autogrowOnEnterWasFocused = true;
  594. t.autogrowEditorOnEnter();
  595. } else if (!t.o.autogrow) {
  596. t.$ed.css({height: t.$ed.css('min-height')});
  597. t.$c.trigger('tbwresize');
  598. }
  599. }
  600. })
  601. .on('keyup focus', function () {
  602. if (!t.$ta.val().match(/<.*>/) && !t.$ed.html().match(/<.*>/)) {
  603. setTimeout(function () {
  604. var block = t.isIE ? '<p>' : 'p';
  605. t.doc.execCommand('formatBlock', false, block);
  606. t.syncCode();
  607. }, 0);
  608. }
  609. })
  610. .on('cut drop', function () {
  611. setTimeout(function () {
  612. t.semanticCode(false, true);
  613. t.$c.trigger('tbwchange');
  614. }, 0);
  615. })
  616. .on('paste', function (e) {
  617. if (t.o.removeformatPasted) {
  618. e.preventDefault();
  619. if (window.getSelection && window.getSelection().deleteFromDocument) {
  620. window.getSelection().deleteFromDocument();
  621. }
  622. try {
  623. // IE
  624. var text = window.clipboardData.getData('Text');
  625. try {
  626. // <= IE10
  627. t.doc.selection.createRange().pasteHTML(text);
  628. } catch (c) {
  629. // IE 11
  630. t.doc.getSelection().getRangeAt(0).insertNode(t.doc.createTextNode(text));
  631. }
  632. t.$c.trigger('tbwchange', e);
  633. } catch (d) {
  634. // Not IE
  635. t.execCmd('insertText', (e.originalEvent || e).clipboardData.getData('text/plain'));
  636. }
  637. }
  638. // Call pasteHandlers
  639. $.each(t.pasteHandlers, function (i, pasteHandler) {
  640. pasteHandler(e);
  641. });
  642. setTimeout(function () {
  643. t.semanticCode(false, true);
  644. t.$c.trigger('tbwpaste', e);
  645. t.$c.trigger('tbwchange');
  646. }, 0);
  647. });
  648. t.$ta
  649. .on('keyup', function () {
  650. t.$c.trigger('tbwchange');
  651. })
  652. .on('paste', function () {
  653. setTimeout(function () {
  654. t.$c.trigger('tbwchange');
  655. }, 0);
  656. });
  657. $(t.doc.body).on('keydown.' + t.eventNamespace, function (e) {
  658. if (e.which === 27 && $('.' + prefix + 'modal-box').length >= 1) {
  659. t.closeModal();
  660. return false;
  661. }
  662. });
  663. },
  664. //autogrow when entering logic
  665. autogrowEditorOnEnter: function () {
  666. var t = this;
  667. t.$ed.removeClass('autogrow-on-enter');
  668. var oldHeight = t.$ed[0].clientHeight;
  669. t.$ed.height('auto');
  670. var totalHeight = t.$ed[0].scrollHeight;
  671. t.$ed.addClass('autogrow-on-enter');
  672. if (oldHeight !== totalHeight) {
  673. t.$ed.height(oldHeight);
  674. setTimeout(function () {
  675. t.$ed.css({height: totalHeight});
  676. t.$c.trigger('tbwresize');
  677. }, 0);
  678. }
  679. },
  680. // Build button pane, use o.btns option
  681. buildBtnPane: function () {
  682. var t = this,
  683. prefix = t.o.prefix;
  684. var $btnPane = t.$btnPane = $('<div/>', {
  685. class: prefix + 'button-pane'
  686. });
  687. $.each(t.o.btns, function (i, btnGrp) {
  688. if (!$.isArray(btnGrp)) {
  689. btnGrp = [btnGrp];
  690. }
  691. var $btnGroup = $('<div/>', {
  692. class: prefix + 'button-group ' + ((btnGrp.indexOf('fullscreen') >= 0) ? prefix + 'right' : '')
  693. });
  694. $.each(btnGrp, function (i, btn) {
  695. try { // Prevent buildBtn error
  696. if (t.isSupportedBtn(btn)) { // It's a supported button
  697. $btnGroup.append(t.buildBtn(btn));
  698. }
  699. } catch (c) {
  700. }
  701. });
  702. if ($btnGroup.html().trim().length > 0) {
  703. $btnPane.append($btnGroup);
  704. }
  705. });
  706. t.$box.prepend($btnPane);
  707. },
  708. // Build a button and his action
  709. buildBtn: function (btnName) { // btnName is name of the button
  710. var t = this,
  711. prefix = t.o.prefix,
  712. btn = t.btnsDef[btnName],
  713. isDropdown = btn.dropdown,
  714. hasIcon = btn.hasIcon != null ? btn.hasIcon : true,
  715. textDef = t.lang[btnName] || btnName,
  716. $btn = $('<button/>', {
  717. type: 'button',
  718. class: prefix + btnName + '-button ' + (btn.class || '') + (!hasIcon ? ' ' + prefix + 'textual-button' : ''),
  719. html: t.hasSvg && hasIcon ?
  720. '<svg><use xlink:href="' + t.svgPath + '#' + prefix + (btn.ico || btnName).replace(/([A-Z]+)/g, '-$1').toLowerCase() + '"/></svg>' :
  721. t.hideButtonTexts ? '' : (btn.text || btn.title || t.lang[btnName] || btnName),
  722. title: (btn.title || btn.text || textDef) + (btn.key ? ' (' + (t.isMac ? 'Cmd' : 'Ctrl') + ' + ' + btn.key + ')' : ''),
  723. tabindex: -1,
  724. mousedown: function () {
  725. if (!isDropdown || $('.' + btnName + '-' + prefix + 'dropdown', t.$box).is(':hidden')) {
  726. $('body', t.doc).trigger('mousedown');
  727. }
  728. if ((t.$btnPane.hasClass(prefix + 'disable') || t.$box.hasClass(prefix + 'disabled')) &&
  729. !$(this).hasClass(prefix + 'active') &&
  730. !$(this).hasClass(prefix + 'not-disable')) {
  731. return false;
  732. }
  733. t.execCmd((isDropdown ? 'dropdown' : false) || btn.fn || btnName, btn.param || btnName, btn.forceCss);
  734. return false;
  735. }
  736. });
  737. if (isDropdown) {
  738. $btn.addClass(prefix + 'open-dropdown');
  739. var dropdownPrefix = prefix + 'dropdown',
  740. dropdownOptions = { // the dropdown
  741. class: dropdownPrefix + '-' + btnName + ' ' + dropdownPrefix + ' ' + prefix + 'fixed-top ' + (btn.dropdownClass || '')
  742. };
  743. dropdownOptions['data-' + dropdownPrefix] = btnName;
  744. var $dropdown = $('<div/>', dropdownOptions);
  745. $.each(isDropdown, function (i, def) {
  746. if (t.btnsDef[def] && t.isSupportedBtn(def)) {
  747. $dropdown.append(t.buildSubBtn(def));
  748. }
  749. });
  750. t.$box.append($dropdown.hide());
  751. } else if (btn.key) {
  752. t.keys[btn.key] = {
  753. fn: btn.fn || btnName,
  754. param: btn.param || btnName
  755. };
  756. }
  757. if (!isDropdown) {
  758. t.tagToButton[(btn.tag || btnName).toLowerCase()] = btnName;
  759. }
  760. return $btn;
  761. },
  762. // Build a button for dropdown menu
  763. // @param n : name of the subbutton
  764. buildSubBtn: function (btnName) {
  765. var t = this,
  766. prefix = t.o.prefix,
  767. btn = t.btnsDef[btnName],
  768. hasIcon = btn.hasIcon != null ? btn.hasIcon : true;
  769. if (btn.key) {
  770. t.keys[btn.key] = {
  771. fn: btn.fn || btnName,
  772. param: btn.param || btnName
  773. };
  774. }
  775. t.tagToButton[(btn.tag || btnName).toLowerCase()] = btnName;
  776. return $('<button/>', {
  777. type: 'button',
  778. class: prefix + btnName + '-dropdown-button ' + (btn.class || '') + (btn.ico ? ' ' + prefix + btn.ico + '-button' : ''),
  779. html: t.hasSvg && hasIcon ?
  780. '<svg><use xlink:href="' + t.svgPath + '#' + prefix + (btn.ico || btnName).replace(/([A-Z]+)/g, '-$1').toLowerCase() + '"/></svg>' + (btn.text || btn.title || t.lang[btnName] || btnName) :
  781. (btn.text || btn.title || t.lang[btnName] || btnName),
  782. title: (btn.key ? '(' + (t.isMac ? 'Cmd' : 'Ctrl') + ' + ' + btn.key + ')' : null),
  783. style: btn.style || null,
  784. mousedown: function () {
  785. $('body', t.doc).trigger('mousedown');
  786. t.execCmd(btn.fn || btnName, btn.param || btnName, btn.forceCss);
  787. return false;
  788. }
  789. });
  790. },
  791. // Check if button is supported
  792. isSupportedBtn: function (btnName) {
  793. try {
  794. return this.btnsDef[btnName].isSupported();
  795. } catch (e) {
  796. }
  797. return true;
  798. },
  799. // Build overlay for modal box
  800. buildOverlay: function () {
  801. var t = this;
  802. t.$overlay = $('<div/>', {
  803. class: t.o.prefix + 'overlay'
  804. }).appendTo(t.$box);
  805. return t.$overlay;
  806. },
  807. showOverlay: function () {
  808. var t = this;
  809. $(window).trigger('scroll');
  810. t.$overlay.fadeIn(200);
  811. t.$box.addClass(t.o.prefix + 'box-blur');
  812. },
  813. hideOverlay: function () {
  814. var t = this;
  815. t.$overlay.fadeOut(50);
  816. t.$box.removeClass(t.o.prefix + 'box-blur');
  817. },
  818. // Management of fixed button pane
  819. fixedBtnPaneEvents: function () {
  820. var t = this,
  821. fixedFullWidth = t.o.fixedFullWidth,
  822. $box = t.$box;
  823. if (!t.o.fixedBtnPane) {
  824. return;
  825. }
  826. t.isFixed = false;
  827. $(window)
  828. .on('scroll.' + t.eventNamespace + ' resize.' + t.eventNamespace, function () {
  829. if (!$box) {
  830. return;
  831. }
  832. t.syncCode();
  833. var scrollTop = $(window).scrollTop(),
  834. offset = $box.offset().top + 1,
  835. $buttonPane = t.$btnPane,
  836. buttonPaneOuterHeight = $buttonPane.outerHeight() - 2;
  837. if ((scrollTop - offset > 0) && ((scrollTop - offset - t.height) < 0)) {
  838. if (!t.isFixed) {
  839. t.isFixed = true;
  840. $buttonPane.css({
  841. position: 'fixed',
  842. top: 0,
  843. left: fixedFullWidth ? 0 : 'auto',
  844. zIndex: 7
  845. });
  846. t.$box.css({paddingTop: $buttonPane.height()});
  847. }
  848. $buttonPane.css({
  849. width: fixedFullWidth ? '100%' : (($box.width() - 1))
  850. });
  851. $('.' + t.o.prefix + 'fixed-top', $box).css({
  852. position: fixedFullWidth ? 'fixed' : 'absolute',
  853. top: fixedFullWidth ? buttonPaneOuterHeight : buttonPaneOuterHeight + (scrollTop - offset),
  854. zIndex: 15
  855. });
  856. } else if (t.isFixed) {
  857. t.isFixed = false;
  858. $buttonPane.removeAttr('style');
  859. t.$box.css({paddingTop: 0});
  860. $('.' + t.o.prefix + 'fixed-top', $box).css({
  861. position: 'absolute',
  862. top: buttonPaneOuterHeight
  863. });
  864. }
  865. });
  866. },
  867. // Disable editor
  868. setDisabled: function (disable) {
  869. var t = this,
  870. prefix = t.o.prefix;
  871. t.disabled = disable;
  872. if (disable) {
  873. t.$ta.attr('disabled', true);
  874. } else {
  875. t.$ta.removeAttr('disabled');
  876. }
  877. t.$box.toggleClass(prefix + 'disabled', disable);
  878. t.$ed.attr('contenteditable', !disable);
  879. },
  880. // Destroy the editor
  881. destroy: function () {
  882. var t = this,
  883. prefix = t.o.prefix;
  884. if (t.isTextarea) {
  885. t.$box.after(
  886. t.$ta
  887. .css({height: ''})
  888. .val(t.html())
  889. .removeClass(prefix + 'textarea')
  890. .show()
  891. );
  892. } else {
  893. t.$box.after(
  894. t.$ed
  895. .css({height: ''})
  896. .removeClass(prefix + 'editor')
  897. .removeAttr('contenteditable')
  898. .removeAttr('dir')
  899. .html(t.html())
  900. .show()
  901. );
  902. }
  903. t.$ed.off('dblclick', 'img');
  904. t.destroyPlugins();
  905. t.$box.remove();
  906. t.$c.removeData('trumbowyg');
  907. $('body').removeClass(prefix + 'body-fullscreen');
  908. t.$c.trigger('tbwclose');
  909. $(window).off('scroll.' + t.eventNamespace + ' resize.' + t.eventNamespace);
  910. $(t.doc.body).off('keydown.' + t.eventNamespace);
  911. },
  912. // Empty the editor
  913. empty: function () {
  914. this.$ta.val('');
  915. this.syncCode(true);
  916. },
  917. // Function call when click on viewHTML button
  918. toggle: function () {
  919. var t = this,
  920. prefix = t.o.prefix;
  921. if (t.o.autogrowOnEnter) {
  922. t.autogrowOnEnterDontClose = !t.$box.hasClass(prefix + 'editor-hidden');
  923. }
  924. t.semanticCode(false, true);
  925. t.$c.trigger('tbwchange');
  926. setTimeout(function () {
  927. t.doc.activeElement.blur();
  928. t.$box.toggleClass(prefix + 'editor-hidden ' + prefix + 'editor-visible');
  929. t.$btnPane.toggleClass(prefix + 'disable');
  930. $('.' + prefix + 'viewHTML-button', t.$btnPane).toggleClass(prefix + 'active');
  931. if (t.$box.hasClass(prefix + 'editor-visible')) {
  932. t.$ta.attr('tabindex', -1);
  933. } else {
  934. t.$ta.removeAttr('tabindex');
  935. }
  936. if (t.o.autogrowOnEnter && !t.autogrowOnEnterDontClose) {
  937. t.autogrowEditorOnEnter();
  938. }
  939. }, 0);
  940. },
  941. // Remove or add flags to span tags to remove Chrome generated spans
  942. toggleSpan: function (addFlag) {
  943. var t = this;
  944. t.$ed.find('span').each(function () {
  945. if (addFlag === true) {
  946. $(this).attr('data-tbw-flag', true);
  947. } else {
  948. if ($(this).attr('data-tbw-flag')) {
  949. $(this).removeAttr('data-tbw-flag');
  950. } else {
  951. $(this).contents().unwrap();
  952. }
  953. }
  954. });
  955. },
  956. // Open dropdown when click on a button which open that
  957. dropdown: function (name) {
  958. var t = this,
  959. $body = $('body', t.doc),
  960. prefix = t.o.prefix,
  961. $dropdown = $('[data-' + prefix + 'dropdown=' + name + ']', t.$box),
  962. $btn = $('.' + prefix + name + '-button', t.$btnPane),
  963. show = $dropdown.is(':hidden');
  964. $body.trigger('mousedown');
  965. if (show) {
  966. var btnOffsetLeft = $btn.offset().left;
  967. $btn.addClass(prefix + 'active');
  968. $dropdown.css({
  969. position: 'absolute',
  970. top: $btn.offset().top - t.$btnPane.offset().top + $btn.outerHeight(),
  971. left: (t.o.fixedFullWidth && t.isFixed) ? btnOffsetLeft : (btnOffsetLeft - t.$btnPane.offset().left)
  972. }).show();
  973. $(window).trigger('scroll');
  974. $body.on('mousedown.' + t.eventNamespace, function (e) {
  975. if (!$dropdown.is(e.target)) {
  976. $('.' + prefix + 'dropdown', t.$box).hide();
  977. $('.' + prefix + 'active', t.$btnPane).removeClass(prefix + 'active');
  978. $body.off('mousedown.' + t.eventNamespace);
  979. }
  980. });
  981. }
  982. },
  983. // HTML Code management
  984. html: function (html) {
  985. var t = this;
  986. if (html != null) {
  987. t.$ta.val(html);
  988. t.syncCode(true);
  989. t.$c.trigger('tbwchange');
  990. return t;
  991. }
  992. return t.$ta.val();
  993. },
  994. syncTextarea: function () {
  995. var t = this;
  996. t.$ta.val(t.$ed.text().trim().length > 0 || t.$ed.find(t.o.tagsToKeep.join(',')).length > 0 ? t.$ed.html() : '');
  997. },
  998. syncCode: function (force) {
  999. var t = this;
  1000. if (!force && t.$ed.is(':visible')) {
  1001. t.syncTextarea();
  1002. } else {
  1003. // wrap the content in a div it's easier to get the inner html
  1004. var html = $('<div>').html(t.$ta.val());
  1005. // scrub the html before loading into the doc
  1006. var safe = $('<div>').append(html);
  1007. $(t.o.tagsToRemove.join(','), safe).remove();
  1008. t.$ed.html(safe.contents().html());
  1009. }
  1010. if (t.o.autogrow) {
  1011. t.height = t.$ed.height();
  1012. if (t.height !== t.$ta.css('height')) {
  1013. t.$ta.css({height: t.height});
  1014. t.$c.trigger('tbwresize');
  1015. }
  1016. }
  1017. if (t.o.autogrowOnEnter) {
  1018. t.$ed.height('auto');
  1019. var totalHeight = t.autogrowOnEnterWasFocused ? t.$ed[0].scrollHeight : t.$ed.css('min-height');
  1020. if (totalHeight !== t.$ta.css('height')) {
  1021. t.$ed.css({height: totalHeight});
  1022. t.$c.trigger('tbwresize');
  1023. }
  1024. }
  1025. },
  1026. // Analyse and update to semantic code
  1027. // @param force : force to sync code from textarea
  1028. // @param full : wrap text nodes in <p>
  1029. // @param keepRange : leave selection range as it is
  1030. semanticCode: function (force, full, keepRange) {
  1031. var t = this;
  1032. t.saveRange();
  1033. t.syncCode(force);
  1034. var restoreRange = true;
  1035. if (t.range && t.range.collapsed) {
  1036. restoreRange = false;
  1037. }
  1038. if (t.o.semantic) {
  1039. t.semanticTag('b', t.o.semanticKeepAttributes);
  1040. t.semanticTag('i', t.o.semanticKeepAttributes);
  1041. t.semanticTag('s', t.o.semanticKeepAttributes);
  1042. t.semanticTag('strike', t.o.semanticKeepAttributes);
  1043. if (full) {
  1044. var inlineElementsSelector = t.o.inlineElementsSelector,
  1045. blockElementsSelector = ':not(' + inlineElementsSelector + ')';
  1046. // Wrap text nodes in span for easier processing
  1047. t.$ed.contents().filter(function () {
  1048. return this.nodeType === 3 && this.nodeValue.trim().length > 0;
  1049. }).wrap('<span data-tbw/>');
  1050. // Wrap groups of inline elements in paragraphs (recursive)
  1051. var wrapInlinesInParagraphsFrom = function ($from) {
  1052. if ($from.length !== 0) {
  1053. var $finalParagraph = $from.nextUntil(blockElementsSelector).addBack().wrapAll('<p/>').parent(),
  1054. $nextElement = $finalParagraph.nextAll(inlineElementsSelector).first();
  1055. $finalParagraph.next('br').remove();
  1056. wrapInlinesInParagraphsFrom($nextElement);
  1057. }
  1058. };
  1059. wrapInlinesInParagraphsFrom(t.$ed.children(inlineElementsSelector).first());
  1060. t.semanticTag('div', true);
  1061. // Get rid of temporary span's
  1062. $('[data-tbw]', t.$ed).contents().unwrap();
  1063. // Remove empty <p>
  1064. t.$ed.find('p:empty').remove();
  1065. }
  1066. if (!keepRange && restoreRange) {
  1067. t.restoreRange();
  1068. }
  1069. t.syncTextarea();
  1070. }
  1071. },
  1072. semanticTag: function (oldTag, copyAttributes, revert) {
  1073. var newTag, t = this;
  1074. var tmpTag = oldTag;
  1075. if (this.o.semantic != null && typeof this.o.semantic === 'object' && this.o.semantic.hasOwnProperty(oldTag)) {
  1076. newTag = this.o.semantic[oldTag];
  1077. } else if (this.o.semantic === true && this.DEFAULT_SEMANTIC_MAP.hasOwnProperty(oldTag)) {
  1078. newTag = this.DEFAULT_SEMANTIC_MAP[oldTag];
  1079. } else {
  1080. return;
  1081. }
  1082. if(revert) {
  1083. oldTag = newTag;
  1084. newTag = tmpTag;
  1085. }
  1086. $(oldTag, this.$ed).each(function () {
  1087. var resetRange = false;
  1088. var $oldTag = $(this);
  1089. if ($oldTag.contents().length === 0) {
  1090. return false;
  1091. }
  1092. if(t.range.startContainer.parentNode && t.range.startContainer.parentNode === this) {
  1093. resetRange = true;
  1094. }
  1095. var $newTag = $('<' + newTag + '/>');
  1096. $newTag.insertBefore($oldTag);
  1097. if (copyAttributes) {
  1098. $.each($oldTag.prop('attributes'), function () {
  1099. $newTag.attr(this.name, this.value);
  1100. });
  1101. }
  1102. $newTag.html($oldTag.html());
  1103. $oldTag.remove();
  1104. if(resetRange === true) {
  1105. t.range.selectNodeContents($newTag.get(0));
  1106. t.range.collapse(false);
  1107. }
  1108. });
  1109. },
  1110. // Function call when user click on "Insert Link"
  1111. createLink: function () {
  1112. var t = this,
  1113. documentSelection = t.doc.getSelection(),
  1114. selectedRange = documentSelection.getRangeAt(0),
  1115. node = documentSelection.focusNode,
  1116. text = new XMLSerializer().serializeToString(selectedRange.cloneContents()) || selectedRange + '',
  1117. url,
  1118. title,
  1119. target;
  1120. while (['A', 'DIV'].indexOf(node.nodeName) < 0) {
  1121. node = node.parentNode;
  1122. }
  1123. if (node && node.nodeName === 'A') {
  1124. var $a = $(node);
  1125. text = $a.text();
  1126. url = $a.attr('href');
  1127. if (!t.o.minimalLinks) {
  1128. title = $a.attr('title');
  1129. target = $a.attr('target') || t.o.defaultLinkTarget;
  1130. }
  1131. var range = t.doc.createRange();
  1132. range.selectNode(node);
  1133. documentSelection.removeAllRanges();
  1134. documentSelection.addRange(range);
  1135. }
  1136. t.saveRange();
  1137. var options = {
  1138. url: {
  1139. label: t.lang.linkUrl || 'URL',
  1140. required: true,
  1141. value: url
  1142. },
  1143. text: {
  1144. label: t.lang.text,
  1145. value: text
  1146. }
  1147. };
  1148. if (!t.o.minimalLinks) {
  1149. $.extend(options, {
  1150. title: {
  1151. label: t.lang.title,
  1152. value: title
  1153. },
  1154. target: {
  1155. label: t.lang.target,
  1156. value: target
  1157. }
  1158. });
  1159. }
  1160. t.openModalInsert(t.lang.createLink, options, function (v) { // v is value
  1161. var url = t.prependUrlPrefix(v.url);
  1162. if (!url.length) {
  1163. return false;
  1164. }
  1165. var link = $(['<a href="', url, '">', v.text || v.url, '</a>'].join(''));
  1166. if (v.title) {
  1167. link.attr('title', v.title);
  1168. }
  1169. if (v.target || t.o.defaultLinkTarget) {
  1170. link.attr('target', v.target || t.o.defaultLinkTarget);
  1171. }
  1172. t.range.deleteContents();
  1173. t.range.insertNode(link[0]);
  1174. t.syncCode();
  1175. t.$c.trigger('tbwchange');
  1176. return true;
  1177. });
  1178. },
  1179. prependUrlPrefix: function (url) {
  1180. var t = this;
  1181. if (!t.urlPrefix) {
  1182. return url;
  1183. }
  1184. var VALID_LINK_PREFIX = /^([a-z][-+.a-z0-9]*:|\/|#)/i;
  1185. if (VALID_LINK_PREFIX.test(url)) {
  1186. return url;
  1187. }
  1188. var SIMPLE_EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  1189. if (SIMPLE_EMAIL_REGEX.test(url)) {
  1190. return 'mailto:' + url;
  1191. }
  1192. return t.urlPrefix + url;
  1193. },
  1194. unlink: function () {
  1195. var t = this,
  1196. documentSelection = t.doc.getSelection(),
  1197. node = documentSelection.focusNode;
  1198. if (documentSelection.isCollapsed) {
  1199. while (['A', 'DIV'].indexOf(node.nodeName) < 0) {
  1200. node = node.parentNode;
  1201. }
  1202. if (node && node.nodeName === 'A') {
  1203. var range = t.doc.createRange();
  1204. range.selectNode(node);
  1205. documentSelection.removeAllRanges();
  1206. documentSelection.addRange(range);
  1207. }
  1208. }
  1209. t.execCmd('unlink', undefined, undefined, true);
  1210. },
  1211. insertImage: function () {
  1212. var t = this;
  1213. t.saveRange();
  1214. var options = {
  1215. url: {
  1216. label: 'URL',
  1217. required: true
  1218. },
  1219. alt: {
  1220. label: t.lang.description,
  1221. value: t.getRangeText()
  1222. }
  1223. };
  1224. if (t.o.imageWidthModalEdit) {
  1225. options.width = {};
  1226. }
  1227. t.openModalInsert(t.lang.insertImage, options, function (v) { // v are values
  1228. t.execCmd('insertImage', v.url, false, true);
  1229. var $img = $('img[src="' + v.url + '"]:not([alt])', t.$box);
  1230. $img.attr('alt', v.alt);
  1231. if (t.o.imageWidthModalEdit) {
  1232. $img.attr({
  1233. width: v.width
  1234. });
  1235. }
  1236. t.syncCode();
  1237. t.$c.trigger('tbwchange');
  1238. return true;
  1239. });
  1240. },
  1241. fullscreen: function () {
  1242. var t = this,
  1243. prefix = t.o.prefix,
  1244. fullscreenCssClass = prefix + 'fullscreen',
  1245. fullscreenPlaceholderClass = fullscreenCssClass + '-placeholder',
  1246. isFullscreen,
  1247. editorHeight = t.$box.outerHeight();
  1248. t.$box.toggleClass(fullscreenCssClass);
  1249. isFullscreen = t.$box.hasClass(fullscreenCssClass);
  1250. if (isFullscreen) {
  1251. t.$box.before(
  1252. $('<div/>', {
  1253. class: fullscreenPlaceholderClass
  1254. }).css({
  1255. height: editorHeight
  1256. })
  1257. );
  1258. } else {
  1259. $('.' + fullscreenPlaceholderClass).remove();
  1260. }
  1261. $('body').toggleClass(prefix + 'body-fullscreen', isFullscreen);
  1262. $(window).trigger('scroll');
  1263. t.$c.trigger('tbw' + (isFullscreen ? 'open' : 'close') + 'fullscreen');
  1264. },
  1265. /*
  1266. * Call method of trumbowyg if exist
  1267. * else try to call anonymous function
  1268. * and finally native execCommand
  1269. */
  1270. execCmd: function (cmd, param, forceCss, skipTrumbowyg) {
  1271. var t = this;
  1272. skipTrumbowyg = !!skipTrumbowyg || '';
  1273. if (cmd !== 'dropdown') {
  1274. t.$ed.focus();
  1275. }
  1276. if(cmd === 'strikethrough' && t.o.semantic) {
  1277. t.semanticTag('strike', t.o.semanticKeepAttributes, true); // browsers cannot undo e.g. <del> as they expect <strike>
  1278. }
  1279. try {
  1280. t.doc.execCommand('styleWithCSS', false, forceCss || false);
  1281. } catch (c) {
  1282. }
  1283. try {
  1284. t[cmd + skipTrumbowyg](param);
  1285. } catch (c) {
  1286. try {
  1287. cmd(param);
  1288. } catch (e2) {
  1289. if (cmd === 'insertHorizontalRule') {
  1290. param = undefined;
  1291. } else if (cmd === 'formatBlock' && t.isIE) {
  1292. param = '<' + param + '>';
  1293. }
  1294. t.doc.execCommand(cmd, false, param);
  1295. t.syncCode();
  1296. t.semanticCode(false, true);
  1297. try {
  1298. var listId = window.getSelection().focusNode;
  1299. if(!$(window.getSelection().focusNode.parentNode).hasClass('trumbowyg-editor')){
  1300. listId = window.getSelection().focusNode.parentNode;
  1301. }
  1302. var arr = t.o.tagClasses[param];
  1303. if (arr) {
  1304. for (var i = 0; i < arr.length; i+=1) {
  1305. $(listId).addClass(arr[i]);
  1306. }
  1307. }
  1308. } catch (e) {
  1309. }
  1310. }
  1311. if (cmd !== 'dropdown') {
  1312. t.updateButtonPaneStatus();
  1313. t.$c.trigger('tbwchange');
  1314. }
  1315. }
  1316. },
  1317. // Open a modal box
  1318. openModal: function (title, content, buildForm) {
  1319. var t = this,
  1320. prefix = t.o.prefix;
  1321. buildForm = buildForm !== false;
  1322. // No open a modal box when exist other modal box
  1323. if ($('.' + prefix + 'modal-box', t.$box).length > 0) {
  1324. return false;
  1325. }
  1326. if (t.o.autogrowOnEnter) {
  1327. t.autogrowOnEnterDontClose = true;
  1328. }
  1329. t.saveRange();
  1330. t.showOverlay();
  1331. // Disable all btnPane btns
  1332. t.$btnPane.addClass(prefix + 'disable');
  1333. // Build out of ModalBox, it's the mask for animations
  1334. var $modal = $('<div/>', {
  1335. class: prefix + 'modal ' + prefix + 'fixed-top'
  1336. }).css({
  1337. top: t.$box.offset().top + t.$btnPane.height(),
  1338. zIndex: 99999
  1339. }).appendTo($(t.doc.body));
  1340. // Click on overlay close modal by cancelling them
  1341. t.$overlay.one('click', function () {
  1342. $modal.trigger(CANCEL_EVENT);
  1343. return false;
  1344. });
  1345. // Build the form
  1346. var formOrContent;
  1347. if (buildForm) {
  1348. formOrContent = $('<form/>', {
  1349. action: '',
  1350. html: content
  1351. })
  1352. .on('submit', function () {
  1353. $modal.trigger(CONFIRM_EVENT);
  1354. return false;
  1355. })
  1356. .on('reset', function () {
  1357. $modal.trigger(CANCEL_EVENT);
  1358. return false;
  1359. })
  1360. .on('submit reset', function () {
  1361. if (t.o.autogrowOnEnter) {
  1362. t.autogrowOnEnterDontClose = false;
  1363. }
  1364. });
  1365. } else {
  1366. formOrContent = content;
  1367. }
  1368. // Build ModalBox and animate to show them
  1369. var $box = $('<div/>', {
  1370. class: prefix + 'modal-box',
  1371. html: formOrContent
  1372. })
  1373. .css({
  1374. top: '-' + t.$btnPane.outerHeight(),
  1375. opacity: 0,
  1376. paddingBottom: buildForm ? null : '5%',
  1377. })
  1378. .appendTo($modal)
  1379. .animate({
  1380. top: 0,
  1381. opacity: 1
  1382. }, 100);
  1383. // Append title
  1384. if (title) {
  1385. $('<span/>', {
  1386. text: title,
  1387. class: prefix + 'modal-title'
  1388. }).prependTo($box);
  1389. }
  1390. if (buildForm) {
  1391. // Focus in modal box
  1392. $('input:first', $box).focus();
  1393. // Append Confirm and Cancel buttons
  1394. t.buildModalBtn('submit', $box);
  1395. t.buildModalBtn('reset', $box);
  1396. $modal.height($box.outerHeight() + 10);
  1397. }
  1398. $(window).trigger('scroll');
  1399. t.$c.trigger('tbwmodalopen');
  1400. return $modal;
  1401. },
  1402. // @param n is name of modal
  1403. buildModalBtn: function (n, $modal) {
  1404. var t = this,
  1405. prefix = t.o.prefix;
  1406. return $('<button/>', {
  1407. class: prefix + 'modal-button ' + prefix + 'modal-' + n,
  1408. type: n,
  1409. text: t.lang[n] || n
  1410. }).appendTo($('form', $modal));
  1411. },
  1412. // close current modal box
  1413. closeModal: function () {
  1414. var t = this,
  1415. prefix = t.o.prefix;
  1416. t.$btnPane.removeClass(prefix + 'disable');
  1417. t.$overlay.off();
  1418. // Find the modal box
  1419. var $modalBox = $('.' + prefix + 'modal-box', $(t.doc.body));
  1420. $modalBox.animate({
  1421. top: '-' + $modalBox.height()
  1422. }, 100, function () {
  1423. $modalBox.parent().remove();
  1424. t.hideOverlay();
  1425. t.$c.trigger('tbwmodalclose');
  1426. });
  1427. t.restoreRange();
  1428. },
  1429. // Pre-formatted build and management modal
  1430. openModalInsert: function (title, fields, cmd) {
  1431. var t = this,
  1432. prefix = t.o.prefix,
  1433. lg = t.lang,
  1434. html = '';
  1435. $.each(fields, function (fieldName, field) {
  1436. var l = field.label || fieldName,
  1437. n = field.name || fieldName,
  1438. a = field.attributes || {};
  1439. var attr = Object.keys(a).map(function (prop) {
  1440. return prop + '="' + a[prop] + '"';
  1441. }).join(' ');
  1442. html += '<label><input type="' + (field.type || 'text') + '" name="' + n + '"' +
  1443. (field.type === 'checkbox' && field.value ? ' checked="checked"' : ' value="' + (field.value || '').replace(/"/g, '&quot;')) +
  1444. '"' + attr + '><span class="' + prefix + 'input-infos"><span>' +
  1445. (lg[l] ? lg[l] : l) +
  1446. '</span></span></label>';
  1447. });
  1448. return t.openModal(title, html)
  1449. .on(CONFIRM_EVENT, function () {
  1450. var $form = $('form', $(this)),
  1451. valid = true,
  1452. values = {};
  1453. $.each(fields, function (fieldName, field) {
  1454. var n = field.name || fieldName;
  1455. var $field = $('input[name="' + n + '"]', $form),
  1456. inputType = $field.attr('type');
  1457. switch (inputType.toLowerCase()) {
  1458. case 'checkbox':
  1459. values[n] = $field.is(':checked');
  1460. break;
  1461. case 'radio':
  1462. values[n] = $field.filter(':checked').val();
  1463. break;
  1464. default:
  1465. values[n] = $.trim($field.val());
  1466. break;
  1467. }
  1468. // Validate value
  1469. if (field.required && values[n] === '') {
  1470. valid = false;
  1471. t.addErrorOnModalField($field, t.lang.required);
  1472. } else if (field.pattern && !field.pattern.test(values[n])) {
  1473. valid = false;
  1474. t.addErrorOnModalField($field, field.patternError);
  1475. }
  1476. });
  1477. if (valid) {
  1478. t.restoreRange();
  1479. if (cmd(values, fields)) {
  1480. t.syncCode();
  1481. t.$c.trigger('tbwchange');
  1482. t.closeModal();
  1483. $(this).off(CONFIRM_EVENT);
  1484. }
  1485. }
  1486. })
  1487. .one(CANCEL_EVENT, function () {
  1488. $(this).off(CONFIRM_EVENT);
  1489. t.closeModal();
  1490. });
  1491. },
  1492. addErrorOnModalField: function ($field, err) {
  1493. var prefix = this.o.prefix,
  1494. spanErrorClass = prefix + 'msg-error',
  1495. $label = $field.parent();
  1496. $field
  1497. .on('change keyup', function () {
  1498. $label.removeClass(prefix + 'input-error');
  1499. setTimeout(function () {
  1500. $label.find('.' + spanErrorClass).remove();
  1501. }, 150);
  1502. });
  1503. $label
  1504. .addClass(prefix + 'input-error')
  1505. .find('input+span')
  1506. .append(
  1507. $('<span/>', {
  1508. class: spanErrorClass,
  1509. text: err
  1510. })
  1511. );
  1512. },
  1513. getDefaultImgDblClickHandler: function () {
  1514. var t = this;
  1515. return function () {
  1516. var $img = $(this),
  1517. src = $img.attr('src'),
  1518. base64 = '(Base64)';
  1519. if (src.indexOf('data:image') === 0) {
  1520. src = base64;
  1521. }
  1522. var options = {
  1523. url: {
  1524. label: 'URL',
  1525. value: src,
  1526. required: true
  1527. },
  1528. alt: {
  1529. label: t.lang.description,
  1530. value: $img.attr('alt')
  1531. }
  1532. };
  1533. if (t.o.imageWidthModalEdit) {
  1534. options.width = {
  1535. value: $img.attr('width') ? $img.attr('width') : ''
  1536. };
  1537. }
  1538. t.openModalInsert(t.lang.insertImage, options, function (v) {
  1539. if (v.url !== base64) {
  1540. $img.attr({
  1541. src: v.url
  1542. });
  1543. }
  1544. $img.attr({
  1545. alt: v.alt
  1546. });
  1547. if (t.o.imageWidthModalEdit) {
  1548. if (parseInt(v.width) > 0) {
  1549. $img.attr({
  1550. width: v.width
  1551. });
  1552. } else {
  1553. $img.removeAttr('width');
  1554. }
  1555. }
  1556. return true;
  1557. });
  1558. return false;
  1559. };
  1560. },
  1561. // Range management
  1562. saveRange: function () {
  1563. var t = this,
  1564. documentSelection = t.doc.getSelection();
  1565. t.range = null;
  1566. if (!documentSelection || !documentSelection.rangeCount) {
  1567. return;
  1568. }
  1569. var savedRange = t.range = documentSelection.getRangeAt(0),
  1570. range = t.doc.createRange(),
  1571. rangeStart;
  1572. range.selectNodeContents(t.$ed[0]);
  1573. range.setEnd(savedRange.startContainer, savedRange.startOffset);
  1574. rangeStart = (range + '').length;
  1575. t.metaRange = {
  1576. start: rangeStart,
  1577. end: rangeStart + (savedRange + '').length
  1578. };
  1579. },
  1580. restoreRange: function () {
  1581. var t = this,
  1582. metaRange = t.metaRange,
  1583. savedRange = t.range,
  1584. documentSelection = t.doc.getSelection(),
  1585. range;
  1586. if (!savedRange) {
  1587. return;
  1588. }
  1589. if (metaRange && metaRange.start !== metaRange.end) { // Algorithm from http://jsfiddle.net/WeWy7/3/
  1590. var charIndex = 0,
  1591. nodeStack = [t.$ed[0]],
  1592. node,
  1593. foundStart = false,
  1594. stop = false;
  1595. range = t.doc.createRange();
  1596. while (!stop && (node = nodeStack.pop())) {
  1597. if (node.nodeType === 3) {
  1598. var nextCharIndex = charIndex + node.length;
  1599. if (!foundStart && metaRange.start >= charIndex && metaRange.start <= nextCharIndex) {
  1600. range.setStart(node, metaRange.start - charIndex);
  1601. foundStart = true;
  1602. }
  1603. if (foundStart && metaRange.end >= charIndex && metaRange.end <= nextCharIndex) {
  1604. range.setEnd(node, metaRange.end - charIndex);
  1605. stop = true;
  1606. }
  1607. charIndex = nextCharIndex;
  1608. } else {
  1609. var cn = node.childNodes,
  1610. i = cn.length;
  1611. while (i > 0) {
  1612. i -= 1;
  1613. nodeStack.push(cn[i]);
  1614. }
  1615. }
  1616. }
  1617. }
  1618. // Fix IE11 Error 'Could not complete the operation due to error 800a025e'.
  1619. // https://stackoverflow.com/questions/16160996/could-not-complete-the-operation-due-to-error-800a025e
  1620. try {
  1621. documentSelection.removeAllRanges();
  1622. } catch (e) {
  1623. }
  1624. documentSelection.addRange(range || savedRange);
  1625. },
  1626. getRangeText: function () {
  1627. return this.range + '';
  1628. },
  1629. clearButtonPaneStatus: function () {
  1630. var t = this,
  1631. prefix = t.o.prefix,
  1632. activeClasses = prefix + 'active-button ' + prefix + 'active',
  1633. originalIconClass = prefix + 'original-icon';
  1634. // Reset all buttons and dropdown state
  1635. $('.' + prefix + 'active-button', t.$btnPane).removeClass(activeClasses);
  1636. $('.' + originalIconClass, t.$btnPane).each(function () {
  1637. $(this).find('svg use').attr('xlink:href', $(this).data(originalIconClass));
  1638. });
  1639. },
  1640. updateButtonPaneStatus: function () {
  1641. var t = this,
  1642. prefix = t.o.prefix,
  1643. activeClasses = prefix + 'active-button ' + prefix + 'active',
  1644. originalIconClass = prefix + 'original-icon',
  1645. tags = t.getTagsRecursive(t.doc.getSelection().focusNode);
  1646. t.clearButtonPaneStatus();
  1647. $.each(tags, function (i, tag) {
  1648. var btnName = t.tagToButton[tag.toLowerCase()],
  1649. $btn = $('.' + prefix + btnName + '-button', t.$btnPane);
  1650. if ($btn.length > 0) {
  1651. $btn.addClass(activeClasses);
  1652. } else {
  1653. try {
  1654. $btn = $('.' + prefix + 'dropdown .' + prefix + btnName + '-dropdown-button', t.$box);
  1655. var $btnSvgUse = $btn.find('svg use'),
  1656. dropdownBtnName = $btn.parent().data(prefix + 'dropdown'),
  1657. $dropdownBtn = $('.' + prefix + dropdownBtnName + '-button', t.$box),
  1658. $dropdownBtnSvgUse = $dropdownBtn.find('svg use');
  1659. // Highlight the dropdown button
  1660. $dropdownBtn.addClass(activeClasses);
  1661. // Switch dropdown icon to the active sub-icon one
  1662. if (t.o.changeActiveDropdownIcon && $btnSvgUse.length > 0) {
  1663. // Save original icon
  1664. $dropdownBtn
  1665. .addClass(originalIconClass)
  1666. .data(originalIconClass, $dropdownBtnSvgUse.attr('xlink:href'));
  1667. // Put the active sub-button's icon
  1668. $dropdownBtnSvgUse
  1669. .attr('xlink:href', $btnSvgUse.attr('xlink:href'));
  1670. }
  1671. } catch (e) {
  1672. }
  1673. }
  1674. });
  1675. },
  1676. getTagsRecursive: function (element, tags) {
  1677. var t = this;
  1678. tags = tags || (element && element.tagName ? [element.tagName] : []);
  1679. if (element && element.parentNode) {
  1680. element = element.parentNode;
  1681. } else {
  1682. return tags;
  1683. }
  1684. var tag = element.tagName;
  1685. if (tag === 'DIV') {
  1686. return tags;
  1687. }
  1688. if (tag === 'P' && element.style.textAlign !== '') {
  1689. tags.push(element.style.textAlign);
  1690. }
  1691. $.each(t.tagHandlers, function (i, tagHandler) {
  1692. tags = tags.concat(tagHandler(element, t));
  1693. });
  1694. tags.push(tag);
  1695. return t.getTagsRecursive(element, tags).filter(function (tag) {
  1696. return tag != null;
  1697. });
  1698. },
  1699. // Plugins
  1700. initPlugins: function () {
  1701. var t = this;
  1702. t.loadedPlugins = [];
  1703. $.each($.trumbowyg.plugins, function (name, plugin) {
  1704. if (!plugin.shouldInit || plugin.shouldInit(t)) {
  1705. plugin.init(t);
  1706. if (plugin.tagHandler) {
  1707. t.tagHandlers.push(plugin.tagHandler);
  1708. }
  1709. t.loadedPlugins.push(plugin);
  1710. }
  1711. });
  1712. },
  1713. destroyPlugins: function () {
  1714. var t = this;
  1715. $.each(this.loadedPlugins, function (i, plugin) {
  1716. if (plugin.destroy) {
  1717. plugin.destroy(t);
  1718. }
  1719. });
  1720. }
  1721. };
  1722. })(navigator, window, document, jQuery);