diff --git a/frappe/__init__.py b/frappe/__init__.py index c834f2bc08..1f53e8279d 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -14,7 +14,7 @@ import os, sys, importlib, inspect, json from .exceptions import * from .utils.jinja import get_jenv, get_template, render_template, get_email_from_template -__version__ = '8.8.3' +__version__ = '8.8.4' __title__ = "Frappe Framework" local = Local() diff --git a/frappe/core/doctype/file/file.py b/frappe/core/doctype/file/file.py index f9b65e6556..1c364079b5 100755 --- a/frappe/core/doctype/file/file.py +++ b/frappe/core/doctype/file/file.py @@ -218,7 +218,7 @@ class File(NestedSet): if not self.flags.ignore_permissions and \ not frappe.has_permission(self.attached_to_doctype, "write", self.attached_to_name): - frappe.throw(frappe._("No permission to write / remove."), + frappe.throw(frappe._("Cannot delete file as it belongs to {0} {1} for which you do not have permissions").format(self.attached_to_doctype, self.attached_to_name), frappe.PermissionError) except frappe.DoesNotExistError: pass diff --git a/frappe/core/page/data_import_tool/importer.py b/frappe/core/page/data_import_tool/importer.py index 308cef8f55..6ea5a6eecc 100644 --- a/frappe/core/page/data_import_tool/importer.py +++ b/frappe/core/page/data_import_tool/importer.py @@ -93,7 +93,10 @@ def upload(rows = None, submit_after_import=None, ignore_encoding_errors=False, for i, d in enumerate(doctype_row[1:]): if d not in ("~", "-"): if d and doctype_row[i] in (None, '' ,'~', '-', 'DocType:'): - dt, parentfield = d, doctype_row[i+2] or None + dt, parentfield = d, None + # xls format truncates the row, so it may not have more columns + if len(doctype_row) > i+2: + parentfield = doctype_row[i+2] doctypes.append((dt, parentfield)) column_idx_to_fieldname[(dt, parentfield)] = {} column_idx_to_fieldtype[(dt, parentfield)] = {} @@ -210,7 +213,7 @@ def upload(rows = None, submit_after_import=None, ignore_encoding_errors=False, # file is already attached return - file = save_url(file_url, None, doctype, docname, "Home/Attachments", 0) + save_url(file_url, None, doctype, docname, "Home/Attachments", 0) # header if not rows: diff --git a/frappe/core/page/desktop/desktop.js b/frappe/core/page/desktop/desktop.js index 909f3c70fd..eae5b7a35d 100644 --- a/frappe/core/page/desktop/desktop.js +++ b/frappe/core/page/desktop/desktop.js @@ -124,7 +124,7 @@ $.extend(frappe.desktop, { frappe.desktop.wrapper.on("click", ".circle", function() { var doctype = $(this).attr('data-doctype'); if(doctype) { - frappe.set_route('List', doctype, frappe.ui.notifications.get_filters(doctype)); + frappe.ui.notifications.show_open_count_list(doctype); } }); }, diff --git a/frappe/desk/page/modules/modules.js b/frappe/desk/page/modules/modules.js index 87361d951f..02806396c2 100644 --- a/frappe/desk/page/modules/modules.js +++ b/frappe/desk/page/modules/modules.js @@ -41,7 +41,7 @@ frappe.pages['modules'].on_page_load = function(wrapper) { page.main.on("click", '.open-notification', function(event) { var doctype = $(this).attr('data-doctype'); if(doctype) { - frappe.set_route('List', doctype, frappe.ui.notifications.get_filters(doctype)); + frappe.ui.notifications.show_open_count_list(doctype); } }); diff --git a/frappe/handler.py b/frappe/handler.py index 4845b4d725..84002c4e88 100755 --- a/frappe/handler.py +++ b/frappe/handler.py @@ -117,6 +117,7 @@ def uploadfile(): ret = method() except Exception: frappe.errprint(frappe.utils.get_traceback()) + frappe.response['http_status_code'] = 500 ret = None return ret diff --git a/frappe/public/css/form.css b/frappe/public/css/form.css index 2dd5aaa1e2..0214e4923e 100644 --- a/frappe/public/css/form.css +++ b/frappe/public/css/form.css @@ -478,17 +478,16 @@ h6.uppercase, right: 12px; } .signature-reset { + z-index: 10; height: 30px; width: 30px; padding: 4px 0px; } .signature-img { - border: 1px solid #d1d8dd; background: #fff; border-radius: 3px; margin-top: 5px; - width: 100%; - max-height: 300px; + max-height: 150px; } .timeline-new-email { margin: 30px 0px; diff --git a/frappe/public/js/frappe/form/control.js b/frappe/public/js/frappe/form/control.js index f979bb2cb5..6d04b4f823 100755 --- a/frappe/public/js/frappe/form/control.js +++ b/frappe/public/js/frappe/form/control.js @@ -2129,10 +2129,8 @@ frappe.ui.form.ControlSignature = frappe.ui.form.ControlData.extend({ this._super(); // make jSignature field - this.$pad = $('
') - .appendTo(me.wrapper) - .jSignature({height:300, width: "100%", "lineWidth": 0.8}) - .on('change', this.on_save_sign.bind(this)); + this.body = $('
').appendTo(me.wrapper); + this.make_pad(); this.img_wrapper = $(`
@@ -2142,18 +2140,31 @@ frappe.ui.form.ControlSignature = frappe.ui.form.ControlData.extend({ this.img = $("") .appendTo(this.img_wrapper).toggle(false); + }, + make_pad: function() { + let width = this.body.width(); + if (width > 0 && !this.$pad) { + this.$pad = this.body.jSignature({ + height: 300, + width: this.body.width(), + lineWidth: 0.8 + }).on('change', + this.on_save_sign.bind(this)); + this.load_pad(); + this.$reset_button_wrapper = $(`
+ + `) + .appendTo(this.$pad) + .on("click", '.signature-reset', () => { + this.on_reset_sign(); + return false; + }); - this.$btnWrapper = $(`
- - `) - .appendTo(this.$pad) - .on("click", '.signature-reset', function() { - me.on_reset_sign(); - return false; - }); + } }, refresh_input: function(e) { // prevent to load the second time + this.make_pad(); this.$wrapper.find(".control-input").toggle(false); this.set_editable(this.get_status()=="Write"); this.load_pad(); @@ -2196,25 +2207,26 @@ frappe.ui.form.ControlSignature = frappe.ui.form.ControlData.extend({ } }, set_editable: function(editable) { - this.$pad.toggle(editable); + this.$pad && this.$pad.toggle(editable); this.img_wrapper.toggle(!editable); - this.$btnWrapper.toggle(editable); - if (editable) { - this.$btnWrapper.addClass('editing'); - } - else { - this.$btnWrapper.removeClass('editing'); + if (this.$reset_button_wrapper) { + this.$reset_button_wrapper.toggle(editable); + if (editable) { + this.$reset_button_wrapper.addClass('editing'); + } + else { + this.$reset_button_wrapper.removeClass('editing'); + } } }, set_my_value: function(value) { if (this.saving || this.loading) return; this.saving = true; this.set_value(value); - this.value = value; this.saving = false; }, get_value: function() { - return this.value? this.value: this.get_model_value(); + return this.value ? this.value: this.get_model_value(); }, // reset signature canvas on_reset_sign: function() { diff --git a/frappe/public/js/frappe/form/dashboard.js b/frappe/public/js/frappe/form/dashboard.js index ca2b4ab9bd..f4ccd33790 100644 --- a/frappe/public/js/frappe/form/dashboard.js +++ b/frappe/public/js/frappe/form/dashboard.js @@ -233,7 +233,7 @@ frappe.ui.form.Dashboard = Class.extend({ } else { frappe.route_options = this.get_document_filter(doctype); if(show_open) { - $.extend(frappe.route_options, frappe.ui.notifications.get_filters(doctype)); + frappe.ui.notifications.show_open_count_list(doctype); } } diff --git a/frappe/public/js/frappe/form/formatters.js b/frappe/public/js/frappe/form/formatters.js index d627a62f39..ca8866891f 100644 --- a/frappe/public/js/frappe/form/formatters.js +++ b/frappe/public/js/frappe/form/formatters.js @@ -9,7 +9,7 @@ frappe.form.link_formatters = {}; frappe.form.formatters = { _right: function(value, options) { - if(options && options.inline) { + if(options && (options.inline || options.only_value)) { return value; } else { return "
" + value + "
"; diff --git a/frappe/public/js/frappe/form/grid_row.js b/frappe/public/js/frappe/form/grid_row.js index 9326c03da7..aee170b35c 100644 --- a/frappe/public/js/frappe/form/grid_row.js +++ b/frappe/public/js/frappe/form/grid_row.js @@ -79,7 +79,8 @@ frappe.ui.form.GridRow = Class.extend({ this.frm.script_manager.trigger(this.grid.df.fieldname + "_remove", this.doc.doctype, this.doc.name); this.frm.dirty(); - } + this.grid.refresh(); + }, ]).catch((e) => { // aborted console.trace(e); // eslint-disable-line @@ -92,8 +93,9 @@ frappe.ui.form.GridRow = Class.extend({ this.grid.df.data.forEach(function(d, i) { d.idx = i+1; }); + + this.grid.refresh(); } - this.grid.refresh(); } }, insert: function(show, below) { diff --git a/frappe/public/js/frappe/form/layout.js b/frappe/public/js/frappe/form/layout.js index 5ba03bb6ac..9ec3ee38a3 100644 --- a/frappe/public/js/frappe/form/layout.js +++ b/frappe/public/js/frappe/form/layout.js @@ -549,6 +549,13 @@ frappe.ui.form.Section = Class.extend({ this.head.toggleClass("collapsed", hide); this.indicator.toggleClass("octicon-chevron-down", hide); this.indicator.toggleClass("octicon-chevron-up", !hide); + + // refresh signature fields + this.fields_list.forEach((f) => { + if (f.df.fieldtype=='Signature') { + f.refresh(); + } + }); }, has_missing_mandatory: function() { var missing_mandatory = false; diff --git a/frappe/public/js/frappe/request.js b/frappe/public/js/frappe/request.js index 128ac996da..19b622e8ee 100644 --- a/frappe/public/js/frappe/request.js +++ b/frappe/public/js/frappe/request.js @@ -137,8 +137,11 @@ frappe.request.call = function(opts) { 500: function(xhr) { frappe.utils.play_sound("error"); frappe.msgprint({message:__("Server Error: Please check your server logs or contact tech support."), title:__('Something went wrong'), indicator: 'red'}); - opts.error_callback && opts.error_callback(); - frappe.request.report_error(xhr, opts); + try { + opts.error_callback && opts.error_callback(); + } catch (e) { + frappe.request.report_error(xhr, opts); + } }, 504: function(xhr) { frappe.msgprint(__("Request Timed Out")) diff --git a/frappe/public/js/frappe/ui/filters/filters.js b/frappe/public/js/frappe/ui/filters/filters.js index 0d74e61b84..db29525fad 100644 --- a/frappe/public/js/frappe/ui/filters/filters.js +++ b/frappe/public/js/frappe/ui/filters/filters.js @@ -442,15 +442,14 @@ frappe.ui.Filter = Class.extend({ var me = this; // add a button for new filter if missing - this.$btn_group = $('
\ - \ -
') + this.$btn_group = $(`
+ +
`) .insertAfter(this.flist.wrapper.find(".set-filters .new-filter")); this.set_filter_button_text(); diff --git a/frappe/public/js/frappe/ui/toolbar/notifications.js b/frappe/public/js/frappe/ui/toolbar/notifications.js index 0a879f8206..48095c1b8b 100644 --- a/frappe/public/js/frappe/ui/toolbar/notifications.js +++ b/frappe/public/js/frappe/ui/toolbar/notifications.js @@ -103,7 +103,10 @@ frappe.ui.notifications = { show_open_count_list: function(doctype) { let filters = this.boot_info.conditions[doctype]; if(filters && $.isPlainObject(filters)) { - frappe.route_options = filters; + if (!frappe.route_options) { + frappe.route_options = {}; + } + $.extend(frappe.route_options, filters); } let route = frappe.get_route(); if(route[0]==="List" && route[1]===doctype) { diff --git a/frappe/public/js/lib/jSignature.min.js b/frappe/public/js/lib/jSignature.min.js index b89c06ff58..31d8bb2c6b 100755 --- a/frappe/public/js/lib/jSignature.min.js +++ b/frappe/public/js/lib/jSignature.min.js @@ -1,79 +1,1392 @@ -/* - -jSignature v2 "2013-12-09T05:51" "commit ID ebe94c351d7267e21b4fc741c79a8191391cb579" +/** @preserve +jSignature v2 "${buildDate}" "${commitID}" Copyright (c) 2012 Willow Systems Corp http://willow-systems.com Copyright (c) 2010 Brinley Ang http://www.unbolt.net -MIT License - - -base64 encoder -MIT, GPL -http://phpjs.org/functions/base64_encode -+ original by: Tyler Akins (http://rumkin.com) -+ improved by: Bayron Guevara -+ improved by: Thunder.m -+ improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) -+ bugfixed by: Pellentesque Malesuada -+ improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) -+ improved by: Rafal Kukawski (http://kukawski.pl) - - -jSignature v2 jSignature's Undo Button and undo functionality plugin - - -jSignature v2 jSignature's custom "base30" format export and import plugins. - - -jSignature v2 SVG export plugin. - - -Simplify.js BSD -(c) 2012, Vladimir Agafonkin -mourner.github.com/simplify-js +MIT License */ -(function(){function r(c){var a,b=c.css("color"),d;c=c[0];for(var l=!1;c&&!d&&!l;){try{a=$(c).css("background-color")}catch(E){a="transparent"}"transparent"!==a&&"rgba(0, 0, 0, 0)"!==a&&(d=a);l=c.body;c=c.parentNode}c=/rgb[a]*\((\d+),\s*(\d+),\s*(\d+)/;var l=/#([AaBbCcDdEeFf\d]{2})([AaBbCcDdEeFf\d]{2})([AaBbCcDdEeFf\d]{2})/,h;a=void 0;(a=b.match(c))?h={r:parseInt(a[1],10),g:parseInt(a[2],10),b:parseInt(a[3],10)}:(a=b.match(l))&&(h={r:parseInt(a[1],16),g:parseInt(a[2],16),b:parseInt(a[3],16)});var e; -d?(a=void 0,(a=d.match(c))?e={r:parseInt(a[1],10),g:parseInt(a[2],10),b:parseInt(a[3],10)}:(a=d.match(l))&&(e={r:parseInt(a[1],16),g:parseInt(a[2],16),b:parseInt(a[3],16)})):e=h?127
').appendTo(d);this.isCanvasEmulator=!1;a=this.canvas=this.initializeCanvas(e);b=$(a);this.$controlbarLower=$('
').appendTo(d); -this.canvasContext=a.getContext("2d");b.data(f+".this",this);e.lineWidth=function(a,b){return a?a:Math.max(Math.round(b/400),2)}(e.lineWidth,a.width);this.lineCurveThreshold=3*e.lineWidth;e.cssclass&&""!=$.trim(e.cssclass)&&b.addClass(e.cssclass);this.fatFingerCompensation=0;d=function(a){var b,c,d=function(d){d=d.changedTouches&&0e.minFatFingerCompensation?-3*e.lineWidth:e.minFatFingerCompensation;b(l);d.ontouchend=a;d.ontouchstart=b;d.ontouchmove=c},d.onmousedown=function(e){d.ontouchstart=d.ontouchend=d.ontouchmove=void 0;b(e);d.onmousedown=b;d.onmouseup=a;d.onmousemove=c},window.navigator.msPointerEnabled&& -(d.onmspointerdown=b,d.onmspointerup=a,d.onmspointermove=c))}).call(this,d.drawEndHandler,d.drawStartHandler,d.drawMoveHandler);c[f+".windowmouseup"]=l.subscribe(f+".windowmouseup",d.drawEndHandler);this.events.publish(f+".attachingEventHandlers");s.call(this,this,e.width.toString(10),f,l);this.resetCanvas(e.data);this.events.publish(f+".initialized");return this}function w(c){if(c.getContext)return!1;var a=c.ownerDocument.parentWindow,b=a.FlashCanvas?c.ownerDocument.parentWindow.FlashCanvas:"undefined"=== -typeof FlashCanvas?void 0:FlashCanvas;if(b){c=b.initElement(c);b=1;a&&a.screen&&a.screen.deviceXDPI&&a.screen.logicalXDPI&&(b=1*a.screen.deviceXDPI/a.screen.logicalXDPI);if(1!==b)try{$(c).children("object").get(0).resize(Math.ceil(c.width*b),Math.ceil(c.height*b)),c.getContext("2d").scale(b,b)}catch(d){}return!0}throw Error("Canvas element does not support 2d context. jSignature cannot proceed.");}var f="jSignature",u=function(c,a){var b;this.kick=function(){clearTimeout(b);b=setTimeout(a,c)};this.clear= -function(){clearTimeout(b)};return this},t=function(c){this.topics={};this.context=c?c:this;this.publish=function(a,b,c,e){if(this.topics[a]){var f=this.topics[a],h=Array.prototype.slice.call(arguments,1),m=[],q,g,p,x;g=0;for(p=f.length;gthis.lineCurveThreshold){m=2this.lineCurveThreshold)if(1').appendTo(this.$controlbarLower),k=g.width();g.css("left",Math.round((this.canvas.width-k)/2));k!==g.width()&&g.width(k);return g});r.call(this,g,"jSignature",k)}})})})(); -(function(){for(var r={},k={},g="0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWX".split(""),n=g.length/2,s=n-1;-1l&&0a&&(a=1,e.push("Y")),d=Math.abs(l),d>=n?e.push(v(d.toString(n))):e.push(d.toString(n));return e.join("")},f=function(f){var e= -[];f=f.split("");for(var c=f.length,a,b=1,d=[],l=0,g=0;ga?(a=2c){var a=(new k(e.x[a-2],e.y[a-2])).getVectorToPoint(d),d=b.angleTo(a.reverse()),f=0.35*b.getLength(),a=(new r(a.x+b.x,a.y+b.y)).resizeTo(Math.max(0.05,d)*f);return["c",g(a.x,2),g(a.y,2),g(b.x, -2),g(b.y,2),g(b.x,2),g(b.y,2)]}return["l",g(b.x,2),g(b.y,2)]}function v(e,c,a){c=["M",g(e.x[0]-c,2),g(e.y[0]-a,2)];a=1;for(var b=e.x.length-1;a',''],b,d=e.length,f,g=[],h=[],m=f=b=0,k=0,n=[];if(0!==d){for(b=0;bd?0:d;k=0>g?0:g;b-=d;f=h-g}a.push('');b=0;for(d=n.length;b');a.push("");return a.join("")}function u(e,c){return[C,f(e,c)]}function t(e,c){return[q,y(f(e,c))]}(function(e,c){"use strict";(typeof exports!= -c+""?exports:e).simplify=function(a,b,d){b=b!==c?b*b:1;if(!d){var e=a.length,f,g=a[0],m=[g];for(d=1;db&&(m.push(f),g=f)}a=(g!==f&&m.push(f),m)}f=a;d=f.length;var e=new (typeof Uint8Array!=c+""?Uint8Array:Array)(d),g=0,m=d-1,p,q,r=[],s=[],y=[];for(e[g]=e[m]=1;m;){n=0;for(k=g+1;kn&&(q=k,n=p)}n>b&&(e[q]=1,r.push(g),s.push(q),r.push(q),s.push(m));g=r.pop();m=s.pop()}for(k=0;k>18&63,b=f>>12&63,d=f>>6&63,f&=63,k[h++]=c[a]+c[b]+c[d]+c[f];while(g 127){ + backcolorcomponents = {'r':0,'g':0,'b':0} + } else { + backcolorcomponents = {'r':255,'g':255,'b':255} + } + } else { + // arg!!! front color is in format we don't understand (hsl, named colors) + // Let's just go with white background. + backcolorcomponents = {'r':255,'g':255,'b':255} + } + } else { + tmp = undef + tmp = backcolor.match(rgbaregex) + if (tmp){ + backcolorcomponents = {'r':parseInt(tmp[1],10),'g':parseInt(tmp[2],10),'b':parseInt(tmp[3],10)} + } else { + tmp = backcolor.match(hexregex) + if (tmp) { + backcolorcomponents = {'r':parseInt(tmp[1],16),'g':parseInt(tmp[2],16),'b':parseInt(tmp[3],16)} + } + } +// if(!backcolorcomponents){ +// backcolorcomponents = {'r':0,'g':0,'b':0} +// } + } + + // Deriving Decor color + // THis is LAZY!!!! Better way would be to use HSL and adjust luminocity. However, that could be an overkill. + + var toRGBfn = function(o){return 'rgb(' + [o.r, o.g, o.b].join(', ') + ')'} + , decorcolorcomponents + , frontcolorbrightness + , adjusted + + if (frontcolorcomponents && backcolorcomponents){ + var backcolorbrightness = Math.max.apply(null, [frontcolorcomponents.r, frontcolorcomponents.g, frontcolorcomponents.b]) + + frontcolorbrightness = Math.max.apply(null, [backcolorcomponents.r, backcolorcomponents.g, backcolorcomponents.b]) + adjusted = Math.round(frontcolorbrightness + (-1 * (frontcolorbrightness - backcolorbrightness) * 0.75)) // "dimming" the difference between pen and back. + decorcolorcomponents = {'r':adjusted,'g':adjusted,'b':adjusted} // always shade of gray + } else if (frontcolorcomponents) { + frontcolorbrightness = Math.max.apply(null, [frontcolorcomponents.r, frontcolorcomponents.g, frontcolorcomponents.b]) + var polarity = +1 + if (frontcolorbrightness > 127){ + polarity = -1 + } + // shifting by 25% (64 points on RGB scale) + adjusted = Math.round(frontcolorbrightness + (polarity * 96)) // "dimming" the pen's color by 75% to get decor color. + decorcolorcomponents = {'r':adjusted,'g':adjusted,'b':adjusted} // always shade of gray + } else { + decorcolorcomponents = {'r':191,'g':191,'b':191} // always shade of gray + } + + return { + 'color': frontcolor + , 'background-color': backcolorcomponents? toRGBfn(backcolorcomponents) : backcolor + , 'decor-color': toRGBfn(decorcolorcomponents) + } +} + +function Vector(x,y){ + this.x = x + this.y = y + this.reverse = function(){ + return new this.constructor( + this.x * -1 + , this.y * -1 + ) + } + this._length = null + this.getLength = function(){ + if (!this._length){ + this._length = Math.sqrt( Math.pow(this.x, 2) + Math.pow(this.y, 2) ) + } + return this._length + } + + var polarity = function (e){ + return Math.round(e / Math.abs(e)) + } + this.resizeTo = function(length){ + // proportionally changes x,y such that the hypotenuse (vector length) is = new length + if (this.x === 0 && this.y === 0){ + this._length = 0 + } else if (this.x === 0){ + this._length = length + this.y = length * polarity(this.y) + } else if(this.y === 0){ + this._length = length + this.x = length * polarity(this.x) + } else { + var proportion = Math.abs(this.y / this.x) + , x = Math.sqrt(Math.pow(length, 2) / (1 + Math.pow(proportion, 2))) + , y = proportion * x + this._length = length + this.x = x * polarity(this.x) + this.y = y * polarity(this.y) + } + return this + } + + /** + * Calculates the angle between 'this' vector and another. + * @public + * @function + * @returns {Number} The angle between the two vectors as measured in PI. + */ + this.angleTo = function(vectorB) { + var divisor = this.getLength() * vectorB.getLength() + if (divisor === 0) { + return 0 + } else { + // JavaScript floating point math is screwed up. + // because of it, the core of the formula can, on occasion, have values + // over 1.0 and below -1.0. + return Math.acos( + Math.min( + Math.max( + ( this.x * vectorB.x + this.y * vectorB.y ) / divisor + , -1.0 + ) + , 1.0 + ) + ) / Math.PI + } + } +} + +function Point(x,y){ + this.x = x + this.y = y + + this.getVectorToCoordinates = function (x, y) { + return new Vector(x - this.x, y - this.y) + } + this.getVectorFromCoordinates = function (x, y) { + return this.getVectorToCoordinates(x, y).reverse() + } + this.getVectorToPoint = function (point) { + return new Vector(point.x - this.x, point.y - this.y) + } + this.getVectorFromPoint = function (point) { + return this.getVectorToPoint(point).reverse() + } +} + +/* + * About data structure: + * We don't store / deal with "pictures" this signature capture code captures "vectors" + * + * We don't store bitmaps. We store "strokes" as arrays of arrays. (Actually, arrays of objects containing arrays of coordinates. + * + * Stroke = mousedown + mousemoved * n (+ mouseup but we don't record that as that was the "end / lack of movement" indicator) + * + * Vectors = not classical vectors where numbers indicated shift relative last position. Our vectors are actually coordinates against top left of canvas. + * we could calc the classical vectors, but keeping the the actual coordinates allows us (through Math.max / min) + * to calc the size of resulting drawing very quickly. If we want classical vectors later, we can always get them in backend code. + * + * So, the data structure: + * + * var data = [ + * { // stroke starts + * x : [101, 98, 57, 43] // x points + * , y : [1, 23, 65, 87] // y points + * } // stroke ends + * , { // stroke starts + * x : [55, 56, 57, 58] // x points + * , y : [101, 97, 54, 4] // y points + * } // stroke ends + * , { // stroke consisting of just a dot + * x : [53] // x points + * , y : [151] // y points + * } // stroke ends + * ] + * + * we don't care or store stroke width (it's canvas-size-relative), color, shadow values. These can be added / changed on whim post-capture. + * + */ +function DataEngine(storageObject, context, startStrokeFn, addToStrokeFn, endStrokeFn){ + this.data = storageObject // we expect this to be an instance of Array + this.context = context + + if (storageObject.length){ + // we have data to render + var numofstrokes = storageObject.length + , stroke + , numofpoints + + for (var i = 0; i < numofstrokes; i++){ + stroke = storageObject[i] + numofpoints = stroke.x.length + startStrokeFn.call(context, stroke) + for(var j = 1; j < numofpoints; j++){ + addToStrokeFn.call(context, stroke, j) + } + endStrokeFn.call(context, stroke) + } + } + + this.changed = function(){} + + this.startStrokeFn = startStrokeFn + this.addToStrokeFn = addToStrokeFn + this.endStrokeFn = endStrokeFn + + this.inStroke = false + + this._lastPoint = null + this._stroke = null + this.startStroke = function(point){ + if(point && typeof(point.x) == "number" && typeof(point.y) == "number"){ + this._stroke = {'x':[point.x], 'y':[point.y]} + this.data.push(this._stroke) + this._lastPoint = point + this.inStroke = true + // 'this' does not work same inside setTimeout( + var stroke = this._stroke + , fn = this.startStrokeFn + , context = this.context + setTimeout( + // some IE's don't support passing args per setTimeout API. Have to create closure every time instead. + function() {fn.call(context, stroke)} + , 3 + ) + return point + } else { + return null + } + } + // that "5" at the very end of this if is important to explain. + // we do NOT render links between two captured points (in the middle of the stroke) if the distance is shorter than that number. + // not only do we NOT render it, we also do NOT capture (add) these intermediate points to storage. + // when clustering of these is too tight, it produces noise on the line, which, because of smoothing, makes lines too curvy. + // maybe, later, we can expose this as a configurable setting of some sort. + this.addToStroke = function(point){ + if (this.inStroke && + typeof(point.x) === "number" && + typeof(point.y) === "number" && + // calculates absolute shift in diagonal pixels away from original point + (Math.abs(point.x - this._lastPoint.x) + Math.abs(point.y - this._lastPoint.y)) > 4 + ){ + var positionInStroke = this._stroke.x.length + this._stroke.x.push(point.x) + this._stroke.y.push(point.y) + this._lastPoint = point + + var stroke = this._stroke + , fn = this.addToStrokeFn + , context = this.context + setTimeout( + // some IE's don't support passing args per setTimeout API. Have to create closure every time instead. + function() {fn.call(context, stroke, positionInStroke)} + , 3 + ) + return point + } else { + return null + } + } + this.endStroke = function(){ + var c = this.inStroke + this.inStroke = false + this._lastPoint = null + if (c){ + var stroke = this._stroke + , fn = this.endStrokeFn // 'this' does not work same inside setTimeout( + , context = this.context + , changedfn = this.changed + setTimeout( + // some IE's don't support passing args per setTimeout API. Have to create closure every time instead. + function(){ + fn.call(context, stroke) + changedfn.call(context) + } + , 3 + ) + return true + } else { + return null + } + } +} + +var basicDot = function(ctx, x, y, size){ + var fillStyle = ctx.fillStyle + ctx.fillStyle = ctx.strokeStyle + ctx.fillRect(x + size / -2 , y + size / -2, size, size) + ctx.fillStyle = fillStyle +} +, basicLine = function(ctx, startx, starty, endx, endy){ + ctx.beginPath() + ctx.moveTo(startx, starty) + ctx.lineTo(endx, endy) + ctx.stroke() +} +, basicCurve = function(ctx, startx, starty, endx, endy, cp1x, cp1y, cp2x, cp2y){ + ctx.beginPath() + ctx.moveTo(startx, starty) + ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, endx, endy) + ctx.stroke() +} +, strokeStartCallback = function(stroke) { + // this = jSignatureClass instance + basicDot(this.canvasContext, stroke.x[0], stroke.y[0], this.settings.lineWidth) +} +, strokeAddCallback = function(stroke, positionInStroke){ + // this = jSignatureClass instance + + // Because we are funky this way, here we draw TWO curves. + // 1. POSSIBLY "this line" - spanning from point right before us, to this latest point. + // 2. POSSIBLY "prior curve" - spanning from "latest point" to the one before it. + + // Why you ask? + // long lines (ones with many pixels between them) do not look good when they are part of a large curvy stroke. + // You know, the jaggedy crocodile spine instead of a pretty, smooth curve. Yuck! + // We want to approximate pretty curves in-place of those ugly lines. + // To approximate a very nice curve we need to know the direction of line before and after. + // Hence, on long lines we actually wait for another point beyond it to come back from + // mousemoved before we draw this curve. + + // So for "prior curve" to be calc'ed we need 4 points + // A, B, C, D (we are on D now, A is 3 points in the past.) + // and 3 lines: + // pre-line (from points A to B), + // this line (from points B to C), (we call it "this" because if it was not yet, it's the only one we can draw for sure.) + // post-line (from points C to D) (even through D point is 'current' we don't know how we can draw it yet) + // + // Well, actually, we don't need to *know* the point A, just the vector A->B + var Cpoint = new Point(stroke.x[positionInStroke-1], stroke.y[positionInStroke-1]) + , Dpoint = new Point(stroke.x[positionInStroke], stroke.y[positionInStroke]) + , CDvector = Cpoint.getVectorToPoint(Dpoint) + + // Again, we have a chance here to draw TWO things: + // BC Curve (only if it's long, because if it was short, it was drawn by previous callback) and + // CD Line (only if it's short) + + // So, let's start with BC curve. + // if there is only 2 points in stroke array, we don't have "history" long enough to have point B, let alone point A. + // Falling through to drawing line CD is proper, as that's the only line we have points for. + if(positionInStroke > 1) { + // we are here when there are at least 3 points in stroke array. + var Bpoint = new Point(stroke.x[positionInStroke-2], stroke.y[positionInStroke-2]) + , BCvector = Bpoint.getVectorToPoint(Cpoint) + , ABvector + if(BCvector.getLength() > this.lineCurveThreshold){ + // Yey! Pretty curves, here we come! + if(positionInStroke > 2) { + // we are here when at least 4 points in stroke array. + ABvector = (new Point(stroke.x[positionInStroke-3], stroke.y[positionInStroke-3])).getVectorToPoint(Bpoint) + } else { + ABvector = new Vector(0,0) + } + + var minlenfraction = 0.05 + , maxlen = BCvector.getLength() * 0.35 + , ABCangle = BCvector.angleTo(ABvector.reverse()) + , BCDangle = CDvector.angleTo(BCvector.reverse()) + , BCP1vector = new Vector(ABvector.x + BCvector.x, ABvector.y + BCvector.y).resizeTo( + Math.max(minlenfraction, ABCangle) * maxlen + ) + , CCP2vector = (new Vector(BCvector.x + CDvector.x, BCvector.y + CDvector.y)).reverse().resizeTo( + Math.max(minlenfraction, BCDangle) * maxlen + ) + + basicCurve( + this.canvasContext + , Bpoint.x + , Bpoint.y + , Cpoint.x + , Cpoint.y + , Bpoint.x + BCP1vector.x + , Bpoint.y + BCP1vector.y + , Cpoint.x + CCP2vector.x + , Cpoint.y + CCP2vector.y + ) + } + } + if(CDvector.getLength() <= this.lineCurveThreshold){ + basicLine( + this.canvasContext + , Cpoint.x + , Cpoint.y + , Dpoint.x + , Dpoint.y + ) + } +} +, strokeEndCallback = function(stroke){ + // this = jSignatureClass instance + + // Here we tidy up things left unfinished in last strokeAddCallback run. + + // What's POTENTIALLY left unfinished there is the curve between the last points + // in the stroke, if the len of that line is more than lineCurveThreshold + // If the last line was shorter than lineCurveThreshold, it was drawn there, and there + // is nothing for us here to do. + // We can also be called when there is only one point in the stroke (meaning, the + // stroke was just a dot), in which case, again, there is nothing for us to do. + + // So for "this curve" to be calc'ed we need 3 points + // A, B, C + // and 2 lines: + // pre-line (from points A to B), + // this line (from points B to C) + // Well, actually, we don't need to *know* the point A, just the vector A->B + // so, we really need points B, C and AB vector. + var positionInStroke = stroke.x.length - 1 + + if (positionInStroke > 0){ + // there are at least 2 points in the stroke.we are in business. + var Cpoint = new Point(stroke.x[positionInStroke], stroke.y[positionInStroke]) + , Bpoint = new Point(stroke.x[positionInStroke-1], stroke.y[positionInStroke-1]) + , BCvector = Bpoint.getVectorToPoint(Cpoint) + , ABvector + if (BCvector.getLength() > this.lineCurveThreshold){ + // yep. This one was left undrawn in prior callback. Have to draw it now. + if (positionInStroke > 1){ + // we have at least 3 elems in stroke + ABvector = (new Point(stroke.x[positionInStroke-2], stroke.y[positionInStroke-2])).getVectorToPoint(Bpoint) + var BCP1vector = new Vector(ABvector.x + BCvector.x, ABvector.y + BCvector.y).resizeTo(BCvector.getLength() / 2) + basicCurve( + this.canvasContext + , Bpoint.x + , Bpoint.y + , Cpoint.x + , Cpoint.y + , Bpoint.x + BCP1vector.x + , Bpoint.y + BCP1vector.y + , Cpoint.x + , Cpoint.y + ) + } else { + // Since there is no AB leg, there is no curve to draw. This line is still "long" but no curve. + basicLine( + this.canvasContext + , Bpoint.x + , Bpoint.y + , Cpoint.x + , Cpoint.y + ) + } + } + } +} + + +/* +var getDataStats = function(){ + var strokecnt = strokes.length + , stroke + , pointid + , pointcnt + , x, y + , maxX = Number.NEGATIVE_INFINITY + , maxY = Number.NEGATIVE_INFINITY + , minX = Number.POSITIVE_INFINITY + , minY = Number.POSITIVE_INFINITY + for(strokeid = 0; strokeid < strokecnt; strokeid++){ + stroke = strokes[strokeid] + pointcnt = stroke.length + for(pointid = 0; pointid < pointcnt; pointid++){ + x = stroke.x[pointid] + y = stroke.y[pointid] + if (x > maxX){ + maxX = x + } else if (x < minX) { + minX = x + } + if (y > maxY){ + maxY = y + } else if (y < minY) { + minY = y + } + } + } + return {'maxX': maxX, 'minX': minX, 'maxY': maxY, 'minY': minY} +} +*/ + +function conditionallyLinkCanvasResizeToWindowResize(jSignatureInstance, settingsWidth, apinamespace, globalEvents){ + 'use strict' + if ( settingsWidth === 'ratio' || settingsWidth.split('')[settingsWidth.length - 1] === '%' ) { + + this.eventTokens[apinamespace + '.parentresized'] = globalEvents.subscribe( + apinamespace + '.parentresized' + , (function(eventTokens, $parent, originalParentWidth, sizeRatio){ + 'use strict' + + return function(){ + 'use strict' + + var w = $parent.width() + if (w !== originalParentWidth) { + + // UNsubscribing this particular instance of signature pad only. + // there is a separate `eventTokens` per each instance of signature pad + for (var key in eventTokens){ + if (eventTokens.hasOwnProperty(key)) { + globalEvents.unsubscribe(eventTokens[key]) + delete eventTokens[key] + } + } + + var settings = jSignatureInstance.settings + jSignatureInstance.$parent.children().remove() + for (var key in jSignatureInstance){ + if (jSignatureInstance.hasOwnProperty(key)) { + delete jSignatureInstance[key] + } + } + + // scale data to new signature pad size + settings.data = (function(data, scale){ + var newData = [] + var o, i, l, j, m, stroke + for ( i = 0, l = data.length; i < l; i++) { + stroke = data[i] + + o = {'x':[],'y':[]} + + for ( j = 0, m = stroke.x.length; j < m; j++) { + o.x.push(stroke.x[j] * scale) + o.y.push(stroke.y[j] * scale) + } + + newData.push(o) + } + return newData + })( + settings.data + , w * 1.0 / originalParentWidth + ) + + $parent[apinamespace](settings) + } + } + })( + this.eventTokens + , this.$parent + , this.$parent.width() + , this.canvas.width * 1.0 / this.canvas.height + ) + ) + } +} + + +function jSignatureClass(parent, options, instanceExtensions) { + + var $parent = this.$parent = $(parent) + , eventTokens = this.eventTokens = {} + , events = this.events = new PubSubClass(this) + , globalEvents = $.fn[apinamespace]('globalEvents') + , settings = { + 'width' : 'ratio' + ,'height' : 'ratio' + ,'sizeRatio': 4 // only used when height = 'ratio' + ,'color' : '#000' + ,'background-color': '#fff' + ,'decor-color': '#eee' + ,'lineWidth' : 0 + ,'minFatFingerCompensation' : -10 + ,'showUndoButton': false + ,'data': [] + } + + $.extend(settings, getColors($parent)) + if (options) { + $.extend(settings, options) + } + this.settings = settings + + for (var extensionName in instanceExtensions){ + if (instanceExtensions.hasOwnProperty(extensionName)) { + instanceExtensions[extensionName].call(this, extensionName) + } + } + + this.events.publish(apinamespace+'.initializing') + + // these, when enabled, will hover above the sig area. Hence we append them to DOM before canvas. + this.$controlbarUpper = (function(){ + var controlbarstyle = 'padding:0 !important;margin:0 !important;'+ + 'width: 100% !important; height: 0 !important;'+ + 'margin-top:-1em !important;margin-bottom:1em !important;' + return $('
').appendTo($parent) + })(); + + this.isCanvasEmulator = false // will be flipped by initializer when needed. + var canvas = this.canvas = this.initializeCanvas(settings) + , $canvas = $(canvas) + + this.$controlbarLower = (function(){ + var controlbarstyle = 'padding:0 !important;margin:0 !important;'+ + 'width: 100% !important; height: 0 !important;'+ + 'margin-top:-1.5em !important;margin-bottom:1.5em !important;' + return $('
').appendTo($parent) + })(); + + this.canvasContext = canvas.getContext("2d") + + // Most of our exposed API will be looking for this: + $canvas.data(apinamespace + '.this', this) + + settings.lineWidth = (function(defaultLineWidth, canvasWidth){ + if (!defaultLineWidth){ + return Math.max( + Math.round(canvasWidth / 400) /*+1 pixel for every extra 300px of width.*/ + , 2 /* minimum line width */ + ) + } else { + return defaultLineWidth + } + })(settings.lineWidth, canvas.width); + + this.lineCurveThreshold = settings.lineWidth * 3 + + // Add custom class if defined + if(settings.cssclass && $.trim(settings.cssclass) != "") { + $canvas.addClass(settings.cssclass) + } + + // used for shifting the drawing point up on touch devices, so one can see the drawing above the finger. + this.fatFingerCompensation = 0 + + var movementHandlers = (function(jSignatureInstance) { + + //================================ + // mouse down, move, up handlers: + + // shifts - adjustment values in viewport pixels drived from position of canvas on the page + var shiftX + , shiftY + , setStartValues = function(){ + var tos = $(jSignatureInstance.canvas).offset() + shiftX = tos.left * -1 + shiftY = tos.top * -1 + } + , getPointFromEvent = function(e) { + var firstEvent = (e.changedTouches && e.changedTouches.length > 0 ? e.changedTouches[0] : e) + // All devices i tried report correct coordinates in pageX,Y + // Android Chrome 2.3.x, 3.1, 3.2., Opera Mobile, safari iOS 4.x, + // Windows: Chrome, FF, IE9, Safari + // None of that scroll shift calc vs screenXY other sigs do is needed. + // ... oh, yeah, the "fatFinger.." is for tablets so that people see what they draw. + return new Point( + Math.round(firstEvent.pageX + shiftX) + , Math.round(firstEvent.pageY + shiftY) + jSignatureInstance.fatFingerCompensation + ) + } + , timer = new KickTimerClass( + 750 + , function() { jSignatureInstance.dataEngine.endStroke() } + ) + + this.drawEndHandler = function(e) { + try { e.preventDefault() } catch (ex) {} + timer.clear() + jSignatureInstance.dataEngine.endStroke() + } + this.drawStartHandler = function(e) { + e.preventDefault() + // for performance we cache the offsets + // we recalc these only at the beginning the stroke + setStartValues() + jSignatureInstance.dataEngine.startStroke( getPointFromEvent(e) ) + timer.kick() + } + this.drawMoveHandler = function(e) { + e.preventDefault() + if (!jSignatureInstance.dataEngine.inStroke){ + return + } + jSignatureInstance.dataEngine.addToStroke( getPointFromEvent(e) ) + timer.kick() + } + + return this + + }).call( {}, this ) + + // + //================================ + + ;(function(drawEndHandler, drawStartHandler, drawMoveHandler) { + var canvas = this.canvas + , $canvas = $(canvas) + , undef + if (this.isCanvasEmulator){ + $canvas.bind('mousemove.'+apinamespace, drawMoveHandler) + $canvas.bind('mouseup.'+apinamespace, drawEndHandler) + $canvas.bind('mousedown.'+apinamespace, drawStartHandler) + } else { + canvas.ontouchstart = function(e) { + canvas.onmousedown = undef + canvas.onmouseup = undef + canvas.onmousemove = undef + + this.fatFingerCompensation = ( + settings.minFatFingerCompensation && + settings.lineWidth * -3 > settings.minFatFingerCompensation + ) ? settings.lineWidth * -3 : settings.minFatFingerCompensation + + drawStartHandler(e) + + canvas.ontouchend = drawEndHandler + canvas.ontouchstart = drawStartHandler + canvas.ontouchmove = drawMoveHandler + } + canvas.onmousedown = function(e) { + canvas.ontouchstart = undef + canvas.ontouchend = undef + canvas.ontouchmove = undef + + drawStartHandler(e) + + canvas.onmousedown = drawStartHandler + canvas.onmouseup = drawEndHandler + canvas.onmousemove = drawMoveHandler + } + } + }).call( + this + , movementHandlers.drawEndHandler + , movementHandlers.drawStartHandler + , movementHandlers.drawMoveHandler + ) + + //========================================= + // various event handlers + + // on mouseout + mouseup canvas did not know that mouseUP fired. Continued to draw despite mouse UP. + // it is bettr than + // $canvas.bind('mouseout', drawEndHandler) + // because we don't want to break the stroke where user accidentally gets ouside and wants to get back in quickly. + eventTokens[apinamespace + '.windowmouseup'] = globalEvents.subscribe( + apinamespace + '.windowmouseup' + , movementHandlers.drawEndHandler + ) + + this.events.publish(apinamespace+'.attachingEventHandlers') + + // If we have proportional width, we sign up to events broadcasting "window resized" and checking if + // parent's width changed. If so, we (1) extract settings + data from current signature pad, + // (2) remove signature pad from parent, and (3) reinit new signature pad at new size with same settings, (rescaled) data. + conditionallyLinkCanvasResizeToWindowResize.call( + this + , this + , settings.width.toString(10) + , apinamespace, globalEvents + ) + + // end of event handlers. + // =============================== + + this.resetCanvas(settings.data) + + // resetCanvas renders the data on the screen and fires ONE "change" event + // if there is data. If you have controls that rely on "change" firing + // attach them to something that runs before this.resetCanvas, like + // apinamespace+'.attachingEventHandlers' that fires a bit higher. + this.events.publish(apinamespace+'.initialized') + + return this +} // end of initBase + +//========================================================================= +// jSignatureClass's methods and supporting fn's + +jSignatureClass.prototype.resetCanvas = function(data){ + var canvas = this.canvas + , settings = this.settings + , ctx = this.canvasContext + , isCanvasEmulator = this.isCanvasEmulator + + , cw = canvas.width + , ch = canvas.height + + // preparing colors, drawing area + + ctx.clearRect(0, 0, cw + 30, ch + 30) + + ctx.shadowColor = ctx.fillStyle = settings['background-color'] + if (isCanvasEmulator){ + // FLashCanvas fills with Black by default, covering up the parent div's background + // hence we refill + ctx.fillRect(0,0,cw + 30, ch + 30) + } + + ctx.lineWidth = Math.ceil(parseInt(settings.lineWidth, 10)) + ctx.lineCap = ctx.lineJoin = "round" + + // signature line + ctx.strokeStyle = settings['decor-color'] + ctx.shadowOffsetX = 0 + ctx.shadowOffsetY = 0 + var lineoffset = Math.round( ch / 5 ) + basicLine(ctx, lineoffset * 1.5, ch - lineoffset, cw - (lineoffset * 1.5), ch - lineoffset) + ctx.strokeStyle = settings.color + + if (!isCanvasEmulator){ + ctx.shadowColor = ctx.strokeStyle + ctx.shadowOffsetX = ctx.lineWidth * 0.5 + ctx.shadowOffsetY = ctx.lineWidth * -0.6 + ctx.shadowBlur = 0 + } + + // setting up new dataEngine + + if (!data) { data = [] } + + var dataEngine = this.dataEngine = new DataEngine( + data + , this + , strokeStartCallback + , strokeAddCallback + , strokeEndCallback + ) + + settings.data = data // onwindowresize handler uses it, i think. + $(canvas).data(apinamespace+'.data', data) + .data(apinamespace+'.settings', settings) + + // we fire "change" event on every change in data. + // setting this up: + dataEngine.changed = (function(target, events, apinamespace) { + 'use strict' + return function() { + events.publish(apinamespace+'.change') + target.trigger('change') + } + })(this.$parent, this.events, apinamespace) + // let's trigger change on all data reloads + dataEngine.changed() + + // import filters will be passing this back as indication of "we rendered" + return true +} + +function initializeCanvasEmulator(canvas){ + if (canvas.getContext){ + return false + } else { + // for cases when jSignature, FlashCanvas is inserted + // from one window into another (child iframe) + // 'window' and 'FlashCanvas' may be stuck behind + // in that other parent window. + // we need to find it + var window = canvas.ownerDocument.parentWindow + var FC = window.FlashCanvas ? + canvas.ownerDocument.parentWindow.FlashCanvas : + ( + typeof FlashCanvas === "undefined" ? + undefined : + FlashCanvas + ) + + if (FC) { + canvas = FC.initElement(canvas) + + var zoom = 1 + // FlashCanvas uses flash which has this annoying habit of NOT scaling with page zoom. + // It matches pixel-to-pixel to screen instead. + // Since we are targeting ONLY IE 7, 8 with FlashCanvas, we will test the zoom only the IE8, IE7 way + if (window && window.screen && window.screen.deviceXDPI && window.screen.logicalXDPI){ + zoom = window.screen.deviceXDPI * 1.0 / window.screen.logicalXDPI + } + if (zoom !== 1){ + try { + // We effectively abuse the brokenness of FlashCanvas and force the flash rendering surface to + // occupy larger pixel dimensions than the wrapping, scaled up DIV and Canvas elems. + $(canvas).children('object').get(0).resize(Math.ceil(canvas.width * zoom), Math.ceil(canvas.height * zoom)) + // And by applying "scale" transformation we can talk "browser pixels" to FlashCanvas + // and have it translate the "browser pixels" to "screen pixels" + canvas.getContext('2d').scale(zoom, zoom) + // Note to self: don't reuse Canvas element. Repeated "scale" are cumulative. + } catch (ex) {} + } + return true + } else { + throw new Error("Canvas element does not support 2d context. jSignature cannot proceed.") + } + } + +} + +jSignatureClass.prototype.initializeCanvas = function(settings) { + // =========== + // Init + Sizing code + + var canvas = document.createElement('canvas') + , $canvas = $(canvas) + + // We cannot work with circular dependency + if (settings.width === settings.height && settings.height === 'ratio') { + settings.width = '100%' + } + + $canvas.css( + 'margin' + , 0 + ).css( + 'padding' + , 0 + ).css( + 'border' + , 'none' + ).css( + 'height' + , settings.height === 'ratio' || !settings.height ? 1 : settings.height.toString(10) + ).css( + 'width' + , settings.width === 'ratio' || !settings.width ? 1 : settings.width.toString(10) + ) + + $canvas.appendTo(this.$parent) + + // we could not do this until canvas is rendered (appended to DOM) + if (settings.height === 'ratio') { + $canvas.css( + 'height' + , Math.round( $canvas.width() / settings.sizeRatio ) + ) + } else if (settings.width === 'ratio') { + $canvas.css( + 'width' + , Math.round( $canvas.height() * settings.sizeRatio ) + ) + } + + $canvas.addClass(apinamespace) + + // canvas's drawing area resolution is independent from canvas's size. + // pixels are just scaled up or down when internal resolution does not + // match external size. So... + + canvas.width = $canvas.width() + canvas.height = $canvas.height() + + // Special case Sizing code + + this.isCanvasEmulator = initializeCanvasEmulator(canvas) + + // End of Sizing Code + // =========== + + // normally select preventer would be short, but + // Canvas emulator on IE does NOT provide value for Event. Hence this convoluted line. + canvas.onselectstart = function(e){if(e && e.preventDefault){e.preventDefault()}; if(e && e.stopPropagation){e.stopPropagation()}; return false;} + + return canvas +} + + +var GlobalJSignatureObjectInitializer = function(window){ + + var globalEvents = new PubSubClass() + + // common "window resized" event listener. + // jSignature instances will subscribe to this chanel. + // to resize themselves when needed. + ;(function(globalEvents, apinamespace, $, window){ + 'use strict' + + var resizetimer + , runner = function(){ + globalEvents.publish( + apinamespace + '.parentresized' + ) + } + + // jSignature knows how to resize its content when its parent is resized + // window resize is the only way we can catch resize events though... + $(window).bind('resize.'+apinamespace, function(){ + if (resizetimer) { + clearTimeout(resizetimer) + } + resizetimer = setTimeout( + runner + , 500 + ) + }) + // when mouse exists canvas element and "up"s outside, we cannot catch it with + // callbacks attached to canvas. This catches it outside. + .bind('mouseup.'+apinamespace, function(e){ + globalEvents.publish( + apinamespace + '.windowmouseup' + ) + }) + + })(globalEvents, apinamespace, $, window) + + var jSignatureInstanceExtensions = { + /* + 'exampleExtension':function(extensionName){ + // we are called very early in instance's life. + // right after the settings are resolved and + // jSignatureInstance.events is created + // and right before first ("jSignature.initializing") event is called. + // You don't really need to manupilate + // jSignatureInstance directly, just attach + // a bunch of events to jSignatureInstance.events + // (look at the source of jSignatureClass to see when these fire) + // and your special pieces of code will attach by themselves. + + // this function runs every time a new instance is set up. + // this means every var you create will live only for one instance + // unless you attach it to something outside, like "window." + // and pick it up later from there. + + // when globalEvents' events fire, 'this' is globalEvents object + // when jSignatureInstance's events fire, 'this' is jSignatureInstance + + // Here, + // this = is new jSignatureClass's instance. + + // The way you COULD approch setting this up is: + // if you have multistep set up, attach event to "jSignature.initializing" + // that attaches other events to be fired further lower the init stream. + // Or, if you know for sure you rely on only one jSignatureInstance's event, + // just attach to it directly + + this.events.subscribe( + // name of the event + apinamespace + '.initializing' + // event handlers, can pass args too, but in majority of cases, + // 'this' which is jSignatureClass object instance pointer is enough to get by. + , function(){ + if (this.settings.hasOwnProperty('non-existent setting category?')) { + console.log(extensionName + ' is here') + } + } + ) + } + */ + } + + var exportplugins = { + 'default':function(data){return this.toDataURL()} + , 'native':function(data){return data} + , 'image':function(data){ + /*this = canvas elem */ + var imagestring = this.toDataURL() + + if (typeof imagestring === 'string' && + imagestring.length > 4 && + imagestring.slice(0,5) === 'data:' && + imagestring.indexOf(',') !== -1){ + + var splitterpos = imagestring.indexOf(',') + + return [ + imagestring.slice(5, splitterpos) + , imagestring.substr(splitterpos + 1) + ] + } + return [] + } + } + + // will be part of "importplugins" + function _renderImageOnCanvas( data, formattype, rerendercallable ) { + 'use strict' + // #1. Do NOT rely on this. No worky on IE + // (url max len + lack of base64 decoder + possibly other issues) + // #2. This does NOT affect what is captured as "signature" as far as vector data is + // concerned. This is treated same as "signature line" - i.e. completely ignored + // the only time you see imported image data exported is if you export as image. + + // we do NOT call rerendercallable here (unlike in other import plugins) + // because importing image does absolutely nothing to the underlying vector data storage + // This could be a way to "import" old signatures stored as images + // This could also be a way to import extra decor into signature area. + + var img = new Image() + // this = Canvas DOM elem. Not jQuery object. Not Canvas's parent div. + , c = this + + img.onload = function() { + var ctx = c.getContext("2d").drawImage( + img, 0, 0 + , ( img.width < c.width) ? img.width : c.width + , ( img.height < c.height) ? img.height : c.height + ) + } + + img.src = 'data:' + formattype + ',' + data + } + + var importplugins = { + 'native':function(data, formattype, rerendercallable){ + // we expect data as Array of objects of arrays here - whatever 'default' EXPORT plugin spits out. + // returning Truthy to indicate we are good, all updated. + rerendercallable( data ) + } + , 'image': _renderImageOnCanvas + , 'image/png;base64': _renderImageOnCanvas + , 'image/jpeg;base64': _renderImageOnCanvas + , 'image/jpg;base64': _renderImageOnCanvas + } + + function _clearDrawingArea( data ) { + this.find('canvas.'+apinamespace) + .add(this.filter('canvas.'+apinamespace)) + .data(apinamespace+'.this').resetCanvas( data ) + return this + } + + function _setDrawingData( data, formattype ) { + var undef + + if (formattype === undef && typeof data === 'string' && data.substr(0,5) === 'data:') { + formattype = data.slice(5).split(',')[0] + // 5 chars of "data:" + mimetype len + 1 "," char = all skipped. + data = data.slice(6 + formattype.length) + if (formattype === data) return + } + + var $canvas = this.find('canvas.'+apinamespace).add(this.filter('canvas.'+apinamespace)) + + if (!importplugins.hasOwnProperty(formattype)){ + throw new Error(apinamespace + " is unable to find import plugin with for format '"+ String(formattype) +"'") + } else if ($canvas.length !== 0){ + importplugins[formattype].call( + $canvas[0] + , data + , formattype + , (function(jSignatureInstance){ + return function(){ return jSignatureInstance.resetCanvas.apply(jSignatureInstance, arguments) } + })($canvas.data(apinamespace+'.this')) + ) + } + + return this + } + + var elementIsOrphan = function(e){ + var topOfDOM = false + e = e.parentNode + while (e && !topOfDOM){ + topOfDOM = e.body + e = e.parentNode + } + return !topOfDOM + } + + //These are exposed as methods under $obj.jSignature('methodname', *args) + var plugins = {'export':exportplugins, 'import':importplugins, 'instance': jSignatureInstanceExtensions} + , methods = { + 'init' : function( options ) { + return this.each( function() { + if (!elementIsOrphan(this)) { + new jSignatureClass(this, options, jSignatureInstanceExtensions) + } + }) + } + , 'getSettings' : function() { + return this.find('canvas.'+apinamespace) + .add(this.filter('canvas.'+apinamespace)) + .data(apinamespace+'.this').settings + } + // around since v1 + , 'clear' : _clearDrawingArea + // was mistakenly introduced instead of 'clear' in v2 + , 'reset' : _clearDrawingArea + , 'addPlugin' : function(pluginType, pluginName, callable){ + if (plugins.hasOwnProperty(pluginType)){ + plugins[pluginType][pluginName] = callable + } + return this + } + , 'listPlugins' : function(pluginType){ + var answer = [] + if (plugins.hasOwnProperty(pluginType)){ + var o = plugins[pluginType] + for (var k in o){ + if (o.hasOwnProperty(k)){ + answer.push(k) + } + } + } + return answer + } + , 'getData' : function( formattype ) { + var undef, $canvas=this.find('canvas.'+apinamespace).add(this.filter('canvas.'+apinamespace)) + if (formattype === undef) formattype = 'default' + if ($canvas.length !== 0 && exportplugins.hasOwnProperty(formattype)){ + return exportplugins[formattype].call( + $canvas.get(0) // canvas dom elem + , $canvas.data(apinamespace+'.data') // raw signature data as array of objects of arrays + ) + } + } + // around since v1. Took only one arg - data-url-formatted string with (likely png of) signature image + , 'importData' : _setDrawingData + // was mistakenly introduced instead of 'importData' in v2 + , 'setData' : _setDrawingData + // this is one and same instance for all jSignature. + , 'globalEvents' : function(){return globalEvents} + // there will be a separate one for each jSignature instance. + , 'events' : function() { + return this.find('canvas.'+apinamespace) + .add(this.filter('canvas.'+apinamespace)) + .data(apinamespace+'.this').events + } + } // end of methods declaration. + + $.fn[apinamespace] = function(method) { + 'use strict' + if ( !method || typeof method === 'object' ) { + return methods.init.apply( this, arguments ) + } else if ( typeof method === 'string' && methods[method] ) { + return methods[method].apply( this, Array.prototype.slice.call( arguments, 1 )) + } else { + $.error( 'Method ' + String(method) + ' does not exist on jQuery.' + apinamespace ) + } + } + +} // end of GlobalJSignatureObjectInitializer + +GlobalJSignatureObjectInitializer(window) + +})(); \ No newline at end of file diff --git a/frappe/public/less/form.less b/frappe/public/less/form.less index 3440d6f4b5..e63cbe892c 100644 --- a/frappe/public/less/form.less +++ b/frappe/public/less/form.less @@ -608,18 +608,17 @@ h6.uppercase, .h6.uppercase { right: 12px; } .signature-reset { + z-index: 10; height: 30px; width: 30px; padding: 4px 0px; } .signature-img { - border: 1px solid @border-color; background: #fff; border-radius: 3px; margin-top: 5px; - width: 100%; - max-height: 300px; + max-height: 150px; } diff --git a/frappe/templates/print_formats/standard_macros.html b/frappe/templates/print_formats/standard_macros.html index 6fb0ba3da5..3cfa9dc016 100644 --- a/frappe/templates/print_formats/standard_macros.html +++ b/frappe/templates/print_formats/standard_macros.html @@ -108,7 +108,8 @@ data-fieldname="{{ df.fieldname }}" data-fieldtype="{{ df.fieldtype }}" class="img-responsive" {%- if df.print_width %} style="width: {{ get_width(df) }};"{% endif %}> {% elif df.fieldtype=="Signature" %} - + {% elif df.fieldtype in ("Attach", "Attach Image") and doc[df.fieldname] and (guess_mimetype(doc[df.fieldname])[0] or "").startswith("image/") %}