richtext.js 87 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947
  1. (function ( $ ) {
  2. $.fn.richText = function( options ) {
  3. // set default options
  4. // and merge them with the parameter options
  5. var settings = $.extend({
  6. //id
  7. //id: "",
  8. // text formatting
  9. bold: true,
  10. italic: true,
  11. underline: true,
  12. // text alignment
  13. leftAlign: true,
  14. centerAlign: true,
  15. rightAlign: true,
  16. justify: true,
  17. // lists
  18. ol: true,
  19. ul: true,
  20. // title
  21. heading: false,
  22. // fonts
  23. fonts: false,
  24. fontList: ["Arial",
  25. "Arial Black",
  26. "Comic Sans MS",
  27. "Courier New",
  28. "Geneva",
  29. "Georgia",
  30. "Helvetica",
  31. "Impact",
  32. "Lucida Console",
  33. "Tahoma",
  34. "Times New Roman",
  35. "Verdana"
  36. ],
  37. fontColor: false,
  38. fontSize: false,
  39. // uploads
  40. imageUpload: false,
  41. fileUpload: false,
  42. // media
  43. videoEmbed: false,
  44. // link
  45. urls: false,
  46. // tables
  47. table: false,
  48. // code
  49. removeStyles: false,
  50. code: false,
  51. // colors
  52. colors: [],
  53. // dropdowns
  54. fileHTML: '',
  55. imageHTML: '',
  56. // translations
  57. translations: {
  58. 'title': 'Título',
  59. 'white': 'Blanco',
  60. 'black': 'Negro',
  61. 'brown': 'Café',
  62. 'beige': 'Beige',
  63. 'darkBlue': 'Azul Marino',
  64. 'blue': 'Azul',
  65. 'lightBlue': 'Azul Claro',
  66. 'darkRed': 'Rojo Obscuro',
  67. 'red': 'Rojo',
  68. 'darkGreen': 'Verde Obscuro',
  69. 'green': 'Verde',
  70. 'purple': 'Morado',
  71. 'darkTurquois': 'Turquesa Obscuro',
  72. 'turquois': 'Turquesa',
  73. 'darkOrange': 'naranja Obscuro',
  74. 'orange': 'Naranja',
  75. 'yellow': 'Amarillo',
  76. 'imageURL': 'URL Imagen',
  77. 'fileURL': 'URL Archivo',
  78. 'linkText': 'Hipervínculo',
  79. 'url': 'URL',
  80. 'size': 'Tamaño',
  81. 'responsive': 'Responsivo',
  82. 'text': 'Texto',
  83. 'openIn': 'Abrir en',
  84. 'sameTab': 'Misma página',
  85. 'newTab': 'Nueva Página',
  86. 'align': 'Alinear',
  87. 'left': 'Izquierda',
  88. 'justify': 'Justificado',
  89. 'center': 'Centrado',
  90. 'right': 'Derecha',
  91. 'rows': 'Renglones',
  92. 'columns': 'Columnas',
  93. 'add': 'Agregar',
  94. 'pleaseEnterURL': 'Ingresa una URL',
  95. 'videoURLnotSupported': 'URL de video no soportado',
  96. 'pleaseSelectImage': 'Seleccione una imagen',
  97. 'pleaseSelectFile': 'Seleccione un archivo',
  98. 'bold': 'Negrita',
  99. 'italic': 'Cursiva',
  100. 'underline': 'Subrayado',
  101. 'alignLeft': 'Alinear a la izquierda',
  102. 'alignCenter': 'Alinear centrado',
  103. 'alignRight': 'Alinear a la derecha',
  104. 'addOrderedList': 'Numeración',
  105. 'addUnorderedList': 'Viñetas',
  106. 'addHeading': 'Títulos',
  107. 'addFont': 'Cambiar fuente',
  108. 'addFontColor': 'Cambiar color de fuente',
  109. 'addFontSize' : 'Cambiar tamaño de fuente',
  110. 'addImage': 'Agregar imagen',
  111. 'addVideo': 'Agregar video',
  112. 'addFile': 'Agregar archivo',
  113. 'addURL': 'Vínculo',
  114. 'addTable': 'Tabla',
  115. 'removeStyles': 'Remover estilos',
  116. 'code': 'Mostrar código',
  117. 'undo': 'Deshacer',
  118. 'redo': 'Repetir',
  119. 'close': 'Cerrar'
  120. },
  121. // privacy
  122. youtubeCookies: false,
  123. // dev settings
  124. useSingleQuotes: false,
  125. height: 0,
  126. heightPercentage: 0,
  127. id: "",
  128. class: "",
  129. useParagraph: false
  130. }, options );
  131. /* prepare toolbar */
  132. var $inputElement = $(this);
  133. $inputElement.addClass("richText-initial");
  134. var $editor,
  135. $toolbarList = $('<ul />'),
  136. $toolbarElement = $('<li />'),
  137. $btnBold = $('<a />', {class: "richText-btn", "data-command": "bold", "title": settings.translations.bold, html: '<span class="fa fa-bold"></span>'}), // bold
  138. $btnItalic = $('<a />', {class: "richText-btn", "data-command": "italic", "title": settings.translations.italic, html: '<span class="fa fa-italic"></span>'}), // italic
  139. $btnUnderline = $('<a />', {class: "richText-btn", "data-command": "underline", "title": settings.translations.underline, html: '<span class="fa fa-underline"></span>'}), // underline
  140. $btnJustify = $('<a />', {class: "richText-btn", "data-command": "justifyFull", "title": settings.translations.justify, html: '<span class="fa fa-align-justify"></span>'}), // left align
  141. $btnLeftAlign = $('<a />', {class: "richText-btn", "data-command": "justifyLeft", "title": settings.translations.alignLeft, html: '<span class="fa fa-align-left"></span>'}), // left align
  142. $btnCenterAlign = $('<a />', {class: "richText-btn", "data-command": "justifyCenter", "title": settings.translations.alignCenter, html: '<span class="fa fa-align-center"></span>'}), // centered
  143. $btnRightAlign = $('<a />', {class: "richText-btn", "data-command": "justifyRight", "title": settings.translations.alignRight, html: '<span class="fa fa-align-right"></span>'}), // right align
  144. $btnOL = $('<a />', {class: "richText-btn", "data-command": "insertOrderedList", "title": settings.translations.addOrderedList, html: '<span class="fa fa-list-ol"></span>'}), // ordered list
  145. $btnUL = $('<a />', {class: "richText-btn", "data-command": "insertUnorderedList", "title": settings.translations.addUnorderedList, html: '<span class="fa fa-list"></span>'}), // unordered list
  146. $btnHeading = $('<a />', {class: "richText-btn", "title": settings.translations.addHeading, html: '<span class="fa fa-header fa-heading"></span>'}), // title/header
  147. $btnFont = $('<a />', {class: "richText-btn", "title": settings.translations.addFont, html: '<span class="fa fa-font"></span>'}), // font color
  148. $btnFontColor = $('<a />', {class: "richText-btn", "title": settings.translations.addFontColor, html: '<span class="fa fa-paint-brush"></span>'}), // font color
  149. $btnFontSize = $('<a />', {class: "richText-btn", "title": settings.translations.addFontSize, html: '<span class="fa fa-text-height"></span>'}), // font color
  150. $btnImageUpload = $('<a />', {class: "richText-btn", "title": settings.translations.addImage, html: '<span class="fa fa-image"></span>'}), // image
  151. $btnVideoEmbed = $('<a />', {class: "richText-btn", "title": settings.translations.addVideo, html: '<span class="fa fa-video-camera fa-video"></span>'}), // video
  152. $btnFileUpload = $('<a />', {class: "richText-btn", "title": settings.translations.addFile, html: '<span class="fa fa-file-text-o far fa-file-alt"></span>'}), // file
  153. $btnURLs = $('<a />', {class: "richText-btn", "title": settings.translations.addURL, html: '<span class="fa fa-link"></span>'}), // urls/links
  154. $btnTable = $('<a />', {class: "richText-btn", "title": settings.translations.addTable, html: '<span class="fa fa-table"></span>'}), // table
  155. $btnRemoveStyles = $('<a />', {class: "richText-btn", "data-command": "removeFormat", "title": settings.translations.removeStyles, html: '<span class="fa fa-recycle"></span>'}), // clean up styles
  156. $btnCode = $('<a />', {class: "richText-btn ocultar", "data-command": "toggleCode", "title": settings.translations.code, html: '<span class="fa fa-code"></span>'}); // code
  157. $btnUndo = $('<a />', {class: 'richText-undo is-disabled', html: '<span class="fa fa-undo"></span>', 'title': settings.translations.undo}); //undo
  158. $btnRedo = $('<a />', {class: 'richText-redo is-disabled', html: '<span class="fa fa-repeat fa-redo"></span>', 'title': settings.translations.redo});//redo
  159. /* prepare toolbar dropdowns */
  160. var $dropdownOuter = $('<div />', {class: "richText-dropdown-outer"});
  161. var $dropdownClose = $('<span />', {class: "richText-dropdown-close", html: '<span title="' + settings.translations.close + '"><span class="fa fa-times"></span></span>'});
  162. var $dropdownList = $('<ul />', {class: "richText-dropdown"}), // dropdown lists
  163. $dropdownBox = $('<div />', {class: "richText-dropdown"}), // dropdown boxes / custom dropdowns
  164. $form = $('<div />', {class: "richText-form"}), // symbolic form
  165. $formItem = $('<div />', {class: 'richText-form-item'}), // form item
  166. $formLabel = $('<label />'), // form label
  167. $formInput = $('<input />', {type: "text"}), //form input field
  168. $formRadio = $('<input />', {type: "radio"}), //form input field
  169. $formInputFile = $('<input />', {type: "file"}), // form file input field
  170. $formInputSelect = $('<select />'),
  171. $formButton = $('<button />', {text: settings.translations.add, class: "btn btn-accept"}); // button
  172. /* internal settings */
  173. var savedSelection; // caret position/selection
  174. var editorID = "richText-" + Math.random().toString(36).substring(7);
  175. //var editorID = settings.id;
  176. var ignoreSave = false, $resizeImage = null;
  177. /* prepare editor history */
  178. var history = [];
  179. history[editorID] = [];
  180. var historyPosition = [];
  181. historyPosition[editorID] = 0;
  182. /* list dropdown for titles */
  183. var $titles = $dropdownList.clone();
  184. $titles.append($('<li />', {html: '<a data-command="formatBlock" data-option="h1">' + settings.translations.title + ' #1</a>'}));
  185. $titles.append($('<li />', {html: '<a data-command="formatBlock" data-option="h2">' + settings.translations.title + ' #2</a>'}));
  186. $titles.append($('<li />', {html: '<a data-command="formatBlock" data-option="h3">' + settings.translations.title + ' #3</a>'}));
  187. $titles.append($('<li />', {html: '<a data-command="formatBlock" data-option="h4">' + settings.translations.title + ' #4</a>'}));
  188. $btnHeading.append($dropdownOuter.clone().append($titles.prepend($dropdownClose.clone())));
  189. /* list dropdown for fonts */
  190. var fonts = settings.fontList;
  191. var $fonts = $dropdownList.clone();
  192. for(var i = 0; i < fonts.length; i++) {
  193. $fonts.append($('<li />', {html: '<a style="font-family:' + fonts[i] + ';" data-command="fontName" data-option="' + fonts[i] + '">' + fonts[i] + '</a>'}));
  194. }
  195. $btnFont.append($dropdownOuter.clone().append($fonts.prepend($dropdownClose.clone())));
  196. /* list dropdown for font sizes */
  197. var fontSizes = [24,18,16,14,12];
  198. var $fontSizes = $dropdownList.clone();
  199. for(var i = 0; i < fontSizes.length; i++) {
  200. $fontSizes.append($('<li />', {html: '<a style="font-size:' + fontSizes[i] + 'px;" data-command="fontSize" data-option="' + fontSizes[i] + '">Text ' + fontSizes[i] + 'px</a>'}));
  201. }
  202. $btnFontSize.append($dropdownOuter.clone().append($fontSizes.prepend($dropdownClose.clone())));
  203. /* font colors */
  204. var $fontColors = $dropdownList.clone();
  205. $fontColors.html(loadColors("forecolor"));
  206. $btnFontColor.append($dropdownOuter.clone().append($fontColors.prepend($dropdownClose.clone())));
  207. /* background colors */
  208. //var $bgColors = $dropdownList.clone();
  209. //$bgColors.html(loadColors("hiliteColor"));
  210. //$btnBGColor.append($dropdownOuter.clone().append($bgColors));
  211. /* box dropdown for links */
  212. var $linksDropdown = $dropdownBox.clone();
  213. var $linksForm = $form.clone().attr("id", "richText-URL").attr("data-editor", editorID);
  214. $linksForm.append(
  215. $formItem.clone().addClass('my-2')
  216. .append($formLabel.clone().text(settings.translations.url).attr("for", "url").addClass('col-lg-3'))
  217. .append($formInput.clone().attr("id", "url"))
  218. );
  219. $linksForm.append(
  220. $formItem.clone().addClass('my-2')
  221. .append($formLabel.clone().text(settings.translations.text).attr("for", "urlText").addClass('col-lg-3'))
  222. .append($formInput.clone().attr("id", "urlText"))
  223. );
  224. $linksForm.append(
  225. $formItem.clone().addClass('my-2')
  226. .append($formLabel.clone().text(settings.translations.openIn).attr("for", "openIn").addClass('col-lg-3'))
  227. .append($formInputSelect
  228. .clone().attr("id", "openIn")
  229. .append($("<option />", {value: '_self', text: settings.translations.sameTab}))
  230. .append($("<option />", {value: '_blank', text: settings.translations.newTab}))
  231. )
  232. );
  233. $linksForm.append( $formItem.clone().append($formButton.clone()).addClass('justify-content-center'));
  234. $linksDropdown.append($linksForm);
  235. $btnURLs.append($dropdownOuter.clone().append($linksDropdown.prepend($dropdownClose.clone())));
  236. /* box dropdown for video embedding */
  237. var $videoDropdown = $dropdownBox.clone();
  238. var $videoForm = $form.clone().attr("id", "richText-Video").attr("data-editor", editorID);
  239. $videoForm.append(
  240. $formItem.clone()
  241. .append($formLabel.clone().text(settings.translations.url).attr("for", "videoURL"))
  242. .append($formInput.clone().attr("id", "videoURL"))
  243. );
  244. $videoForm.append(
  245. $formItem.clone()
  246. .append($formLabel.clone().text(settings.translations.size).attr("for", "size"))
  247. .append(
  248. $formInputSelect
  249. .clone().attr("id", "size")
  250. .append($("<option />", {value: 'responsive', text: settings.translations.responsive}))
  251. .append($("<option />", {value: '640x360', text: '640x360'}))
  252. .append($("<option />", {value: '560x315', text: '560x315'}))
  253. .append($("<option />", {value: '480x270', text: '480x270'}))
  254. .append($("<option />", {value: '320x180', text: '320x180'}))
  255. )
  256. );
  257. $videoForm.append( $formItem.clone().append($formButton.clone()) );
  258. $videoDropdown.append($videoForm);
  259. $btnVideoEmbed.append($dropdownOuter.clone().append($videoDropdown.prepend($dropdownClose.clone())));
  260. /* box dropdown for image upload/image selection */
  261. var $imageDropdown = $dropdownBox.clone();
  262. var $imageForm = $form.clone().attr("id", "richText-Image").attr("data-editor", editorID);
  263. if(settings.imageHTML
  264. && ($(settings.imageHTML).find('#imageURL').length > 0 || $(settings.imageHTML).attr("id") === "imageURL")) {
  265. // custom image form
  266. $imageForm.html(settings.imageHTML);
  267. } else {
  268. // default image form
  269. $imageForm.append(
  270. $formItem.clone()
  271. .append($formLabel.clone().text(settings.translations.imageURL).attr("for", "imageURL"))
  272. .append($formInput.clone().attr("id", "imageURL"))
  273. );
  274. $imageForm.append(
  275. $formItem.clone()
  276. .append($formLabel.clone().text(settings.translations.align).attr("for", "align"))
  277. .append(
  278. $formInputSelect
  279. .clone().attr("id", "align")
  280. .append($("<option />", {value: 'left', text: settings.translations.left}))
  281. .append($("<option />", {value: 'center', text: settings.translations.center}))
  282. .append($("<option />", {value: 'right', text: settings.translations.right}))
  283. )
  284. );
  285. }
  286. $imageForm.append( $formItem.clone().append($formButton.clone()) );
  287. $imageDropdown.append($imageForm);
  288. $btnImageUpload.append($dropdownOuter.clone().append($imageDropdown.prepend($dropdownClose.clone())));
  289. /* box dropdown for file upload/file selection */
  290. var $fileDropdown = $dropdownBox.clone();
  291. var $fileForm = $form.clone().attr("id", "richText-File").attr("data-editor", editorID);
  292. if(settings.fileHTML
  293. && ($(settings.fileHTML).find('#fileURL').length > 0 || $(settings.fileHTML).attr("id") === "fileURL")) {
  294. // custom file form
  295. $fileForm.html(settings.fileHTML);
  296. } else {
  297. // default file form
  298. $fileForm.append(
  299. $formItem.clone()
  300. .append($formLabel.clone().text(settings.translations.fileURL).attr("for", "fileURL"))
  301. .append($formInput.clone().attr("id", "fileURL"))
  302. );
  303. $fileForm.append(
  304. $formItem.clone()
  305. .append($formLabel.clone().text(settings.translations.linkText).attr("for", "fileText"))
  306. .append($formInput.clone().attr("id", "fileText"))
  307. );
  308. }
  309. $fileForm.append( $formItem.clone().append($formButton.clone()) );
  310. $fileDropdown.append($fileForm);
  311. $btnFileUpload.append($dropdownOuter.clone().append($fileDropdown.prepend($dropdownClose.clone())));
  312. /* box dropdown for tables */
  313. var $tableDropdown = $dropdownBox.clone();
  314. var $tableForm = $form.clone().attr("id", "richText-Table").attr("data-editor", editorID);
  315. $tableForm.append(
  316. $formItem.clone()
  317. .append($formLabel.clone().text(settings.translations.rows).attr("for", "tableRows"))
  318. .append($formInput.clone().attr("id", "tableRows").attr("type", "number"))
  319. );
  320. $tableForm.append(
  321. $formItem.clone()
  322. .append($formLabel.clone().text(settings.translations.columns).attr("for", "tableColumns"))
  323. .append($formInput.clone().attr("id", "tableColumns").attr("type", "number"))
  324. );
  325. $tableForm.append( $formItem.clone().append($formButton.clone()) );
  326. $tableDropdown.append($tableForm);
  327. $btnTable.append($dropdownOuter.clone().append($tableDropdown.prepend($dropdownClose.clone())));
  328. /* initizalize editor */
  329. function init() {
  330. var value, attributes, attributes_html = '';
  331. if(settings.useParagraph !== false) {
  332. // set default tag when pressing ENTER to <p> instead of <div>
  333. document.execCommand("DefaultParagraphSeparator", false, 'p');
  334. }
  335. // reformat $inputElement to textarea
  336. if($inputElement.prop("tagName") === "TEXTAREA") {
  337. // everything perfect
  338. } else if($inputElement.val()) {
  339. value = $inputElement.val();
  340. attributes = $inputElement.prop("attributes");
  341. // loop through <select> attributes and apply them on <div>
  342. $.each(attributes, function() {
  343. if(this.name) {
  344. attributes_html += ' ' + this.name + '="' + this.value + '"';
  345. }
  346. });
  347. $inputElement.replaceWith($('<textarea' + attributes_html + ' data-richtext="init">' + value + '</textarea>'));
  348. $inputElement = $('[data-richtext="init"]');
  349. $inputElement.removeAttr("data-richtext");
  350. } else if($inputElement.html()) {
  351. value = $inputElement.html();
  352. attributes = $inputElement.prop("attributes");
  353. // loop through <select> attributes and apply them on <div>
  354. $.each(attributes, function() {
  355. if(this.name) {
  356. attributes_html += ' ' + this.name + '="' + this.value + '"';
  357. }
  358. });
  359. $inputElement.replaceWith($('<textarea' + attributes_html + ' data-richtext="init">' + value + '</textarea>'));
  360. $inputElement = $('[data-richtext="init"]');
  361. $inputElement.removeAttr("data-richtext");
  362. } else {
  363. attributes = $inputElement.prop("attributes");
  364. // loop through <select> attributes and apply them on <div>
  365. $.each(attributes, function() {
  366. if(this.name) {
  367. attributes_html += ' ' + this.name + '="' + this.value + '"';
  368. }
  369. });
  370. $inputElement.replaceWith($('<textarea' + attributes_html + ' data-richtext="init"></textarea>'));
  371. $inputElement = $('[data-richtext="init"]');
  372. $inputElement.removeAttr("data-richtext");
  373. }
  374. $editor = $('<div />', {class: "richText"});
  375. var $toolbar = $('<div />', {class: "richText-toolbar"});
  376. //var $editorView = $('<div />', {class: "richText-editor", id: editorID, contenteditable: true, name: settings.id});
  377. var $editorView = $('<div />', {class: "richText-editor", id: editorID, contenteditable: true});
  378. $toolbar.append($toolbarList);
  379. $toolbarList.append($toolbarElement.clone().append($btnUndo));
  380. $toolbarList.append($toolbarElement.clone().append($btnRedo));
  381. /* text formatting */
  382. if(settings.bold === true) {
  383. $toolbarList.append($toolbarElement.clone().append($btnBold));
  384. }
  385. if(settings.italic === true) {
  386. $toolbarList.append($toolbarElement.clone().append($btnItalic));
  387. }
  388. if(settings.underline === true) {
  389. $toolbarList.append($toolbarElement.clone().append($btnUnderline));
  390. }
  391. /* align */
  392. if(settings.leftAlign === true) {
  393. $toolbarList.append($toolbarElement.clone().append($btnLeftAlign));
  394. }
  395. if(settings.centerAlign === true) {
  396. $toolbarList.append($toolbarElement.clone().append($btnCenterAlign));
  397. }
  398. if(settings.rightAlign === true) {
  399. $toolbarList.append($toolbarElement.clone().append($btnRightAlign));
  400. }
  401. if(settings.justify === true) {
  402. $toolbarList.append($toolbarElement.clone().append($btnJustify));
  403. }
  404. /* lists */
  405. if(settings.ol === true) {
  406. $toolbarList.append($toolbarElement.clone().append($btnOL));
  407. }
  408. if(settings.ul === true) {
  409. $toolbarList.append($toolbarElement.clone().append($btnUL));
  410. }
  411. /* fonts */
  412. if(settings.fonts === true && settings.fontList.length > 0) {
  413. $toolbarList.append($toolbarElement.clone().append($btnFont));
  414. }
  415. if(settings.fontSize === true) {
  416. $toolbarList.append($toolbarElement.clone().append($btnFontSize));
  417. }
  418. /* heading */
  419. if(settings.heading === true) {
  420. $toolbarList.append($toolbarElement.clone().append($btnHeading));
  421. }
  422. /* colors */
  423. if(settings.fontColor === true) {
  424. $toolbarList.append($toolbarElement.clone().append($btnFontColor));
  425. }
  426. /* uploads */
  427. if(settings.imageUpload === true) {
  428. $toolbarList.append($toolbarElement.clone().append($btnImageUpload));
  429. }
  430. if(settings.fileUpload === true) {
  431. $toolbarList.append($toolbarElement.clone().append($btnFileUpload));
  432. }
  433. /* media */
  434. if(settings.videoEmbed === true) {
  435. $toolbarList.append($toolbarElement.clone().append($btnVideoEmbed));
  436. }
  437. /* urls */
  438. if(settings.urls === true) {
  439. $toolbarList.append($toolbarElement.clone().append($btnURLs));
  440. }
  441. if(settings.table === true) {
  442. $toolbarList.append($toolbarElement.clone().append($btnTable));
  443. }
  444. /* code */
  445. if(settings.removeStyles === true) {
  446. $toolbarList.append($toolbarElement.clone().append($btnRemoveStyles));
  447. }
  448. if(settings.code === true) {
  449. $toolbarList.append($toolbarElement.clone().append($btnCode));
  450. }
  451. // set current textarea value to editor
  452. $editorView.html($inputElement.val());
  453. $editor.append($toolbar);
  454. $editor.append($editorView);
  455. $editor.append($inputElement.clone().hide());
  456. $inputElement.replaceWith($editor);
  457. // append bottom toolbar
  458. /*$editor.append(
  459. $('<div />', {class: 'richText-toolbar'})
  460. .append($('<a />', {class: 'richText-undo is-disabled', html: '<span class="fa fa-undo"></span>', 'title': settings.translations.undo}))
  461. .append($('<a />', {class: 'richText-redo is-disabled', html: '<span class="fa fa-repeat fa-redo"></span>', 'title': settings.translations.redo}))
  462. .append($('<a />', {class: 'richText-help', html: '<span class="fa fa-question-circle"></span>'}))
  463. );*/
  464. if(settings.height && settings.height > 0) {
  465. // set custom editor height
  466. $editor.children(".richText-editor, .richText-initial").css({'min-height' : settings.height + 'px', 'height' : settings.height + 'px'});
  467. } else if(settings.heightPercentage && settings.heightPercentage > 0) {
  468. // set custom editor height in percentage
  469. var parentHeight = $editor.parent().innerHeight(); // get editor parent height
  470. var height = (settings.heightPercentage/100) * parentHeight; // calculate pixel value from percentage
  471. height -= $toolbar.outerHeight()*2; // remove toolbar size
  472. height -= parseInt($editor.css("margin-top")); // remove margins
  473. height -= parseInt($editor.css("margin-bottom")); // remove margins
  474. height -= parseInt($editor.find(".richText-editor").css("padding-top")); // remove paddings
  475. height -= parseInt($editor.find(".richText-editor").css("padding-bottom")); // remove paddings
  476. $editor.children(".richText-editor, .richText-initial").css({'min-height' : height + 'px', 'height' : height + 'px'});
  477. }
  478. // add custom class
  479. if(settings.class) {
  480. $editor.addClass(settings.class);
  481. }
  482. if(settings.id) {
  483. $editor.attr("id", settings.id);
  484. }
  485. // fix the first line
  486. fixFirstLine();
  487. // save history
  488. history[editorID].push($editor.find("textarea").val());
  489. }
  490. // initialize editor
  491. init();
  492. /** EVENT HANDLERS */
  493. // Help popup
  494. $editor.find(".richText-help").on("click", function() {
  495. var $editor = $(this).parents(".richText");
  496. if($editor) {
  497. var $outer = $('<div />', {class: 'richText-help-popup', style: 'position:absolute;top:0;right:0;bottom:0;left:0;background-color: rgba(0,0,0,0.3);'});
  498. var $inner = $('<div />', {style: 'position:relative;margin:60px auto;padding:20px;background-color:#FAFAFA;width:70%;font-family:Calibri,Verdana,Helvetica,sans-serif;font-size:small;'});
  499. var $content = $('<div />', {html: '<span id="closeHelp" style="display:block;position:absolute;top:0;right:0;padding:10px;cursor:pointer;" title="' + settings.translations.close + '"><span class="fa fa-times"></span></span>'});
  500. $content.append('<h3 style="margin:0;">RichText</h3>');
  501. $content.append('<hr><br>Powered by <a href="https://github.com/webfashionist/RichText" target="_blank">webfashionist/RichText</a> (Github) <br>License: <a href="https://github.com/webfashionist/RichText/blob/master/LICENSE" target="_blank">AGPL-3.0</a>');
  502. $outer.append($inner.append($content));
  503. $editor.append($outer);
  504. $outer.on("click", "#closeHelp", function() {
  505. $(this).parents('.richText-help-popup').remove();
  506. });
  507. }
  508. });
  509. // undo / redo
  510. $(document).on("click", ".richText-undo, .richText-redo", function(e) {
  511. var $this = $(this);
  512. var $editor = $this.parents('.richText');
  513. if($this.hasClass("richText-undo") && !$this.hasClass("is-disabled")) {
  514. undo($editor);
  515. } else if($this.hasClass("richText-redo") && !$this.hasClass("is-disabled")) {
  516. redo($editor);
  517. }
  518. });
  519. // Saving changes from editor to textarea
  520. $(document).on("input change blur keydown keyup", ".richText-editor", function(e) {
  521. if((e.keyCode === 9 || e.keyCode === "9") && e.type === "keydown") {
  522. // tab through table cells
  523. e.preventDefault();
  524. tabifyEditableTable(window, e);
  525. return false;
  526. }
  527. fixFirstLine();
  528. updateTextarea();
  529. doSave($(this).attr("id"));
  530. });
  531. // add context menu to several Node elements
  532. $(document).on('contextmenu', '.richText-editor', function(e) {
  533. var $list = $('<ul />', {'class': 'list-rightclick richText-list'});
  534. var $li = $('<li />');
  535. // remove Node selection
  536. $('.richText-editor').find('.richText-editNode').removeClass('richText-editNode');
  537. var $target = $(e.target);
  538. var $richText = $target.parents('.richText');
  539. var $toolbar = $richText.find('.richText-toolbar');
  540. var positionX = e.pageX - $richText.offset().left;
  541. var positionY = e.pageY - $richText.offset().top;
  542. $list.css({
  543. 'top': positionY,
  544. 'left': positionX
  545. });
  546. if($target.prop("tagName") === "A") {
  547. // edit URL
  548. e.preventDefault();
  549. $list.append($li.clone().html('<span class="fa fa-link"></span>'));
  550. $target.parents('.richText').append($list);
  551. $list.find('.fa-link').on('click', function() {
  552. $('.list-rightclick.richText-list').remove();
  553. $target.addClass('richText-editNode');
  554. var $popup = $toolbar.find('#richText-URL');
  555. $popup.find('input#url').val($target.attr('href'));
  556. $popup.find('input#urlText').val($target.text());
  557. $popup.find('select#openIn').val($target.attr('target'));
  558. $toolbar.find('.richText-btn').children('.fa-link').parents('li').addClass('is-selected');
  559. });
  560. return false;
  561. } else if($target.prop("tagName") === "IMG") {
  562. // edit image
  563. e.preventDefault();
  564. $list.append($li.clone().html('<span class="fa fa-image"></span>'));
  565. $target.parents('.richText').append($list);
  566. $list.find('.fa-image').on('click', function() {
  567. var align;
  568. if($target.parent('div').length > 0 && $target.parent('div').attr('style') === 'text-align:center;') {
  569. align = 'center';
  570. } else {
  571. align = $target.attr('align');
  572. }
  573. $('.list-rightclick.richText-list').remove();
  574. $target.addClass('richText-editNode');
  575. var $popup = $toolbar.find('#richText-Image');
  576. $popup.find('input#imageURL').val($target.attr('src'));
  577. $popup.find('select#align').val(align);
  578. $toolbar.find('.richText-btn').children('.fa-image').parents('li').addClass('is-selected');
  579. });
  580. return false;
  581. }
  582. });
  583. // Saving changes from textarea to editor
  584. $(document).on("input change blur", ".richText-initial", function() {
  585. if(settings.useSingleQuotes === true) {
  586. $(this).val(changeAttributeQuotes($(this).val()));
  587. }
  588. var editorID = $(this).siblings('.richText-editor').attr("id");
  589. updateEditor(editorID);
  590. doSave(editorID);
  591. });
  592. // Save selection seperately (mainly needed for Safari)
  593. $(document).on("dblclick mouseup", ".richText-editor", function() {
  594. var editorID = $(this).attr("id");
  595. doSave(editorID);
  596. });
  597. // embedding video
  598. $(document).on("click", "#richText-Video button.btn", function(event) {
  599. event.preventDefault();
  600. var $button = $(this);
  601. var $form = $button.parent('.richText-form-item').parent('.richText-form');
  602. if($form.attr("data-editor") === editorID) {
  603. // only for the currently selected editor
  604. var url = $form.find('input#videoURL').val();
  605. var size = $form.find('select#size').val();
  606. if(!url) {
  607. // no url set
  608. $form.prepend($('<div />', {style: 'color:red;display:none;', class: 'form-item is-error', text: settings.translations.pleaseEnterURL}));
  609. $form.children('.form-item.is-error').slideDown();
  610. setTimeout(function() {
  611. $form.children('.form-item.is-error').slideUp(function () {
  612. $(this).remove();
  613. });
  614. }, 5000);
  615. } else {
  616. // write html in editor
  617. var html = '';
  618. html = getVideoCode(url, size);
  619. if(!html) {
  620. $form.prepend($('<div />', {style: 'color:red;display:none;', class: 'form-item is-error', text: settings.translations.videoURLnotSupported}));
  621. $form.children('.form-item.is-error').slideDown();
  622. setTimeout(function() {
  623. $form.children('.form-item.is-error').slideUp(function () {
  624. $(this).remove();
  625. });
  626. }, 5000);
  627. } else {
  628. if(settings.useSingleQuotes === true) {
  629. } else {
  630. }
  631. restoreSelection(editorID, true);
  632. pasteHTMLAtCaret(html);
  633. updateTextarea();
  634. // reset input values
  635. $form.find('input#videoURL').val('');
  636. $('.richText-toolbar li.is-selected').removeClass("is-selected");
  637. }
  638. }
  639. }
  640. });
  641. // Resize images
  642. $(document).on('mousedown', function(e) {
  643. var $target = $(e.target);
  644. if(!$target.hasClass('richText-list') && $target.parents('.richText-list').length === 0) {
  645. // remove context menu
  646. $('.richText-list.list-rightclick').remove();
  647. if(!$target.hasClass('richText-form') && $target.parents('.richText-form').length === 0) {
  648. $('.richText-editNode').each(function () {
  649. var $this = $(this);
  650. $this.removeClass('richText-editNode');
  651. if ($this.attr('class') === '') {
  652. $this.removeAttr('class');
  653. }
  654. });
  655. }
  656. }
  657. if($target.prop("tagName") === "IMG" && $target.parents("#" + editorID)) {
  658. startX = e.pageX;
  659. startY = e.pageY;
  660. startW = $target.innerWidth();
  661. startH = $target.innerHeight();
  662. var left = $target.offset().left;
  663. var right = $target.offset().left + $target.innerWidth();
  664. var bottom = $target.offset().top + $target.innerHeight();
  665. var top = $target.offset().top;
  666. var resize = false;
  667. $target.css({'cursor' : 'default'});
  668. if(startY <= bottom && startY >= bottom-20 && startX >= right-20 && startX <= right) {
  669. // bottom right corner
  670. $resizeImage = $target;
  671. $resizeImage.css({'cursor' : 'nwse-resize'});
  672. resize = true;
  673. }
  674. if((resize === true || $resizeImage) && !$resizeImage.data("width")) {
  675. // set initial image size and prevent dragging image while resizing
  676. $resizeImage.data("width", $target.parents("#" + editorID).innerWidth());
  677. $resizeImage.data("height", $target.parents("#" + editorID).innerHeight()*3);
  678. e.preventDefault();
  679. } else if(resize === true || $resizeImage) {
  680. // resizing active, prevent other events
  681. e.preventDefault();
  682. } else {
  683. // resizing disabled, allow dragging image
  684. $resizeImage = null;
  685. }
  686. }
  687. });
  688. $(document)
  689. .mouseup(function(){
  690. if($resizeImage) {
  691. $resizeImage.css({'cursor' : 'default'});
  692. }
  693. $resizeImage = null;
  694. })
  695. .mousemove(function(e){
  696. if($resizeImage!==null){
  697. var maxWidth = $resizeImage.data('width');
  698. var currentWidth = $resizeImage.width();
  699. var maxHeight = $resizeImage.data('height');
  700. var currentHeight = $resizeImage.height();
  701. if((startW + e.pageX-startX) <= maxWidth && (startH + e.pageY-startY) <= maxHeight) {
  702. // only resize if new size is smaller than the original image size
  703. $resizeImage.innerWidth (startW + e.pageX-startX); // only resize width to adapt height proportionally
  704. // $box.innerHeight(startH + e.pageY-startY);
  705. updateTextarea();
  706. } else if((startW + e.pageX-startX) <= currentWidth && (startH + e.pageY-startY) <= currentHeight) {
  707. // only resize if new size is smaller than the previous size
  708. $resizeImage.innerWidth (startW + e.pageX-startX); // only resize width to adapt height proportionally
  709. updateTextarea();
  710. }
  711. }
  712. });
  713. // adding URL
  714. $(document).on("click", "#richText-URL button.btn", function(event) {
  715. event.preventDefault();
  716. var $button = $(this);
  717. var $form = $button.parent('.richText-form-item').parent('.richText-form');
  718. if($form.attr("data-editor") === editorID) {
  719. // only for currently selected editor
  720. var url = $form.find('input#url').val();
  721. var text = $form.find('input#urlText').val();
  722. var target = $form.find('#openIn').val();
  723. // set default values
  724. if(!target) {
  725. target = '_self';
  726. }
  727. if(!text) {
  728. text = url;
  729. }
  730. if(!url) {
  731. // no url set
  732. $form.prepend($('<div />', {style: 'color:red;display:none;', class: 'form-item is-error', text: settings.translations.pleaseEnterURL}));
  733. $form.children('.form-item.is-error').slideDown();
  734. setTimeout(function() {
  735. $form.children('.form-item.is-error').slideUp(function () {
  736. $(this).remove();
  737. });
  738. }, 5000);
  739. } else {
  740. // write html in editor
  741. var html = '';
  742. if(settings.useSingleQuotes === true) {
  743. html = "<a href='" + url + "' target='" + target + "'>" + text + "</a>";
  744. } else {
  745. html = '<a href="' + url + '" target="' + target + '">' + text + '</a>';
  746. }
  747. restoreSelection(editorID, false, true);
  748. var $editNode = $('.richText-editNode');
  749. if($editNode.length > 0 && $editNode.prop("tagName") === "A") {
  750. $editNode.attr("href", url);
  751. $editNode.attr("target", target);
  752. $editNode.text(text);
  753. $editNode.removeClass('richText-editNode');
  754. if($editNode.attr('class') === '') {
  755. $editNode.removeAttr('class');
  756. }
  757. } else {
  758. pasteHTMLAtCaret(html);
  759. }
  760. // reset input values
  761. $form.find('input#url').val('');
  762. $form.find('input#urlText').val('');
  763. $('.richText-toolbar li.is-selected').removeClass("is-selected");
  764. }
  765. }
  766. });
  767. // adding image
  768. $(document).on("click", "#richText-Image button.btn", function(event) {
  769. event.preventDefault();
  770. var $button = $(this);
  771. var $form = $button.parent('.richText-form-item').parent('.richText-form');
  772. if($form.attr("data-editor") === editorID) {
  773. // only for currently selected editor
  774. var url = $form.find('#imageURL').val();
  775. var align = $form.find('select#align').val();
  776. // set default values
  777. if(!align) {
  778. align = 'center';
  779. }
  780. if(!url) {
  781. // no url set
  782. $form.prepend($('<div />', {style: 'color:red;display:none;', class: 'form-item is-error', text: settings.translations.pleaseSelectImage}));
  783. $form.children('.form-item.is-error').slideDown();
  784. setTimeout(function() {
  785. $form.children('.form-item.is-error').slideUp(function () {
  786. $(this).remove();
  787. });
  788. }, 5000);
  789. } else {
  790. // write html in editor
  791. var html = '';
  792. if(settings.useSingleQuotes === true) {
  793. if(align === "center") {
  794. html = "<div style='text-align:center;'><img src='" + url + "'></div>";
  795. } else {
  796. html = "<img src='" + url + "' align='" + align + "'>";
  797. }
  798. } else {
  799. if(align === "center") {
  800. html = '<div style="text-align:center;"><img src="' + url + '"></div>';
  801. } else {
  802. html = '<img src="' + url + '" align="' + align + '">';
  803. }
  804. }
  805. restoreSelection(editorID, true);
  806. var $editNode = $('.richText-editNode');
  807. if($editNode.length > 0 && $editNode.prop("tagName") === "IMG") {
  808. $editNode.attr("src", url);
  809. if($editNode.parent('div').length > 0 && $editNode.parent('div').attr('style') === 'text-align:center;' && align !== 'center') {
  810. $editNode.unwrap('div');
  811. $editNode.attr('align', align);
  812. } else if(($editNode.parent('div').length === 0 || $editNode.parent('div').attr('style') !== 'text-align:center;') && align === 'center' ) {
  813. $editNode.wrap('<div style="text-align:center;"></div>');
  814. $editNode.removeAttr('align');
  815. } else {
  816. $editNode.attr('align', align);
  817. }
  818. $editNode.removeClass('richText-editNode');
  819. if($editNode.attr('class') === '') {
  820. $editNode.removeAttr('class');
  821. }
  822. } else {
  823. pasteHTMLAtCaret(html);
  824. }
  825. // reset input values
  826. $form.find('input#imageURL').val('');
  827. $('.richText-toolbar li.is-selected').removeClass("is-selected");
  828. }
  829. }
  830. });
  831. // adding file
  832. $(document).on("click", "#richText-File button.btn", function(event) {
  833. event.preventDefault();
  834. var $button = $(this);
  835. var $form = $button.parent('.richText-form-item').parent('.richText-form');
  836. if($form.attr("data-editor") === editorID) {
  837. // only for currently selected editor
  838. var url = $form.find('#fileURL').val();
  839. var text = $form.find('#fileText').val();
  840. // set default values
  841. if(!text) {
  842. text = url;
  843. }
  844. if(!url) {
  845. // no url set
  846. $form.prepend($('<div />', {style: 'color:red;display:none;', class: 'form-item is-error', text: settings.translations.pleaseSelectFile}));
  847. $form.children('.form-item.is-error').slideDown();
  848. setTimeout(function() {
  849. $form.children('.form-item.is-error').slideUp(function () {
  850. $(this).remove();
  851. });
  852. }, 5000);
  853. } else {
  854. // write html in editor
  855. var html = '';
  856. if(settings.useSingleQuotes === true) {
  857. html = "<a href='" + url + "' target='_blank'>" + text + "</a>";
  858. } else {
  859. html = '<a href="' + url + '" target="_blank">' + text + '</a>';
  860. }
  861. restoreSelection(editorID, true);
  862. pasteHTMLAtCaret(html);
  863. // reset input values
  864. $form.find('input#fileURL').val('');
  865. $form.find('input#fileText').val('');
  866. $('.richText-toolbar li.is-selected').removeClass("is-selected");
  867. }
  868. }
  869. });
  870. // adding table
  871. $(document).on("click", "#richText-Table button.btn", function(event) {
  872. event.preventDefault();
  873. var $button = $(this);
  874. var $form = $button.parent('.richText-form-item').parent('.richText-form');
  875. if($form.attr("data-editor") === editorID) {
  876. // only for currently selected editor
  877. var rows = $form.find('input#tableRows').val();
  878. var columns = $form.find('input#tableColumns').val();
  879. // set default values
  880. if(!rows || rows <= 0) {
  881. rows = 2;
  882. }
  883. if(!columns || columns <= 0) {
  884. columns = 2;
  885. }
  886. // generate table
  887. var html = '';
  888. if(settings.useSingleQuotes === true) {
  889. html = "<table class='table-1'><tbody>";
  890. } else {
  891. html = '<table class="table-1"><tbody>';
  892. }
  893. for(var i = 1; i <= rows; i++) {
  894. // start new row
  895. html += '<tr>';
  896. for(var n = 1; n <= columns; n++) {
  897. // start new column in row
  898. html += '<td> </td>';
  899. }
  900. html += '</tr>';
  901. }
  902. html += '</tbody></table>';
  903. // write html in editor
  904. restoreSelection(editorID, true);
  905. pasteHTMLAtCaret(html);
  906. // reset input values
  907. $form.find('input#tableColumns').val('');
  908. $form.find('input#tableRows').val('');
  909. $('.richText-toolbar li.is-selected').removeClass("is-selected");
  910. }
  911. });
  912. // opening / closing toolbar dropdown
  913. $(document).on("click", function(event) {
  914. var $clickedElement = $(event.target);
  915. if($clickedElement.parents('.richText-toolbar').length === 0) {
  916. // element not in toolbar
  917. // ignore
  918. } else if($clickedElement.hasClass("richText-dropdown-outer")) {
  919. // closing dropdown by clicking inside the editor
  920. $clickedElement.parent('a').parent('li').removeClass("is-selected");
  921. } else if($clickedElement.find(".richText").length > 0) {
  922. // closing dropdown by clicking outside of the editor
  923. $('.richText-toolbar li').removeClass("is-selected");
  924. } else if($clickedElement.parent().hasClass("richText-dropdown-close")) {
  925. // closing dropdown by clicking on the close button
  926. $('.richText-toolbar li').removeClass("is-selected");
  927. } else if($clickedElement.hasClass("richText-btn") && $(event.target).children('.richText-dropdown-outer').length > 0) {
  928. // opening dropdown by clicking on toolbar button
  929. $clickedElement.parent('li').addClass("is-selected");
  930. if($clickedElement.children('.fa,svg').hasClass("fa-link")) {
  931. // put currently selected text in URL form to replace it
  932. restoreSelection(editorID, false, true);
  933. var selectedText = getSelectedText();
  934. $clickedElement.find("input#urlText").val('');
  935. $clickedElement.find("input#url").val('');
  936. if(selectedText) {
  937. $clickedElement.find("input#urlText").val(selectedText);
  938. }
  939. } else if($clickedElement.hasClass("fa-image")) {
  940. // image
  941. }
  942. }
  943. });
  944. // Executing editor commands
  945. $(document).on("click", ".richText-toolbar a[data-command]", function(event) {
  946. var $button = $(this);
  947. var $toolbar = $button.closest('.richText-toolbar');
  948. var $editor = $toolbar.siblings('.richText-editor');
  949. var id = $editor.attr("id");
  950. if($editor.length > 0 && id === editorID && (!$button.parent("li").attr('data-disable') || $button.parent("li").attr('data-disable') === "false")) {
  951. event.preventDefault();
  952. var command = $(this).data("command");
  953. if(command === "toggleCode") {
  954. toggleCode($editor.attr("id"));
  955. } else {
  956. var option = null;
  957. if ($(this).data('option')) {
  958. option = $(this).data('option').toString();
  959. if (option.match(/^h[1-6]$/)) {
  960. command = "heading";
  961. }
  962. }
  963. formatText(command, option, id);
  964. if (command === "removeFormat") {
  965. // remove HTML/CSS formatting
  966. $editor.find('*').each(function() {
  967. // remove all, but very few, attributes from the nodes
  968. var keepAttributes = [
  969. "id", "class",
  970. "name", "action", "method",
  971. "src", "align", "alt", "title",
  972. "style", "webkitallowfullscreen", "mozallowfullscreen", "allowfullscreen",
  973. "width", "height", "frameborder"
  974. ];
  975. var element = $(this);
  976. var attributes = $.map(this.attributes, function(item) {
  977. return item.name;
  978. });
  979. $.each(attributes, function(i, item) {
  980. if(keepAttributes.indexOf(item) < 0 && item.substr(0, 5) !== 'data-') {
  981. element.removeAttr(item);
  982. }
  983. });
  984. if(element.prop('tagName') === "A") {
  985. // remove empty URL tags
  986. element.replaceWith(function() {
  987. return $('<span />', {html: $(this).html()});
  988. });
  989. }
  990. });
  991. formatText('formatBlock', 'div', id);
  992. }
  993. // clean up empty tags, which can be created while replacing formatting or when copy-pasting from other tools
  994. $editor.find('div:empty,p:empty,li:empty,h1:empty,h2:empty,h3:empty,h4:empty,h5:empty,h6:empty').remove();
  995. $editor.find('h1,h2,h3,h4,h5,h6').unwrap('h1,h2,h3,h4,h5,h6');
  996. }
  997. }
  998. // close dropdown after click
  999. $button.parents('li.is-selected').removeClass('is-selected');
  1000. });
  1001. /** INTERNAL METHODS **/
  1002. /**
  1003. * Format text in editor
  1004. * @param {string} command
  1005. * @param {string|null} option
  1006. * @param {string} editorID
  1007. * @private
  1008. */
  1009. function formatText(command, option, editorID) {
  1010. if (typeof option === "undefined") {
  1011. option = null;
  1012. }
  1013. // restore selection from before clicking on any button
  1014. doRestore(editorID);
  1015. // Temporarily enable designMode so that
  1016. // document.execCommand() will work
  1017. // document.designMode = "ON";
  1018. // Execute the command
  1019. if(command === "heading" && getSelectedText()) {
  1020. // IE workaround
  1021. pasteHTMLAtCaret('<' + option + '>' + getSelectedText() + '</' + option + '>');
  1022. } else if(command === "fontSize" && parseInt(option) > 0) {
  1023. var selection = getSelectedText();
  1024. selection = (selection + '').replace(/([^>\r\n]?)(\r\n|\n\r|\r|\n)/g, '$1' + '<br>' + '$2');
  1025. var html = (settings.useSingleQuotes ? "<span style='font-size:" + option + "px;'>" + selection + "</span>" : '<span style="font-size:' + option + 'px;">' + selection + '</span>');
  1026. pasteHTMLAtCaret(html);
  1027. } else {
  1028. document.execCommand(command, false, option);
  1029. }
  1030. // Disable designMode
  1031. // document.designMode = "OFF";
  1032. }
  1033. /**
  1034. * Update textarea when updating editor
  1035. * @private
  1036. */
  1037. function updateTextarea() {
  1038. var $editor = $('#' + editorID);
  1039. var content = $editor.html();
  1040. if(settings.useSingleQuotes === true) {
  1041. content = changeAttributeQuotes(content);
  1042. }
  1043. $editor.siblings('.richText-initial').val(content);
  1044. }
  1045. /**
  1046. * Update editor when updating textarea
  1047. * @private
  1048. */
  1049. function updateEditor(editorID) {
  1050. var $editor = $('#' + editorID);
  1051. var content = $editor.siblings('.richText-initial').val();
  1052. $editor.html(content);
  1053. }
  1054. /**
  1055. * Save caret position and selection
  1056. * @return object
  1057. **/
  1058. function saveSelection(editorID) {
  1059. var containerEl = document.getElementById(editorID);
  1060. var range, start, end, type;
  1061. if(window.getSelection && document.createRange) {
  1062. var sel = window.getSelection && window.getSelection();
  1063. if (sel && sel.rangeCount > 0 && $(sel.anchorNode).parents('#' + editorID).length > 0) {
  1064. range = window.getSelection().getRangeAt(0);
  1065. var preSelectionRange = range.cloneRange();
  1066. preSelectionRange.selectNodeContents(containerEl);
  1067. preSelectionRange.setEnd(range.startContainer, range.startOffset);
  1068. start = preSelectionRange.toString().length;
  1069. end = (start + range.toString().length);
  1070. type = (start === end ? 'caret' : 'selection');
  1071. anchor = sel.anchorNode; //(type === "caret" && sel.anchorNode.tagName ? sel.anchorNode : false);
  1072. start = (type === 'caret' && anchor !== false ? 0 : preSelectionRange.toString().length);
  1073. end = (type === 'caret' && anchor !== false ? 0 : (start + range.toString().length));
  1074. return {
  1075. start: start,
  1076. end: end,
  1077. type: type,
  1078. anchor: anchor,
  1079. editorID: editorID
  1080. }
  1081. }
  1082. }
  1083. return (savedSelection ? savedSelection : {
  1084. start: 0,
  1085. end: 0
  1086. });
  1087. }
  1088. /**
  1089. * Restore selection
  1090. **/
  1091. function restoreSelection(editorID, media, url) {
  1092. var containerEl = document.getElementById(editorID);
  1093. var savedSel = savedSelection;
  1094. if(!savedSel) {
  1095. // fix selection if editor has not been focused
  1096. savedSel = {
  1097. 'start': 0,
  1098. 'end': 0,
  1099. 'type': 'caret',
  1100. 'editorID': editorID,
  1101. 'anchor': $('#' + editorID).children('div')[0]
  1102. };
  1103. }
  1104. if(savedSel.editorID !== editorID) {
  1105. return false;
  1106. } else if(media === true) {
  1107. containerEl = (savedSel.anchor ? savedSel.anchor : containerEl); // fix selection issue
  1108. } else if(url === true) {
  1109. if(savedSel.start === 0 && savedSel.end === 0) {
  1110. containerEl = (savedSel.anchor ? savedSel.anchor : containerEl); // fix selection issue
  1111. }
  1112. }
  1113. if (window.getSelection && document.createRange) {
  1114. var charIndex = 0, range = document.createRange();
  1115. if(!range || !containerEl) { window.getSelection().removeAllRanges(); return true; }
  1116. range.setStart(containerEl, 0);
  1117. range.collapse(true);
  1118. var nodeStack = [containerEl], node, foundStart = false, stop = false;
  1119. while (!stop && (node = nodeStack.pop())) {
  1120. if (node.nodeType === 3) {
  1121. var nextCharIndex = charIndex + node.length;
  1122. if (!foundStart && savedSel.start >= charIndex && savedSel.start <= nextCharIndex) {
  1123. range.setStart(node, savedSel.start - charIndex);
  1124. foundStart = true;
  1125. }
  1126. if (foundStart && savedSel.end >= charIndex && savedSel.end <= nextCharIndex) {
  1127. range.setEnd(node, savedSel.end - charIndex);
  1128. stop = true;
  1129. }
  1130. charIndex = nextCharIndex;
  1131. } else {
  1132. var i = node.childNodes.length;
  1133. while (i--) {
  1134. nodeStack.push(node.childNodes[i]);
  1135. }
  1136. }
  1137. }
  1138. var sel = window.getSelection();
  1139. sel.removeAllRanges();
  1140. sel.addRange(range);
  1141. }
  1142. }
  1143. /**
  1144. * Save caret position and selection
  1145. * @return object
  1146. **/
  1147. /*
  1148. function saveSelection(editorID) {
  1149. var containerEl = document.getElementById(editorID);
  1150. var start;
  1151. if (window.getSelection && document.createRange) {
  1152. var sel = window.getSelection && window.getSelection();
  1153. if (sel && sel.rangeCount > 0) {
  1154. var range = window.getSelection().getRangeAt(0);
  1155. var preSelectionRange = range.cloneRange();
  1156. preSelectionRange.selectNodeContents(containerEl);
  1157. preSelectionRange.setEnd(range.startContainer, range.startOffset);
  1158. start = preSelectionRange.toString().length;
  1159. return {
  1160. start: start,
  1161. end: start + range.toString().length,
  1162. editorID: editorID
  1163. }
  1164. } else {
  1165. return (savedSelection ? savedSelection : {
  1166. start: 0,
  1167. end: 0
  1168. });
  1169. }
  1170. } else if (document.selection && document.body.createTextRange) {
  1171. var selectedTextRange = document.selection.createRange();
  1172. var preSelectionTextRange = document.body.createTextRange();
  1173. preSelectionTextRange.moveToElementText(containerEl);
  1174. preSelectionTextRange.setEndPoint("EndToStart", selectedTextRange);
  1175. start = preSelectionTextRange.text.length;
  1176. return {
  1177. start: start,
  1178. end: start + selectedTextRange.text.length,
  1179. editorID: editorID
  1180. };
  1181. }
  1182. }
  1183. */
  1184. /**
  1185. * Restore selection
  1186. **/
  1187. /*
  1188. function restoreSelection(editorID) {
  1189. var containerEl = document.getElementById(editorID);
  1190. var savedSel = savedSelection;
  1191. if(savedSel.editorID !== editorID) {
  1192. return false;
  1193. }
  1194. if (window.getSelection && document.createRange) {
  1195. var charIndex = 0, range = document.createRange();
  1196. range.setStart(containerEl, 0);
  1197. range.collapse(true);
  1198. var nodeStack = [containerEl], node, foundStart = false, stop = false;
  1199. while (!stop && (node = nodeStack.pop())) {
  1200. if (node.nodeType === 3) {
  1201. var nextCharIndex = charIndex + node.length;
  1202. if (!foundStart && savedSel.start >= charIndex && savedSel.start <= nextCharIndex) {
  1203. range.setStart(node, savedSel.start - charIndex);
  1204. foundStart = true;
  1205. }
  1206. if (foundStart && savedSel.end >= charIndex && savedSel.end <= nextCharIndex) {
  1207. range.setEnd(node, savedSel.end - charIndex);
  1208. stop = true;
  1209. }
  1210. charIndex = nextCharIndex;
  1211. } else {
  1212. var i = node.childNodes.length;
  1213. while (i--) {
  1214. nodeStack.push(node.childNodes[i]);
  1215. }
  1216. }
  1217. }
  1218. var sel = window.getSelection();
  1219. sel.removeAllRanges();
  1220. sel.addRange(range);
  1221. } else if (document.selection && document.body.createTextRange) {
  1222. var textRange = document.body.createTextRange();
  1223. textRange.moveToElementText(containerEl);
  1224. textRange.collapse(true);
  1225. textRange.moveEnd("character", savedSel.end);
  1226. textRange.moveStart("character", savedSel.start);
  1227. textRange.select();
  1228. }
  1229. }
  1230. */
  1231. /**
  1232. * Enables tabbing/shift-tabbing between contentEditable table cells
  1233. * @param {Window} win - Active window context.
  1234. * @param {Event} e - jQuery Event object for the keydown that fired.
  1235. */
  1236. function tabifyEditableTable(win, e) {
  1237. if (e.keyCode !== 9) {
  1238. return false;
  1239. }
  1240. var sel;
  1241. if (win.getSelection) {
  1242. sel = win.getSelection();
  1243. if (sel.rangeCount > 0) {
  1244. var textNode = null,
  1245. direction = null;
  1246. if (!e.shiftKey) {
  1247. direction = "next";
  1248. textNode = (sel.focusNode.nodeName === "TD")
  1249. ? (sel.focusNode.nextSibling != null)
  1250. ? sel.focusNode.nextSibling
  1251. : (sel.focusNode.parentNode.nextSibling != null)
  1252. ? sel.focusNode.parentNode.nextSibling.childNodes[0]
  1253. : null
  1254. : (sel.focusNode.parentNode.nextSibling != null)
  1255. ? sel.focusNode.parentNode.nextSibling
  1256. : (sel.focusNode.parentNode.parentNode.nextSibling != null)
  1257. ? sel.focusNode.parentNode.parentNode.nextSibling.childNodes[0]
  1258. : null;
  1259. } else {
  1260. direction = "previous";
  1261. textNode = (sel.focusNode.nodeName === "TD")
  1262. ? (sel.focusNode.previousSibling != null)
  1263. ? sel.focusNode.previousSibling
  1264. : (sel.focusNode.parentNode.previousSibling != null)
  1265. ? sel.focusNode.parentNode.previousSibling.childNodes[sel.focusNode.parentNode.previousSibling.childNodes.length - 1]
  1266. : null
  1267. : (sel.focusNode.parentNode.previousSibling != null)
  1268. ? sel.focusNode.parentNode.previousSibling
  1269. : (sel.focusNode.parentNode.parentNode.previousSibling != null)
  1270. ? sel.focusNode.parentNode.parentNode.previousSibling.childNodes[sel.focusNode.parentNode.parentNode.previousSibling.childNodes.length - 1]
  1271. : null;
  1272. }
  1273. if (textNode != null) {
  1274. sel.collapse(textNode, Math.min(textNode.length, sel.focusOffset + 1));
  1275. if (textNode.textContent != null) {
  1276. sel.selectAllChildren(textNode);
  1277. }
  1278. e.preventDefault();
  1279. return true;
  1280. } else if(textNode === null && direction === "next" && sel.focusNode.nodeName === "TD") {
  1281. // add new row on TAB if arrived at the end of the row
  1282. var $table = $(sel.focusNode).parents("table");
  1283. var cellsPerLine = $table.find("tr").first().children("td").length;
  1284. var $tr = $("<tr />");
  1285. var $td = $("<td />");
  1286. for(var i = 1; i <= cellsPerLine; i++) {
  1287. $tr.append($td.clone());
  1288. }
  1289. $table.append($tr);
  1290. // simulate tabing through table
  1291. tabifyEditableTable(window, {keyCode: 9, shiftKey: false, preventDefault: function(){}});
  1292. }
  1293. }
  1294. }
  1295. return false;
  1296. }
  1297. /**
  1298. * Returns the text from the current selection
  1299. * @private
  1300. * @return {string|boolean}
  1301. */
  1302. function getSelectedText() {
  1303. var range;
  1304. if (window.getSelection) { // all browsers, except IE before version 9
  1305. range = window.getSelection();
  1306. return range.toString() ? range.toString() : range.focusNode.nodeValue;
  1307. } else if (document.selection.createRange) { // Internet Explorer
  1308. range = document.selection.createRange();
  1309. return range.text;
  1310. }
  1311. return false;
  1312. }
  1313. /**
  1314. * Save selection
  1315. */
  1316. function doSave(editorID) {
  1317. var $textarea = $('.richText-editor#' + editorID).siblings('.richText-initial');
  1318. addHistory($textarea.val(), editorID);
  1319. savedSelection = saveSelection(editorID);
  1320. }
  1321. /**
  1322. * Add to history
  1323. * @param val Editor content
  1324. * @param id Editor ID
  1325. */
  1326. function addHistory(val, id) {
  1327. if(!history[id]) {
  1328. return false;
  1329. }
  1330. if(history[id].length-1 > historyPosition[id]) {
  1331. history[id].length = historyPosition[id] + 1;
  1332. }
  1333. if(history[id][history[id].length-1] !== val) {
  1334. history[id].push(val);
  1335. }
  1336. historyPosition[id] = history[id].length-1;
  1337. setHistoryButtons(id);
  1338. }
  1339. function setHistoryButtons(id) {
  1340. if(historyPosition[id] <= 0) {
  1341. $editor.find(".richText-undo").addClass("is-disabled");
  1342. } else {
  1343. $editor.find(".richText-undo").removeClass("is-disabled");
  1344. }
  1345. if(historyPosition[id] >= history[id].length-1 || history[id].length === 0) {
  1346. $editor.find(".richText-redo").addClass("is-disabled");
  1347. } else {
  1348. $editor.find(".richText-redo").removeClass("is-disabled");
  1349. }
  1350. }
  1351. /**
  1352. * Undo
  1353. * @param $editor
  1354. */
  1355. function undo($editor) {
  1356. var id = $editor.children('.richText-editor').attr('id');
  1357. historyPosition[id]--;
  1358. if(!historyPosition[id] && historyPosition[id] !== 0) {
  1359. return false;
  1360. }
  1361. var value = history[id][historyPosition[id]];
  1362. $editor.find('textarea').val(value);
  1363. $editor.find('.richText-editor').html(value);
  1364. setHistoryButtons(id);
  1365. }
  1366. /**
  1367. * Undo
  1368. * @param $editor
  1369. */
  1370. function redo($editor) {
  1371. var id = $editor.children('.richText-editor').attr('id');
  1372. historyPosition[id]++;
  1373. if(!historyPosition[id] && historyPosition[id] !== 0) {
  1374. return false;
  1375. }
  1376. var value = history[id][historyPosition[id]];
  1377. $editor.find('textarea').val(value);
  1378. $editor.find('.richText-editor').html(value);
  1379. setHistoryButtons(id);
  1380. }
  1381. /**
  1382. * Restore selection
  1383. */
  1384. function doRestore(id) {
  1385. if(savedSelection) {
  1386. restoreSelection((id ? id : savedSelection.editorID));
  1387. }
  1388. }
  1389. /**
  1390. * Paste HTML at caret position
  1391. * @param {string} html HTML code
  1392. * @private
  1393. */
  1394. function pasteHTMLAtCaret(html) {
  1395. // add HTML code for Internet Explorer
  1396. var sel, range;
  1397. if (window.getSelection) {
  1398. // IE9 and non-IE
  1399. sel = window.getSelection();
  1400. if (sel.getRangeAt && sel.rangeCount) {
  1401. range = sel.getRangeAt(0);
  1402. range.deleteContents();
  1403. // Range.createContextualFragment() would be useful here but is
  1404. // only relatively recently standardized and is not supported in
  1405. // some browsers (IE9, for one)
  1406. var el = document.createElement("div");
  1407. el.innerHTML = html;
  1408. var frag = document.createDocumentFragment(), node, lastNode;
  1409. while ( (node = el.firstChild) ) {
  1410. lastNode = frag.appendChild(node);
  1411. }
  1412. range.insertNode(frag);
  1413. // Preserve the selection
  1414. if (lastNode) {
  1415. range = range.cloneRange();
  1416. range.setStartAfter(lastNode);
  1417. range.collapse(true);
  1418. sel.removeAllRanges();
  1419. sel.addRange(range);
  1420. }
  1421. }
  1422. } else if (document.selection && document.selection.type !== "Control") {
  1423. // IE < 9
  1424. document.selection.createRange().pasteHTML(html);
  1425. }
  1426. }
  1427. /**
  1428. * Change quotes around HTML attributes
  1429. * @param {string} string
  1430. * @return {string}
  1431. */
  1432. function changeAttributeQuotes(string) {
  1433. if(!string) {
  1434. return '';
  1435. }
  1436. var regex;
  1437. var rstring;
  1438. if(settings.useSingleQuotes === true) {
  1439. regex = /\s+(\w+\s*=\s*(["][^"]*["])|(['][^']*[']))+/g;
  1440. rstring = string.replace(regex, function($0,$1,$2){
  1441. if(!$2) {return $0;}
  1442. return $0.replace($2, $2.replace(/\"/g, "'"));
  1443. });
  1444. } else {
  1445. regex = /\s+(\w+\s*=\s*(['][^']*['])|(["][^"]*["]))+/g;
  1446. rstring = string.replace(regex, function($0,$1,$2){
  1447. if(!$2) {return $0;}
  1448. return $0.replace($2, $2.replace(/'/g, '"'));
  1449. });
  1450. }
  1451. return rstring;
  1452. }
  1453. /**
  1454. * Load colors for font or background
  1455. * @param {string} command Command
  1456. * @returns {string}
  1457. * @private
  1458. */
  1459. function loadColors(command) {
  1460. var colors = [];
  1461. var result = '';
  1462. colors["#FFFFFF"] = settings.translations.white;
  1463. colors["#000000"] = settings.translations.black;
  1464. colors["#7F6000"] = settings.translations.brown;
  1465. colors["#938953"] = settings.translations.beige;
  1466. colors["#1F497D"] = settings.translations.darkBlue;
  1467. colors["blue"] = settings.translations.blue;
  1468. colors["#4F81BD"] = settings.translations.lightBlue;
  1469. colors["#953734"] = settings.translations.darkRed;
  1470. colors["red"] = settings.translations.red;
  1471. colors["#4F6128"] = settings.translations.darkGreen;
  1472. colors["green"] = settings.translations.green;
  1473. colors["#3F3151"] = settings.translations.purple;
  1474. colors["#31859B"] = settings.translations.darkTurquois;
  1475. colors["#4BACC6"] = settings.translations.turquois;
  1476. colors["#E36C09"] = settings.translations.darkOrange;
  1477. colors["#F79646"] = settings.translations.orange;
  1478. colors["#FFFF00"] = settings.translations.yellow;
  1479. if(settings.colors && settings.colors.length > 0) {
  1480. colors = settings.colors;
  1481. }
  1482. for (var i in colors) {
  1483. result += '<li class="inline"><a data-command="' + command + '" data-option="' + i + '" style="text-align:left;" title="' + colors[i] + '"><span class="box-color" style="background-color:' + i + '"></span></a></li>';
  1484. }
  1485. return result;
  1486. }
  1487. /**
  1488. * Toggle (show/hide) code or editor
  1489. * @private
  1490. */
  1491. function toggleCode(editorID) {
  1492. doRestore(editorID);
  1493. if($editor.find('.richText-editor').is(":visible")) {
  1494. // show code
  1495. $editor.find('.richText-initial').show();
  1496. $editor.find('.richText-editor').hide();
  1497. // disable non working buttons
  1498. $('.richText-toolbar').find('.richText-btn').each(function() {
  1499. if($(this).children('.fa-code').length === 0) {
  1500. $(this).parent('li').attr("data-disable", "true");
  1501. }
  1502. });
  1503. convertCaretPosition(editorID, savedSelection);
  1504. } else {
  1505. // show editor
  1506. $editor.find('.richText-initial').hide();
  1507. $editor.find('.richText-editor').show();
  1508. convertCaretPosition(editorID, savedSelection, true);
  1509. // enable all buttons again
  1510. $('.richText-toolbar').find('li').removeAttr("data-disable");
  1511. }
  1512. }
  1513. /**
  1514. * Convert caret position from editor to code view (or in reverse)
  1515. * @param {string} editorID
  1516. * @param {object} selection
  1517. * @param {boolean} reverse
  1518. **/
  1519. function convertCaretPosition(editorID, selection, reverse) {
  1520. var $editor = $('#' + editorID);
  1521. var $textarea = $editor.siblings(".richText-initial");
  1522. var code = $textarea.val();
  1523. if(!selection || !code) {
  1524. return {start: 0, end: 0};
  1525. }
  1526. if(reverse === true) {
  1527. savedSelection = {start: $editor.text().length, end: $editor.text().length, editorID: editorID};
  1528. restoreSelection(editorID);
  1529. return true;
  1530. }
  1531. selection.node = $textarea[0];
  1532. var states = {start: false, end: false, tag: false, isTag: false, tagsCount: 0, isHighlight: (selection.start !== selection.end)};
  1533. for(var i = 0; i < code.length; i++) {
  1534. if(code[i] === "<") {
  1535. // HTML tag starts
  1536. states.isTag = true;
  1537. states.tag = false;
  1538. states.tagsCount++;
  1539. } else if(states.isTag === true && code[i] !== ">") {
  1540. states.tagsCount++;
  1541. } else if(states.isTag === true && code[i] === ">") {
  1542. states.isTag = false;
  1543. states.tag = true;
  1544. states.tagsCount++;
  1545. } else if(states.tag === true) {
  1546. states.tag = false;
  1547. }
  1548. if(!reverse) {
  1549. if((selection.start + states.tagsCount) <= i && states.isHighlight && !states.isTag && !states.tag && !states.start) {
  1550. selection.start = i;
  1551. states.start = true;
  1552. } else if((selection.start + states.tagsCount) <= i+1 && !states.isHighlight && !states.isTag && !states.tag && !states.start) {
  1553. selection.start = i+1;
  1554. states.start = true;
  1555. }
  1556. if((selection.end + states.tagsCount) <= i+1 && !states.isTag && !states.tag && !states.end) {
  1557. selection.end = i+1;
  1558. states.end = true;
  1559. }
  1560. }
  1561. }
  1562. createSelection(selection.node, selection.start, selection.end);
  1563. return selection;
  1564. }
  1565. /**
  1566. * Create selection on node element
  1567. * @param {Node} field
  1568. * @param {int} start
  1569. * @param {int} end
  1570. **/
  1571. function createSelection(field, start, end) {
  1572. if( field.createTextRange ) {
  1573. var selRange = field.createTextRange();
  1574. selRange.collapse(true);
  1575. selRange.moveStart('character', start);
  1576. selRange.moveEnd('character', end);
  1577. selRange.select();
  1578. field.focus();
  1579. } else if( field.setSelectionRange ) {
  1580. field.focus();
  1581. field.setSelectionRange(start, end);
  1582. } else if( typeof field.selectionStart != 'undefined' ) {
  1583. field.selectionStart = start;
  1584. field.selectionEnd = end;
  1585. field.focus();
  1586. }
  1587. }
  1588. /**
  1589. * Get video embed code from URL
  1590. * @param {string} url Video URL
  1591. * @param {string} size Size in the form of widthxheight
  1592. * @return {string|boolean}
  1593. * @private
  1594. **/
  1595. function getVideoCode(url, size) {
  1596. var video = getVideoID(url);
  1597. var responsive = false, success = false;
  1598. if(!video) {
  1599. // video URL not supported
  1600. return false;
  1601. }
  1602. if(!size) {
  1603. size = "640x360";
  1604. size = size.split("x");
  1605. } else if(size !== "responsive") {
  1606. size = size.split("x");
  1607. } else {
  1608. responsive = true;
  1609. size = "640x360";
  1610. size = size.split("x");
  1611. }
  1612. var html = '<br><br>';
  1613. if(responsive === true) {
  1614. html += '<div class="videoEmbed" style="position:relative;height:0;padding-bottom:56.25%">';
  1615. }
  1616. var allowfullscreen = 'webkitallowfullscreen mozallowfullscreen allowfullscreen';
  1617. if(video.platform === "YouTube") {
  1618. var youtubeDomain = (settings.youtubeCookies ? 'www.youtube.com' : 'www.youtube-nocookie.com');
  1619. html += '<iframe src="https://' + youtubeDomain + '/embed/' + video.id + '?ecver=2" width="' + size[0] + '" height="' + size[1] + '" frameborder="0"' + (responsive === true ? ' style="position:absolute;width:100%;height:100%;left:0"' : '') + ' ' + allowfullscreen + '></iframe>';
  1620. success = true;
  1621. } else if(video.platform === "Vimeo") {
  1622. html += '<iframe src="https://player.vimeo.com/video/' + video.id + '" width="' + size[0] + '" height="' + size[1] + '" frameborder="0"' + (responsive === true ? ' style="position:absolute;width:100%;height:100%;left:0"' : '') + ' ' + allowfullscreen + '></iframe>';
  1623. success = true;
  1624. } else if(video.platform === "Facebook") {
  1625. html += '<iframe src="https://www.facebook.com/plugins/video.php?href=' + encodeURI(url) + '&show_text=0&width=' + size[0] + '" width="' + size[0] + '" height="' + size[1] + '" style="' + (responsive === true ? 'position:absolute;width:100%;height:100%;left:0;border:none;overflow:hidden"' : 'border:none;overflow:hidden') + '" scrolling="no" frameborder="0" allowTransparency="true" ' + allowfullscreen + '></iframe>';
  1626. success = true;
  1627. } else if(video.platform === "Dailymotion") {
  1628. html += '<iframe frameborder="0" width="' + size[0] + '" height="' + size[1] + '" src="//www.dailymotion.com/embed/video/' + video.id + '"' + (responsive === true ? ' style="position:absolute;width:100%;height:100%;left:0"' : '') + ' ' + allowfullscreen + '></iframe>';
  1629. success = true;
  1630. }
  1631. if(responsive === true) {
  1632. html += '</div>';
  1633. }
  1634. html += '<br><br>';
  1635. if(success) {
  1636. return html;
  1637. }
  1638. return false;
  1639. }
  1640. /**
  1641. * Returns the unique video ID
  1642. * @param {string} url
  1643. * return {object|boolean}
  1644. **/
  1645. function getVideoID(url) {
  1646. var vimeoRegExp = /(?:http?s?:\/\/)?(?:www\.)?(?:vimeo\.com)\/?(.+)/;
  1647. var youtubeRegExp = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|\&v=)([^#\&\?]*).*/;
  1648. var facebookRegExp = /(?:http?s?:\/\/)?(?:www\.)?(?:facebook\.com)\/.*\/videos\/[0-9]+/;
  1649. var dailymotionRegExp = /(?:http?s?:\/\/)?(?:www\.)?(?:dailymotion\.com)\/video\/([a-zA-Z0-9]+)/;
  1650. var youtubeMatch = url.match(youtubeRegExp);
  1651. var vimeoMatch = url.match(vimeoRegExp);
  1652. var facebookMatch = url.match(facebookRegExp);
  1653. var dailymotionMatch = url.match(dailymotionRegExp);
  1654. if (youtubeMatch && youtubeMatch[2].length === 11) {
  1655. return {
  1656. "platform": "YouTube",
  1657. "id": youtubeMatch[2]
  1658. };
  1659. } else if(vimeoMatch && vimeoMatch[1]) {
  1660. return {
  1661. "platform": "Vimeo",
  1662. "id": vimeoMatch[1]
  1663. };
  1664. } else if(facebookMatch && facebookMatch[0]) {
  1665. return {
  1666. "platform": "Facebook",
  1667. "id" : facebookMatch[0]
  1668. };
  1669. } else if(dailymotionMatch && dailymotionMatch[1]) {
  1670. return {
  1671. "platform": "Dailymotion",
  1672. "id" : dailymotionMatch[1]
  1673. };
  1674. }
  1675. return false;
  1676. }
  1677. /**
  1678. * Fix the first line as by default the first line has no tag container
  1679. */
  1680. function fixFirstLine() {
  1681. if($editor && !$editor.find(".richText-editor").html()) {
  1682. // set first line with the right tags
  1683. if(settings.useParagraph !== false) {
  1684. $editor.find(".richText-editor").html('<p><br></p>');
  1685. } else {
  1686. //$editor.find(".richText-editor").html('<div><br></div>');
  1687. $editor.find(".richText-editor").html('');
  1688. }
  1689. } else {
  1690. // replace tags, to force <div> or <p> tags and fix issues
  1691. if(settings.useParagraph !== false) {
  1692. $editor.find(".richText-editor").find('div:not(.videoEmbed)').replaceWith(function() {
  1693. return $('<p />', {html: $(this).html()});
  1694. });
  1695. } else {
  1696. $editor.find(".richText-editor").find('p').replaceWith(function() {
  1697. return $('<div />', {html: $(this).html()});
  1698. });
  1699. }
  1700. }
  1701. updateTextarea();
  1702. }
  1703. return $(this);
  1704. };
  1705. $.fn.unRichText = function( options ) {
  1706. // set default options
  1707. // and merge them with the parameter options
  1708. var settings = $.extend({
  1709. delay: 0 // delay in ms
  1710. }, options);
  1711. var $editor, $textarea, $main;
  1712. var $el = $(this);
  1713. /**
  1714. * Initialize undoing RichText and call remove() method
  1715. */
  1716. function init() {
  1717. if($el.hasClass('richText')) {
  1718. $main = $el;
  1719. } else if($el.hasClass('richText-initial') || $el.hasClass('richText-editor')) {
  1720. $main = $el.parents('.richText');
  1721. }
  1722. if(!$main) {
  1723. // node element does not correspond to RichText elements
  1724. return false;
  1725. }
  1726. $editor = $main.find('.richText-editor');
  1727. $textarea = $main.find('.richText-initial');
  1728. if(parseInt(settings.delay) > 0) {
  1729. // a delay has been set
  1730. setTimeout(remove, parseInt(settings.delay));
  1731. } else {
  1732. remove();
  1733. }
  1734. }
  1735. init();
  1736. /**
  1737. * Remove RichText elements
  1738. */
  1739. function remove() {
  1740. $main.find('.richText-toolbar').remove();
  1741. $main.find('.richText-editor').remove();
  1742. $textarea
  1743. .unwrap('.richText')
  1744. .data('editor', 'richText')
  1745. .removeClass('richText-initial')
  1746. .show();
  1747. }
  1748. };
  1749. }( jQuery ));