You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

преди 11 години
преди 11 години
преди 11 години
преди 11 години
преди 11 години
преди 11 години
преди 11 години
преди 11 години
преди 11 години
преди 11 години
преди 11 години
преди 11 години
преди 11 години
преди 11 години
преди 11 години
преди 11 години
преди 11 години
преди 11 години
преди 11 години
преди 11 години
преди 11 години
преди 11 години
преди 11 години
преди 11 години
преди 11 години
преди 11 години
преди 11 години
преди 11 години
преди 11 години
преди 11 години
преди 11 години
преди 11 години
преди 11 години
преди 11 години
преди 11 години
преди 11 години
преди 11 години
преди 11 години
преди 11 години
преди 11 години
преди 11 години
преди 11 години
преди 11 години
преди 11 години
преди 11 години
преди 11 години
преди 11 години
преди 11 години
преди 11 години
преди 11 години
преди 11 години
преди 11 години
преди 11 години
преди 11 години
преди 11 години
преди 11 години
преди 11 години
преди 11 години
преди 11 години
преди 11 години
преди 11 години
преди 11 години
преди 11 години
преди 11 години
преди 11 години
преди 11 години
преди 11 години
преди 11 години
преди 11 години
преди 11 години
преди 11 години
преди 11 години
преди 11 години
преди 11 години
преди 11 години
преди 11 години
преди 11 години
преди 11 години
преди 11 години
преди 11 години
преди 11 години
преди 11 години
преди 11 години
преди 11 години
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514
  1. // Copyright (c) 2013, Web Notes Technologies Pvt. Ltd.
  2. // MIT License. See license.txt
  3. /* Inspired from: http://github.com/mindmup/bootstrap-wysiwyg */
  4. // todo
  5. // make it inline friendly
  6. bsEditor = Class.extend({
  7. init: function(options) {
  8. this.options = $.extend(options || {}, this.default_options);
  9. if(this.options.editor) {
  10. this.setup_editor(this.options.editor);
  11. this.setup_fixed_toolbar();
  12. } else if(this.options.parent) {
  13. this.wrapper = $("<div></div>").appendTo(this.options.parent);
  14. this.setup_editor($("<div class='wn-editor'></div>").appendTo(this.wrapper));
  15. this.setup_inline_toolbar();
  16. this.editor.css(this.options.inline_editor_style);
  17. this.set_editing();
  18. }
  19. },
  20. setup_editor: function(editor) {
  21. var me = this;
  22. this.editor = $(editor);
  23. this.editor.on("click", function() {
  24. if(!me.editing) {
  25. me.set_editing();
  26. }
  27. }).on("mouseup keyup mouseout", function() {
  28. if(me.editing) {
  29. me.toolbar.save_selection();
  30. me.toolbar.update();
  31. me.options.change && me.options.change(me.clean_html());
  32. }
  33. }).data("object", this);
  34. this.bind_hotkeys();
  35. this.init_file_drops();
  36. },
  37. set_editing: function() {
  38. this.editor.attr('contenteditable', true);
  39. this.original_html = this.editor.html();
  40. this.toolbar.show();
  41. if(this.options.editor)
  42. this.toolbar.editor = this.editor.focus();
  43. this.editing = true;
  44. },
  45. setup_fixed_toolbar: function() {
  46. if(!window.bs_editor_toolbar) {
  47. window.bs_editor_toolbar = new bsEditorToolbar(this.options)
  48. }
  49. this.toolbar = window.bs_editor_toolbar;
  50. },
  51. setup_inline_toolbar: function() {
  52. this.toolbar = new bsEditorToolbar(this.options, this.wrapper, this.editor);
  53. },
  54. onhide: function(action) {
  55. this.editing = false;
  56. if(action==="Cancel") {
  57. // restore original html?
  58. if(window.confirm("Do you want to undo all your changes?")) {
  59. this.editor.html(this.original_html);
  60. this.options.oncancel && this.options.oncancel(this);
  61. }
  62. } else {
  63. this.options.onsave && this.options.onsave(this);
  64. this.options.change && this.options.change(this.get_value());
  65. }
  66. },
  67. default_options: {
  68. hotKeys: {
  69. 'ctrl+b meta+b': 'bold',
  70. 'ctrl+i meta+i': 'italic',
  71. 'ctrl+u meta+u': 'underline',
  72. 'ctrl+z meta+z': 'undo',
  73. 'ctrl+y meta+y meta+shift+z': 'redo',
  74. 'ctrl+l meta+l': 'justifyleft',
  75. 'ctrl+e meta+e': 'justifycenter',
  76. 'ctrl+j meta+j': 'justifyfull',
  77. 'shift+tab': 'outdent',
  78. 'tab': 'indent'
  79. },
  80. inline_editor_style: {
  81. "height": "400px",
  82. "background-color": "white",
  83. "border-collapse": "separate",
  84. "border": "1px solid rgb(204, 204, 204)",
  85. "padding": "4px",
  86. "box-sizing": "content-box",
  87. "-webkit-box-shadow": "rgba(0, 0, 0, 0.0745098) 0px 1px 1px 0px inset",
  88. "box-shadow": "rgba(0, 0, 0, 0.0745098) 0px 1px 1px 0px inset",
  89. "border-radius": "3px",
  90. "overflow": "scroll",
  91. "outline": "none"
  92. },
  93. toolbar_selector: '[data-role=editor-toolbar]',
  94. command_role: 'edit',
  95. active_toolbar_class: 'btn-info',
  96. selection_marker: 'edit-focus-marker',
  97. selection_color: 'darkgrey',
  98. remove_typography: true,
  99. max_file_size: 1,
  100. },
  101. bind_hotkeys: function () {
  102. var me = this;
  103. $.each(this.options.hotKeys, function (hotkey, command) {
  104. me.editor.keydown(hotkey, function (e) {
  105. if (me.editor.attr('contenteditable') && me.editor.is(':visible')) {
  106. e.preventDefault();
  107. e.stopPropagation();
  108. me.toolbar.execCommand(command);
  109. }
  110. }).keyup(hotkey, function (e) {
  111. if (me.editor.attr('contenteditable') && me.editor.is(':visible')) {
  112. e.preventDefault();
  113. e.stopPropagation();
  114. }
  115. });
  116. });
  117. },
  118. clean_html: function() {
  119. var html = this.editor.html() || "";
  120. html = html.replace(/(<br>|\s|<div><br><\/div>|&nbsp;)*$/, '');
  121. // remove custom typography (use CSS!)
  122. if(this.options.remove_typography) {
  123. html = html.replace(/(font-family|font-size|line-height):[^;]*;/g, '');
  124. html = html.replace(/<[^>]*(font=['"][^'"]*['"])>/g, function(a,b) { return a.replace(b, ''); });
  125. html = html.replace(/\s*style\s*=\s*["']\s*["']/g, '');
  126. return html;
  127. }
  128. },
  129. init_file_drops: function () {
  130. var me = this;
  131. this.editor.on('dragenter dragover', false)
  132. .on('drop', function (e) {
  133. var dataTransfer = e.originalEvent.dataTransfer;
  134. e.stopPropagation();
  135. e.preventDefault();
  136. if (dataTransfer && dataTransfer.files && dataTransfer.files.length > 0) {
  137. me.insert_files(dataTransfer.files);
  138. }
  139. });
  140. },
  141. insert_files: function (files) {
  142. var me = this;
  143. this.editor.focus();
  144. $.each(files, function (i, file) {
  145. if (/^image\//.test(file.type)) {
  146. me.get_image(file, function(image_url) {
  147. me.toolbar.execCommand('insertimage', image_url);
  148. })
  149. }
  150. });
  151. },
  152. get_image: function (fileobj, callback) {
  153. var freader = new FileReader(),
  154. me = this;
  155. freader.onload = function() {
  156. var dataurl = freader.result;
  157. // add filename to dataurl
  158. var parts = dataurl.split(",");
  159. parts[0] += ";filename=" + fileobj.name;
  160. dataurl = parts[0] + ',' + parts[1];
  161. if(me.options.max_file_size) {
  162. if(dataurl.length > (me.options.max_file_size * 1024 * 1024 * 1.4)) {
  163. bs_get_modal("Upload Error", "Max file size (" + me.options.max_file_size + "M) exceeded.").modal("show");
  164. throw "file size exceeded";
  165. }
  166. }
  167. callback(dataurl);
  168. }
  169. freader.readAsDataURL(fileobj);
  170. },
  171. get_value: function() {
  172. return this.clean_html()
  173. },
  174. set_input: function(value) {
  175. if(this.options.field && this.options.field.inside_change_event)
  176. return;
  177. this.editor.html(value==null ? "" : value);
  178. }
  179. })
  180. bsEditorToolbar = Class.extend({
  181. init: function(options, parent, editor) {
  182. this.options = options;
  183. this.editor = editor;
  184. this.inline = !!parent;
  185. this.options.toolbar_style = $.extend((this.inline ? this.inline_style : this.fixed_style),
  186. this.options.toolbar_style || {});
  187. this.make(parent);
  188. this.toolbar.css(this.options.toolbar_style);
  189. this.setup_image_button();
  190. this.bind_events();
  191. //this.bind_touch();
  192. },
  193. fixed_style: {
  194. position: "fixed",
  195. top: "0px",
  196. padding: "5px",
  197. width: "100%",
  198. height: "45px",
  199. "background-color": "black",
  200. display: "none"
  201. },
  202. inline_style: {
  203. padding: "5px",
  204. },
  205. make: function(parent) {
  206. if(!parent)
  207. parent = $("body");
  208. if(!parent.find(".wn-editor-toolbar").length) {
  209. this.toolbar = $('<div class="wn-editor-toolbar wn-ignore-click">\
  210. <div class="btn-toolbar" data-role="editor-toolbar" style="margin-bottom: 7px;">\
  211. <div class="btn-group form-group">\
  212. <a class="btn btn-default btn-small dropdown-toggle" data-toggle="dropdown" \
  213. title="Font Size"><i class="icon-text-height"></i> <b class="caret"></b></a>\
  214. <ul class="dropdown-menu" role="menu">\
  215. <li><a href="#" data-edit="formatBlock &lt;p&gt;"><p>Paragraph</p></a></li>\
  216. <li><a href="#" data-edit="formatBlock &lt;h1&gt;"><h1>Heading 1</h1></a></li>\
  217. <li><a href="#" data-edit="formatBlock &lt;h2&gt;"><h2>Heading 2</h2></a></li>\
  218. <li><a href="#" data-edit="formatBlock &lt;h3&gt;"><h3>Heading 3</h3></a></li>\
  219. <li><a href="#" data-edit="formatBlock &lt;h4&gt;"><h4>Heading 4</h4></a></li>\
  220. <li><a href="#" data-edit="formatBlock &lt;h5&gt;"><h5>Heading 5</h5></a></li>\
  221. </ul>\
  222. </div>\
  223. <div class="btn-group form-group">\
  224. <a class="btn btn-default btn-small" data-edit="bold" title="Bold (Ctrl/Cmd+B)">\
  225. <i class="icon-bold"></i></a>\
  226. <a class="btn btn-default btn-small" data-edit="insertunorderedlist" title="Bullet list">\
  227. <i class="icon-list-ul"></i></a>\
  228. <a class="btn btn-default btn-small" data-edit="insertorderedlist" title="Number list">\
  229. <i class="icon-list-ol"></i></a>\
  230. <a class="btn btn-default btn-small" data-edit="outdent" title="Reduce indent (Shift+Tab)">\
  231. <i class="icon-indent-left"></i></a>\
  232. <a class="btn btn-default btn-small" data-edit="indent" title="Indent (Tab)">\
  233. <i class="icon-indent-right"></i></a>\
  234. </div>\
  235. <div class="btn-group hidden-xs form-group">\
  236. <a class="btn btn-default btn-small" data-edit="justifyleft" title="Align Left (Ctrl/Cmd+L)">\
  237. <i class="icon-align-left"></i></a>\
  238. <a class="btn btn-default btn-small" data-edit="justifycenter" title="Center (Ctrl/Cmd+E)">\
  239. <i class="icon-align-center"></i></a>\
  240. <a class="btn btn-default btn-small btn-add-link" title="Insert Link">\
  241. <i class="icon-link"></i></a>\
  242. <a class="btn btn-default btn-small" title="Remove Link" data-edit="unlink">\
  243. <i class="icon-unlink"></i></a>\
  244. <a class="btn btn-default btn-small btn-insert-img" title="Insert picture (or just drag & drop)">\
  245. <i class="icon-picture"></i></a>\
  246. <a class="btn btn-default btn-small" data-edit="insertHorizontalRule" \
  247. title="Horizontal Line Break">─</a>\
  248. </div>\
  249. <div class="btn-group form-group">\
  250. <a class="btn btn-default btn-small btn-html" title="HTML">\
  251. <i class="icon-code"></i></a>\
  252. <a class="btn btn-default btn-small btn-cancel" data-action="Cancel" title="Cancel">\
  253. <i class="icon-remove"></i></a>\
  254. <a class="btn btn-default btn-small btn-success" data-action="Save" title="Save">\
  255. <i class="icon-save"></i></a>\
  256. </div>\
  257. <input type="file" data-edit="insertImage" />\
  258. </div>').prependTo(parent);
  259. if(this.inline) {
  260. this.toolbar.find("[data-action]").remove();
  261. } else {
  262. this.toolbar.find(".btn-toolbar").addClass("container");
  263. }
  264. }
  265. },
  266. setup_image_button: function() {
  267. // magic-overlay
  268. var me = this;
  269. this.file_input = this.toolbar.find('input[type="file"]')
  270. .css({
  271. 'opacity':0,
  272. 'position':'absolute',
  273. 'left':0,
  274. 'width':0,
  275. 'height':0
  276. });
  277. this.toolbar.find(".btn-insert-img").on("click", function() {
  278. me.file_input.trigger("click");
  279. })
  280. },
  281. show: function() {
  282. var me = this;
  283. this.toolbar.toggle(true);
  284. if(!this.inline) {
  285. $("body").animate({"padding-top": this.toolbar.outerHeight() }, {
  286. complete: function() { me.toolbar.css("z-index", 1001); }
  287. });
  288. }
  289. },
  290. hide: function(action) {
  291. if(!this.editor)
  292. return;
  293. var me = this;
  294. this.toolbar.css("z-index", 0);
  295. if(!this.inline) {
  296. $("body").animate({"padding-top": 0 }, {complete: function() {
  297. me.toolbar.toggle(false);
  298. }});
  299. }
  300. this.editor && this.editor.attr('contenteditable', false).data("object").onhide(action);
  301. this.editor = null;
  302. },
  303. bind_events: function () {
  304. var me = this;
  305. // standard button events
  306. this.toolbar.find('a[data-' + me.options.command_role + ']').click(function () {
  307. me.restore_selection();
  308. me.editor.focus();
  309. me.execCommand($(this).data(me.options.command_role));
  310. me.save_selection();
  311. // close dropdown
  312. if(me.toolbar.find("ul.dropdown-menu:visible").length)
  313. me.toolbar.find('[data-toggle="dropdown"]').dropdown("toggle");
  314. return false;
  315. });
  316. this.toolbar.find('[data-toggle=dropdown]').click(function() { me.restore_selection() });
  317. // link
  318. this.toolbar.find(".btn-add-link").on("click", function() {
  319. if(!window.bs_link_editor) {
  320. window.bs_link_editor = new bsLinkEditor(me);
  321. }
  322. window.bs_link_editor.show();
  323. })
  324. // file event
  325. this.toolbar.find('input[type=file][data-' + me.options.command_role + ']').change(function () {
  326. me.restore_selection();
  327. if (this.type === 'file' && this.files && this.files.length > 0) {
  328. me.editor.data("object").insert_files(this.files);
  329. }
  330. me.save_selection();
  331. this.value = '';
  332. return false;
  333. });
  334. // save
  335. this.toolbar.find("[data-action='Save']").on("click", function() {
  336. me.hide("Save");
  337. })
  338. // cancel
  339. this.toolbar.find("[data-action='Cancel']").on("click", function() {
  340. me.hide("Cancel");
  341. })
  342. // edit html
  343. this.toolbar.find(".btn-html").on("click", function() {
  344. if(!window.bs_html_editor)
  345. window.bs_html_editor = new bsHTMLEditor();
  346. window.bs_html_editor.show(me.editor);
  347. })
  348. },
  349. update: function () {
  350. var me = this;
  351. if (this.toolbar) {
  352. $(this.toolbar).find('.btn[data-' + this.options.command_role + ']').each(function () {
  353. var command = $(this).data(me.options.command_role);
  354. if (document.queryCommandState(command)) {
  355. $(this).addClass(me.options.active_toolbar_class);
  356. } else {
  357. $(this).removeClass(me.options.active_toolbar_class);
  358. }
  359. });
  360. }
  361. },
  362. execCommand: function (commandWithArgs, valueArg) {
  363. var commandArr = commandWithArgs.split(' '),
  364. command = commandArr.shift(),
  365. args = commandArr.join(' ') + (valueArg || '');
  366. document.execCommand(command, 0, args);
  367. this.update();
  368. },
  369. get_current_range: function () {
  370. var sel = window.getSelection();
  371. if (sel.getRangeAt && sel.rangeCount) {
  372. return sel.getRangeAt(0);
  373. }
  374. },
  375. save_selection: function () {
  376. this.selected_range = this.get_current_range();
  377. },
  378. restore_selection: function () {
  379. var selection = window.getSelection();
  380. if (this.selected_range) {
  381. selection.removeAllRanges();
  382. selection.addRange(this.selected_range);
  383. }
  384. },
  385. mark_selection: function (input, color) {
  386. this.restore_selection();
  387. document.execCommand('hiliteColor', 0, color || 'transparent');
  388. this.save_selection();
  389. input.data(this.options.selection_marker, color);
  390. },
  391. // bind_touch: function() {
  392. // var me = this;
  393. // $(window).bind('touchend', function (e) {
  394. // var isInside = (me.editor.is(e.target) || me.editor.has(e.target).length > 0),
  395. // current_range = me.get_current_range(),
  396. // clear = current_range && (current_range.startContainer === current_range.endContainer && current_range.startOffset === current_range.endOffset);
  397. // if (!clear || isInside) {
  398. // me.save_selection();
  399. // me.update();
  400. // }
  401. // });
  402. // }
  403. });
  404. bsHTMLEditor = Class.extend({
  405. init: function() {
  406. var me = this;
  407. this.modal = bs_get_modal("<i class='icon-code'></i> Edit HTML", '<textarea class="form-control" \
  408. style="height: 400px; width: 100%; font-family: Monaco, Courier New, Fixed; font-size: 11px">\
  409. </textarea><br>\
  410. <button class="btn btn-primary" style="margin-top: 7px;">Save</button>');
  411. this.modal.addClass("wn-ignore-click");
  412. this.modal.find(".btn-primary").on("click", function() {
  413. var html = me.modal.find("textarea").val();
  414. $.each(me.editor.dataurls, function(key, val) {
  415. html = html.replace(key, val);
  416. });
  417. var editor = me.editor.data("object")
  418. editor.set_input(html)
  419. editor.options.change && editor.options.change(editor.clean_html());
  420. me.modal.modal("hide");
  421. });
  422. },
  423. show: function(editor) {
  424. var me = this;
  425. this.editor = editor;
  426. this.modal.modal("show")
  427. var html = me.editor.html();
  428. // pack dataurls so that html display is faster
  429. this.editor.dataurls = {}
  430. html = html.replace(/<img\s*src=\s*["\'](data:[^,]*),([^"\']*)["\']/g, function(full, g1, g2) {
  431. var key = g2.slice(0,5) + "..." + g2.slice(-5);
  432. me.editor.dataurls[key] = g1 + "," + g2;
  433. return '<img src="'+g1 + "," + key+'"';
  434. });
  435. this.modal.find("textarea").val(html_beautify(html));
  436. }
  437. });
  438. bsLinkEditor = Class.extend({
  439. init: function(toolbar) {
  440. var me = this;
  441. this.toolbar = toolbar;
  442. this.modal = bs_get_modal("<i class='icon-globe'></i> Insert Link", '<div class="form-group">\
  443. <input type="text" class="form-control" placeholder="http://example.com" />\
  444. </div>\
  445. <div class="checkbox" style="position: static;">\
  446. <label>\
  447. <input type="checkbox"> <span>Open Link in a new Window</span>\
  448. </label>\
  449. </div>\
  450. <button class="btn btn-primary" style="margin-top: 7px;">Insert</button>');
  451. this.modal.addClass("wn-ignore-click");
  452. this.modal.find(".btn-primary").on("click", function() {
  453. me.toolbar.restore_selection();
  454. var url = me.modal.find("input[type=text]").val();
  455. var selection = me.toolbar.selected_range.toString();
  456. if(url) {
  457. if(me.modal.find("input[type=checkbox]:checked").length) {
  458. var html = "<a href='" + url + "' target='_blank'>" + selection + "</a>";
  459. document.execCommand("insertHTML", false, html);
  460. } else {
  461. document.execCommand("CreateLink", false, url);
  462. }
  463. }
  464. me.modal.modal("hide");
  465. return false;
  466. });
  467. },
  468. show: function() {
  469. this.modal.find("input[type=text]").val("");
  470. this.modal.modal("show");
  471. }
  472. });
  473. bs_get_modal = wn.get_modal;