From 3b826eb93b481916f68b9b503421aab63cd03082 Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Wed, 16 Oct 2013 17:55:27 +0530 Subject: [PATCH] [editor] [minor] fixed saving, dataurl, links, html editing --- public/build.json | 10 +- public/js/wn/dom.js | 18 ++ public/js/wn/ui/dialog.js | 16 +- public/js/wn/ui/editor.js | 252 ++++++++++++------ public/js/wn/website/web_page_editable.js | 33 --- webnotes/model/bean.py | 85 +++--- webnotes/utils/file_manager.py | 29 +- .../templates/generators/web_page.html | 26 +- website/js/website.js | 12 + 9 files changed, 297 insertions(+), 184 deletions(-) delete mode 100644 public/js/wn/website/web_page_editable.js diff --git a/public/build.json b/public/build.json index b76fc47555..e4f7128d60 100644 --- a/public/build.json +++ b/public/build.json @@ -13,10 +13,18 @@ "lib/public/js/lib/bootstrap.min.js", "lib/public/js/wn/misc/number_format.js", "lib/public/js/lib/nprogress.js", - "lib/public/js/wn/class.js", "lib/website/js/website.js" ] }, + + { + "public/js/editor.min.js": [ + "lib/public/js/lib/jquery/jquery.hotkeys.js", + "lib/public/js/wn/class.js", + "lib/public/js/lib/beautify-html.js", + "lib/public/js/wn/ui/editor.js", + ] + }, { "public/css/all-app.css": [ diff --git a/public/js/wn/dom.js b/public/js/wn/dom.js index 6f0c6f1b44..479c75082a 100644 --- a/public/js/wn/dom.js +++ b/public/js/wn/dom.js @@ -105,6 +105,24 @@ wn.dom = { } } +wn.get_modal = function(title, body_html) { + var modal = $('').appendTo(document.body); + + return modal; +}; + var pending_req = 0 wn.set_loading = function() { pending_req++; diff --git a/public/js/wn/ui/dialog.js b/public/js/wn/ui/dialog.js index bae73db818..ca12a51472 100644 --- a/public/js/wn/ui/dialog.js +++ b/public/js/wn/ui/dialog.js @@ -28,21 +28,7 @@ wn.ui.Dialog = wn.ui.FieldGroup.extend({ this.make(); }, make: function() { - // ui-front class is used as appendTo by jquery.autocomplete - this.$wrapper = $('') - .appendTo(document.body); + this.$wrapper = wn.get_modal("", ""); this.wrapper = this.$wrapper.find('.modal-dialog').get(0); this.make_head(); this.body = this.$wrapper.find(".modal-body").get(0); diff --git a/public/js/wn/ui/editor.js b/public/js/wn/ui/editor.js index 17b8f44d58..ac24cabfa6 100644 --- a/public/js/wn/ui/editor.js +++ b/public/js/wn/ui/editor.js @@ -4,9 +4,6 @@ /* Inspired from: http://github.com/mindmup/bootstrap-wysiwyg */ // todo -// html editing -// image -// links // onsave, oncancel wn.provide("wn.ui"); @@ -15,7 +12,6 @@ wn.ui.Editor = Class.extend({ var me = this; this.editor = $(editor); this.options = $.extend(options || {}, this.default_options); - this.files = []; this.editor.on("click", function() { if(!this.editing) { @@ -23,17 +19,18 @@ wn.ui.Editor = Class.extend({ me.editor.attr('contenteditable', true); me.original_html = me.editor.html(); wn._editor_toolbar.show(); - wn._current_editor = me.editor.focus(); + wn._editor_toolbar.editor = me.editor.focus(); me.editing = true; } }).on("mouseup keyup mouseout", function() { if(me.editing) { - wn._editor_toolbar.saveSelection(); + wn._editor_toolbar.save_selection(); wn._editor_toolbar.update(); } }).on("blur", function() { - if(wn._editor_toolbar.clicked.parents(".wn-editor-toolbar").length) - return; + if(!wn._editor_toolbar.clicked || wn._editor_toolbar.clicked.parents(".wn-ignore-click").length) { + return false; + } wn._editor_toolbar.toolbar.find("[data-action='Save']").trigger("click"); }).data("object", this); @@ -49,7 +46,10 @@ wn.ui.Editor = Class.extend({ this.editing = false; if(action==="Cancel") { this.editor.html(this.original_html); - } + this.options.oncancel && this.options.oncancel(this); + } else { + this.options.onsave && this.options.onsave(this); + } }, default_options: { hotKeys: { @@ -64,17 +64,15 @@ wn.ui.Editor = Class.extend({ 'shift+tab': 'outdent', 'tab': 'indent' }, - toolbarSelector: '[data-role=editor-toolbar]', - commandRole: 'edit', - activeToolbarClass: 'btn-info', - selectionMarker: 'edit-focus-marker', - selectionColor: 'darkgrey', + toolbar_selector: '[data-role=editor-toolbar]', + command_role: 'edit', + active_toolbar_class: 'btn-info', + selection_marker: 'edit-focus-marker', + selection_color: 'darkgrey', remove_typography: true, + max_file_size: 1, }, - show: function() { - }, - bind_hotkeys: function () { var me = this; $.each(this.options.hotKeys, function (hotkey, command) { @@ -137,7 +135,16 @@ wn.ui.Editor = Class.extend({ freader.onload = function() { var dataurl = freader.result; - me.files.push(dataurl); + // add filename to dataurl + var parts = dataurl.split(","); + parts[0] += ";filename=" + fileobj.name; + dataurl = parts[0] + ',' + parts[1]; + if(me.options.max_file_size) { + if(dataurl.length > (me.options.max_file_size * 1024 * 1024 * 1.4)) { + wn.msgprint("Max file size (" + me.options.max_file_size + "M) exceeded."); + throw "file size exceeded"; + } + } callback(dataurl); } freader.readAsDataURL(fileobj); @@ -166,13 +173,12 @@ wn.ui.EditorToolbar = Class.extend({ padding: "5px", width: "100%", height: "45px", - "background-color": "#ddd", - "z-index": "1001" // more than navbar + "background-color": "#777" }, make: function() { if(!$(".wn-editor-toolbar").length) { - $('
\ -
\ + $('
\ +
\
\ \ @@ -212,17 +218,13 @@ wn.ui.EditorToolbar = Class.extend({ -\
\ - ').prependTo("body"); } @@ -239,85 +241,84 @@ wn.ui.EditorToolbar = Class.extend({ }, show: function() { - $("body").animate({"padding-top": this.toolbar.outerHeight() }); + var me = this; this.toolbar.toggle(true); + $("body").animate({"padding-top": this.toolbar.outerHeight() }, { + complete: function() { me.toolbar.css("z-index", 1001); } + }); }, hide: function(action) { - $("body").animate({"padding-top": 0 }); - this.toolbar.toggle(false); - wn._current_editor.attr('contenteditable', false).data("object").onhide(action); - wn._current_editor = null; + var me = this; + this.toolbar.css("z-index", 0); + $("body").animate({"padding-top": 0 }, {complete: function() { + me.toolbar.toggle(false); + }}); + + this.editor && this.editor.attr('contenteditable', false).data("object").onhide(action); + this.editor = null; }, bind_events: function () { var me = this; // standard button events - this.toolbar.find('a[data-' + me.options.commandRole + ']').click(function () { - me.restoreSelection(); - wn._current_editor.focus(); - me.execCommand($(this).data(me.options.commandRole)); - me.saveSelection(); + this.toolbar.find('a[data-' + me.options.command_role + ']').click(function () { + me.restore_selection(); + me.editor.focus(); + me.execCommand($(this).data(me.options.command_role)); + me.save_selection(); return false; }); - this.toolbar.find('[data-toggle=dropdown]').click(function() { me.restoreSelection() }); + this.toolbar.find('[data-toggle=dropdown]').click(function() { me.restore_selection() }); // link - this.toolbar.find('input[type=text][data-' + this.options.commandRole + ']') - .on('webkitspeechchange change', function () { - var newValue = this.value; /* ugly but prevents fake double-calls due to selection restoration */ - this.value = ''; - me.restoreSelection(); - if (newValue) { - wn._current_editor.focus(); - me.execCommand($(this).data(me.options.commandRole), newValue); + this.toolbar.find(".btn-add-link").on("click", function() { + if(!wn._link_editor) { + wn._link_editor = new wn.ui.LinkEditor(); } - me.saveSelection(); - return false; - }).on('focus', function () { - var input = $(this); - if (!input.data(me.options.selectionMarker)) { - me.markSelection(input, me.options.selectionColor); - input.focus(); - } - }).on('blur', function () { - var input = $(this); - if (input.data(me.options.selectionMarker)) { - me.markSelection(input, false); - } - }); + wn._link_editor.show(); + }) // file event - this.toolbar.find('input[type=file][data-' + me.options.commandRole + ']').change(function () { - me.restoreSelection(); + this.toolbar.find('input[type=file][data-' + me.options.command_role + ']').change(function () { + me.restore_selection(); if (this.type === 'file' && this.files && this.files.length > 0) { - wn._current_editor.data("object").insert_files(this.files); + me.editor.data("object").insert_files(this.files); } - me.saveSelection(); + me.save_selection(); this.value = ''; return false; }); - // cancel + // save this.toolbar.find("[data-action='Save']").on("click", function() { me.hide("Save"); }) + // cancel this.toolbar.find("[data-action='Cancel']").on("click", function() { me.hide("Cancel"); }) + + // edit html + this.toolbar.find(".btn-html").on("click", function() { + if(!wn._html_editor) + wn._html_editor = new wn.ui.HTMLEditor(); + + wn._html_editor.show(me.editor); + }) }, update: function () { var me = this; - if (this.options.activeToolbarClass) { - $(this.options.toolbarSelector).find('.btn[data-' + this.options.commandRole + ']').each(function () { - var command = $(this).data(me.options.commandRole); + if (this.options.active_toolbar_class) { + $(this.options.toolbar_selector).find('.btn[data-' + this.options.command_role + ']').each(function () { + var command = $(this).data(me.options.command_role); if (document.queryCommandState(command)) { - $(this).addClass(me.options.activeToolbarClass); + $(this).addClass(me.options.active_toolbar_class); } else { - $(this).removeClass(me.options.activeToolbarClass); + $(this).removeClass(me.options.active_toolbar_class); } }); } @@ -331,42 +332,119 @@ wn.ui.EditorToolbar = Class.extend({ this.update(); }, - getCurrentRange: function () { + get_current_range: function () { var sel = window.getSelection(); if (sel.getRangeAt && sel.rangeCount) { return sel.getRangeAt(0); } }, - saveSelection: function () { - this.selectedRange = this.getCurrentRange(); + save_selection: function () { + this.selected_range = this.get_current_range(); + this.selected_html = this.get_current_html(); }, - restoreSelection: function () { + get_current_html: function() { + var html = ""; + if (typeof window.getSelection != "undefined") { + var sel = window.getSelection(); + if (sel.rangeCount) { + var container = document.createElement("div"); + for (var i = 0, len = sel.rangeCount; i < len; ++i) { + container.appendChild(sel.getRangeAt(i).cloneContents()); + } + html = container.innerHTML; + } + } else if (typeof document.selection != "undefined") { + if (document.selection.type == "Text") { + html = document.selection.createRange().htmlText; + } + } + return html; + }, + + restore_selection: function () { var selection = window.getSelection(); - if (this.selectedRange) { + if (this.selected_range) { selection.removeAllRanges(); - selection.addRange(this.selectedRange); + selection.addRange(this.selected_range); } }, - markSelection: function (input, color) { - this.restoreSelection(); + mark_selection: function (input, color) { + this.restore_selection(); document.execCommand('hiliteColor', 0, color || 'transparent'); - this.saveSelection(); - input.data(this.options.selectionMarker, color); + this.save_selection(); + input.data(this.options.selection_marker, color); }, bind_touch: function() { var me = this; $(window).bind('touchend', function (e) { - var isInside = (wn._current_editor.is(e.target) || wn._current_editor.has(e.target).length > 0), - currentRange = me.getCurrentRange(), - clear = currentRange && (currentRange.startContainer === currentRange.endContainer && currentRange.startOffset === currentRange.endOffset); + var isInside = (me.editor.is(e.target) || me.editor.has(e.target).length > 0), + current_range = me.get_current_range(), + clear = current_range && (current_range.startContainer === current_range.endContainer && current_range.startOffset === current_range.endOffset); if (!clear || isInside) { - me.saveSelection(); + me.save_selection(); me.update(); } }); - } + } +}); + +wn.ui.HTMLEditor = Class.extend({ + init: function() { + var me = this; + this.modal = wn.get_modal("Edit HTML", '
\ + '); + this.modal.addClass("wn-ignore-click"); + this.modal.find(".btn-primary").on("click", function() { + me.editor.html(me.modal.find("textarea").val()); + me.modal.modal("hide"); + }); + }, + show: function(editor) { + var me = this; + this.editor = editor; + this.modal.modal("show") + this.modal.find("textarea").html(html_beautify(me.editor.html())); + } +}); + +wn.ui.LinkEditor = Class.extend({ + init: function() { + var me = this; + this.modal = wn.get_modal("Edit HTML", '
\ + \ +
\ +
\ + \ +
\ + '); + + this.modal.addClass("wn-ignore-click"); + this.modal.find(".btn-primary").on("click", function() { + wn._editor_toolbar.restore_selection(); + var url = me.modal.find("input[type=text]").val(); + var selection = wn._editor_toolbar.selected_range.toString(); + if(url) { + if(me.modal.find("input[type=checkbox]:checked").length) { + var html = "" + selection + ""; + document.execCommand("insertHTML", false, html); + } else { + document.execCommand("CreateLink", false, url); + } + } + me.modal.modal("hide"); + return false; + }); + }, + show: function() { + this.modal.find("input[type=text]").val(""); + this.modal.modal("show"); + } }) \ No newline at end of file diff --git a/public/js/wn/website/web_page_editable.js b/public/js/wn/website/web_page_editable.js deleted file mode 100644 index af9132e0a6..0000000000 --- a/public/js/wn/website/web_page_editable.js +++ /dev/null @@ -1,33 +0,0 @@ -$(function() { - $('
').appendTo($("footer.container")) - $(".editable-button").click(function() { - if(window.editable) { - $(".web-page-content").attr("contentEditable", false).removeClass("web-page-editable"); - window.editable = false; - $(this).html("Edit"); - var html = $(".web-page-content").html() || ""; - html = html.replace(/(font-family|font-size|line-height):[^;]*;/g, ''); - html = html.replace(/<[^>]*(font=['"][^'"]*['"])>/g, function(a,b) { return a.replace(b, ''); }); - html = html.replace(/\s*style\s*=\s*["']\s*["']/g, ''); - $(".web-page-content").html(html); - - wn.call({ - type: "POST", - method: "webnotes.client.set_value", - args: { - doctype:"Web Page", - name: window.name, - fieldname: "main_section", - value: html - }, - callback: function(r) { - wn.msgprint(r.exc ? "Error" : "Saved"); - } - }); - } else { - $(".web-page-content").attr("contentEditable", true).addClass("web-page-editable").focus(); - $(this).html("Save"); - window.editable = true; - } - }); -}) diff --git a/webnotes/model/bean.py b/webnotes/model/bean.py index b4b6d58490..f16f2ce995 100644 --- a/webnotes/model/bean.py +++ b/webnotes/model/bean.py @@ -238,45 +238,6 @@ class Bean: self.make_controller() return getattr(self.controller, method, None) - def save_main(self): - try: - self.doc.save(check_links = False, ignore_fields = self.ignore_fields) - except NameError, e: - webnotes.msgprint('%s "%s" already exists' % (self.doc.doctype, self.doc.name)) - - # prompt if cancelled - if webnotes.conn.get_value(self.doc.doctype, self.doc.name, 'docstatus')==2: - webnotes.msgprint('[%s "%s" has been cancelled]' % (self.doc.doctype, self.doc.name)) - webnotes.errprint(webnotes.utils.getTraceback()) - raise e - - def save_children(self): - child_map = {} - for d in self.doclist[1:]: - if d.fields.get("parent") or d.fields.get("parentfield"): - d.parent = self.doc.name # rename if reqd - d.parenttype = self.doc.doctype - - d.save(check_links=False, ignore_fields = self.ignore_fields) - - child_map.setdefault(d.doctype, []).append(d.name) - - # delete all children in database that are not in the child_map - - # get all children types - tablefields = webnotes.model.meta.get_table_fields(self.doc.doctype) - - for dt in tablefields: - if dt[0] not in self.ignore_children_type: - cnames = child_map.get(dt[0]) or [] - if cnames: - webnotes.conn.sql("""delete from `tab%s` where parent=%s and parenttype=%s and - name not in (%s)""" % (dt[0], '%s', '%s', ','.join(['%s'] * len(cnames))), - tuple([self.doc.name, self.doc.doctype] + cnames)) - else: - webnotes.conn.sql("""delete from `tab%s` where parent=%s and parenttype=%s""" \ - % (dt[0], '%s', '%s'), (self.doc.name, self.doc.doctype)) - def insert(self): self.doc.fields["__islocal"] = 1 @@ -379,6 +340,46 @@ class Bean: self.no_permission_to(_("Update")) return self + + def save_main(self): + try: + self.doc.save(check_links = False, ignore_fields = self.ignore_fields) + self.extract_images_from_text_editor() + except NameError, e: + webnotes.msgprint('%s "%s" already exists' % (self.doc.doctype, self.doc.name)) + + # prompt if cancelled + if webnotes.conn.get_value(self.doc.doctype, self.doc.name, 'docstatus')==2: + webnotes.msgprint('[%s "%s" has been cancelled]' % (self.doc.doctype, self.doc.name)) + webnotes.errprint(webnotes.utils.getTraceback()) + raise e + + def save_children(self): + child_map = {} + for d in self.doclist[1:]: + if d.fields.get("parent") or d.fields.get("parentfield"): + d.parent = self.doc.name # rename if reqd + d.parenttype = self.doc.doctype + + d.save(check_links=False, ignore_fields = self.ignore_fields) + + child_map.setdefault(d.doctype, []).append(d.name) + + # delete all children in database that are not in the child_map + + # get all children types + tablefields = webnotes.model.meta.get_table_fields(self.doc.doctype) + + for dt in tablefields: + if dt[0] not in self.ignore_children_type: + cnames = child_map.get(dt[0]) or [] + if cnames: + webnotes.conn.sql("""delete from `tab%s` where parent=%s and parenttype=%s and + name not in (%s)""" % (dt[0], '%s', '%s', ','.join(['%s'] * len(cnames))), + tuple([self.doc.name, self.doc.doctype] + cnames)) + else: + webnotes.conn.sql("""delete from `tab%s` where parent=%s and parenttype=%s""" \ + % (dt[0], '%s', '%s'), (self.doc.name, self.doc.doctype)) def delete(self): webnotes.delete_doc(self.doc.doctype, self.doc.name) @@ -426,6 +427,12 @@ class Bean: doc.fields[df.fieldname] = flt(doc.fields.get(df.fieldname)) doc.docstatus = cint(doc.docstatus) + + def extract_images_from_text_editor(self): + from webnotes.utils.file_manager import extract_images_from_html + self._files = [] + for df in self.meta.get({"doctype": "DocField", "parent": self.doc.doctype, "fieldtype":"Text Editor"}): + extract_images_from_html(self.doc, df.fieldname) def clone(source_wrapper): """ make a clone of a document""" diff --git a/webnotes/utils/file_manager.py b/webnotes/utils/file_manager.py index d4b9caa181..787567fc4e 100644 --- a/webnotes/utils/file_manager.py +++ b/webnotes/utils/file_manager.py @@ -3,7 +3,7 @@ from __future__ import unicode_literals import webnotes -import os, base64 +import os, base64, re from webnotes.utils import cstr, cint, get_site_path from webnotes import _ from webnotes import conf @@ -61,6 +61,25 @@ def get_uploaded_content(): webnotes.msgprint('No File') return None, None +def extract_images_from_html(doc, fieldname): + content = doc.get(fieldname) + webnotes.flags.has_dataurl = False + + def _save_file(match): + data = match.group(1) + headers, content = data.split(",") + filename = headers.split("filename=")[-1] + filename = save_file(filename, content, doc.doctype, doc.name, decode=True).get("file_name") + if not webnotes.flags.has_dataurl: + webnotes.flags.has_dataurl = True + + return ' - diff --git a/website/js/website.js b/website/js/website.js index 2f85e6ffde..25246a11d6 100644 --- a/website/js/website.js +++ b/website/js/website.js @@ -22,6 +22,18 @@ $.extend(wn, { } return parent; }, + require: function(url) { + $.ajax({ + url: url + "?q=" + Math.floor(Math.random() * 1000), + async: false, + dataType: "text", + success: function(data) { + var el = document.createElement('script'); + el.appendChild(document.createTextNode(data)); + document.getElementsByTagName('head')[0].appendChild(el); + } + }); + }, hide_message: function(text) { $('.message-overlay').remove(); },