Ви не можете вибрати більше 25 тем Теми мають розпочинатися з літери або цифри, можуть містити дефіси (-) і не повинні перевищувати 35 символів.

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 роки тому
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506
  1. // Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors
  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. this.edit_mode = true;
  10. if(this.options.editor) {
  11. this.setup_editor(this.options.editor);
  12. this.setup_fixed_toolbar();
  13. } else if(this.options.parent) {
  14. this.wrapper = $("<div></div>").appendTo(this.options.parent);
  15. this.setup_editor($("<div class='wn-editor'></div>").appendTo(this.wrapper));
  16. this.setup_inline_toolbar();
  17. this.editor.css(this.options.inline_editor_style);
  18. this.set_editing();
  19. }
  20. },
  21. setup_editor: function(editor) {
  22. var me = this;
  23. this.editor = $(editor);
  24. this.editor.on("click", function() {
  25. if(me.edit_mode && !me.editing) {
  26. me.set_editing();
  27. }
  28. }).on("mouseup keyup mouseout", function() {
  29. if(me.editing) {
  30. me.toolbar.save_selection();
  31. me.toolbar.update();
  32. me.options.change && me.options.change(me.clean_html());
  33. }
  34. }).data("object", this);
  35. this.bind_hotkeys();
  36. this.init_file_drops();
  37. },
  38. set_editing: function() {
  39. this.editor.attr('contenteditable', true);
  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() {
  55. this.editing = false;
  56. this.options.onsave && this.options.onsave(this);
  57. this.options.change && this.options.change(this.get_value());
  58. },
  59. toggle_edit_mode: function(bool) {
  60. // switch to enter editing mode
  61. this.edit_mode = bool;
  62. if(this.edit_mode) {
  63. this.editor.trigger("click");
  64. }
  65. },
  66. default_options: {
  67. hotKeys: {
  68. 'ctrl+b meta+b': 'bold',
  69. 'ctrl+i meta+i': 'italic',
  70. 'ctrl+u meta+u': 'underline',
  71. 'ctrl+z meta+z': 'undo',
  72. 'ctrl+y meta+y meta+shift+z': 'redo',
  73. 'ctrl+l meta+l': 'justifyleft',
  74. 'ctrl+e meta+e': 'justifycenter',
  75. 'ctrl+j meta+j': 'justifyfull',
  76. 'shift+tab': 'outdent',
  77. 'tab': 'indent'
  78. },
  79. inline_editor_style: {
  80. "height": "400px",
  81. "background-color": "white",
  82. "border-collapse": "separate",
  83. "border": "1px solid rgb(204, 204, 204)",
  84. "padding": "4px",
  85. "box-sizing": "content-box",
  86. "-webkit-box-shadow": "rgba(0, 0, 0, 0.0745098) 0px 1px 1px 0px inset",
  87. "box-shadow": "rgba(0, 0, 0, 0.0745098) 0px 1px 1px 0px inset",
  88. "border-radius": "3px",
  89. "overflow": "scroll",
  90. "outline": "none"
  91. },
  92. toolbar_selector: '[data-role=editor-toolbar]',
  93. command_role: 'edit',
  94. active_toolbar_class: 'btn-info',
  95. selection_marker: 'edit-focus-marker',
  96. selection_color: 'darkgrey',
  97. remove_typography: true,
  98. max_file_size: 1,
  99. },
  100. bind_hotkeys: function () {
  101. var me = this;
  102. $.each(this.options.hotKeys, function (hotkey, command) {
  103. me.editor.keydown(hotkey, function (e) {
  104. if (me.editor.attr('contenteditable') && me.editor.is(':visible')) {
  105. e.preventDefault();
  106. e.stopPropagation();
  107. me.toolbar.execCommand(command);
  108. }
  109. }).keyup(hotkey, function (e) {
  110. if (me.editor.attr('contenteditable') && me.editor.is(':visible')) {
  111. e.preventDefault();
  112. e.stopPropagation();
  113. }
  114. });
  115. });
  116. },
  117. clean_html: function() {
  118. var html = this.editor.html() || "";
  119. if(!strip(this.editor.text()) && !(this.editor.find("img"))) 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-success" data-action="Save" title="Save">\
  253. <i class="icon-save"></i></a>\
  254. </div>\
  255. <input type="file" data-edit="insertImage" />\
  256. </div>').prependTo(parent);
  257. if(this.inline) {
  258. this.toolbar.find("[data-action]").remove();
  259. } else {
  260. this.toolbar.find(".btn-toolbar").addClass("container");
  261. }
  262. }
  263. },
  264. setup_image_button: function() {
  265. // magic-overlay
  266. var me = this;
  267. this.file_input = this.toolbar.find('input[type="file"]')
  268. .css({
  269. 'opacity':0,
  270. 'position':'absolute',
  271. 'left':0,
  272. 'width':0,
  273. 'height':0
  274. });
  275. this.toolbar.find(".btn-insert-img").on("click", function() {
  276. me.file_input.trigger("click");
  277. })
  278. },
  279. show: function() {
  280. var me = this;
  281. this.toolbar.toggle(true);
  282. if(!this.inline) {
  283. $("body").animate({"padding-top": this.toolbar.outerHeight() }, {
  284. complete: function() { me.toolbar.css("z-index", 1001); }
  285. });
  286. }
  287. },
  288. hide: function() {
  289. if(!this.editor)
  290. return;
  291. var me = this;
  292. this.toolbar.css("z-index", 0);
  293. if(!this.inline) {
  294. $("body").animate({"padding-top": 0 }, {complete: function() {
  295. me.toolbar.toggle(false);
  296. }});
  297. }
  298. this.editor && this.editor.attr('contenteditable', false).data("object").onhide();
  299. this.editor = null;
  300. },
  301. bind_events: function () {
  302. var me = this;
  303. // standard button events
  304. this.toolbar.find('a[data-' + me.options.command_role + ']').click(function () {
  305. me.restore_selection();
  306. me.editor.focus();
  307. me.execCommand($(this).data(me.options.command_role));
  308. me.save_selection();
  309. // close dropdown
  310. if(me.toolbar.find("ul.dropdown-menu:visible").length)
  311. me.toolbar.find('[data-toggle="dropdown"]').dropdown("toggle");
  312. return false;
  313. });
  314. this.toolbar.find('[data-toggle=dropdown]').click(function() { me.restore_selection() });
  315. // link
  316. this.toolbar.find(".btn-add-link").on("click", function() {
  317. if(!window.bs_link_editor) {
  318. window.bs_link_editor = new bsLinkEditor(me);
  319. }
  320. window.bs_link_editor.show();
  321. })
  322. // file event
  323. this.toolbar.find('input[type=file][data-' + me.options.command_role + ']').change(function () {
  324. me.restore_selection();
  325. if (this.type === 'file' && this.files && this.files.length > 0) {
  326. me.editor.data("object").insert_files(this.files);
  327. }
  328. me.save_selection();
  329. this.value = '';
  330. return false;
  331. });
  332. // save
  333. this.toolbar.find("[data-action='Save']").on("click", function() { me.hide(); });
  334. // edit html
  335. this.toolbar.find(".btn-html").on("click", function() {
  336. if(!window.bs_html_editor)
  337. window.bs_html_editor = new bsHTMLEditor();
  338. window.bs_html_editor.show(me.editor);
  339. })
  340. },
  341. update: function () {
  342. var me = this;
  343. if (this.toolbar) {
  344. $(this.toolbar).find('.btn[data-' + this.options.command_role + ']').each(function () {
  345. var command = $(this).data(me.options.command_role);
  346. if (document.queryCommandState(command)) {
  347. $(this).addClass(me.options.active_toolbar_class);
  348. } else {
  349. $(this).removeClass(me.options.active_toolbar_class);
  350. }
  351. });
  352. }
  353. },
  354. execCommand: function (commandWithArgs, valueArg) {
  355. var commandArr = commandWithArgs.split(' '),
  356. command = commandArr.shift(),
  357. args = commandArr.join(' ') + (valueArg || '');
  358. document.execCommand(command, 0, args);
  359. this.update();
  360. },
  361. get_current_range: function () {
  362. var sel = window.getSelection();
  363. if (sel.getRangeAt && sel.rangeCount) {
  364. return sel.getRangeAt(0);
  365. }
  366. },
  367. save_selection: function () {
  368. this.selected_range = this.get_current_range();
  369. },
  370. restore_selection: function () {
  371. var selection = window.getSelection();
  372. if (this.selected_range) {
  373. selection.removeAllRanges();
  374. selection.addRange(this.selected_range);
  375. }
  376. },
  377. mark_selection: function (input, color) {
  378. this.restore_selection();
  379. document.execCommand('hiliteColor', 0, color || 'transparent');
  380. this.save_selection();
  381. input.data(this.options.selection_marker, color);
  382. },
  383. // bind_touch: function() {
  384. // var me = this;
  385. // $(window).bind('touchend', function (e) {
  386. // var isInside = (me.editor.is(e.target) || me.editor.has(e.target).length > 0),
  387. // current_range = me.get_current_range(),
  388. // clear = current_range && (current_range.startContainer === current_range.endContainer && current_range.startOffset === current_range.endOffset);
  389. // if (!clear || isInside) {
  390. // me.save_selection();
  391. // me.update();
  392. // }
  393. // });
  394. // }
  395. });
  396. bsHTMLEditor = Class.extend({
  397. init: function() {
  398. var me = this;
  399. this.modal = bs_get_modal("<i class='icon-code'></i> Edit HTML", '<textarea class="form-control" \
  400. style="height: 400px; width: 100%; font-family: Monaco, \'Courier New\', monospace; font-size: 11px">\
  401. </textarea><br>\
  402. <button class="btn btn-primary" style="margin-top: 7px;">Save</button>');
  403. this.modal.addClass("wn-ignore-click");
  404. this.modal.find(".btn-primary").on("click", function() {
  405. var html = me.modal.find("textarea").val();
  406. $.each(me.editor.dataurls, function(key, val) {
  407. html = html.replace(key, val);
  408. });
  409. var editor = me.editor.data("object")
  410. editor.set_input(html)
  411. editor.options.change && editor.options.change(editor.clean_html());
  412. me.modal.modal("hide");
  413. });
  414. },
  415. show: function(editor) {
  416. var me = this;
  417. this.editor = editor;
  418. this.modal.modal("show")
  419. var html = me.editor.html();
  420. // pack dataurls so that html display is faster
  421. this.editor.dataurls = {}
  422. html = html.replace(/<img\s*src=\s*["\'](data:[^,]*),([^"\']*)["\']/g, function(full, g1, g2) {
  423. var key = g2.slice(0,5) + "..." + g2.slice(-5);
  424. me.editor.dataurls[key] = g1 + "," + g2;
  425. return '<img src="'+g1 + "," + key+'"';
  426. });
  427. this.modal.find("textarea").val(html_beautify(html));
  428. }
  429. });
  430. bsLinkEditor = Class.extend({
  431. init: function(toolbar) {
  432. var me = this;
  433. this.toolbar = toolbar;
  434. this.modal = bs_get_modal("<i class='icon-globe'></i> Insert Link", '<div class="form-group">\
  435. <input type="text" class="form-control" placeholder="http://example.com" />\
  436. </div>\
  437. <div class="checkbox" style="position: static;">\
  438. <label>\
  439. <input type="checkbox"> <span>Open Link in a new Window</span>\
  440. </label>\
  441. </div>\
  442. <button class="btn btn-primary" style="margin-top: 7px;">Insert</button>');
  443. this.modal.addClass("wn-ignore-click");
  444. this.modal.find(".btn-primary").on("click", function() {
  445. me.toolbar.restore_selection();
  446. var url = me.modal.find("input[type=text]").val();
  447. var selection = me.toolbar.selected_range.toString();
  448. if(url) {
  449. if(me.modal.find("input[type=checkbox]:checked").length) {
  450. var html = "<a href='" + url + "' target='_blank'>" + selection + "</a>";
  451. document.execCommand("insertHTML", false, html);
  452. } else {
  453. document.execCommand("CreateLink", false, url);
  454. }
  455. }
  456. me.modal.modal("hide");
  457. return false;
  458. });
  459. },
  460. show: function() {
  461. this.modal.find("input[type=text]").val("");
  462. this.modal.modal("show");
  463. }
  464. });
  465. bs_get_modal = wn.get_modal;