@ -0,0 +1,6 @@ |
|||||
|
{ |
||||
|
"name": "depositroy", |
||||
|
"version": "1.0.0", |
||||
|
"dependencies": { |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,4 @@ |
|||||
|
package com.dreamchaser.depository_manage.entity; |
||||
|
|
||||
|
public class ApplicationOutRecordMin { |
||||
|
} |
||||
|
After Width: | Height: | Size: 625 B |
|
After Width: | Height: | Size: 3.7 KiB |
@ -0,0 +1 @@ |
|||||
|
(function(e,t){function n(e,t){var n=e.createElement("p"),i=e.getElementsByTagName("head")[0]||e.documentElement;return n.innerHTML="x<style>"+t+"</style>",i.insertBefore(n.lastChild,i.firstChild)}function i(){var e=m.elements;return"string"==typeof e?e.split(" "):e}function r(e){var t={},n=e.createElement,r=e.createDocumentFragment,o=r();e.createElement=function(e){m.shivMethods||n(e);var i;return i=t[e]?t[e].cloneNode():g.test(e)?(t[e]=n(e)).cloneNode():n(e),i.canHaveChildren&&!f.test(e)?o.appendChild(i):i},e.createDocumentFragment=Function("h,f","return function(){var n=f.cloneNode(),c=n.createElement;h.shivMethods&&("+i().join().replace(/\w+/g,function(e){return t[e]=n(e),o.createElement(e),'c("'+e+'")'})+");return n}")(m,o)}function o(e){var t;return e.documentShived?e:(m.shivCSS&&!d&&(t=!!n(e,"article,aside,details,figcaption,figure,footer,header,hgroup,nav,section{display:block}audio{display:none}canvas,video{display:inline-block;*display:inline;*zoom:1}[hidden]{display:none}audio[controls]{display:inline-block;*display:inline;*zoom:1}mark{background:#FF0;color:#000}")),h||(t=!r(e)),t&&(e.documentShived=t),e)}function a(e){for(var t,n=e.getElementsByTagName("*"),r=n.length,o=RegExp("^(?:"+i().join("|")+")$","i"),a=[];r--;)t=n[r],o.test(t.nodeName)&&a.push(t.applyElement(s(t)));return a}function s(e){for(var t,n=e.attributes,i=n.length,r=e.ownerDocument.createElement(b+":"+e.nodeName);i--;)t=n[i],t.specified&&r.setAttribute(t.nodeName,t.nodeValue);return r.style.cssText=e.style.cssText,r}function l(e){for(var t,n=e.split("{"),r=n.length,o=RegExp("(^|[\\s,>+~])("+i().join("|")+")(?=[[\\s,>+~#.:]|$)","gi"),a="$1"+b+"\\:$2";r--;)t=n[r]=n[r].split("}"),t[t.length-1]=t[t.length-1].replace(o,a),n[r]=t.join("}");return n.join("{")}function c(e){for(var t=e.length;t--;)e[t].removeNode()}function u(e){var t,i,r=e.namespaces,o=e.parentWindow;return!y||e.printShived?e:(r[b]===void 0&&r.add(b),o.attachEvent("onbeforeprint",function(){for(var r,o,s,c=e.styleSheets,u=[],d=c.length,h=Array(d);d--;)h[d]=c[d];for(;s=h.pop();)if(!s.disabled&&v.test(s.media)){for(r=s.imports,d=0,o=r.length;o>d;d++)h.push(r[d]);try{u.push(s.cssText)}catch(p){}}u=l(u.reverse().join("")),i=a(e),t=n(e,u)}),o.attachEvent("onafterprint",function(){c(i),t.removeNode(!0)}),e.printShived=!0,e)}var d,h,p=e.html5||{},f=/^<|^(?:button|form|map|select|textarea|object|iframe)$/i,g=/^<|^(?:a|b|button|code|div|fieldset|form|h1|h2|h3|h4|h5|h6|i|iframe|img|input|label|li|link|ol|option|p|param|q|script|select|span|strong|style|table|tbody|td|textarea|tfoot|th|thead|tr|ul)$/i;(function(){var n=t.createElement("a");n.innerHTML="<xyz></xyz>",d="hidden"in n,d&&"function"==typeof injectElementWithStyles&&injectElementWithStyles("#modernizr{}",function(t){t.hidden=!0,d="none"==(e.getComputedStyle?getComputedStyle(t,null):t.currentStyle).display}),h=1==n.childNodes.length||function(){try{t.createElement("a")}catch(e){return!0}var n=t.createDocumentFragment();return n.cloneNode===void 0||n.createDocumentFragment===void 0||n.createElement===void 0}()})();var m={elements:p.elements||"abbr article aside audio bdi canvas data datalist details figcaption figure footer header hgroup mark meter nav output progress section summary time video",shivCSS:p.shivCSS!==!1,shivMethods:p.shivMethods!==!1,type:"default",shivDocument:o};e.html5=m,o(t);var v=/^$|\b(?:all|print)\b/,b="html5shiv",y=!h&&function(){var n=t.documentElement;return t.namespaces!==void 0&&t.parentWindow!==void 0&&n.applyElement!==void 0&&n.removeNode!==void 0&&e.attachEvent!==void 0}();m.type+=" print",m.shivPrint=u,u(t)})(this,document); |
||||
@ -0,0 +1,84 @@ |
|||||
|
(function(){var E;var g=window,n=document,p=function(a){var b=g._gaUserPrefs;if(b&&b.ioo&&b.ioo()||a&&!0===g["ga-disable-"+a])return!0;try{var c=g.external;if(c&&c._gaUserPrefs&&"oo"==c._gaUserPrefs)return!0}catch(f){}a=[];b=n.cookie.split(";");c=/^\s*AMP_TOKEN=\s*(.*?)\s*$/;for(var d=0;d<b.length;d++){var e=b[d].match(c);e&&a.push(e[1])}for(b=0;b<a.length;b++)if("$OPT_OUT"==decodeURIComponent(a[b]))return!0;return!1};var q=function(a){return encodeURIComponent?encodeURIComponent(a).replace(/\(/g,"%28").replace(/\)/g,"%29"):a},r=/^(www\.)?google(\.com?)?(\.[a-z]{2})?$/,u=/(^|\.)doubleclick\.net$/i;function Aa(a,b){switch(b){case 0:return""+a;case 1:return 1*a;case 2:return!!a;case 3:return 1E3*a}return a}function Ba(a){return"function"==typeof a}function Ca(a){return void 0!=a&&-1<(a.constructor+"").indexOf("String")}function F(a,b){return void 0==a||"-"==a&&!b||""==a}function Da(a){if(!a||""==a)return"";for(;a&&-1<" \n\r\t".indexOf(a.charAt(0));)a=a.substring(1);for(;a&&-1<" \n\r\t".indexOf(a.charAt(a.length-1));)a=a.substring(0,a.length-1);return a} |
||||
|
function Ea(){return Math.round(2147483647*Math.random())}function Fa(){}function G(a,b){if(encodeURIComponent instanceof Function)return b?encodeURI(a):encodeURIComponent(a);H(68);return escape(a)}function I(a){a=a.split("+").join(" ");if(decodeURIComponent instanceof Function)try{return decodeURIComponent(a)}catch(b){H(17)}else H(68);return unescape(a)}var Ga=function(a,b,c,d){a.addEventListener?a.addEventListener(b,c,!!d):a.attachEvent&&a.attachEvent("on"+b,c)}; |
||||
|
function Ia(a,b){if(a){var c=J.createElement("script");c.type="text/javascript";c.async=!0;c.src=a;c.id=b;a=J.getElementsByTagName("script")[0];a.parentNode.insertBefore(c,a);return c}}function K(a){return a&&0<a.length?a[0]:""}function L(a){var b=a?a.length:0;return 0<b?a[b-1]:""}var nf=function(){this.prefix="ga.";this.values={}};nf.prototype.set=function(a,b){this.values[this.prefix+a]=b};nf.prototype.get=function(a){return this.values[this.prefix+a]}; |
||||
|
nf.prototype.contains=function(a){return void 0!==this.get(a)};function Ka(a){0==a.indexOf("www.")&&(a=a.substring(4));return a.toLowerCase()} |
||||
|
function La(a,b){var c={url:a,protocol:"http",host:"",path:"",R:new nf,anchor:""};if(!a)return c;var d=a.indexOf("://");0<=d&&(c.protocol=a.substring(0,d),a=a.substring(d+3));d=a.search("/|\\?|#");if(0<=d)c.host=a.substring(0,d).toLowerCase(),a=a.substring(d);else return c.host=a.toLowerCase(),c;d=a.indexOf("#");0<=d&&(c.anchor=a.substring(d+1),a=a.substring(0,d));d=a.indexOf("?");0<=d&&(Na(c.R,a.substring(d+1)),a=a.substring(0,d));c.anchor&&b&&Na(c.R,c.anchor);a&&"/"==a.charAt(0)&&(a=a.substring(1)); |
||||
|
c.path=a;return c} |
||||
|
function Oa(a,b){function c(a){var b=(a.hostname||"").split(":")[0].toLowerCase(),c=(a.protocol||"").toLowerCase();c=1*a.port||("http:"==c?80:"https:"==c?443:"");a=a.pathname||"";0==a.indexOf("/")||(a="/"+a);return[b,""+c,a]}b=b||J.createElement("a");b.href=J.location.href;var d=(b.protocol||"").toLowerCase(),e=c(b),f=b.search||"",Be=d+"//"+e[0]+(e[1]?":"+e[1]:"");0==a.indexOf("//")?a=d+a:0==a.indexOf("/")?a=Be+a:a&&0!=a.indexOf("?")?0>a.split("/")[0].indexOf(":")&&(a=Be+e[2].substring(0,e[2].lastIndexOf("/"))+ |
||||
|
"/"+a):a=Be+e[2]+(a||f);b.href=a;d=c(b);return{protocol:(b.protocol||"").toLowerCase(),host:d[0],port:d[1],path:d[2],query:b.search||"",url:a||""}}function Na(a,b){function c(b,c){a.contains(b)||a.set(b,[]);a.get(b).push(c)}b=Da(b).split("&");for(var d=0;d<b.length;d++)if(b[d]){var e=b[d].indexOf("=");0>e?c(b[d],"1"):c(b[d].substring(0,e),b[d].substring(e+1))}} |
||||
|
function Pa(a,b){return F(a)||"["==a.charAt(0)&&"]"==a.charAt(a.length-1)?"-":a.indexOf(J.domain+(b&&"/"!=b?b:""))==(0==a.indexOf("http://")?7:0==a.indexOf("https://")?8:0)?"0":a};var Qa=0;function Ra(a,b,c){1<=Qa||1<=100*Math.random()||ld()||(a=["utmt=error","utmerr="+a,"utmwv=5.7.2","utmn="+Ea(),"utmsp=1"],b&&a.push("api="+b),c&&a.push("msg="+G(c.substring(0,100))),M.w&&a.push("aip=1"),Sa(a.join("&")),Qa++)};var Ta=0,Ua={};function N(a){return Va("x"+Ta++,a)}function Va(a,b){Ua[a]=!!b;return a} |
||||
|
var Wa=N(),Xa=Va("anonymizeIp"),Ya=N(),$a=N(),ab=N(),bb=N(),O=N(),P=N(),cb=N(),db=N(),eb=N(),fb=N(),gb=N(),hb=N(),ib=N(),jb=N(),kb=N(),lb=N(),nb=N(),ob=N(),pb=N(),qb=N(),rb=N(),sb=N(),tb=N(),ub=N(),vb=N(),wb=N(),xb=N(),yb=N(),zb=N(),Ab=N(),Bb=N(),Cb=N(),Db=N(),Eb=N(),Fb=N(!0),Gb=Va("currencyCode"),v=Va("storeGac"),Hb=Va("page"),Ib=Va("title"),Jb=N(),Kb=N(),Lb=N(),Mb=N(),Nb=N(),Ob=N(),Pb=N(),Qb=N(),Rb=N(),Q=N(!0),Sb=N(!0),Tb=N(!0),Ub=N(!0),Vb=N(!0),Wb=N(!0),Zb=N(!0),$b=N(!0),ac=N(!0),bc=N(!0),cc=N(!0), |
||||
|
R=N(!0),dc=N(!0),ec=N(!0),fc=N(!0),gc=N(!0),hc=N(!0),ic=N(!0),jc=N(!0),S=N(!0),kc=N(!0),lc=N(!0),mc=N(!0),nc=N(!0),oc=N(!0),pc=N(!0),qc=N(!0),rc=Va("campaignParams"),sc=N(),tc=Va("hitCallback"),uc=N();N();var vc=N(),wc=N(),xc=N(),yc=N(),zc=N(),Ac=N(),Bc=N(),Cc=N(),Dc=N(),Ec=N(),Fc=N(),Gc=N(),Hc=N(),Ic=N();N(); |
||||
|
var Mc=N(),Nc=N(),Yb=N(),Jc=N(),Kc=N(),Lc=Va("utmtCookieName"),Cd=Va("displayFeatures"),Oc=N(),of=Va("gtmid"),Oe=Va("uaName"),Pe=Va("uaDomain"),Qe=Va("uaPath"),pf=Va("linkid"),w=N(),x=N(),y=N(),z=N();var Re=function(){function a(a,c,d){T(qf.prototype,a,c,d)}a("_createTracker",qf.prototype.hb,55);a("_getTracker",qf.prototype.oa,0);a("_getTrackerByName",qf.prototype.u,51);a("_getTrackers",qf.prototype.pa,130);a("_anonymizeIp",qf.prototype.aa,16);a("_forceSSL",qf.prototype.la,125);a("_getPlugin",Pc,120)},Se=function(){function a(a,c,d){T(U.prototype,a,c,d)}Qc("_getName",$a,58);Qc("_getAccount",Wa,64);Qc("_visitCode",Q,54);Qc("_getClientInfo",ib,53,1);Qc("_getDetectTitle",lb,56,1);Qc("_getDetectFlash", |
||||
|
jb,65,1);Qc("_getLocalGifPath",wb,57);Qc("_getServiceMode",xb,59);V("_setClientInfo",ib,66,2);V("_setAccount",Wa,3);V("_setNamespace",Ya,48);V("_setAllowLinker",fb,11,2);V("_setDetectFlash",jb,61,2);V("_setDetectTitle",lb,62,2);V("_setLocalGifPath",wb,46,0);V("_setLocalServerMode",xb,92,void 0,0);V("_setRemoteServerMode",xb,63,void 0,1);V("_setLocalRemoteServerMode",xb,47,void 0,2);V("_setSampleRate",vb,45,1);V("_setCampaignTrack",kb,36,2);V("_setAllowAnchor",gb,7,2);V("_setCampNameKey",ob,41);V("_setCampContentKey", |
||||
|
tb,38);V("_setCampIdKey",nb,39);V("_setCampMediumKey",rb,40);V("_setCampNOKey",ub,42);V("_setCampSourceKey",qb,43);V("_setCampTermKey",sb,44);V("_setCampCIdKey",pb,37);V("_setCookiePath",P,9,0);V("_setMaxCustomVariables",yb,0,1);V("_setVisitorCookieTimeout",cb,28,1);V("_setSessionCookieTimeout",db,26,1);V("_setCampaignCookieTimeout",eb,29,1);V("_setReferrerOverride",Jb,49);V("_setSiteSpeedSampleRate",Dc,132);V("_storeGac",v,143);a("_trackPageview",U.prototype.Fa,1);a("_trackEvent",U.prototype.F,4); |
||||
|
a("_trackPageLoadTime",U.prototype.Ea,100);a("_trackSocial",U.prototype.Ga,104);a("_trackTrans",U.prototype.Ia,18);a("_sendXEvent",U.prototype.ib,78);a("_createEventTracker",U.prototype.ia,74);a("_getVersion",U.prototype.qa,60);a("_setDomainName",U.prototype.B,6);a("_setAllowHash",U.prototype.va,8);a("_getLinkerUrl",U.prototype.na,52);a("_link",U.prototype.link,101);a("_linkByPost",U.prototype.ua,102);a("_setTrans",U.prototype.za,20);a("_addTrans",U.prototype.$,21);a("_addItem",U.prototype.Y,19); |
||||
|
a("_clearTrans",U.prototype.ea,105);a("_setTransactionDelim",U.prototype.Aa,82);a("_setCustomVar",U.prototype.wa,10);a("_deleteCustomVar",U.prototype.ka,35);a("_getVisitorCustomVar",U.prototype.ra,50);a("_setXKey",U.prototype.Ca,83);a("_setXValue",U.prototype.Da,84);a("_getXKey",U.prototype.sa,76);a("_getXValue",U.prototype.ta,77);a("_clearXKey",U.prototype.fa,72);a("_clearXValue",U.prototype.ga,73);a("_createXObj",U.prototype.ja,75);a("_addIgnoredOrganic",U.prototype.W,15);a("_clearIgnoredOrganic", |
||||
|
U.prototype.ba,97);a("_addIgnoredRef",U.prototype.X,31);a("_clearIgnoredRef",U.prototype.ca,32);a("_addOrganic",U.prototype.Z,14);a("_clearOrganic",U.prototype.da,70);a("_cookiePathCopy",U.prototype.ha,30);a("_get",U.prototype.ma,106);a("_set",U.prototype.xa,107);a("_addEventListener",U.prototype.addEventListener,108);a("_removeEventListener",U.prototype.removeEventListener,109);a("_addDevId",U.prototype.V);a("_getPlugin",Pc,122);a("_setPageGroup",U.prototype.ya,126);a("_trackTiming",U.prototype.Ha, |
||||
|
124);a("_initData",U.prototype.initData,2);a("_setVar",U.prototype.Ba,22);V("_setSessionTimeout",db,27,3);V("_setCookieTimeout",eb,25,3);V("_setCookiePersistence",cb,24,1);a("_setAutoTrackOutbound",Fa,79);a("_setTrackOutboundSubdomains",Fa,81);a("_setHrefExamineLimit",Fa,80)};function Pc(a){var b=this.plugins_;if(b)return b.get(a)} |
||||
|
var T=function(a,b,c,d){a[b]=function(){try{return void 0!=d&&H(d),c.apply(this,arguments)}catch(e){throw Ra("exc",b,e&&e.name),e;}}},Qc=function(a,b,c,d){U.prototype[a]=function(){try{return H(c),Aa(this.a.get(b),d)}catch(e){throw Ra("exc",a,e&&e.name),e;}}},V=function(a,b,c,d,e){U.prototype[a]=function(f){try{H(c),void 0==e?this.a.set(b,Aa(f,d)):this.a.set(b,e)}catch(Be){throw Ra("exc",a,Be&&Be.name),Be;}}},Te=function(a,b){return{type:b,target:a,stopPropagation:function(){throw"aborted";}}};var Rc=new RegExp(/(^|\.)doubleclick\.net$/i),Sc=function(a,b){return Rc.test(J.location.hostname)?!0:"/"!==b?!1:0!=a.indexOf("www.google.")&&0!=a.indexOf(".google.")&&0!=a.indexOf("google.")||-1<a.indexOf("google.org")?!1:!0},Tc=function(a){var b=a.get(bb),c=a.c(P,"/");Sc(b,c)&&a.stopPropagation()};var Zc=function(){var a={},b={},c=new Uc;this.g=function(a,b){c.add(a,b)};var d=new Uc;this.v=function(a,b){d.add(a,b)};var e=!1,f=!1,Be=!0;this.T=function(){e=!0};this.j=function(a){this.load();this.set(sc,a,!0);a=new Vc(this);e=!1;d.cb(this);e=!0;b={};this.store();a.Ja()};this.load=function(){e&&(e=!1,this.Ka(),Wc(this),f||(f=!0,c.cb(this),Xc(this),Wc(this)),e=!0)};this.store=function(){e&&(f?(e=!1,Xc(this),e=!0):this.load())};this.get=function(c){Ua[c]&&this.load();return void 0!==b[c]?b[c]:a[c]}; |
||||
|
this.set=function(c,d,e){Ua[c]&&this.load();e?b[c]=d:a[c]=d;Ua[c]&&this.store()};this.Za=function(b){a[b]=this.b(b,0)+1};this.b=function(a,b){a=this.get(a);return void 0==a||""===a?b:1*a};this.c=function(a,b){a=this.get(a);return void 0==a?b:a+""};this.Ka=function(){if(Be){var b=this.c(bb,""),c=this.c(P,"/");Sc(b,c)||(a[O]=a[hb]&&""!=b?Yc(b):1,Be=!1)}}};Zc.prototype.stopPropagation=function(){throw"aborted";}; |
||||
|
var Vc=function(a){var b=this;this.fb=0;var c=a.get(tc);this.Ua=function(){0<b.fb&&c&&(b.fb--,b.fb||c())};this.Ja=function(){!b.fb&&c&&setTimeout(c,10)};a.set(uc,b,!0)};function $c(a,b){b=b||[];for(var c=0;c<b.length;c++){var d=b[c];if(""+a==d||0==d.indexOf(a+"."))return d}return"-"} |
||||
|
var bd=function(a,b,c){c=c?"":a.c(O,"1");b=b.split(".");if(6!==b.length||ad(b[0],c))return!1;c=1*b[1];var d=1*b[2],e=1*b[3],f=1*b[4];b=1*b[5];if(!(0<=c&&0<d&&0<e&&0<f&&0<=b))return!1;a.set(Q,c);a.set(Vb,d);a.set(Wb,e);a.set(Zb,f);a.set($b,b);return!0},cd=function(a){var b=a.get(Q),c=a.get(Vb),d=a.get(Wb),e=a.get(Zb),f=a.b($b,1);return[a.b(O,1),void 0!=b?b:"-",c||"-",d||"-",e||"-",f].join(".")},dd=function(a){return[a.b(O,1),a.b(cc,0),a.b(R,1),a.b(dc,0)].join(".")},ed=function(a,b,c){c=c?"":a.c(O, |
||||
|
"1");var d=b.split(".");if(4!==d.length||ad(d[0],c))d=null;a.set(cc,d?1*d[1]:0);a.set(R,d?1*d[2]:10);a.set(dc,d?1*d[3]:a.get(ab));return null!=d||!ad(b,c)},fd=function(a,b){var c=G(a.c(Tb,"")),d=[],e=a.get(Fb);if(!b&&e){for(b=0;b<e.length;b++){var f=e[b];f&&1==f.scope&&d.push(b+"="+G(f.name)+"="+G(f.value)+"=1")}0<d.length&&(c+="|"+d.join("^"))}return c?a.b(O,1)+"."+c:null},gd=function(a,b,c){c=c?"":a.c(O,"1");b=b.split(".");if(2>b.length||ad(b[0],c))return!1;b=b.slice(1).join(".").split("|");0<b.length&& |
||||
|
a.set(Tb,I(b[0]));if(1>=b.length)return!0;b=b[1].split(-1==b[1].indexOf(",")?"^":",");for(c=0;c<b.length;c++){var d=b[c].split("=");if(4==d.length){var e={};e.name=I(d[1]);e.value=I(d[2]);e.scope=1;a.get(Fb)[d[0]]=e}}return!0},hd=function(a,b){return(b=Ue(a,b))?[a.b(O,1),a.b(ec,0),a.b(fc,1),a.b(gc,1),b].join("."):""},Ue=function(a){function b(b,e){F(a.get(b))||(b=a.c(b,""),b=b.split(" ").join("%20"),b=b.split("+").join("%20"),c.push(e+"="+b))}var c=[];b(ic,"utmcid");b(nc,"utmcsr");b(S,"utmgclid"); |
||||
|
b(kc,"utmgclsrc");b(lc,"utmdclid");b(mc,"utmdsid");b(jc,"utmccn");b(oc,"utmcmd");b(pc,"utmctr");b(qc,"utmcct");return c.join("|")},id=function(a,b,c){c=c?"":a.c(O,"1");b=b.split(".");if(5>b.length||ad(b[0],c))return a.set(ec,void 0),a.set(fc,void 0),a.set(gc,void 0),a.set(ic,void 0),a.set(jc,void 0),a.set(nc,void 0),a.set(oc,void 0),a.set(pc,void 0),a.set(qc,void 0),a.set(S,void 0),a.set(kc,void 0),a.set(lc,void 0),a.set(mc,void 0),!1;a.set(ec,1*b[1]);a.set(fc,1*b[2]);a.set(gc,1*b[3]);Ve(a,b.slice(4).join(".")); |
||||
|
return!0},Ve=function(a,b){function c(a){return(a=b.match(a+"=(.*?)(?:\\|utm|$)"))&&2==a.length?a[1]:void 0}function d(b,c){c?(c=e?I(c):c.split("%20").join(" "),a.set(b,c)):a.set(b,void 0)}-1==b.indexOf("=")&&(b=I(b));var e="2"==c("utmcvr");d(ic,c("utmcid"));d(jc,c("utmccn"));d(nc,c("utmcsr"));d(oc,c("utmcmd"));d(pc,c("utmctr"));d(qc,c("utmcct"));d(S,c("utmgclid"));d(kc,c("utmgclsrc"));d(lc,c("utmdclid"));d(mc,c("utmdsid"))},ad=function(a,b){return b?a!=b:!/^\d+$/.test(a)};var Uc=function(){this.filters=[]};Uc.prototype.add=function(a,b){this.filters.push({name:a,s:b})};Uc.prototype.cb=function(a){try{for(var b=0;b<this.filters.length;b++)this.filters[b].s.call(W,a)}catch(c){}};function jd(a){100!=a.get(vb)&&a.get(Q)%1E4>=100*a.get(vb)&&a.stopPropagation()}function kd(a){ld(a.get(Wa))&&a.stopPropagation()}function md(a){"file:"==J.location.protocol&&a.stopPropagation()}function Ge(a){He()&&a.stopPropagation()} |
||||
|
function nd(a){a.get(Ib)||a.set(Ib,J.title,!0);a.get(Hb)||a.set(Hb,J.location.pathname+J.location.search,!0)}function lf(a){a.get(Wa)&&"UA-XXXXX-X"!=a.get(Wa)||a.stopPropagation()};var od=new function(){var a=[];this.set=function(b){a[b]=!0};this.encode=function(){for(var b=[],c=0;c<a.length;c++)a[c]&&(b[Math.floor(c/6)]^=1<<c%6);for(c=0;c<b.length;c++)b[c]="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_".charAt(b[c]||0);return b.join("")+"~"}};function H(a){od.set(a)};var W=window,J=document,ld=function(a){var b=W._gaUserPrefs;if(b&&b.ioo&&b.ioo()||a&&!0===W["ga-disable-"+a])return!0;try{var c=W.external;if(c&&c._gaUserPrefs&&"oo"==c._gaUserPrefs)return!0}catch(d){}return!1},He=function(){return W.navigator&&"preview"==W.navigator.loadPurpose},We=function(a,b){setTimeout(a,b)},pd=function(a){var b=[],c=J.cookie.split(";");a=new RegExp("^\\s*"+a+"=\\s*(.*?)\\s*$");for(var d=0;d<c.length;d++){var e=c[d].match(a);e&&b.push(e[1])}return b},X=function(a,b,c,d,e,f){e= |
||||
|
ld(e)?!1:Sc(d,c)?!1:He()?!1:!0;e&&((b=mf(b))&&2E3<b.length&&(b=b.substring(0,2E3),H(69)),a=a+"="+b+"; path="+c+"; ",f&&(a+="expires="+(new Date((new Date).getTime()+f)).toGMTString()+"; "),d&&(a+="domain="+d+";"),J.cookie=a)},mf=function(a){if(!a)return a;var b=a.indexOf(";");-1!=b&&(a=a.substring(0,b),H(141));if(!(0<=W.navigator.userAgent.indexOf("Firefox")))return a;a=a.replace(/\n|\r/g," ");b=0;for(var c=a.length;b<c;++b){var d=a.charCodeAt(b)&255;if(10==d||13==d)a=a.substring(0,b)+"?"+a.substring(b+ |
||||
|
1)}return a};var A,B=/^.*Version\/?(\d+)[^\d].*$/i;var qd,rd,sd=function(){if(!qd){var a={},b=W.navigator,c=W.screen;a.jb=c?c.width+"x"+c.height:"-";a.P=c?c.colorDepth+"-bit":"-";a.language=(b&&(b.language||b.browserLanguage)||"-").toLowerCase();a.javaEnabled=b&&b.javaEnabled()?1:0;a.characterSet=J.characterSet||J.charset||"-";try{var d=J.documentElement,e=J.body,f=e&&e.clientWidth&&e.clientHeight;b=[];d&&d.clientWidth&&d.clientHeight&&("CSS1Compat"===J.compatMode||!f)?b=[d.clientWidth,d.clientHeight]:f&&(b=[e.clientWidth,e.clientHeight]);var Be= |
||||
|
0>=b[0]||0>=b[1]?"":b.join("x");a.Wa=Be}catch(k){H(135)}qd=a}},td=function(){sd();var a=qd,b=W.navigator;a=b.appName+b.version+a.language+b.platform+b.userAgent+a.javaEnabled+a.jb+a.P+(J.cookie?J.cookie:"")+(J.referrer?J.referrer:"");b=a.length;for(var c=W.history.length;0<c;)a+=c--^b++;return Yc(a)},ud=function(a){sd();var b=qd;a.set(Lb,b.jb);a.set(Mb,b.P);a.set(Pb,b.language);a.set(Qb,b.characterSet);a.set(Nb,b.javaEnabled);a.set(Rb,b.Wa);if(a.get(ib)&&a.get(jb)){if(!(b=rd)){var c,d;var e="ShockwaveFlash"; |
||||
|
if((b=(b=W.navigator)?b.plugins:void 0)&&0<b.length)for(c=0;c<b.length&&!d;c++)e=b[c],-1<e.name.indexOf("Shockwave Flash")&&(d=e.description.split("Shockwave Flash ")[1]);else{e=e+"."+e;try{c=new ActiveXObject(e+".7"),d=c.GetVariable("$version")}catch(f){}if(!d)try{c=new ActiveXObject(e+".6"),d="WIN 6,0,21,0",c.AllowScriptAccess="always",d=c.GetVariable("$version")}catch(f){}if(!d)try{c=new ActiveXObject(e),d=c.GetVariable("$version")}catch(f){}d&&(d=d.split(" ")[1].split(","),d=d[0]+"."+d[1]+" r"+ |
||||
|
d[2])}b=d?d:"-"}rd=b;a.set(Ob,rd)}else a.set(Ob,"-")};var vd=function(a){if(Ba(a))this.s=a;else{var b=a[0],c=b.lastIndexOf(":"),d=b.lastIndexOf(".");this.h=this.i=this.l="";-1==c&&-1==d?this.h=b:-1==c&&-1!=d?(this.i=b.substring(0,d),this.h=b.substring(d+1)):-1!=c&&-1==d?(this.l=b.substring(0,c),this.h=b.substring(c+1)):c>d?(this.i=b.substring(0,d),this.l=b.substring(d+1,c),this.h=b.substring(c+1)):(this.i=b.substring(0,d),this.h=b.substring(d+1));this.Xa=a.slice(1);this.Ma=!this.l&&"_require"==this.h;this.J=!this.i&&!this.l&&"_provide"==this.h}},Y=function(){T(Y.prototype, |
||||
|
"push",Y.prototype.push,5);T(Y.prototype,"_getPlugin",Pc,121);T(Y.prototype,"_createAsyncTracker",Y.prototype.Sa,33);T(Y.prototype,"_getAsyncTracker",Y.prototype.Ta,34);this.I=new nf;this.eb=[]};E=Y.prototype;E.Na=function(a,b,c){var d=this.I.get(a);if(!Ba(d))return!1;b.plugins_=b.plugins_||new nf;b.plugins_.set(a,new d(b,c||{}));return!0};E.push=function(a){var b=Z.Va.apply(this,arguments);b=Z.eb.concat(b);for(Z.eb=[];0<b.length&&!Z.O(b[0])&&!(b.shift(),0<Z.eb.length););Z.eb=Z.eb.concat(b);return 0}; |
||||
|
E.Va=function(a){for(var b=[],c=0;c<arguments.length;c++)try{var d=new vd(arguments[c]);d.J?this.O(d):b.push(d)}catch(e){}return b}; |
||||
|
E.O=function(a){try{if(a.s)a.s.apply(W);else if(a.J)this.I.set(a.Xa[0],a.Xa[1]);else{var b="_gat"==a.i?M:"_gaq"==a.i?Z:M.u(a.i);if(a.Ma){if(!this.Na(a.Xa[0],b,a.Xa[2])){if(!a.Pa){var c=Oa(""+a.Xa[1]);var d=c.protocol,e=J.location.protocol;var f;if(f="https:"==d||d==e?!0:"http:"!=d?!1:"http:"==e)a:{var Be=Oa(J.location.href);if(!(c.query||0<=c.url.indexOf("?")||0<=c.path.indexOf("://")||c.host==Be.host&&c.port==Be.port)){var k="http:"==c.protocol?80:443,Ja=M.S;for(b=0;b<Ja.length;b++)if(c.host==Ja[b][0]&& |
||||
|
(c.port||k)==(Ja[b][1]||k)&&0==c.path.indexOf(Ja[b][2])){f=!0;break a}}f=!1}f&&!ld()&&(a.Pa=Ia(c.url))}return!0}}else a.l&&(b=b.plugins_.get(a.l)),b[a.h].apply(b,a.Xa)}}catch(t){}};E.Sa=function(a,b){return M.hb(a,b||"")};E.Ta=function(a){return M.u(a)};var yd=function(){function a(a,b,c,d){void 0==f[a]&&(f[a]={});void 0==f[a][b]&&(f[a][b]=[]);f[a][b][c]=d}function b(a,b,c){if(void 0!=f[a]&&void 0!=f[a][b])return f[a][b][c]}function c(a,b){if(void 0!=f[a]&&void 0!=f[a][b]){f[a][b]=void 0;b=!0;var c;for(c=0;c<Be.length;c++)if(void 0!=f[a][Be[c]]){b=!1;break}b&&(f[a]=void 0)}}function d(a){var b="",c=!1,d;for(d=0;d<Be.length;d++){var e=a[Be[d]];if(void 0!=e){c&&(b+=Be[d]);var f=e,Ja=[];for(e=0;e<f.length;e++)if(void 0!=f[e]){c="";1!=e&&void 0==f[e- |
||||
|
1]&&(c+=e.toString()+"!");var fa,Ke=f[e],Le="";for(fa=0;fa<Ke.length;fa++){var Me=Ke.charAt(fa);var m=k[Me];Le+=void 0!=m?m:Me}c+=Le;Ja.push(c)}b+="("+Ja.join("*")+")";c=!1}else c=!0}return b}var e=this,f=[],Be=["k","v"],k={"'":"'0",")":"'1","*":"'2","!":"'3"};e.Ra=function(a){return void 0!=f[a]};e.A=function(){for(var a="",b=0;b<f.length;b++)void 0!=f[b]&&(a+=b.toString()+d(f[b]));return a};e.Qa=function(a){if(void 0==a)return e.A();for(var b=a.A(),c=0;c<f.length;c++)void 0==f[c]||a.Ra(c)||(b+= |
||||
|
c.toString()+d(f[c]));return b};e.f=function(b,c,d){if(!wd(d))return!1;a(b,"k",c,d);return!0};e.o=function(b,c,d){if(!xd(d))return!1;a(b,"v",c,d.toString());return!0};e.getKey=function(a,c){return b(a,"k",c)};e.N=function(a,c){return b(a,"v",c)};e.L=function(a){c(a,"k")};e.M=function(a){c(a,"v")};T(e,"_setKey",e.f,89);T(e,"_setValue",e.o,90);T(e,"_getKey",e.getKey,87);T(e,"_getValue",e.N,88);T(e,"_clearKey",e.L,85);T(e,"_clearValue",e.M,86)};function wd(a){return"string"==typeof a} |
||||
|
function xd(a){return!("number"==typeof a||void 0!=Number&&a instanceof Number)||Math.round(a)!=a||isNaN(a)||Infinity==a?!1:!0};var zd=function(a){var b=W.gaGlobal;a&&!b&&(W.gaGlobal=b={});return b},Ad=function(){var a=zd(!0).hid;null==a&&(a=Ea(),zd(!0).hid=a);return a},Dd=function(a){a.set(Kb,Ad());var b=zd();if(b&&b.dh==a.get(O)){var c=b.sid;c&&(a.get(ac)?H(112):H(132),a.set(Zb,c),a.get(Sb)&&a.set(Wb,c));b=b.vid;a.get(Sb)&&b&&(b=b.split("."),a.set(Q,1*b[0]),a.set(Vb,1*b[1]))}};var Ed,Fd=function(a,b,c,d){var e=a.c(bb,""),f=a.c(P,"/");d=void 0!=d?d:a.b(cb,0);a=a.c(Wa,"");X(b,c,f,e,a,d)},Xc=function(a){var b=a.c(bb,""),c=a.c(P,"/"),d=a.c(Wa,"");X("__utma",cd(a),c,b,d,a.get(cb));X("__utmb",dd(a),c,b,d,a.get(db));X("__utmc",""+a.b(O,1),c,b,d);var e=hd(a,!0);e?X("__utmz",e,c,b,d,a.get(eb)):X("__utmz","",c,b,"",-1);(e=fd(a,!1))?X("__utmv",e,c,b,d,a.get(cb)):X("__utmv","",c,b,"",-1);if(1==a.get(v)&&(e=a.get(w))){var f=a.get(x);b=a.c(bb,"");c=a.c(P,"/");d=a.c(Wa,"");var Be=a.b(y, |
||||
|
0);a=Math.min(a.b(cb,7776E6),a.b(eb,7776E6),7776E6);a=Math.min(a,1E3*Be+a-(new Date).getTime());if(!f||"aw.ds"==f)if(f=["1",Be+"",q(e)].join("."),0<a&&(e="_gac_"+q(d),!(p(d)||u.test(J.location.hostname)||"/"==c&&r.test(b))&&((d=f)&&1200<d.length&&(d=d.substring(0,1200)),c=e+"="+d+"; path="+c+"; ",a&&(c+="expires="+(new Date((new Date).getTime()+a)).toGMTString()+"; "),b&&"none"!==b&&(c+="domain="+b+";"),b=J.cookie,J.cookie=c,b==J.cookie)))for(b=[],c=J.cookie.split(";"),a=new RegExp("^\\s*"+e+"=\\s*(.*?)\\s*$"), |
||||
|
d=0;d<c.length;d++)(e=c[d].match(a))&&b.push(e[1])}},Wc=function(a){var b=a.b(O,1);if(!bd(a,$c(b,pd("__utma"))))return a.set(Ub,!0),!1;var c=!ed(a,$c(b,pd("__utmb")));a.set(bc,c);id(a,$c(b,pd("__utmz")));gd(a,$c(b,pd("__utmv")));if(1==a.get(v)){b=a.get(w);var d=a.get(x);if(!b||d&&"aw.ds"!=d){if(J){b=[];d=J.cookie.split(";");for(var e=/^\s*_gac_(UA-\d+-\d+)=\s*(.+?)\s*$/,f=0;f<d.length;f++){var Be=d[f].match(e);Be&&b.push({Oa:Be[1],value:Be[2]})}d={};if(b&&b.length)for(e=0;e<b.length;e++)f=b[e].value.split("."), |
||||
|
"1"==f[0]&&3==f.length&&f[1]&&(d[b[e].Oa]||(d[b[e].Oa]=[]),d[b[e].Oa].push({timestamp:f[1],kb:f[2]}));b=d}else b={};(b=b[a.get(Wa)])&&0<b.length&&(b=b[0],a.set(y,b.timestamp),a.set(w,b.kb),a.set(x,void 0))}}Ed=!c;return!0},Gd=function(a){Ed||0<pd("__utmb").length||(X("__utmd","1",a.c(P,"/"),a.c(bb,""),a.c(Wa,""),1E4),0==pd("__utmd").length&&a.stopPropagation())};var h=0,Jd=function(a){void 0==a.get(Q)?Hd(a):a.get(Ub)&&!a.get(Mc)?Hd(a):a.get(bc)&&Id(a)},Kd=function(a){a.get(hc)&&!a.get(ac)&&(Id(a),a.set(fc,a.get($b)))},Hd=function(a){h++;1<h&&H(137);var b=a.get(ab);a.set(Sb,!0);a.set(Q,Ea()^td(a)&2147483647);a.set(Tb,"");a.set(Vb,b);a.set(Wb,b);a.set(Zb,b);a.set($b,1);a.set(ac,!0);a.set(cc,0);a.set(R,10);a.set(dc,b);a.set(Fb,[]);a.set(Ub,!1);a.set(bc,!1)},Id=function(a){h++;1<h&&H(137);a.set(Wb,a.get(Zb));a.set(Zb,a.get(ab));a.Za($b);a.set(ac,!0);a.set(cc, |
||||
|
0);a.set(R,10);a.set(dc,a.get(ab));a.set(bc,!1)};var Ld="daum:q eniro:search_word naver:query pchome:q images.google:q google:q yahoo:p yahoo:q msn:q bing:q aol:query aol:q lycos:q lycos:query ask:q cnn:query virgilio:qs baidu:wd baidu:word alice:qs yandex:text najdi:q seznam:q rakuten:qt biglobe:q goo.ne:MT search.smt.docomo:MT onet:qt onet:q kvasir:q terra:query rambler:query conduit:q babylon:q search-results:q avg:q comcast:q incredimail:q startsiden:q go.mail.ru:q centrum.cz:q 360.cn:q sogou:query tut.by:query globo:q ukr:q so.com:q haosou.com:q auone:q".split(" "), |
||||
|
Sd=function(a){if(a.get(kb)&&!a.get(Mc)){var b=!F(a.get(ic))||!F(a.get(nc))||!F(a.get(S))||!F(a.get(lc));for(var c={},d=0;d<Md.length;d++){var e=Md[d];c[e]=a.get(e)}(d=a.get(rc))?(H(149),e=new nf,Na(e,d),d=e):d=La(J.location.href,a.get(gb)).R;if("1"!=L(d.get(a.get(ub)))||!b)if(d=Xe(a,d)||Qd(a),d||b||!a.get(ac)||(Pd(a,void 0,"(direct)",void 0,void 0,void 0,"(direct)","(none)",void 0,void 0),d=!0),d&&(a.set(hc,Rd(a,c)),b="(direct)"==a.get(nc)&&"(direct)"==a.get(jc)&&"(none)"==a.get(oc),a.get(hc)||a.get(ac)&& |
||||
|
!b))a.set(ec,a.get(ab)),a.set(fc,a.get($b)),a.Za(gc)}},Xe=function(a,b){function c(c,d){d=d||"-";return(c=L(b.get(a.get(c))))&&"-"!=c?I(c):d}var d=L(b.get(a.get(nb)))||"-",e=L(b.get(a.get(qb)))||"-",f=L(b.get(a.get(pb)))||"-",Be=L(b.get("gclsrc"))||"-",k=L(b.get("dclid"))||"-";"-"!=f&&a.set(w,f);"-"!=Be&&a.set(x,Be);var Ja=c(ob,"(not set)"),t=c(rb,"(not set)"),Za=c(sb),Ma=c(tb);if(F(d)&&F(f)&&F(k)&&F(e))return!1;var mb=!F(f)&&!F(Be);mb=F(e)&&(!F(k)||mb);var Xb=F(Za);if(mb||Xb){var Bd=Nd(a);Bd=La(Bd, |
||||
|
!0);(Bd=Od(a,Bd))&&!F(Bd[1]&&!Bd[2])&&(mb&&(e=Bd[0]),Xb&&(Za=Bd[1]))}Pd(a,d,e,f,Be,k,Ja,t,Za,Ma);return!0},Qd=function(a){var b=Nd(a),c=La(b,!0);(b=!(void 0!=b&&null!=b&&""!=b&&"0"!=b&&"-"!=b&&0<=b.indexOf("://")))||(b=c&&-1<c.host.indexOf("google")&&c.R.contains("q")&&"cse"==c.path);if(b)return!1;if((b=Od(a,c))&&!b[2])return Pd(a,void 0,b[0],void 0,void 0,void 0,"(organic)","organic",b[1],void 0),!0;if(b||!a.get(ac))return!1;a:{b=a.get(Bb);for(var d=Ka(c.host),e=0;e<b.length;++e)if(-1<d.indexOf(b[e])){a= |
||||
|
!1;break a}Pd(a,void 0,d,void 0,void 0,void 0,"(referral)","referral",void 0,"/"+c.path);a=!0}return a},Od=function(a,b){for(var c=a.get(zb),d=0;d<c.length;++d){var e=c[d].split(":");if(-1<b.host.indexOf(e[0].toLowerCase())){var f=b.R.get(e[1]);if(f&&(f=K(f),!f&&-1<b.host.indexOf("google.")&&(f="(not provided)"),!e[3]||-1<b.url.indexOf(e[3]))){f||H(151);a:{b=f;a=a.get(Ab);b=I(b).toLowerCase();for(c=0;c<a.length;++c)if(b==a[c]){a=!0;break a}a=!1}return[e[2]||e[0],f,a]}}}return null},Pd=function(a, |
||||
|
b,c,d,e,f,Be,k,Ja,t){a.set(ic,b);a.set(nc,c);a.set(S,d);a.set(kc,e);a.set(lc,f);a.set(jc,Be);a.set(oc,k);a.set(pc,Ja);a.set(qc,t)},Md=[jc,ic,S,lc,nc,oc,pc,qc],Rd=function(a,b){function c(a){a=(""+a).split("+").join("%20");return a=a.split(" ").join("%20")}function d(c){var d=""+(a.get(c)||"");c=""+(b[c]||"");return 0<d.length&&d==c}if(d(S)||d(lc))return H(131),!1;for(var e=0;e<Md.length;e++){var f=Md[e],Be=b[f]||"-";f=a.get(f)||"-";if(c(Be)!=c(f))return!0}return!1},Td=new RegExp(/^https?:\/\/(www\.)?google(\.com?)?(\.[a-z]{2}t?)?\/?$/i), |
||||
|
jf=/^https?:\/\/(r\.)?search\.yahoo\.com?(\.jp)?\/?[^?]*$/i,rf=/^https?:\/\/(www\.)?bing\.com\/?$/i,Nd=function(a){a=Pa(a.get(Jb),a.get(P));try{if(Td.test(a))return H(136),a+"?q=";if(jf.test(a))return H(150),a+"?p=(not provided)";if(rf.test(a))return a+"?q=(not provided)"}catch(b){H(145)}return a};var Ud,Vd,Wd=function(a){Ud=a.c(S,"");Vd=a.c(kc,"")},Xd=function(a){var b=a.c(S,""),c=a.c(kc,"");b!=Ud&&(-1<c.indexOf("ds")?a.set(mc,void 0):!F(Ud)&&-1<Vd.indexOf("ds")&&a.set(mc,Ud))};var Zd=function(a){Yd(a,J.location.href)?(a.set(Mc,!0),H(12)):a.set(Mc,!1)},Yd=function(a,b){if(!a.get(fb))return!1;var c=La(b,a.get(gb));b=K(c.R.get("__utma"));var d=K(c.R.get("__utmb")),e=K(c.R.get("__utmc")),f=K(c.R.get("__utmx")),Be=K(c.R.get("__utmz")),k=K(c.R.get("__utmv"));c=K(c.R.get("__utmk"));if(Yc(""+b+d+e+f+Be+k)!=c){b=I(b);d=I(d);e=I(e);f=I(f);e=$d(b+d+e+f,Be,k,c);if(!e)return!1;Be=e[0];k=e[1]}if(!bd(a,b,!0))return!1;ed(a,d,!0);id(a,Be,!0);gd(a,k,!0);ae(a,f,!0);return!0},ce=function(a, |
||||
|
b,c){var d=cd(a)||"-";var e=dd(a)||"-",f=""+a.b(O,1)||"-",Be=be(a)||"-",k=hd(a,!1)||"-";a=fd(a,!1)||"-";var Ja=Yc(""+d+e+f+Be+k+a),t=[];t.push("__utma="+d);t.push("__utmb="+e);t.push("__utmc="+f);t.push("__utmx="+Be);t.push("__utmz="+k);t.push("__utmv="+a);t.push("__utmk="+Ja);d=t.join("&");if(!d)return b;e=b.indexOf("#");if(c)return 0>e?b+"#"+d:b+"&"+d;c="";0<e&&(c=b.substring(e),b=b.substring(0,e));return 0>b.indexOf("?")?b+"?"+d+c:b+"&"+d+c},$d=function(a,b,c,d){for(var e=0;3>e;e++){for(var f= |
||||
|
0;3>f;f++){if(d==Yc(a+b+c))return H(127),[b,c];var Be=b.replace(/ /g,"%20"),k=c.replace(/ /g,"%20");if(d==Yc(a+Be+k))return H(128),[Be,k];Be=Be.replace(/\+/g,"%20");k=k.replace(/\+/g,"%20");if(d==Yc(a+Be+k))return H(129),[Be,k];try{var Ja=b.match("utmctr=(.*?)(?:\\|utm|$)");if(Ja&&2==Ja.length&&(Be=b.replace(Ja[1],G(I(Ja[1]))),d==Yc(a+Be+c)))return H(139),[Be,c]}catch(t){}b=I(b)}c=I(c)}};var de="|",fe=function(a,b,c,d,e,f,Be,k,Ja){var t=ee(a,b);t||(t={},a.get(Cb).push(t));t.id_=b;t.affiliation_=c;t.total_=d;t.tax_=e;t.shipping_=f;t.city_=Be;t.state_=k;t.country_=Ja;t.items_=t.items_||[];return t},ge=function(a,b,c,d,e,f,Be){a=ee(a,b)||fe(a,b,"",0,0,0,"","","");a:{if(a&&a.items_){var k=a.items_;for(var Ja=0;Ja<k.length;Ja++)if(k[Ja].sku_==c){k=k[Ja];break a}}k=null}Ja=k||{};Ja.transId_=b;Ja.sku_=c;Ja.name_=d;Ja.category_=e;Ja.price_=f;Ja.quantity_=Be;k||a.items_.push(Ja);return Ja}, |
||||
|
ee=function(a,b){a=a.get(Cb);for(var c=0;c<a.length;c++)if(a[c].id_==b)return a[c];return null};var he,ie=function(a){if(!he){var b=J.location.hash;var c=W.name,d=/^#?gaso=([^&]*)/;if(c=(b=(b=b&&b.match(d)||c&&c.match(d))?b[1]:K(pd("GASO")))&&b.match(/^(?:!([-0-9a-z.]{1,40})!)?([-.\w]{10,1200})$/i))Fd(a,"GASO",""+b,0),M._gasoDomain=a.get(bb),M._gasoCPath=a.get(P),a=c[1],Ia("https://www.google.com/analytics/web/inpage/pub/inpage.js?"+(a?"prefix="+a+"&":"")+Ea(),"_gasojs");he=!0}};var ae=function(a,b,c){c&&(b=I(b));c=a.b(O,1);b=b.split(".");2>b.length||!/^\d+$/.test(b[0])||(b[0]=""+c,Fd(a,"__utmx",b.join("."),void 0))},be=function(a,b){a=$c(a.get(O),pd("__utmx"));"-"==a&&(a="");return b?G(a):a},Ye=function(a){try{var b=La(J.location.href,!1),c=decodeURIComponent(L(b.R.get("utm_referrer")))||"";c&&a.set(Jb,c);var d=decodeURIComponent(K(b.R.get("utm_expid")))||"";d&&(d=d.split(".")[0],a.set(Oc,""+d))}catch(e){H(146)}},l=function(a){var b=W.gaData&&W.gaData.expId;b&&a.set(Oc, |
||||
|
""+b)};var ke=function(a,b){var c=Math.min(a.b(Dc,0),100);if(a.b(Q,0)%100>=c)return!1;c=Ze()||$e();if(void 0==c)return!1;var d=c[0];if(void 0==d||Infinity==d||isNaN(d))return!1;0<d?af(c)?b(je(c)):b(je(c.slice(0,1))):Ga(W,"load",function(){ke(a,b)},!1);return!0},me=function(a,b,c,d){var e=new yd;e.f(14,90,b.substring(0,500));e.f(14,91,a.substring(0,150));e.f(14,92,""+le(c));void 0!=d&&e.f(14,93,d.substring(0,500));e.o(14,90,c);return e},af=function(a){for(var b=1;b<a.length;b++)if(isNaN(a[b])||Infinity== |
||||
|
a[b]||0>a[b])return!1;return!0},le=function(a){return isNaN(a)||0>a?0:5E3>a?10*Math.floor(a/10):5E4>a?100*Math.floor(a/100):41E5>a?1E3*Math.floor(a/1E3):41E5},je=function(a){for(var b=new yd,c=0;c<a.length;c++)b.f(14,c+1,""+le(a[c])),b.o(14,c+1,a[c]);return b},Ze=function(){var a=W.performance||W.webkitPerformance;if(a=a&&a.timing){var b=a.navigationStart;if(0==b)H(133);else return[a.loadEventStart-b,a.domainLookupEnd-a.domainLookupStart,a.connectEnd-a.connectStart,a.responseStart-a.requestStart, |
||||
|
a.responseEnd-a.responseStart,a.fetchStart-b,a.domInteractive-b,a.domContentLoadedEventStart-b]}},$e=function(){if(W.top==W){var a=W.external,b=a&&a.onloadT;a&&!a.isValidLoadTime&&(b=void 0);2147483648<b&&(b=void 0);0<b&&a.setPageReadyTime();if(void 0!=b)return[b]}};var cf=function(a){if(a.get(Sb))try{a:{var b=pd(a.get(Oe)||"_ga");if(b&&!(1>b.length)){for(var c=[],d=0;d<b.length;d++){var e=b[d].split("."),f=e.shift();if(("GA1"==f||"1"==f)&&1<e.length){var Be=e.shift().split("-");1==Be.length&&(Be[1]="1");Be[0]*=1;Be[1]*=1;var k={Ya:Be,$a:e.join(".")}}else k=void 0;k&&c.push(k)}if(1==c.length){var Ja=c[0].$a;break a}if(0!=c.length){var t=a.get(Pe)||a.get(bb);c=bf(c,(0==t.indexOf(".")?t.substr(1):t).split(".").length,0);if(1==c.length){Ja=c[0].$a;break a}var Za= |
||||
|
a.get(Qe)||a.get(P);(b=Za)?(1<b.length&&"/"==b.charAt(b.length-1)&&(b=b.substr(0,b.length-1)),0!=b.indexOf("/")&&(b="/"+b),Za=b):Za="/";c=bf(c,"/"==Za?1:Za.split("/").length,1);Ja=c[0].$a;break a}}Ja=void 0}if(Ja){var Ma=(""+Ja).split(".");2==Ma.length&&/[0-9.]/.test(Ma)&&(H(114),a.set(Q,Ma[0]),a.set(Vb,Ma[1]),a.set(Sb,!1))}}catch(mb){H(115)}},bf=function(a,b,c){for(var d=[],e=[],f=128,Be=0;Be<a.length;Be++){var k=a[Be];k.Ya[c]==b?d.push(k):k.Ya[c]==f?e.push(k):k.Ya[c]<f&&(e=[k],f=k.Ya[c])}return 0< |
||||
|
d.length?d:e};var kf=/^gtm\d+$/,hf=function(a){var b=!!a.b(Cd,1);if(b)if(H(140),"page"!=a.get(sc))a.set(Kc,"",!0);else if(b=a.c(Lc,""),b||(b=(b=a.c($a,""))&&"~0"!=b?kf.test(b)?"__utmt_"+G(a.c(Wa,"")):"__utmt_"+G(b):"__utmt"),0<pd(b).length)a.set(Kc,"",!0);else if(X(b,"1",a.c(P,"/"),a.c(bb,""),a.c(Wa,""),6E5),0<pd(b).length){a.set(Kc,Ea(),!0);a.set(Yb,1,!0);if(void 0!==W.__ga4__)b=W.__ga4__;else{if(void 0===A){var c=W.navigator.userAgent;if(c){b=c;try{b=decodeURIComponent(c)}catch(d){}if(c=!(0<=b.indexOf("Chrome"))&& |
||||
|
!(0<=b.indexOf("CriOS"))&&(0<=b.indexOf("Safari/")||0<=b.indexOf("Safari,")))b=B.exec(b),c=11<=(b?Number(b[1]):-1);A=c}else A=!1}b=A}b?(a.set(z,C(a),!0),a.set(Jc,"https://ssl.google-analytics.com/j/__utm.gif",!0)):a.set(Jc,Ne()+"/r/__utm.gif?",!0)}},C=function(a){a=aa(a);return{gb:"t=dc&_r=3&"+a,google:"t=sr&slf_rd=1&_r=4&"+a,count:0}},aa=function(a){function b(a,b){c.push(a+"="+G(b))}var c=[];b("v","1");b("_v","5.7.2");b("tid",a.get(Wa));b("cid",a.get(Q)+"."+a.get(Vb));b("jid",a.get(Kc));b("aip", |
||||
|
"1");return c.join("&")+"&z="+Ea()};var U=function(a,b,c){function d(a){return function(b){if((b=b.get(Nc)[a])&&b.length)for(var c=Te(e,a),d=0;d<b.length;d++)b[d].call(e,c)}}var e=this;this.a=new Zc;this.get=function(a){return this.a.get(a)};this.set=function(a,b,c){this.a.set(a,b,c)};this.set(Wa,b||"UA-XXXXX-X");this.set($a,a||"");this.set(Ya,c||"");this.set(ab,Math.round((new Date).getTime()/1E3));this.set(P,"/");this.set(cb,63072E6);this.set(eb,15768E6);this.set(db,18E5);this.set(fb,!1);this.set(yb,50);this.set(gb,!1);this.set(hb, |
||||
|
!0);this.set(ib,!0);this.set(jb,!0);this.set(kb,!0);this.set(lb,!0);this.set(ob,"utm_campaign");this.set(nb,"utm_id");this.set(pb,"gclid");this.set(qb,"utm_source");this.set(rb,"utm_medium");this.set(sb,"utm_term");this.set(tb,"utm_content");this.set(ub,"utm_nooverride");this.set(vb,100);this.set(Dc,1);this.set(Ec,!1);this.set(wb,"/__utm.gif");this.set(xb,1);this.set(Cb,[]);this.set(Fb,[]);this.set(zb,Ld.slice(0));this.set(Ab,[]);this.set(Bb,[]);this.B("auto");this.set(Jb,J.referrer);this.set(v,!0); |
||||
|
this.set(y,Math.round((new Date).getTime()/1E3));Ye(this.a);this.set(Nc,{hit:[],load:[]});this.a.g("0",Zd);this.a.g("1",Wd);this.a.g("2",Jd);this.a.g("3",cf);this.a.g("4",Sd);this.a.g("5",Xd);this.a.g("6",Kd);this.a.g("7",d("load"));this.a.g("8",ie);this.a.v("A",kd);this.a.v("B",md);this.a.v("C",Ge);this.a.v("D",Jd);this.a.v("E",jd);this.a.v("F",Tc);this.a.v("G",ne);this.a.v("H",lf);this.a.v("I",Gd);this.a.v("J",nd);this.a.v("K",ud);this.a.v("L",Dd);this.a.v("M",l);this.a.v("N",hf);this.a.v("O",d("hit")); |
||||
|
this.a.v("P",oe);this.a.v("Q",pe);0===this.get(ab)&&H(111);this.a.T();this.H=void 0};E=U.prototype;E.m=function(){var a=this.get(Db);a||(a=new yd,this.set(Db,a));return a};E.La=function(a){for(var b in a){var c=a[b];a.hasOwnProperty(b)&&this.set(b,c,!0)}};E.K=function(a){if(this.get(Ec))return!1;var b=this,c=ke(this.a,function(c){b.set(Hb,a,!0);b.ib(c)});this.set(Ec,c);return c}; |
||||
|
E.Fa=function(a){a&&Ca(a)?(H(13),this.set(Hb,a,!0)):"object"===typeof a&&null!==a&&this.La(a);this.H=a=this.get(Hb);this.a.j("page");this.K(a)};E.F=function(a,b,c,d,e){if(""==a||!wd(a)||""==b||!wd(b)||void 0!=c&&!wd(c)||void 0!=d&&!xd(d))return!1;this.set(wc,a,!0);this.set(xc,b,!0);this.set(yc,c,!0);this.set(zc,d,!0);this.set(vc,!!e,!0);this.a.j("event");return!0}; |
||||
|
E.Ha=function(a,b,c,d,e){var f=this.a.b(Dc,0);1*e===e&&(f=e);if(this.a.b(Q,0)%100>=f)return!1;c=1*(""+c);if(""==a||!wd(a)||""==b||!wd(b)||!xd(c)||isNaN(c)||0>c||0>f||100<f||void 0!=d&&(""==d||!wd(d)))return!1;this.ib(me(a,b,c,d));return!0};E.Ga=function(a,b,c,d){if(!a||!b)return!1;this.set(Ac,a,!0);this.set(Bc,b,!0);this.set(Cc,c||J.location.href,!0);d&&this.set(Hb,d,!0);this.a.j("social");return!0};E.Ea=function(){this.set(Dc,10);this.K(this.H)};E.Ia=function(){this.a.j("trans")}; |
||||
|
E.ib=function(a){this.set(Eb,a,!0);this.a.j("event")};E.ia=function(a){this.initData();var b=this;return{_trackEvent:function(c,d,e){H(91);b.F(a,c,d,e)}}};E.ma=function(a){return this.get(a)};E.xa=function(a,b){if(a)if(Ca(a))this.set(a,b);else if("object"==typeof a)for(var c in a)a.hasOwnProperty(c)&&this.set(c,a[c])};E.addEventListener=function(a,b){(a=this.get(Nc)[a])&&a.push(b)};E.removeEventListener=function(a,b){a=this.get(Nc)[a];for(var c=0;a&&c<a.length;c++)if(a[c]==b){a.splice(c,1);break}}; |
||||
|
E.qa=function(){return"5.7.2"};E.B=function(a){this.get(hb);a="auto"==a?Ka(J.domain):a&&"-"!=a&&"none"!=a?a.toLowerCase():"";this.set(bb,a)};E.va=function(a){this.set(hb,!!a)};E.na=function(a,b){return ce(this.a,a,b)};E.link=function(a,b){this.a.get(fb)&&a&&(J.location.href=ce(this.a,a,b))};E.ua=function(a,b){this.a.get(fb)&&a&&a.action&&(a.action=ce(this.a,a.action,b))}; |
||||
|
E.za=function(){this.initData();var a=this.a,b=J.getElementById?J.getElementById("utmtrans"):J.utmform&&J.utmform.utmtrans?J.utmform.utmtrans:null;if(b&&b.value){a.set(Cb,[]);b=b.value.split("UTM:");for(var c=0;c<b.length;c++){b[c]=Da(b[c]);for(var d=b[c].split(de),e=0;e<d.length;e++)d[e]=Da(d[e]);"T"==d[0]?fe(a,d[1],d[2],d[3],d[4],d[5],d[6],d[7],d[8]):"I"==d[0]&&ge(a,d[1],d[2],d[3],d[4],d[5],d[6])}}};E.$=function(a,b,c,d,e,f,Be,k){return fe(this.a,a,b,c,d,e,f,Be,k)}; |
||||
|
E.Y=function(a,b,c,d,e,f){return ge(this.a,a,b,c,d,e,f)};E.Aa=function(a){de=a||"|"};E.ea=function(){this.set(Cb,[])};E.wa=function(a,b,c,d){var e=this.a;if(0>=a||a>e.get(yb))a=!1;else if(!b||!c||128<b.length+c.length)a=!1;else{1!=d&&2!=d&&(d=3);var f={};f.name=b;f.value=c;f.scope=d;e.get(Fb)[a]=f;a=!0}a&&this.a.store();return a};E.ka=function(a){this.a.get(Fb)[a]=void 0;this.a.store()};E.ra=function(a){return(a=this.a.get(Fb)[a])&&1==a.scope?a.value:void 0}; |
||||
|
E.Ca=function(a,b,c){12==a&&1==b?this.set(pf,c):this.m().f(a,b,c)};E.Da=function(a,b,c){this.m().o(a,b,c)};E.sa=function(a,b){return this.m().getKey(a,b)};E.ta=function(a,b){return this.m().N(a,b)};E.fa=function(a){this.m().L(a)};E.ga=function(a){this.m().M(a)};E.ja=function(){return new yd};E.W=function(a){a&&this.get(Ab).push(a.toLowerCase())};E.ba=function(){this.set(Ab,[])};E.X=function(a){a&&this.get(Bb).push(a.toLowerCase())};E.ca=function(){this.set(Bb,[])}; |
||||
|
E.Z=function(a,b,c,d,e){if(a&&b){a=[a,b.toLowerCase()].join(":");if(d||e)a=[a,d,e].join(":");d=this.get(zb);d.splice(c?0:d.length,0,a)}};E.da=function(){this.set(zb,[])};E.ha=function(a){this.a.load();var b=this.get(P),c=be(this.a);this.set(P,a);this.a.store();ae(this.a,c);this.set(P,b)};E.ya=function(a,b){if(0<a&&5>=a&&Ca(b)&&""!=b){var c=this.get(Fc)||[];c[a]=b;this.set(Fc,c)}};E.V=function(a){a=""+a;if(a.match(/^[A-Za-z0-9]{1,5}$/)){var b=this.get(Ic)||[];b.push(a);this.set(Ic,b)}}; |
||||
|
E.initData=function(){this.a.load()};E.Ba=function(a){a&&""!=a&&(this.set(Tb,a),this.a.j("var"))};var ne=function(a){"trans"!==a.get(sc)&&500<=a.b(cc,0)&&a.stopPropagation();if("event"===a.get(sc)){var b=(new Date).getTime(),c=a.b(dc,0),d=a.b(Zb,0);c=Math.floor((b-(c!=d?c:1E3*c))/1E3);0<c&&(a.set(dc,b),a.set(R,Math.min(10,a.b(R,0)+c)));0>=a.b(R,0)&&a.stopPropagation()}},pe=function(a){"event"===a.get(sc)&&a.set(R,Math.max(0,a.b(R,10)-1))};var qe=function(){var a=[];this.add=function(b,c,d){d&&(c=G(""+c));a.push(b+"="+c)};this.toString=function(){return a.join("&")}},re=function(a,b){(b||2!=a.get(xb))&&a.Za(cc)},se=function(a,b){b.add("utmwv","5.7.2");b.add("utms",a.get(cc));b.add("utmn",Ea());var c=J.location.hostname;F(c)||b.add("utmhn",c,!0);a=a.get(vb);100!=a&&b.add("utmsp",a,!0)},te=function(a,b){b.add("utmht",(new Date).getTime());b.add("utmac",Da(a.get(Wa)));a.get(Oc)&&b.add("utmxkey",a.get(Oc),!0);a.get(vc)&&b.add("utmni",1); |
||||
|
a.get(of)&&b.add("utmgtm",a.get(of),!0);var c=a.get(Ic);c&&0<c.length&&b.add("utmdid",c.join("."));ff(a,b);!1!==a.get(Xa)&&(a.get(Xa)||M.w)&&b.add("aip",1);void 0!==a.get(Kc)&&b.add("utmjid",a.c(Kc,""),!0);a.b(Yb,0)&&b.add("utmredir",a.b(Yb,0),!0);M.bb||(M.bb=a.get(Wa));(1<M.ab()||M.bb!=a.get(Wa))&&b.add("utmmt",1);b.add("utmu",od.encode())},ue=function(a,b){a=a.get(Fc)||[];for(var c=[],d=1;d<a.length;d++)a[d]&&c.push(d+":"+G(a[d].replace(/%/g,"%25").replace(/:/g,"%3A").replace(/,/g,"%2C")));c.length&& |
||||
|
b.add("utmpg",c.join(","))},ff=function(a,b){function c(a,b){b&&d.push(a+"="+b+";")}var d=[];c("__utma",cd(a));c("__utmz",hd(a,!1));c("__utmv",fd(a,!0));c("__utmx",be(a));b.add("utmcc",d.join("+"),!0)},ve=function(a,b){a.get(ib)&&(b.add("utmcs",a.get(Qb),!0),b.add("utmsr",a.get(Lb)),a.get(Rb)&&b.add("utmvp",a.get(Rb)),b.add("utmsc",a.get(Mb)),b.add("utmul",a.get(Pb)),b.add("utmje",a.get(Nb)),b.add("utmfl",a.get(Ob),!0))},we=function(a,b){a.get(lb)&&a.get(Ib)&&b.add("utmdt",a.get(Ib),!0);b.add("utmhid", |
||||
|
a.get(Kb));b.add("utmr",Pa(a.get(Jb),a.get(P)),!0);b.add("utmp",G(a.get(Hb),!0),!0)},xe=function(a,b){for(var c=a.get(Db),d=a.get(Eb),e=a.get(Fb)||[],f=0;f<e.length;f++){var Be=e[f];Be&&(c||(c=new yd),c.f(8,f,Be.name),c.f(9,f,Be.value),3!=Be.scope&&c.f(11,f,""+Be.scope))}F(a.get(wc))||F(a.get(xc),!0)||(c||(c=new yd),c.f(5,1,a.get(wc)),c.f(5,2,a.get(xc)),e=a.get(yc),void 0!=e&&c.f(5,3,e),e=a.get(zc),void 0!=e&&c.o(5,1,e));F(a.get(pf))||(c||(c=new yd),c.f(12,1,a.get(pf)));c?b.add("utme",c.Qa(d),!0): |
||||
|
d&&b.add("utme",d.A(),!0)},ye=function(a,b,c){var d=new qe;re(a,c);se(a,d);d.add("utmt","tran");d.add("utmtid",b.id_,!0);d.add("utmtst",b.affiliation_,!0);d.add("utmtto",b.total_,!0);d.add("utmttx",b.tax_,!0);d.add("utmtsp",b.shipping_,!0);d.add("utmtci",b.city_,!0);d.add("utmtrg",b.state_,!0);d.add("utmtco",b.country_,!0);xe(a,d);ve(a,d);we(a,d);(b=a.get(Gb))&&d.add("utmcu",b,!0);c||(ue(a,d),te(a,d));return d.toString()},ze=function(a,b,c){var d=new qe;re(a,c);se(a,d);d.add("utmt","item");d.add("utmtid", |
||||
|
b.transId_,!0);d.add("utmipc",b.sku_,!0);d.add("utmipn",b.name_,!0);d.add("utmiva",b.category_,!0);d.add("utmipr",b.price_,!0);d.add("utmiqt",b.quantity_,!0);xe(a,d);ve(a,d);we(a,d);(b=a.get(Gb))&&d.add("utmcu",b,!0);c||(ue(a,d),te(a,d));return d.toString()},Ae=function(a,b){var c=a.get(sc);if("page"==c)c=new qe,re(a,b),se(a,c),xe(a,c),ve(a,c),we(a,c),b||(ue(a,c),te(a,c)),a=[c.toString()];else if("event"==c)c=new qe,re(a,b),se(a,c),c.add("utmt","event"),xe(a,c),ve(a,c),we(a,c),b||(ue(a,c),te(a,c)), |
||||
|
a=[c.toString()];else if("var"==c)c=new qe,re(a,b),se(a,c),c.add("utmt","var"),!b&&te(a,c),a=[c.toString()];else if("trans"==c){c=[];for(var d=a.get(Cb),e=0;e<d.length;++e){c.push(ye(a,d[e],b));for(var f=d[e].items_,Be=0;Be<f.length;++Be)c.push(ze(a,f[Be],b))}a=c}else"social"==c?b?a=[]:(c=new qe,re(a,b),se(a,c),c.add("utmt","social"),c.add("utmsn",a.get(Ac),!0),c.add("utmsa",a.get(Bc),!0),c.add("utmsid",a.get(Cc),!0),xe(a,c),ve(a,c),we(a,c),ue(a,c),te(a,c),a=[c.toString()]):"feedback"==c?b?a=[]:(c= |
||||
|
new qe,re(a,b),se(a,c),c.add("utmt","feedback"),c.add("utmfbid",a.get(Gc),!0),c.add("utmfbpr",a.get(Hc),!0),xe(a,c),ve(a,c),we(a,c),ue(a,c),te(a,c),a=[c.toString()]):a=[];return a},oe=function(a){var b=a.get(xb),c=a.get(uc),d=c&&c.Ua,e=0,f=a.get(z);if(0==b||2==b){var Be=a.get(wb)+"?";var k=Ae(a,!0);for(var Ja=0,t=k.length;Ja<t;Ja++)Sa(k[Ja],d,Be,!0),e++}if(1==b||2==b)for(k=Ae(a),a=a.c(Jc,""),Ja=0,t=k.length;Ja<t;Ja++)try{if(f){var Za=k[Ja];b=(b=d)||Fa;df("",b,a+"?"+Za,f)}else Sa(k[Ja],d,a);e++}catch(Ma){Ma&& |
||||
|
Ra(Ma.name,void 0,Ma.message)}c&&(c.fb=e)};var Ne=function(){return"https:"==J.location.protocol||M.G?"https://ssl.google-analytics.com":"http://www.google-analytics.com"},Ce=function(a){this.name="len";this.message=a+"-8192"},De=function(a){this.name="ff2post";this.message=a+"-2036"},Sa=function(a,b,c,d){b=b||Fa;if(d||2036>=a.length)gf(a,b,c);else if(8192>=a.length){if(0<=W.navigator.userAgent.indexOf("Firefox")&&![].reduce)throw new De(a.length);df(a,b)||ef(a,b)||Ee(a,b)||b()}else throw new Ce(a.length);},gf=function(a,b,c){c=c||Ne()+"/__utm.gif?"; |
||||
|
var d=new Image(1,1);d.src=c+a;d.onload=function(){d.onload=null;d.onerror=null;b()};d.onerror=function(){d.onload=null;d.onerror=null;b()}},ef=function(a,b){if(0!=Ne().indexOf(J.location.protocol))return!1;var c=W.XDomainRequest;if(!c)return!1;c=new c;c.open("POST",Ne()+"/p/__utm.gif");c.onerror=function(){b()};c.onload=b;c.send(a);return!0},df=function(a,b,c,d){var e=W.XMLHttpRequest;if(!e)return!1;var f=new e;if(!("withCredentials"in f))return!1;f.open("POST",c||Ne()+"/p/__utm.gif",!0);f.withCredentials= |
||||
|
!0;f.setRequestHeader("Content-Type","text/plain");f.onreadystatechange=function(){if(4==f.readyState){if(d)try{var a=f.responseText;if(1>a.length||"1"!=a.charAt(0))Ra("xhr","ver",a),b();else if(3<d.count++)Ra("xhr","tmr",""+d.count),b();else if(1==a.length)b();else{var c=a.charAt(1);if("d"==c){var e=d.gb;a=(a=b)||Fa;df("",a,"https://stats.g.doubleclick.net/j/collect?"+e,d)}else if("g"==c){var t="https://www.google.%/ads/ga-audiences?".replace("%","com");gf(d.google,b,t);var Za=a.substring(2);if(Za)if(/^[a-z.]{1,6}$/.test(Za)){var Ma= |
||||
|
"https://www.google.%/ads/ga-audiences?".replace("%",Za);gf(d.google,Fa,Ma)}else Ra("tld","bcc",Za)}else Ra("xhr","brc",c),b()}}catch(mb){b()}else b();f=null}};f.send(a);return!0},Ee=function(a,b){if(!J.body)return We(function(){Ee(a,b)},100),!0;a=encodeURIComponent(a);try{var c=J.createElement('<iframe name="'+a+'"></iframe>')}catch(e){c=J.createElement("iframe"),c.name=a}c.height="0";c.width="0";c.style.display="none";c.style.visibility="hidden";var d=Ne()+"/u/post_iframe.html";Ga(W,"beforeunload", |
||||
|
function(){c.src="";c.parentNode&&c.parentNode.removeChild(c)});setTimeout(b,1E3);J.body.appendChild(c);c.src=d;return!0};var qf=function(){this.G=this.w=!1;0==Ea()%1E4&&(H(142),this.G=!0);this.C={};this.D=[];this.U=0;this.S=[["www.google-analytics.com","","/plugins/"]];this._gasoCPath=this._gasoDomain=this.bb=void 0;Re();Se()};E=qf.prototype;E.oa=function(a,b){return this.hb(a,void 0,b)};E.hb=function(a,b,c){b&&H(23);c&&H(67);void 0==b&&(b="~"+M.U++);a=new U(b,a,c);M.C[b]=a;M.D.push(a);return a};E.u=function(a){a=a||"";return M.C[a]||M.hb(void 0,a)};E.pa=function(){return M.D.slice(0)};E.ab=function(){return M.D.length}; |
||||
|
E.aa=function(){this.w=!0};E.la=function(){this.G=!0};var Fe=function(a){if("prerender"==J.visibilityState)return!1;a();return!0};var M=new qf;var D=W._gat;D&&Ba(D._getTracker)?M=D:W._gat=M;var Z=new Y;(function(a){if(!Fe(a)){H(123);var b=!1,c=function(){if(!b&&Fe(a)){b=!0;var d=J,e=c;d.removeEventListener?d.removeEventListener("visibilitychange",e,!1):d.detachEvent&&d.detachEvent("onvisibilitychange",e)}};Ga(J,"visibilitychange",c)}})(function(){var a=W._gaq,b=!1;if(a&&Ba(a.push)&&(b="[object Array]"==Object.prototype.toString.call(Object(a)),!b)){Z=a;return}W._gaq=Z;b&&Z.push.apply(Z,a)});function Yc(a){var b=1,c;if(a)for(b=0,c=a.length-1;0<=c;c--){var d=a.charCodeAt(c);b=(b<<6&268435455)+d+(d<<14);d=b&266338304;b=0!=d?b^d>>21:b}return b};}).call(this); |
||||
@ -0,0 +1,240 @@ |
|||||
|
// QRCODE reader Copyright 2011 Lazar Laszlo
|
||||
|
// http://www.webqr.com
|
||||
|
|
||||
|
var gCtx = null; |
||||
|
var gCanvas = null; |
||||
|
var c=0; |
||||
|
var stype=0; |
||||
|
var gUM=false; |
||||
|
var webkit=false; |
||||
|
var moz=false; |
||||
|
var v=null; |
||||
|
|
||||
|
var imghtml='<div id="qrfile"><canvas id="out-canvas" width="320" height="240"></canvas>'+ |
||||
|
'<div id="imghelp">drag and drop a QRCode here'+ |
||||
|
'<br>or select a file'+ |
||||
|
'<input type="file" onchange="handleFiles(this.files)"/>'+ |
||||
|
'</div>'+ |
||||
|
'</div>'; |
||||
|
|
||||
|
var vidhtml = '<video id="v" autoplay></video>'; |
||||
|
|
||||
|
function dragenter(e) { |
||||
|
e.stopPropagation(); |
||||
|
e.preventDefault(); |
||||
|
} |
||||
|
|
||||
|
function dragover(e) { |
||||
|
e.stopPropagation(); |
||||
|
e.preventDefault(); |
||||
|
} |
||||
|
function drop(e) { |
||||
|
e.stopPropagation(); |
||||
|
e.preventDefault(); |
||||
|
|
||||
|
var dt = e.dataTransfer; |
||||
|
var files = dt.files; |
||||
|
if(files.length>0) |
||||
|
{ |
||||
|
handleFiles(files); |
||||
|
} |
||||
|
else |
||||
|
if(dt.getData('URL')) |
||||
|
{ |
||||
|
qrcode.decode(dt.getData('URL')); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function handleFiles(f) |
||||
|
{ |
||||
|
var o=[]; |
||||
|
|
||||
|
for(var i =0;i<f.length;i++) |
||||
|
{ |
||||
|
var reader = new FileReader(); |
||||
|
reader.onload = (function(theFile) { |
||||
|
return function(e) { |
||||
|
gCtx.clearRect(0, 0, gCanvas.width, gCanvas.height); |
||||
|
|
||||
|
qrcode.decode(e.target.result); |
||||
|
}; |
||||
|
})(f[i]); |
||||
|
reader.readAsDataURL(f[i]); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function initCanvas(w,h) |
||||
|
{ |
||||
|
gCanvas = document.getElementById("qr-canvas"); |
||||
|
gCanvas.style.width = w + "px"; |
||||
|
gCanvas.style.height = h + "px"; |
||||
|
gCanvas.width = w; |
||||
|
gCanvas.height = h; |
||||
|
gCtx = gCanvas.getContext("2d"); |
||||
|
gCtx.clearRect(0, 0, w, h); |
||||
|
} |
||||
|
|
||||
|
|
||||
|
function captureToCanvas() { |
||||
|
if(stype!=1) |
||||
|
return; |
||||
|
if(gUM) |
||||
|
{ |
||||
|
try{ |
||||
|
gCtx.drawImage(v,0,0); |
||||
|
try{ |
||||
|
qrcode.decode(); |
||||
|
} |
||||
|
catch(e){ |
||||
|
console.log(e); |
||||
|
setTimeout(captureToCanvas, 500); |
||||
|
}; |
||||
|
} |
||||
|
catch(e){ |
||||
|
console.log(e); |
||||
|
setTimeout(captureToCanvas, 500); |
||||
|
}; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function htmlEntities(str) { |
||||
|
return String(str).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"'); |
||||
|
} |
||||
|
|
||||
|
function read(a) |
||||
|
{ |
||||
|
var html="<br>"; |
||||
|
if(a.indexOf("http://") === 0 || a.indexOf("https://") === 0) |
||||
|
html+="<a target='_blank' href='"+a+"'>"+a+"</a><br>"; |
||||
|
html+="<b>"+htmlEntities(a)+"</b><br><br>"; |
||||
|
document.getElementById("result").innerHTML=html; |
||||
|
} |
||||
|
|
||||
|
function isCanvasSupported(){ |
||||
|
var elem = document.createElement('canvas'); |
||||
|
return !!(elem.getContext && elem.getContext('2d')); |
||||
|
} |
||||
|
function success(stream) |
||||
|
{ |
||||
|
|
||||
|
v.srcObject = stream; |
||||
|
v.play(); |
||||
|
|
||||
|
gUM=true; |
||||
|
setTimeout(captureToCanvas, 500); |
||||
|
} |
||||
|
|
||||
|
function error(error) |
||||
|
{ |
||||
|
gUM=false; |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
function load() |
||||
|
{ |
||||
|
if(isCanvasSupported() && window.File && window.FileReader) |
||||
|
{ |
||||
|
initCanvas(800, 600); |
||||
|
qrcode.callback = read; |
||||
|
document.getElementById("mainbody").style.display="inline"; |
||||
|
setwebcam(); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
document.getElementById("mainbody").style.display="inline"; |
||||
|
document.getElementById("mainbody").innerHTML='<p id="mp1">QR code scanner for HTML5 capable browsers</p><br>'+ |
||||
|
'<br><p id="mp2">sorry your browser is not supported</p><br><br>'+ |
||||
|
'<p id="mp1">try <a href="http://www.mozilla.com/firefox"><img src="firefox.png"/></a> or <a href="http://chrome.google.com"><img src="chrome_logo.gif"/></a> or <a href="http://www.opera.com"><img src="Opera-logo.png"/></a></p>'; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function setwebcam() |
||||
|
{ |
||||
|
|
||||
|
var options = true; |
||||
|
if(navigator.mediaDevices && navigator.mediaDevices.enumerateDevices) |
||||
|
{ |
||||
|
try{ |
||||
|
navigator.mediaDevices.enumerateDevices() |
||||
|
.then(function(devices) { |
||||
|
devices.forEach(function(device) { |
||||
|
if (device.kind === 'videoinput') { |
||||
|
if(device.label.toLowerCase().search("back") >-1) |
||||
|
options={'deviceId': {'exact':device.deviceId}, 'facingMode':'environment'} ; |
||||
|
} |
||||
|
console.log(device.kind + ": " + device.label +" id = " + device.deviceId); |
||||
|
}); |
||||
|
setwebcam2(options); |
||||
|
}); |
||||
|
} |
||||
|
catch(e) |
||||
|
{ |
||||
|
console.log(e); |
||||
|
} |
||||
|
} |
||||
|
else{ |
||||
|
console.log("no navigator.mediaDevices.enumerateDevices" ); |
||||
|
setwebcam2(options); |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
|
||||
|
function setwebcam2(options) |
||||
|
{ |
||||
|
console.log(options); |
||||
|
document.getElementById("result").innerHTML="- scanning -"; |
||||
|
if(stype==1) |
||||
|
{ |
||||
|
setTimeout(captureToCanvas, 500); |
||||
|
return; |
||||
|
} |
||||
|
var n=navigator; |
||||
|
document.getElementById("outdiv").innerHTML = vidhtml; |
||||
|
v=document.getElementById("v"); |
||||
|
|
||||
|
|
||||
|
if(n.mediaDevices.getUserMedia) |
||||
|
{ |
||||
|
n.mediaDevices.getUserMedia({video: options, audio: false}). |
||||
|
then(function(stream){ |
||||
|
success(stream); |
||||
|
}).catch(function(error){ |
||||
|
error(error) |
||||
|
}); |
||||
|
} |
||||
|
else |
||||
|
if(n.getUserMedia) |
||||
|
{ |
||||
|
webkit=true; |
||||
|
n.getUserMedia({video: options, audio: false}, success, error); |
||||
|
} |
||||
|
else |
||||
|
if(n.webkitGetUserMedia) |
||||
|
{ |
||||
|
webkit=true; |
||||
|
n.webkitGetUserMedia({video:options, audio: false}, success, error); |
||||
|
} |
||||
|
|
||||
|
document.getElementById("qrimg").style.opacity=0.2; |
||||
|
document.getElementById("webcamimg").style.opacity=1.0; |
||||
|
|
||||
|
stype=1; |
||||
|
setTimeout(captureToCanvas, 500); |
||||
|
} |
||||
|
|
||||
|
function setimg() |
||||
|
{ |
||||
|
document.getElementById("result").innerHTML=""; |
||||
|
if(stype==2) |
||||
|
return; |
||||
|
document.getElementById("outdiv").innerHTML = imghtml; |
||||
|
//document.getElementById("qrimg").src="qrimg.png";
|
||||
|
//document.getElementById("webcamimg").src="webcam2.png";
|
||||
|
document.getElementById("qrimg").style.opacity=1.0; |
||||
|
document.getElementById("webcamimg").style.opacity=0.2; |
||||
|
var qrfile = document.getElementById("qrfile"); |
||||
|
qrfile.addEventListener("dragenter", dragenter, false); |
||||
|
qrfile.addEventListener("dragover", dragover, false); |
||||
|
qrfile.addEventListener("drop", drop, false); |
||||
|
stype=2; |
||||
|
} |
||||
@ -0,0 +1,40 @@ |
|||||
|
# Logs |
||||
|
logs |
||||
|
*.log |
||||
|
npm-debug.log* |
||||
|
|
||||
|
# Runtime data |
||||
|
pids |
||||
|
*.pid |
||||
|
*.seed |
||||
|
|
||||
|
# Directory for instrumented libs generated by jscoverage/JSCover |
||||
|
lib-cov |
||||
|
|
||||
|
# Coverage directory used by tools like istanbul |
||||
|
coverage |
||||
|
|
||||
|
# nyc test coverage |
||||
|
.nyc_output |
||||
|
|
||||
|
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) |
||||
|
.grunt |
||||
|
|
||||
|
# node-waf configuration |
||||
|
.lock-wscript |
||||
|
|
||||
|
# Compiled binary addons (http://nodejs.org/api/addons.html) |
||||
|
build/Release |
||||
|
|
||||
|
# Dependency directories |
||||
|
node_modules |
||||
|
jspm_packages |
||||
|
|
||||
|
# Optional npm cache directory |
||||
|
.npm |
||||
|
|
||||
|
# Optional REPL history |
||||
|
.node_repl_history |
||||
|
|
||||
|
# Temporary directory |
||||
|
tmp |
||||
@ -0,0 +1,40 @@ |
|||||
|
# Logs |
||||
|
logs |
||||
|
*.log |
||||
|
npm-debug.log* |
||||
|
|
||||
|
# Runtime data |
||||
|
pids |
||||
|
*.pid |
||||
|
*.seed |
||||
|
|
||||
|
# Directory for instrumented libs generated by jscoverage/JSCover |
||||
|
lib-cov |
||||
|
|
||||
|
# Coverage directory used by tools like istanbul |
||||
|
coverage |
||||
|
|
||||
|
# nyc test coverage |
||||
|
.nyc_output |
||||
|
|
||||
|
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) |
||||
|
.grunt |
||||
|
|
||||
|
# node-waf configuration |
||||
|
.lock-wscript |
||||
|
|
||||
|
# Compiled binary addons (http://nodejs.org/api/addons.html) |
||||
|
build/Release |
||||
|
|
||||
|
# Dependency directories |
||||
|
node_modules |
||||
|
jspm_packages |
||||
|
|
||||
|
# Optional npm cache directory |
||||
|
.npm |
||||
|
|
||||
|
# Optional REPL history |
||||
|
.node_repl_history |
||||
|
|
||||
|
# Temporary directory |
||||
|
tmp |
||||
@ -0,0 +1,21 @@ |
|||||
|
MIT License |
||||
|
|
||||
|
Copyright (c) 2017 Franck Freiburger |
||||
|
|
||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy |
||||
|
of this software and associated documentation files (the "Software"), to deal |
||||
|
in the Software without restriction, including without limitation the rights |
||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
||||
|
copies of the Software, and to permit persons to whom the Software is |
||||
|
furnished to do so, subject to the following conditions: |
||||
|
|
||||
|
The above copyright notice and this permission notice shall be included in all |
||||
|
copies or substantial portions of the Software. |
||||
|
|
||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
||||
|
SOFTWARE. |
||||
@ -0,0 +1,316 @@ |
|||||
|
### :tada: **http-vue-loader** evolved into [**vue3-sfc-loader**](https://github.com/FranckFreiburger/vue3-sfc-loader) that supports Vue2 and Vue3 :tada: |
||||
|
### (see the [announcement](https://github.com/FranckFreiburger/http-vue-loader/issues/127)) |
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
# http-vue-loader |
||||
|
Load .vue files directly from your html/js. No node.js environment, no build step. |
||||
|
|
||||
|
## examples |
||||
|
|
||||
|
`my-component.vue` |
||||
|
```vue |
||||
|
<template> |
||||
|
<div class="hello">Hello {{who}}</div> |
||||
|
</template> |
||||
|
|
||||
|
<script> |
||||
|
module.exports = { |
||||
|
data: function() { |
||||
|
return { |
||||
|
who: 'world' |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<style> |
||||
|
.hello { |
||||
|
background-color: #ffe; |
||||
|
} |
||||
|
</style> |
||||
|
``` |
||||
|
|
||||
|
`index.html` |
||||
|
```html |
||||
|
<!doctype html> |
||||
|
<html lang="en"> |
||||
|
<head> |
||||
|
<script src="https://unpkg.com/vue"></script> |
||||
|
<script src="https://unpkg.com/http-vue-loader"></script> |
||||
|
</head> |
||||
|
|
||||
|
<body> |
||||
|
<div id="my-app"> |
||||
|
<my-component></my-component> |
||||
|
</div> |
||||
|
|
||||
|
<script type="text/javascript"> |
||||
|
new Vue({ |
||||
|
el: '#my-app', |
||||
|
components: { |
||||
|
'my-component': httpVueLoader('my-component.vue') |
||||
|
} |
||||
|
}); |
||||
|
</script> |
||||
|
</body> |
||||
|
</html> |
||||
|
``` |
||||
|
|
||||
|
## More examples |
||||
|
using `httpVueLoader()` |
||||
|
|
||||
|
```html |
||||
|
... |
||||
|
<script type="text/javascript"> |
||||
|
|
||||
|
new Vue({ |
||||
|
components: { |
||||
|
'my-component': httpVueLoader('my-component.vue') |
||||
|
}, |
||||
|
... |
||||
|
``` |
||||
|
|
||||
|
or, using `httpVueLoader.register()` |
||||
|
|
||||
|
```html |
||||
|
... |
||||
|
<script type="text/javascript"> |
||||
|
|
||||
|
httpVueLoader.register(Vue, 'my-component.vue'); |
||||
|
|
||||
|
new Vue({ |
||||
|
components: [ |
||||
|
'my-component' |
||||
|
] |
||||
|
}, |
||||
|
... |
||||
|
``` |
||||
|
|
||||
|
or, using `httpVueLoader` as a plugin |
||||
|
|
||||
|
```html |
||||
|
... |
||||
|
<script type="text/javascript"> |
||||
|
|
||||
|
Vue.use(httpVueLoader); |
||||
|
|
||||
|
new Vue({ |
||||
|
components: { |
||||
|
'my-component': 'url:my-component.vue' |
||||
|
}, |
||||
|
... |
||||
|
``` |
||||
|
|
||||
|
or, using an array |
||||
|
``` |
||||
|
new Vue({ |
||||
|
components: [ |
||||
|
'url:my-component.vue' |
||||
|
] |
||||
|
}, |
||||
|
... |
||||
|
``` |
||||
|
|
||||
|
## Features |
||||
|
* `<template>`, `<script>` and `<style>` support the `src` attribute. |
||||
|
* `<style scoped>` is supported. |
||||
|
* `module.exports` may be a promise. |
||||
|
* Support of relative urls in `<template>` and `<style>` sections. |
||||
|
* Support custom CSS, HTML and scripting languages, eg. `<script lang="coffee">` (see VueLoader.langProcessor). |
||||
|
|
||||
|
|
||||
|
## Browser Support |
||||
|
|
||||
|
 |  |  |  |  |  | |
||||
|
--- | --- | --- | --- | --- | --- | |
||||
|
Latest ✔ | Latest ✔ | ? | ? | Latest ✔ | 9+ ✔ | |
||||
|
|
||||
|
|
||||
|
## Requirements |
||||
|
* [Vue.js 2](https://vuejs.org/) ([compiler and runtime](https://vuejs.org/v2/guide/installation.html#Explanation-of-Different-Builds)) |
||||
|
* [es6-promise](https://github.com/stefanpenner/es6-promise) (optional, except for IE, Chrome < 33, FireFox < 29, [...](http://caniuse.com/#search=promise) ) |
||||
|
* webserver (optional)... |
||||
|
|
||||
|
Since some browsers do not allow XMLHttpRequest to access local files (Cross origin requests are only supported for protocol schemes: http, data, chrome, chrome-extension, https), |
||||
|
you can start a small express server to run this example. |
||||
|
|
||||
|
Run the following commands to start a basic web server: |
||||
|
``` |
||||
|
npm install express |
||||
|
node -e "require('express')().use(require('express').static(__dirname, {index:'index.html'})).listen(8181)" |
||||
|
``` |
||||
|
|
||||
|
## API |
||||
|
|
||||
|
##### httpVueLoader(`url`) |
||||
|
|
||||
|
`url`: any url to a .vue file |
||||
|
|
||||
|
|
||||
|
##### httpVueLoader.register(`vue`, `url`) |
||||
|
|
||||
|
`vue`: a Vue instance |
||||
|
`url`: any url to a .vue file |
||||
|
|
||||
|
|
||||
|
##### httpVueLoader.httpRequest(`url`) |
||||
|
|
||||
|
This is the default httpLoader. |
||||
|
|
||||
|
Use axios instead of the default http loader: |
||||
|
```Javascript |
||||
|
httpVueLoader.httpRequest = function(url) { |
||||
|
|
||||
|
return axios.get(url) |
||||
|
.then(function(res) { |
||||
|
|
||||
|
return res.data; |
||||
|
}) |
||||
|
.catch(function(err) { |
||||
|
|
||||
|
return Promise.reject(err.status); |
||||
|
}); |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
##### httpVueLoader.langProcessor |
||||
|
|
||||
|
This is an object that contains language processors related to the `lang` attribute of the `<script>` section. |
||||
|
The language is a simple function that accepts a script source as argument and returns a javascript script source. |
||||
|
|
||||
|
Example - CoffeeScript: |
||||
|
|
||||
|
```JavaScript |
||||
|
<script src="http://coffeescript.org/v1/browser-compiler/coffee-script.js"></script> |
||||
|
<script src="httpVueLoader.js"></script> |
||||
|
|
||||
|
<script> |
||||
|
|
||||
|
httpVueLoader.langProcessor.coffee = function(scriptText) { |
||||
|
|
||||
|
return window.CoffeeScript.compile(scriptText, {bare: true}); |
||||
|
} |
||||
|
|
||||
|
</script> |
||||
|
``` |
||||
|
|
||||
|
Then, in you `.vue` file: |
||||
|
|
||||
|
```CoffeeScript |
||||
|
... |
||||
|
<script lang="coffee"> |
||||
|
|
||||
|
module.exports = |
||||
|
components: {} |
||||
|
data: -> |
||||
|
{} |
||||
|
computed: {} |
||||
|
methods: {} |
||||
|
|
||||
|
</script> |
||||
|
... |
||||
|
|
||||
|
``` |
||||
|
|
||||
|
|
||||
|
Example - Stylus: |
||||
|
|
||||
|
```JavaScript |
||||
|
<script src="//stylus-lang.com/try/stylus.min.js"></script> |
||||
|
<script src="httpVueLoader.js"></script> |
||||
|
|
||||
|
<script> |
||||
|
|
||||
|
httpVueLoader.langProcessor.stylus = function(stylusText) { |
||||
|
|
||||
|
return new Promise(function(resolve, reject) { |
||||
|
|
||||
|
stylus.render(stylusText.trim(), {}, function(err, css) { |
||||
|
|
||||
|
if (err) reject(err); |
||||
|
resolve(css); |
||||
|
}); |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
</script> |
||||
|
``` |
||||
|
|
||||
|
```stylus |
||||
|
... |
||||
|
<style lang="stylus"> |
||||
|
|
||||
|
border-radius() |
||||
|
-webkit-border-radius: arguments |
||||
|
-moz-border-radius: arguments |
||||
|
border-radius: arguments |
||||
|
|
||||
|
form input |
||||
|
padding: 5px |
||||
|
border: 1px solid |
||||
|
border-radius: 5px |
||||
|
|
||||
|
</style> |
||||
|
... |
||||
|
``` |
||||
|
|
||||
|
Sass (SCSS) example. Since `sass.compile()` is asynchronous, a promise needs to be returned: |
||||
|
|
||||
|
```JavaScript |
||||
|
<script src="sass.js"></script> |
||||
|
<script src="httpVueLoader.js"></script> |
||||
|
|
||||
|
<script> |
||||
|
httpVueLoader.langProcessor.scss = function (scssText) { |
||||
|
return new Promise(function(resolve, reject) { |
||||
|
sass.compile(scssText, function (result) { |
||||
|
if ( result.status === 0 ) |
||||
|
resolve(result.text) |
||||
|
else |
||||
|
reject(result) |
||||
|
}); |
||||
|
}); |
||||
|
} |
||||
|
// .... |
||||
|
``` |
||||
|
|
||||
|
```scss |
||||
|
... |
||||
|
<style lang="scss"> |
||||
|
$font-stack: Helvetica, sans-serif; |
||||
|
$primary-color: #333; |
||||
|
|
||||
|
body { |
||||
|
font: 100% $font-stack; |
||||
|
color: $primary-color; |
||||
|
} |
||||
|
</style> |
||||
|
``` |
||||
|
|
||||
|
## How it works |
||||
|
|
||||
|
1. http request the vue file |
||||
|
1. load the vue file in a document fragment |
||||
|
1. process each section (template, script and style) |
||||
|
1. return a promise to Vue.js (async components) |
||||
|
1. then Vue.js compiles and cache the component |
||||
|
|
||||
|
|
||||
|
## Notes |
||||
|
|
||||
|
The aim of http-vue-loader is to quickly test .vue components without any compilation step. |
||||
|
However, for production, I recommend to use [webpack module bundler](https://webpack.github.io/docs/) with [vue-loader](https://github.com/vuejs/vue-loader), |
||||
|
[webpack-simple](https://github.com/vuejs-templates/webpack-simple) is a good minimalist webpack config you should look at. |
||||
|
BTW, see also [why Vue.js doesn't support templateURL](https://vuejs.org/2015/10/28/why-no-template-url/). |
||||
|
|
||||
|
|
||||
|
## Caveat |
||||
|
|
||||
|
Due to the lack of suitable API in Google Chrome and Internet Explorer, syntax errors in the `<script>` section are only reported on FireFox. |
||||
|
|
||||
|
|
||||
|
## Credits |
||||
|
|
||||
|
[Franck Freiburger](https://www.franck-freiburger.com) |
||||
@ -0,0 +1,24 @@ |
|||||
|
{ |
||||
|
"name": "http-vue-loader", |
||||
|
"version": "1.4.2", |
||||
|
"description": "Load .vue files directly from your html/js. No node.js environment, no build step.", |
||||
|
"main": "./src/httpVueLoader.js", |
||||
|
"scripts": { |
||||
|
"test": "echo \"Error: no test specified\" && exit 1" |
||||
|
}, |
||||
|
"repository": { |
||||
|
"type": "git", |
||||
|
"url": "git+https://github.com/FranckFreiburger/http-vue-loader.git" |
||||
|
}, |
||||
|
"keywords": [ |
||||
|
"http", |
||||
|
"vue", |
||||
|
"loader" |
||||
|
], |
||||
|
"author": "Franck Freiburger", |
||||
|
"license": "MIT", |
||||
|
"bugs": { |
||||
|
"url": "https://github.com/FranckFreiburger/http-vue-loader/issues" |
||||
|
}, |
||||
|
"homepage": "https://github.com/FranckFreiburger/http-vue-loader#readme" |
||||
|
} |
||||
@ -0,0 +1,478 @@ |
|||||
|
(function umd(root,factory){ |
||||
|
if(typeof module==='object' && typeof exports === 'object' ) |
||||
|
module.exports=factory() |
||||
|
else if(typeof define==='function' && define.amd) |
||||
|
define([],factory) |
||||
|
else |
||||
|
root.httpVueLoader=factory() |
||||
|
})(this,function factory() { |
||||
|
'use strict'; |
||||
|
|
||||
|
var scopeIndex = 0; |
||||
|
|
||||
|
StyleContext.prototype = { |
||||
|
|
||||
|
withBase: function(callback) { |
||||
|
|
||||
|
var tmpBaseElt; |
||||
|
if ( this.component.baseURI ) { |
||||
|
|
||||
|
// firefox and chrome need the <base> to be set while inserting or modifying <style> in a document.
|
||||
|
tmpBaseElt = document.createElement('base'); |
||||
|
tmpBaseElt.href = this.component.baseURI; |
||||
|
|
||||
|
var headElt = this.component.getHead(); |
||||
|
headElt.insertBefore(tmpBaseElt, headElt.firstChild); |
||||
|
} |
||||
|
|
||||
|
callback.call(this); |
||||
|
|
||||
|
if ( tmpBaseElt ) |
||||
|
this.component.getHead().removeChild(tmpBaseElt); |
||||
|
}, |
||||
|
|
||||
|
scopeStyles: function(styleElt, scopeName) { |
||||
|
|
||||
|
function process() { |
||||
|
|
||||
|
var sheet = styleElt.sheet; |
||||
|
var rules = sheet.cssRules; |
||||
|
|
||||
|
for ( var i = 0; i < rules.length; ++i ) { |
||||
|
|
||||
|
var rule = rules[i]; |
||||
|
if ( rule.type !== 1 ) |
||||
|
continue; |
||||
|
|
||||
|
var scopedSelectors = []; |
||||
|
|
||||
|
rule.selectorText.split(/\s*,\s*/).forEach(function(sel) { |
||||
|
|
||||
|
scopedSelectors.push(scopeName+' '+sel); |
||||
|
var segments = sel.match(/([^ :]+)(.+)?/); |
||||
|
scopedSelectors.push(segments[1] + scopeName + (segments[2]||'')); |
||||
|
}); |
||||
|
|
||||
|
var scopedRule = scopedSelectors.join(',') + rule.cssText.substr(rule.selectorText.length); |
||||
|
sheet.deleteRule(i); |
||||
|
sheet.insertRule(scopedRule, i); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
// firefox may fail sheet.cssRules with InvalidAccessError
|
||||
|
process(); |
||||
|
} catch (ex) { |
||||
|
|
||||
|
if ( ex instanceof DOMException && ex.code === DOMException.INVALID_ACCESS_ERR ) { |
||||
|
|
||||
|
styleElt.sheet.disabled = true; |
||||
|
styleElt.addEventListener('load', function onStyleLoaded() { |
||||
|
|
||||
|
styleElt.removeEventListener('load', onStyleLoaded); |
||||
|
|
||||
|
// firefox need this timeout otherwise we have to use document.importNode(style, true)
|
||||
|
setTimeout(function() { |
||||
|
|
||||
|
process(); |
||||
|
styleElt.sheet.disabled = false; |
||||
|
}); |
||||
|
}); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
throw ex; |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
compile: function() { |
||||
|
|
||||
|
var hasTemplate = this.template !== null; |
||||
|
|
||||
|
var scoped = this.elt.hasAttribute('scoped'); |
||||
|
|
||||
|
if ( scoped ) { |
||||
|
|
||||
|
// no template, no scopable style needed
|
||||
|
if ( !hasTemplate ) |
||||
|
return; |
||||
|
|
||||
|
// firefox does not tolerate this attribute
|
||||
|
this.elt.removeAttribute('scoped'); |
||||
|
} |
||||
|
|
||||
|
this.withBase(function() { |
||||
|
|
||||
|
this.component.getHead().appendChild(this.elt); |
||||
|
}); |
||||
|
|
||||
|
if ( scoped ) |
||||
|
this.scopeStyles(this.elt, '['+this.component.getScopeId()+']'); |
||||
|
|
||||
|
return Promise.resolve(); |
||||
|
}, |
||||
|
|
||||
|
getContent: function() { |
||||
|
|
||||
|
return this.elt.textContent; |
||||
|
}, |
||||
|
|
||||
|
setContent: function(content) { |
||||
|
|
||||
|
this.withBase(function() { |
||||
|
|
||||
|
this.elt.textContent = content; |
||||
|
}); |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
function StyleContext(component, elt) { |
||||
|
|
||||
|
this.component = component; |
||||
|
this.elt = elt; |
||||
|
} |
||||
|
|
||||
|
|
||||
|
ScriptContext.prototype = { |
||||
|
|
||||
|
getContent: function() { |
||||
|
|
||||
|
return this.elt.textContent; |
||||
|
}, |
||||
|
|
||||
|
setContent: function(content) { |
||||
|
|
||||
|
this.elt.textContent = content; |
||||
|
}, |
||||
|
|
||||
|
compile: function(module) { |
||||
|
|
||||
|
var childModuleRequire = function(childURL) { |
||||
|
|
||||
|
return httpVueLoader.require(resolveURL(this.component.baseURI, childURL)); |
||||
|
}.bind(this); |
||||
|
|
||||
|
var childLoader = function(childURL, childName) { |
||||
|
|
||||
|
return httpVueLoader(resolveURL(this.component.baseURI, childURL), childName); |
||||
|
}.bind(this); |
||||
|
|
||||
|
try { |
||||
|
Function('exports', 'require', 'httpVueLoader', 'module', this.getContent()).call(this.module.exports, this.module.exports, childModuleRequire, childLoader, this.module); |
||||
|
} catch(ex) { |
||||
|
|
||||
|
if ( !('lineNumber' in ex) ) { |
||||
|
|
||||
|
return Promise.reject(ex); |
||||
|
} |
||||
|
var vueFileData = responseText.replace(/\r?\n/g, '\n'); |
||||
|
var lineNumber = vueFileData.substr(0, vueFileData.indexOf(script)).split('\n').length + ex.lineNumber - 1; |
||||
|
throw new (ex.constructor)(ex.message, url, lineNumber); |
||||
|
} |
||||
|
|
||||
|
return Promise.resolve(this.module.exports) |
||||
|
.then(httpVueLoader.scriptExportsHandler.bind(this)) |
||||
|
.then(function(exports) { |
||||
|
|
||||
|
this.module.exports = exports; |
||||
|
}.bind(this)); |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
function ScriptContext(component, elt) { |
||||
|
|
||||
|
this.component = component; |
||||
|
this.elt = elt; |
||||
|
this.module = { exports:{} }; |
||||
|
} |
||||
|
|
||||
|
|
||||
|
TemplateContext.prototype = { |
||||
|
|
||||
|
getContent: function() { |
||||
|
|
||||
|
return this.elt.innerHTML; |
||||
|
}, |
||||
|
|
||||
|
setContent: function(content) { |
||||
|
|
||||
|
this.elt.innerHTML = content; |
||||
|
}, |
||||
|
|
||||
|
getRootElt: function() { |
||||
|
|
||||
|
var tplElt = this.elt.content || this.elt; |
||||
|
|
||||
|
if ( 'firstElementChild' in tplElt ) |
||||
|
return tplElt.firstElementChild; |
||||
|
|
||||
|
for ( tplElt = tplElt.firstChild; tplElt !== null; tplElt = tplElt.nextSibling ) |
||||
|
if ( tplElt.nodeType === Node.ELEMENT_NODE ) |
||||
|
return tplElt; |
||||
|
|
||||
|
return null; |
||||
|
}, |
||||
|
|
||||
|
compile: function() { |
||||
|
|
||||
|
return Promise.resolve(); |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
function TemplateContext(component, elt) { |
||||
|
|
||||
|
this.component = component; |
||||
|
this.elt = elt; |
||||
|
} |
||||
|
|
||||
|
|
||||
|
|
||||
|
Component.prototype = { |
||||
|
|
||||
|
getHead: function() { |
||||
|
|
||||
|
return document.head || document.getElementsByTagName('head')[0]; |
||||
|
}, |
||||
|
|
||||
|
getScopeId: function() { |
||||
|
|
||||
|
if ( this._scopeId === '' ) { |
||||
|
|
||||
|
this._scopeId = 'data-s-' + (scopeIndex++).toString(36); |
||||
|
this.template.getRootElt().setAttribute(this._scopeId, ''); |
||||
|
} |
||||
|
return this._scopeId; |
||||
|
}, |
||||
|
|
||||
|
load: function(componentURL) { |
||||
|
|
||||
|
return httpVueLoader.httpRequest(componentURL) |
||||
|
.then(function(responseText) { |
||||
|
|
||||
|
this.baseURI = componentURL.substr(0, componentURL.lastIndexOf('/')+1); |
||||
|
var doc = document.implementation.createHTMLDocument(''); |
||||
|
|
||||
|
// IE requires the <base> to come with <style>
|
||||
|
doc.body.innerHTML = (this.baseURI ? '<base href="'+this.baseURI+'">' : '') + responseText; |
||||
|
|
||||
|
for ( var it = doc.body.firstChild; it; it = it.nextSibling ) { |
||||
|
|
||||
|
switch ( it.nodeName ) { |
||||
|
case 'TEMPLATE': |
||||
|
this.template = new TemplateContext(this, it); |
||||
|
break; |
||||
|
case 'SCRIPT': |
||||
|
this.script = new ScriptContext(this, it); |
||||
|
break; |
||||
|
case 'STYLE': |
||||
|
this.styles.push(new StyleContext(this, it)); |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return this; |
||||
|
}.bind(this)); |
||||
|
}, |
||||
|
|
||||
|
_normalizeSection: function(eltCx) { |
||||
|
|
||||
|
var p; |
||||
|
|
||||
|
if ( eltCx === null || !eltCx.elt.hasAttribute('src') ) { |
||||
|
|
||||
|
p = Promise.resolve(null); |
||||
|
} else { |
||||
|
|
||||
|
p = httpVueLoader.httpRequest(eltCx.elt.getAttribute('src')) |
||||
|
.then(function(content) { |
||||
|
|
||||
|
eltCx.elt.removeAttribute('src'); |
||||
|
return content; |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
return p |
||||
|
.then(function(content) { |
||||
|
|
||||
|
if ( eltCx !== null && eltCx.elt.hasAttribute('lang') ) { |
||||
|
|
||||
|
var lang = eltCx.elt.getAttribute('lang'); |
||||
|
eltCx.elt.removeAttribute('lang'); |
||||
|
return httpVueLoader.langProcessor[lang.toLowerCase()].call(this, content === null ? eltCx.getContent() : content); |
||||
|
} |
||||
|
return content; |
||||
|
}.bind(this)) |
||||
|
.then(function(content) { |
||||
|
|
||||
|
if ( content !== null ) |
||||
|
eltCx.setContent(content); |
||||
|
}); |
||||
|
}, |
||||
|
|
||||
|
normalize: function() { |
||||
|
|
||||
|
return Promise.all(Array.prototype.concat( |
||||
|
this._normalizeSection(this.template), |
||||
|
this._normalizeSection(this.script), |
||||
|
this.styles.map(this._normalizeSection) |
||||
|
)) |
||||
|
.then(function() { |
||||
|
|
||||
|
return this; |
||||
|
}.bind(this)); |
||||
|
}, |
||||
|
|
||||
|
compile: function() { |
||||
|
|
||||
|
return Promise.all(Array.prototype.concat( |
||||
|
this.template && this.template.compile(), |
||||
|
this.script && this.script.compile(), |
||||
|
this.styles.map(function(style) { return style.compile(); }) |
||||
|
)) |
||||
|
.then(function() { |
||||
|
|
||||
|
return this; |
||||
|
}.bind(this)); |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
function Component(name) { |
||||
|
|
||||
|
this.name = name; |
||||
|
this.template = null; |
||||
|
this.script = null; |
||||
|
this.styles = []; |
||||
|
this._scopeId = ''; |
||||
|
} |
||||
|
|
||||
|
function identity(value) { |
||||
|
|
||||
|
return value; |
||||
|
} |
||||
|
|
||||
|
function parseComponentURL(url) { |
||||
|
|
||||
|
var comp = url.match(/(.*?)([^/]+?)\/?(\.vue)?(\?.*|#.*|$)/); |
||||
|
return { |
||||
|
name: comp[2], |
||||
|
url: comp[1] + comp[2] + (comp[3] === undefined ? '/index.vue' : comp[3]) + comp[4] |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
function resolveURL(baseURL, url) { |
||||
|
|
||||
|
if (url.substr(0, 2) === './' || url.substr(0, 3) === '../') { |
||||
|
return baseURL + url; |
||||
|
} |
||||
|
return url; |
||||
|
} |
||||
|
|
||||
|
|
||||
|
httpVueLoader.load = function(url, name) { |
||||
|
|
||||
|
return function() { |
||||
|
|
||||
|
return new Component(name).load(url) |
||||
|
.then(function(component) { |
||||
|
|
||||
|
return component.normalize(); |
||||
|
}) |
||||
|
.then(function(component) { |
||||
|
|
||||
|
return component.compile(); |
||||
|
}) |
||||
|
.then(function(component) { |
||||
|
|
||||
|
var exports = component.script !== null ? component.script.module.exports : {}; |
||||
|
|
||||
|
if ( component.template !== null ) |
||||
|
exports.template = component.template.getContent(); |
||||
|
|
||||
|
if ( exports.name === undefined ) |
||||
|
if ( component.name !== undefined ) |
||||
|
exports.name = component.name; |
||||
|
|
||||
|
exports._baseURI = component.baseURI; |
||||
|
|
||||
|
return exports; |
||||
|
}); |
||||
|
}; |
||||
|
}; |
||||
|
|
||||
|
|
||||
|
httpVueLoader.register = function(Vue, url) { |
||||
|
|
||||
|
var comp = parseComponentURL(url); |
||||
|
Vue.component(comp.name, httpVueLoader.load(comp.url)); |
||||
|
}; |
||||
|
|
||||
|
httpVueLoader.install = function(Vue) { |
||||
|
|
||||
|
Vue.mixin({ |
||||
|
|
||||
|
beforeCreate: function () { |
||||
|
|
||||
|
var components = this.$options.components; |
||||
|
|
||||
|
for ( var componentName in components ) { |
||||
|
|
||||
|
if ( typeof(components[componentName]) === 'string' && components[componentName].substr(0, 4) === 'url:' ) { |
||||
|
|
||||
|
var comp = parseComponentURL(components[componentName].substr(4)); |
||||
|
|
||||
|
var componentURL = ('_baseURI' in this.$options) ? resolveURL(this.$options._baseURI, comp.url) : comp.url; |
||||
|
|
||||
|
if ( isNaN(componentName) ) |
||||
|
components[componentName] = httpVueLoader.load(componentURL, componentName); |
||||
|
else |
||||
|
components[componentName] = Vue.component(comp.name, httpVueLoader.load(componentURL, comp.name)); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
}); |
||||
|
}; |
||||
|
|
||||
|
httpVueLoader.require = function(moduleName) { |
||||
|
|
||||
|
return window[moduleName]; |
||||
|
}; |
||||
|
|
||||
|
httpVueLoader.httpRequest = function(url) { |
||||
|
|
||||
|
return new Promise(function(resolve, reject) { |
||||
|
|
||||
|
var xhr = new XMLHttpRequest(); |
||||
|
xhr.open('GET', url); |
||||
|
xhr.responseType = 'text'; |
||||
|
|
||||
|
xhr.onreadystatechange = function() { |
||||
|
|
||||
|
if ( xhr.readyState === 4 ) { |
||||
|
|
||||
|
if ( xhr.status >= 200 && xhr.status < 300 ) |
||||
|
resolve(xhr.responseText); |
||||
|
else |
||||
|
reject(xhr.status); |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
xhr.send(null); |
||||
|
}); |
||||
|
}; |
||||
|
|
||||
|
httpVueLoader.langProcessor = { |
||||
|
html: identity, |
||||
|
js: identity, |
||||
|
css: identity |
||||
|
}; |
||||
|
|
||||
|
httpVueLoader.scriptExportsHandler = identity; |
||||
|
|
||||
|
function httpVueLoader(url, name) { |
||||
|
|
||||
|
var comp = parseComponentURL(url); |
||||
|
return httpVueLoader.load(comp.url, name); |
||||
|
} |
||||
|
|
||||
|
return httpVueLoader; |
||||
|
}); |
||||
@ -0,0 +1,3 @@ |
|||||
|
> 1% |
||||
|
last 2 versions |
||||
|
ie >= 10 |
||||
@ -0,0 +1,18 @@ |
|||||
|
root = true |
||||
|
|
||||
|
[*] |
||||
|
indent_style = space |
||||
|
indent_size = 2 |
||||
|
trim_trailing_whitespace = true |
||||
|
end_of_line = lf |
||||
|
insert_final_newline = true |
||||
|
|
||||
|
[docs/rules/linebreak-style.md] |
||||
|
end_of_line = disabled |
||||
|
|
||||
|
[{docs/rules/{indent.md,no-mixed-spaces-and-tabs.md}] |
||||
|
indent_style = disabled |
||||
|
indent_size = disabled |
||||
|
|
||||
|
[docs/rules/no-trailing-spaces.md] |
||||
|
trim_trailing_whitespace = false |
||||
@ -0,0 +1,25 @@ |
|||||
|
module.exports = { |
||||
|
root: true, |
||||
|
env: { |
||||
|
node: true |
||||
|
}, |
||||
|
extends: ["plugin:vue/essential", "eslint:recommended", "@vue/prettier"], |
||||
|
parserOptions: { |
||||
|
parser: "babel-eslint" |
||||
|
}, |
||||
|
rules: { |
||||
|
"no-console": process.env.NODE_ENV === "production" ? "warn" : "off", |
||||
|
"no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off" |
||||
|
}, |
||||
|
overrides: [ |
||||
|
{ |
||||
|
files: [ |
||||
|
"**/__tests__/*.{j,t}s?(x)", |
||||
|
"**/tests/unit/**/*.spec.{j,t}s?(x)" |
||||
|
], |
||||
|
env: { |
||||
|
mocha: true |
||||
|
} |
||||
|
} |
||||
|
] |
||||
|
}; |
||||
@ -0,0 +1,28 @@ |
|||||
|
--- |
||||
|
name: Bug report |
||||
|
about: Create a report to help us improve |
||||
|
title: '' |
||||
|
labels: '' |
||||
|
assignees: '' |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
**Describe the bug** |
||||
|
A clear and concise description of what the bug is. Can you reproduce this issue with [one of the demos](https://gruhn.github.io/vue-qrcode-reader/demos/DecodeAll.html)? |
||||
|
|
||||
|
**To Reproduce** |
||||
|
Please provide a link to a minimal reproduction of the bug. For example on jsFiddle, CodePen, etc. Please don't attach a ZIP file with your entire code base. I know this is additional effort but if it takes too much time to reproduce your issue you'll likely won't get help at all. |
||||
|
|
||||
|
**Screenshots** |
||||
|
If applicable, add screenshots to help explain your problem. |
||||
|
|
||||
|
**Desktop (please complete the following information):** |
||||
|
- OS: [e.g. iOS] |
||||
|
- Browser [e.g. chrome, safari] |
||||
|
- Version [e.g. 22] |
||||
|
|
||||
|
**Smartphone (please complete the following information):** |
||||
|
- Device: [e.g. iPhone6] |
||||
|
- OS: [e.g. iOS8.1] |
||||
|
- Browser [e.g. stock browser, safari] |
||||
|
- Version [e.g. 22] |
||||
@ -0,0 +1,19 @@ |
|||||
|
--- |
||||
|
name: Wrong camera selected |
||||
|
about: |
||||
|
title: '' |
||||
|
labels: '' |
||||
|
assignees: '' |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
If your device defaults to the wrong camera, please [open this demo](https://gruhn.github.io/vue-qrcode-reader/select-camera-demo.html). |
||||
|
You should see a list of all cameras installed on your device. |
||||
|
Copy the list and mark the camera that was picked by default and the camera that should actually be picked. |
||||
|
For example like this: |
||||
|
|
||||
|
``` |
||||
|
FaceTime HD Camera (Built-in) [DEFAULT] |
||||
|
A different Camera [PREFERRED] |
||||
|
Another different Camera |
||||
|
``` |
||||
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 328 KiB |
|
After Width: | Height: | Size: 106 KiB |
|
After Width: | Height: | Size: 275 KiB |
|
After Width: | Height: | Size: 91 KiB |
@ -0,0 +1,33 @@ |
|||||
|
name: Release |
||||
|
on: |
||||
|
push: |
||||
|
branches: |
||||
|
- master |
||||
|
jobs: |
||||
|
release: |
||||
|
name: Release |
||||
|
runs-on: ubuntu-latest |
||||
|
steps: |
||||
|
- name: Checkout |
||||
|
uses: actions/checkout@v2 |
||||
|
with: |
||||
|
fetch-depth: 0 |
||||
|
- name: Setup Node |
||||
|
uses: actions/setup-node@v1 |
||||
|
with: |
||||
|
node-version: 16 |
||||
|
- name: Install & Build |
||||
|
run: | |
||||
|
npm ci |
||||
|
npm run build |
||||
|
npm run build:docs |
||||
|
- name: Deploy Docs |
||||
|
uses: JamesIves/github-pages-deploy-action@4.1.4 |
||||
|
with: |
||||
|
branch: gh-pages |
||||
|
folder: docs/.vuepress/dist |
||||
|
- name: Release |
||||
|
env: |
||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} |
||||
|
NPM_TOKEN: ${{ secrets.NPM_TOKEN }} |
||||
|
run: npx semantic-release |
||||
@ -0,0 +1,26 @@ |
|||||
|
.DS_Store |
||||
|
node_modules |
||||
|
/dist |
||||
|
/docs/.vuepress/dist |
||||
|
|
||||
|
/tests/e2e/videos/ |
||||
|
/tests/e2e/screenshots/ |
||||
|
|
||||
|
# local env files |
||||
|
.env.local |
||||
|
.env.*.local |
||||
|
|
||||
|
# Log files |
||||
|
npm-debug.log* |
||||
|
yarn-debug.log* |
||||
|
yarn-error.log* |
||||
|
pnpm-debug.log* |
||||
|
|
||||
|
# Editor directories and files |
||||
|
.idea |
||||
|
.vscode |
||||
|
*.suo |
||||
|
*.ntvs* |
||||
|
*.njsproj |
||||
|
*.sln |
||||
|
*.sw? |
||||
@ -0,0 +1,4 @@ |
|||||
|
branches: |
||||
|
- name: master |
||||
|
- name: beta |
||||
|
prerelease: true |
||||
@ -0,0 +1,23 @@ |
|||||
|
Your contributions are welcome. |
||||
|
Don't hesitate to open an issue if you have trouble. |
||||
|
|
||||
|
### Setup Dev Environment |
||||
|
|
||||
|
Clone the repository and run |
||||
|
|
||||
|
``` |
||||
|
npm install |
||||
|
``` |
||||
|
|
||||
|
We use a locally served version of the [demo page](https://gruhn.github.io/vue-qrcode-reader/demos/DecodeAll.html) during development. |
||||
|
To get that started run |
||||
|
|
||||
|
``` |
||||
|
npm run dev |
||||
|
``` |
||||
|
|
||||
|
### Commit Messages |
||||
|
|
||||
|
The version number of releases is automatically determined form commit messages. |
||||
|
This only works if we follow [Angular Commit Message Conventions](https://github.com/semantic-release/semantic-release#commit-message-format). |
||||
|
|
||||
@ -0,0 +1,21 @@ |
|||||
|
MIT License |
||||
|
|
||||
|
Copyright (c) 2017 Niklas Gruhn |
||||
|
|
||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy |
||||
|
of this software and associated documentation files (the "Software"), to deal |
||||
|
in the Software without restriction, including without limitation the rights |
||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
||||
|
copies of the Software, and to permit persons to whom the Software is |
||||
|
furnished to do so, subject to the following conditions: |
||||
|
|
||||
|
The above copyright notice and this permission notice shall be included in all |
||||
|
copies or substantial portions of the Software. |
||||
|
|
||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
||||
|
SOFTWARE. |
||||
@ -0,0 +1,183 @@ |
|||||
|
<p align="center"> |
||||
|
<img src="https://gruhn.github.io/vue-qrcode-reader/logo.png" alt="Logo" width="240" height="240" style="max-width: 100%;"> |
||||
|
|
||||
|
<br> |
||||
|
<br> |
||||
|
|
||||
|
<a href="https://vuejs.org/"> |
||||
|
<img src="https://img.shields.io/badge/vue-2.x-brightgreen.svg" alt="for Vue.js 2"> |
||||
|
</a> |
||||
|
|
||||
|
<a href="https://www.npmjs.com/package/vue-qrcode-reader"> |
||||
|
<img src="https://img.shields.io/npm/dm/vue-qrcode-reader.svg" alt="npm monthly downloads"> |
||||
|
</a> |
||||
|
|
||||
|
<br> |
||||
|
|
||||
|
<img src="https://img.shields.io/badge/Maintained%3F-yes-green.svg" alt="is maintained? yes"> |
||||
|
|
||||
|
<a href="http://opensource.org/licenses/MIT"> |
||||
|
<img src="https://img.shields.io/github/license/Naereen/StrapDown.js.svg" alt="licence: MIT"> |
||||
|
</a> |
||||
|
|
||||
|
<a href="https://github.com/Naereen/badges"> |
||||
|
<img src="https://img.shields.io/badge/badges-awesome-green.svg" alt="badges = awesome"> |
||||
|
</a> |
||||
|
|
||||
|
<br> |
||||
|
|
||||
|
<a href="https://github.com/semantic-release/semantic-release"> |
||||
|
<img src="https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg" alt="uses semantic release"> |
||||
|
</a> |
||||
|
|
||||
|
<a href="https://github.com/prettier/prettier"> |
||||
|
<img src="https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat-square" alt="code style: prettier"> |
||||
|
</a> |
||||
|
|
||||
|
<br> |
||||
|
|
||||
|
<a href="https://bundlephobia.com/result?p=vue-qrcode-reader"> |
||||
|
<img src="https://badgen.net/bundlephobia/minzip/vue-qrcode-reader" alt="size minified + gzipped"> |
||||
|
</a> |
||||
|
|
||||
|
<a href="https://www.npmjs.com/package/vue-qrcode-reader"> |
||||
|
<img src="https://img.shields.io/npm/v/vue-qrcode-reader.svg" alt="npm current version"> |
||||
|
</a> |
||||
|
|
||||
|
|
||||
|
<br> |
||||
|
|
||||
|
<a href="https://github.com/vuejs/awesome-vue"> |
||||
|
<img src="https://awesome.re/mentioned-badge.svg" alt="Mentioned in Awesome Vue"> |
||||
|
</a> |
||||
|
|
||||
|
<br> |
||||
|
<br> |
||||
|
<a href="https://gruhn.github.io/vue-qrcode-reader/demos/DecodeAll.html">live demos</a> | |
||||
|
<a href="https://gruhn.github.io/vue-qrcode-reader/api/QrcodeStream.html">api reference</a> |
||||
|
</p> |
||||
|
|
||||
|
A set of Vue.js components, allowing you to detect and decode QR codes, without leaving the browser. |
||||
|
|
||||
|
- :movie_camera: `QrcodeStream` accesses the device camera and continuously scans incoming frames. |
||||
|
- :put_litter_in_its_place: `QrcodeDropZone` renders to an empty region where you can drag-and-drop images to be decoded. |
||||
|
- :open_file_folder: `QrcodeCapture` is a classic file upload field, instantly scanning all files you select. |
||||
|
|
||||
|
All components are responsive. Beyond that, close to zero styling. Make them fit your layout. Usage is simple and straight forward: |
||||
|
|
||||
|
```html |
||||
|
<qrcode-stream @decode="onDecode"></qrcode-stream> |
||||
|
``` |
||||
|
|
||||
|
```js |
||||
|
methods: { |
||||
|
onDecode (decodedString) { |
||||
|
// ... |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
### Screenshots |
||||
|
|
||||
|
<p align="center"> |
||||
|
<img src="https://raw.githubusercontent.com/gruhn/vue-qrcode-reader/master/.github/screenshot1.png" height="500" alt="Screenshot 1"> |
||||
|
<img src="https://raw.githubusercontent.com/gruhn/vue-qrcode-reader/master/.github/screenshot2.png" height="500" alt="Screenshot 2"> |
||||
|
<img src="https://raw.githubusercontent.com/gruhn/vue-qrcode-reader/master/.github/screenshot3.png" height="500" alt="Screenshot 3"> |
||||
|
</p> |
||||
|
|
||||
|
# Installation :package: |
||||
|
|
||||
|
## With NPM |
||||
|
|
||||
|
Run |
||||
|
|
||||
|
```bash |
||||
|
npm install vue-qrcode-reader |
||||
|
``` |
||||
|
|
||||
|
You can import the components independantly |
||||
|
|
||||
|
```javascript |
||||
|
import { QrcodeStream, QrcodeDropZone, QrcodeCapture } from 'vue-qrcode-reader' |
||||
|
|
||||
|
const MyComponent = { |
||||
|
|
||||
|
components: { |
||||
|
QrcodeStream, |
||||
|
QrcodeDropZone, |
||||
|
QrcodeCapture |
||||
|
}, |
||||
|
|
||||
|
// ... |
||||
|
)) |
||||
|
``` |
||||
|
|
||||
|
or register all of them globally right away |
||||
|
|
||||
|
```javascript |
||||
|
import Vue from "vue"; |
||||
|
import VueQrcodeReader from "vue-qrcode-reader"; |
||||
|
|
||||
|
Vue.use(VueQrcodeReader); |
||||
|
``` |
||||
|
|
||||
|
## Without NPM |
||||
|
|
||||
|
Include the following JS file: |
||||
|
|
||||
|
https://unpkg.com/vue-qrcode-reader/dist/VueQrcodeReader.umd.min.js |
||||
|
|
||||
|
Make sure to include it after Vue: |
||||
|
|
||||
|
```html |
||||
|
<script src="./vue.js"></script> |
||||
|
<script src="./VueQrcodeReader.umd.min.js"></script> |
||||
|
``` |
||||
|
|
||||
|
All components are automatically registered globally. |
||||
|
Use kebab-case to reference them in your templates: |
||||
|
|
||||
|
```html |
||||
|
<qrcode-stream></qrcode-stream> |
||||
|
<qrcode-drop-zone></qrcode-drop-zone> |
||||
|
<qrcode-capture></qrcode-capture> |
||||
|
``` |
||||
|
|
||||
|
# Troubleshooting :zap: |
||||
|
|
||||
|
### The wrong camera is picked by default. Can I set it explicitly? |
||||
|
|
||||
|
Modern devices sometimes have multiple rear cameras. |
||||
|
Not all are optimal for scanning QR codes. |
||||
|
For example wide angle, infrared and virtual cameras. |
||||
|
With the current web API it's hard to pick the right camera automatically. |
||||
|
It's technically possible to let the user make that decision. |
||||
|
For example by displaying list of installed cameras and letting the user select the right one. |
||||
|
However, this is a user experience trade-off. |
||||
|
Native QR code reader applications don't face this trade-off. |
||||
|
That's why we want to find a different solution. |
||||
|
|
||||
|
Please create a GitHub issue from the [wrong camera selected](https://github.com/gruhn/vue-qrcode-reader/issues/new?assignees=&labels=&template=wrong_camera.md&title=) template and follow the instructions in the text. |
||||
|
#### I don't see the camera when using `QrcodeStream`. |
||||
|
|
||||
|
- Check if it works on the demo page. Especially the [Decode All](https://gruhn.github.io/vue-qrcode-reader/demos/DecodeAll.html) demo, since it renders error messages. If you see errors, consult the docs to understand their meaning. |
||||
|
- The demo works but it doesn't work in my project: Listen for the `init` event to investigate errors. |
||||
|
- The demo doesn't work: Carefully review the Browser Support section above. Maybe your device is just not supported. |
||||
|
|
||||
|
#### I'm running a dev server on localhost. How to test on my mobile device without HTTPS? |
||||
|
|
||||
|
- If your setup is Desktop Chrome + Android Chrome, use [Remote Debugging](https://developers.google.com/web/tools/chrome-devtools/remote-debugging/) which allows your Android device to [access your local server as localhost](https://developers.google.com/web/tools/chrome-devtools/remote-debugging/local-server). |
||||
|
- Otherwise use a reverse proxy like [ngrok](https://ngrok.com/) or [serveo](https://serveo.net/) to temporarily make your local server publicly available with HTTPS. |
||||
|
- There are also loads of serverless/static hosting services that have HTTPS enabled by default and where you can deploy your web app for free (e.g. *GitHub Pages*, *GitLab Pages*, *Google Firebase*, *Netlify*, *Heroku*, *ZEIT Now*, ...) |
||||
|
|
||||
|
#### Some of my QR codes are not being detected. |
||||
|
|
||||
|
- Make sure, there is some white border around the QR code. |
||||
|
- Color inverted QR codes are not supported (dark background, light foreground). |
||||
|
- [Model 1 QR codes](https://en.wikipedia.org/wiki/QR_code#Model_1) are also not supported. |
||||
|
|
||||
|
# Thanks :pray: |
||||
|
|
||||
|
<a href="https://browserstack.com"> |
||||
|
<img height="38" src="https://raw.githubusercontent.com/gruhn/vue-qrcode-reader/master/.github/browserstack-logo.png" alt="BrowserStack Logo"> |
||||
|
</a> |
||||
@ -0,0 +1,3 @@ |
|||||
|
module.exports = { |
||||
|
presets: ["@vue/cli-plugin-babel/preset"] |
||||
|
}; |
||||
@ -0,0 +1,54 @@ |
|||||
|
<template> |
||||
|
<component :is="currentDemo" /> |
||||
|
</template> |
||||
|
|
||||
|
<script> |
||||
|
import CustomTracking from './demos/CustomTracking.vue' |
||||
|
import DecodeAll from './demos/DecodeAll.vue' |
||||
|
import SwitchCamera from './demos/SwitchCamera.vue' |
||||
|
import DragDrop from './demos/DragDrop.vue' |
||||
|
import Upload from './demos/Upload.vue' |
||||
|
import Fullscreen from './demos/Fullscreen.vue' |
||||
|
import LoadingIndicator from './demos/LoadingIndicator.vue' |
||||
|
import Torch from './demos/Torch.vue' |
||||
|
import Validate from './demos/Validate.vue' |
||||
|
import ScanSameQrcodeMoreThanOnce from './demos/ScanSameQrcodeMoreThanOnce.vue' |
||||
|
|
||||
|
export default { |
||||
|
components: { |
||||
|
DecodeAll, |
||||
|
CustomTracking, |
||||
|
SwitchCamera, |
||||
|
DragDrop, |
||||
|
Upload, |
||||
|
Fullscreen, |
||||
|
LoadingIndicator, |
||||
|
Torch, |
||||
|
Validate, |
||||
|
ScanSameQrcodeMoreThanOnce |
||||
|
}, |
||||
|
|
||||
|
props: { |
||||
|
component: String |
||||
|
}, |
||||
|
|
||||
|
data () { |
||||
|
return { |
||||
|
currentDemo: null |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
mounted () { |
||||
|
this.currentDemo = this.component |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<style> |
||||
|
.decode-result { |
||||
|
overflow: hidden; |
||||
|
text-overflow: ellipsis; |
||||
|
white-space: nowrap; |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,92 @@ |
|||||
|
<template> |
||||
|
<div> |
||||
|
<p> |
||||
|
Track function: |
||||
|
<select v-model="selected"> |
||||
|
<option v-for="option in options" :key="option.text" :value="option"> |
||||
|
{{ option.text }} |
||||
|
</option> |
||||
|
</select> |
||||
|
</p> |
||||
|
|
||||
|
<qrcode-stream :key="_uid" :track="selected.value" @init="logErrors" /> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script> |
||||
|
import { QrcodeStream } from '../../../../src' |
||||
|
|
||||
|
export default { |
||||
|
|
||||
|
components: { QrcodeStream }, |
||||
|
|
||||
|
data () { |
||||
|
const options = [ |
||||
|
{ text: "nothing (default)", value: undefined }, |
||||
|
{ text: "outline", value: this.paintOutline }, |
||||
|
{ text: "centered text", value: this.paintCenterText }, |
||||
|
{ text: "bounding box", value: this.paintBoundingBox }, |
||||
|
] |
||||
|
|
||||
|
const selected = options[1] |
||||
|
|
||||
|
return { selected, options } |
||||
|
}, |
||||
|
|
||||
|
methods: { |
||||
|
paintOutline (detectedCodes, ctx) { |
||||
|
for (const detectedCode of detectedCodes) { |
||||
|
const [ firstPoint, ...otherPoints ] = detectedCode.cornerPoints |
||||
|
|
||||
|
ctx.strokeStyle = "red"; |
||||
|
|
||||
|
ctx.beginPath(); |
||||
|
ctx.moveTo(firstPoint.x, firstPoint.y); |
||||
|
for (const { x, y } of otherPoints) { |
||||
|
ctx.lineTo(x, y); |
||||
|
} |
||||
|
ctx.lineTo(firstPoint.x, firstPoint.y); |
||||
|
ctx.closePath(); |
||||
|
ctx.stroke(); |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
paintBoundingBox (detectedCodes, ctx) { |
||||
|
for (const detectedCode of detectedCodes) { |
||||
|
const { boundingBox: { x, y, width, height } } = detectedCode |
||||
|
|
||||
|
ctx.lineWidth = 2 |
||||
|
ctx.strokeStyle = '#007bff' |
||||
|
ctx.strokeRect(x, y, width, height) |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
paintCenterText (detectedCodes, ctx) { |
||||
|
for (const detectedCode of detectedCodes) { |
||||
|
const { boundingBox, rawValue } = detectedCode |
||||
|
|
||||
|
const centerX = boundingBox.x + boundingBox.width/ 2 |
||||
|
const centerY = boundingBox.y + boundingBox.height/ 2 |
||||
|
|
||||
|
const fontSize = Math.max(12, 50 * boundingBox.width/ctx.canvas.width) |
||||
|
console.log(boundingBox.width, ctx.canvas.width) |
||||
|
|
||||
|
ctx.font = `bold ${fontSize}px sans-serif` |
||||
|
ctx.textAlign = "center" |
||||
|
|
||||
|
ctx.lineWidth = 3 |
||||
|
ctx.strokeStyle = '#35495e' |
||||
|
ctx.strokeText(detectedCode.rawValue, centerX, centerY) |
||||
|
|
||||
|
ctx.fillStyle = '#5cb984' |
||||
|
ctx.fillText(rawValue, centerX, centerY) |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
logErrors (promise) { |
||||
|
promise.catch(console.error) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
</script> |
||||
@ -0,0 +1,62 @@ |
|||||
|
<template> |
||||
|
<div> |
||||
|
<p class="error">{{ error }}</p> |
||||
|
|
||||
|
<p class="decode-result">Last result: <b>{{ result }}</b></p> |
||||
|
|
||||
|
<qrcode-stream @decode="onDecode" @init="onInit" /> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script> |
||||
|
import { QrcodeStream } from '../../../../src' |
||||
|
|
||||
|
export default { |
||||
|
|
||||
|
components: { QrcodeStream }, |
||||
|
|
||||
|
data () { |
||||
|
return { |
||||
|
result: '', |
||||
|
error: '' |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
methods: { |
||||
|
onDecode (result) { |
||||
|
this.result = result |
||||
|
}, |
||||
|
|
||||
|
async onInit (promise) { |
||||
|
try { |
||||
|
await promise |
||||
|
} catch (error) { |
||||
|
if (error.name === 'NotAllowedError') { |
||||
|
this.error = "ERROR: you need to grant camera access permission" |
||||
|
} else if (error.name === 'NotFoundError') { |
||||
|
this.error = "ERROR: no camera on this device" |
||||
|
} else if (error.name === 'NotSupportedError') { |
||||
|
this.error = "ERROR: secure context required (HTTPS, localhost)" |
||||
|
} else if (error.name === 'NotReadableError') { |
||||
|
this.error = "ERROR: is the camera already in use?" |
||||
|
} else if (error.name === 'OverconstrainedError') { |
||||
|
this.error = "ERROR: installed cameras are not suitable" |
||||
|
} else if (error.name === 'StreamApiNotSupportedError') { |
||||
|
this.error = "ERROR: Stream API is not supported in this browser" |
||||
|
} else if (error.name === 'InsecureContextError') { |
||||
|
this.error = 'ERROR: Camera access is only permitted in secure context. Use HTTPS or localhost rather than HTTP.'; |
||||
|
} else { |
||||
|
this.error = `ERROR: Camera error (${error.name})`; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<style scoped> |
||||
|
.error { |
||||
|
font-weight: bold; |
||||
|
color: red; |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,80 @@ |
|||||
|
<template> |
||||
|
<div> |
||||
|
<p class="decode-result">Last result: <b>{{ result }}</b></p> |
||||
|
|
||||
|
<p v-if="error !== null" class="drop-error"> |
||||
|
{{ error }} |
||||
|
</p> |
||||
|
|
||||
|
<qrcode-drop-zone @detect="onDetect" @dragover="onDragOver" @init="logErrors"> |
||||
|
<div class="drop-area" :class="{ 'dragover': dragover }"> |
||||
|
DROP SOME IMAGES HERE |
||||
|
</div> |
||||
|
</qrcode-drop-zone> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script> |
||||
|
import { QrcodeDropZone } from '../../../../src' |
||||
|
|
||||
|
export default { |
||||
|
|
||||
|
components: { QrcodeDropZone }, |
||||
|
|
||||
|
data () { |
||||
|
return { |
||||
|
result: null, |
||||
|
error: null, |
||||
|
dragover: false |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
methods: { |
||||
|
async onDetect (promise) { |
||||
|
try { |
||||
|
const { content } = await promise |
||||
|
|
||||
|
this.result = content |
||||
|
this.error = null |
||||
|
} catch (error) { |
||||
|
if (error.name === 'DropImageFetchError') { |
||||
|
this.error = 'Sorry, you can\'t load cross-origin images :/' |
||||
|
} else if (error.name === 'DropImageDecodeError') { |
||||
|
this.error = 'Ok, that\'s not an image. That can\'t be decoded.' |
||||
|
} else { |
||||
|
this.error = 'Ups, what kind of error is this?! ' + error.message |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
logErrors (promise) { |
||||
|
promise.catch(console.error) |
||||
|
}, |
||||
|
|
||||
|
onDragOver (isDraggingOver) { |
||||
|
this.dragover = isDraggingOver |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<style> |
||||
|
.drop-area { |
||||
|
height: 300px; |
||||
|
color: #fff; |
||||
|
text-align: center; |
||||
|
font-weight: bold; |
||||
|
padding: 10px; |
||||
|
|
||||
|
background-color: rgba(0,0,0,.5); |
||||
|
} |
||||
|
|
||||
|
.dragover { |
||||
|
background-color: rgba(0,0,0,.8); |
||||
|
} |
||||
|
|
||||
|
.drop-error { |
||||
|
color: red; |
||||
|
font-weight: bold; |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,115 @@ |
|||||
|
<template> |
||||
|
<div :class="{ 'fullscreen': fullscreen }" ref="wrapper" @fullscreenchange="onFullscreenChange"> |
||||
|
<qrcode-stream @init="logErrors"> |
||||
|
<button @click="fullscreen = !fullscreen" class="fullscreen-button"> |
||||
|
<img :src="$withBase(fullscreenIcon)" alt="toggle fullscreen" /> |
||||
|
</button> |
||||
|
</qrcode-stream> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script> |
||||
|
import { QrcodeStream } from '../../../../src' |
||||
|
|
||||
|
// NOTE: calling `requestFullscreen` might prompt the user with another |
||||
|
// permission dialog. You already asked for camera access permission so this is |
||||
|
// a rather invasive move. |
||||
|
// |
||||
|
// Even without calling `requestFullscreen` the entire viewport is covered |
||||
|
// by the camera stream. So consider skipping `requestFullscreen` in your |
||||
|
// implementation. |
||||
|
|
||||
|
export default { |
||||
|
|
||||
|
components: { QrcodeStream }, |
||||
|
|
||||
|
data () { |
||||
|
return { |
||||
|
fullscreen: false |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
computed: { |
||||
|
fullscreenIcon() { |
||||
|
if (this.fullscreen) { |
||||
|
return "/fullscreen-exit.svg" |
||||
|
} else { |
||||
|
return "/fullscreen.svg" |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
watch: { |
||||
|
fullscreen(enterFullscreen) { |
||||
|
if (enterFullscreen) { |
||||
|
this.requestFullscreen() |
||||
|
} else { |
||||
|
this.exitFullscreen() |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
methods: { |
||||
|
onFullscreenChange(event) { |
||||
|
// This becomes important when the user doesn't use the button to exit |
||||
|
// fullscreen but hits ESC on desktop, pushes a physical back button on |
||||
|
// mobile etc. |
||||
|
|
||||
|
this.fullscreen = document.fullscreenElement !== null |
||||
|
}, |
||||
|
|
||||
|
requestFullscreen() { |
||||
|
const elem = this.$refs.wrapper |
||||
|
|
||||
|
if (elem.requestFullscreen) { |
||||
|
elem.requestFullscreen(); |
||||
|
} else if (elem.mozRequestFullScreen) { /* Firefox */ |
||||
|
elem.mozRequestFullScreen(); |
||||
|
} else if (elem.webkitRequestFullscreen) { /* Chrome, Safari and Opera */ |
||||
|
elem.webkitRequestFullscreen(); |
||||
|
} else if (elem.msRequestFullscreen) { /* IE/Edge */ |
||||
|
elem.msRequestFullscreen(); |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
exitFullscreen() { |
||||
|
if (document.exitFullscreen) { |
||||
|
document.exitFullscreen(); |
||||
|
} else if (document.mozCancelFullScreen) { /* Firefox */ |
||||
|
document.mozCancelFullScreen(); |
||||
|
} else if (document.webkitExitFullscreen) { /* Chrome, Safari and Opera */ |
||||
|
document.webkitExitFullscreen(); |
||||
|
} else if (document.msExitFullscreen) { /* IE/Edge */ |
||||
|
document.msExitFullscreen(); |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
logErrors (promise) { |
||||
|
promise.catch(console.error) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<style scoped> |
||||
|
.fullscreen { |
||||
|
position: fixed; |
||||
|
z-index: 1000; |
||||
|
top: 0; |
||||
|
bottom: 0; |
||||
|
right: 0; |
||||
|
left: 0; |
||||
|
} |
||||
|
|
||||
|
.fullscreen-button { |
||||
|
background-color: white; |
||||
|
position: absolute; |
||||
|
bottom: 0; |
||||
|
right: 0; |
||||
|
margin: 1rem; |
||||
|
} |
||||
|
.fullscreen-button img { |
||||
|
width: 2rem; |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,61 @@ |
|||||
|
<template> |
||||
|
<div> |
||||
|
<button @click="reload">Destroy And Re-Create Component</button> |
||||
|
|
||||
|
<qrcode-stream @init="onInit" v-if="!destroyed"> |
||||
|
<div class="loading-indicator" v-if="loading"> |
||||
|
Loading... |
||||
|
</div> |
||||
|
</qrcode-stream> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script> |
||||
|
import { QrcodeStream } from '../../../../src' |
||||
|
|
||||
|
export default { |
||||
|
|
||||
|
components: { QrcodeStream }, |
||||
|
|
||||
|
data () { |
||||
|
return { |
||||
|
loading: false, |
||||
|
destroyed: false |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
methods: { |
||||
|
async onInit (promise) { |
||||
|
this.loading = true |
||||
|
|
||||
|
try { |
||||
|
await promise |
||||
|
} catch (error) { |
||||
|
console.error(error) |
||||
|
} finally { |
||||
|
this.loading = false |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
async reload () { |
||||
|
this.destroyed = true |
||||
|
|
||||
|
await this.$nextTick() |
||||
|
|
||||
|
this.destroyed = false |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<style scoped> |
||||
|
button { |
||||
|
margin-bottom: 20px; |
||||
|
} |
||||
|
|
||||
|
.loading-indicator { |
||||
|
font-weight: bold; |
||||
|
font-size: 2rem; |
||||
|
text-align: center; |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,78 @@ |
|||||
|
<template> |
||||
|
<div> |
||||
|
<p class="decode-result">Last result: <b>{{ result }}</b></p> |
||||
|
|
||||
|
|
||||
|
<qrcode-stream :camera="camera" @decode="onDecode" @init="onInit"> |
||||
|
<div v-show="showScanConfirmation" class="scan-confirmation"> |
||||
|
<img :src="$withBase('/checkmark.svg')" alt="Checkmark" width="128px" /> |
||||
|
</div> |
||||
|
</qrcode-stream> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script> |
||||
|
import { QrcodeStream } from '../../../../src' |
||||
|
|
||||
|
export default { |
||||
|
|
||||
|
components: { QrcodeStream }, |
||||
|
|
||||
|
data () { |
||||
|
return { |
||||
|
camera: 'auto', |
||||
|
result: null, |
||||
|
showScanConfirmation: false |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
methods: { |
||||
|
|
||||
|
async onInit (promise) { |
||||
|
try { |
||||
|
await promise |
||||
|
} catch (e) { |
||||
|
console.error(e) |
||||
|
} finally { |
||||
|
this.showScanConfirmation = this.camera === "off" |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
async onDecode (content) { |
||||
|
this.result = content |
||||
|
|
||||
|
this.pause() |
||||
|
await this.timeout(500) |
||||
|
this.unpause() |
||||
|
}, |
||||
|
|
||||
|
unpause () { |
||||
|
this.camera = 'auto' |
||||
|
}, |
||||
|
|
||||
|
pause () { |
||||
|
this.camera = 'off' |
||||
|
}, |
||||
|
|
||||
|
timeout (ms) { |
||||
|
return new Promise(resolve => { |
||||
|
window.setTimeout(resolve, ms) |
||||
|
}) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<style scoped> |
||||
|
.scan-confirmation { |
||||
|
position: absolute; |
||||
|
width: 100%; |
||||
|
height: 100%; |
||||
|
|
||||
|
background-color: rgba(255, 255, 255, .8); |
||||
|
|
||||
|
display: flex; |
||||
|
flex-flow: row nowrap; |
||||
|
justify-content: center; |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,82 @@ |
|||||
|
<template> |
||||
|
<div> |
||||
|
<p class="error" v-if="noFrontCamera"> |
||||
|
You don't seem to have a front camera on your device |
||||
|
</p> |
||||
|
|
||||
|
<p class="error" v-if="noRearCamera"> |
||||
|
You don't seem to have a rear camera on your device |
||||
|
</p> |
||||
|
|
||||
|
<qrcode-stream :camera="camera" @init="onInit"> |
||||
|
<button @click="switchCamera"> |
||||
|
<img :src="$withBase('/camera-switch.svg')" alt="switch camera"> |
||||
|
</button> |
||||
|
</qrcode-stream> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script> |
||||
|
import { QrcodeStream } from '../../../../src' |
||||
|
|
||||
|
export default { |
||||
|
|
||||
|
components: { QrcodeStream }, |
||||
|
|
||||
|
data () { |
||||
|
return { |
||||
|
camera: 'rear', |
||||
|
|
||||
|
noRearCamera: false, |
||||
|
noFrontCamera: false |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
methods: { |
||||
|
switchCamera () { |
||||
|
switch (this.camera) { |
||||
|
case 'front': |
||||
|
this.camera = 'rear' |
||||
|
break |
||||
|
case 'rear': |
||||
|
this.camera = 'front' |
||||
|
break |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
async onInit (promise) { |
||||
|
try { |
||||
|
await promise |
||||
|
} catch (error) { |
||||
|
const triedFrontCamera = this.camera === 'front' |
||||
|
const triedRearCamera = this.camera === 'rear' |
||||
|
|
||||
|
const cameraMissingError = error.name === 'OverconstrainedError' |
||||
|
|
||||
|
if (triedRearCamera && cameraMissingError) { |
||||
|
this.noRearCamera = true |
||||
|
} |
||||
|
|
||||
|
if (triedFrontCamera && cameraMissingError) { |
||||
|
this.noFrontCamera = true |
||||
|
} |
||||
|
|
||||
|
console.error(error) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<style scoped> |
||||
|
button { |
||||
|
position: absolute; |
||||
|
left: 10px; |
||||
|
top: 10px; |
||||
|
} |
||||
|
|
||||
|
.error { |
||||
|
color: red; |
||||
|
font-weight: bold; |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,64 @@ |
|||||
|
<template> |
||||
|
<div> |
||||
|
<p v-if="torchNotSupported" class="error"> |
||||
|
Torch not supported for active camera |
||||
|
</p> |
||||
|
|
||||
|
<qrcode-stream :torch="torchActive" @init="onInit"> |
||||
|
<button @click="torchActive = !torchActive" :disabled="torchNotSupported"> |
||||
|
<img :src="$withBase(icon)" alt="toggle torch"> |
||||
|
</button> |
||||
|
</qrcode-stream> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script> |
||||
|
import { QrcodeStream } from '../../../../src' |
||||
|
|
||||
|
export default { |
||||
|
|
||||
|
components: { QrcodeStream }, |
||||
|
|
||||
|
data () { |
||||
|
return { |
||||
|
torchActive: false, |
||||
|
torchNotSupported: false |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
computed: { |
||||
|
icon() { |
||||
|
if (this.torchActive) |
||||
|
return '/flash-off.svg' |
||||
|
else |
||||
|
return '/flash-on.svg' |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
methods: { |
||||
|
async onInit (promise) { |
||||
|
try { |
||||
|
const { capabilities } = await promise |
||||
|
|
||||
|
console.log(capabilities); |
||||
|
|
||||
|
this.torchNotSupported = !capabilities.torch |
||||
|
} catch (error) { |
||||
|
console.error(error) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<style scoped> |
||||
|
button { |
||||
|
position: absolute; |
||||
|
left: 10px; |
||||
|
top: 10px; |
||||
|
} |
||||
|
.error { |
||||
|
color: red; |
||||
|
font-weight: bold; |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,47 @@ |
|||||
|
<template> |
||||
|
<div> |
||||
|
<p> |
||||
|
Capture: |
||||
|
<select v-model="selected"> |
||||
|
<option v-for="option in options" :key="option.text" :value="option"> |
||||
|
{{ option.text }} |
||||
|
</option> |
||||
|
</select> |
||||
|
</p> |
||||
|
|
||||
|
<hr/> |
||||
|
|
||||
|
<p class="decode-result">Last result: <b>{{ result }}</b></p> |
||||
|
|
||||
|
<qrcode-capture @decode="onDecode" :capture="selected.value" /> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script> |
||||
|
import { QrcodeCapture } from '../../../../src' |
||||
|
|
||||
|
export default { |
||||
|
|
||||
|
components: { QrcodeCapture }, |
||||
|
|
||||
|
data () { |
||||
|
const options = [ |
||||
|
{ text: "rear camera (default)", value: "environment" }, |
||||
|
{ text: "front camera", value: "user" }, |
||||
|
{ text: "force file dialog", value: false }, |
||||
|
] |
||||
|
|
||||
|
return { |
||||
|
result: '', |
||||
|
options, |
||||
|
selected: options[0] |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
methods: { |
||||
|
onDecode (result) { |
||||
|
this.result = result |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
@ -0,0 +1,118 @@ |
|||||
|
<template> |
||||
|
<div> |
||||
|
<p class="decode-result">Last result: <b>{{ result }}</b></p> |
||||
|
|
||||
|
<qrcode-stream :camera="camera" @decode="onDecode" @init="onInit"> |
||||
|
<div v-if="validationSuccess" class="validation-success"> |
||||
|
This is a URL |
||||
|
</div> |
||||
|
|
||||
|
<div v-if="validationFailure" class="validation-failure"> |
||||
|
This is NOT a URL! |
||||
|
</div> |
||||
|
|
||||
|
<div v-if="validationPending" class="validation-pending"> |
||||
|
Long validation in progress... |
||||
|
</div> |
||||
|
</qrcode-stream> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script> |
||||
|
import { QrcodeStream } from '../../../../src' |
||||
|
|
||||
|
export default { |
||||
|
|
||||
|
components: { QrcodeStream }, |
||||
|
|
||||
|
data () { |
||||
|
return { |
||||
|
isValid: undefined, |
||||
|
camera: 'auto', |
||||
|
result: null, |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
computed: { |
||||
|
validationPending () { |
||||
|
return this.isValid === undefined |
||||
|
&& this.camera === 'off' |
||||
|
}, |
||||
|
|
||||
|
validationSuccess () { |
||||
|
return this.isValid === true |
||||
|
}, |
||||
|
|
||||
|
validationFailure () { |
||||
|
return this.isValid === false |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
methods: { |
||||
|
|
||||
|
onInit (promise) { |
||||
|
promise |
||||
|
.catch(console.error) |
||||
|
.then(this.resetValidationState) |
||||
|
}, |
||||
|
|
||||
|
resetValidationState () { |
||||
|
this.isValid = undefined |
||||
|
}, |
||||
|
|
||||
|
async onDecode (content) { |
||||
|
this.result = content |
||||
|
this.turnCameraOff() |
||||
|
|
||||
|
// pretend it's taking really long |
||||
|
await this.timeout(3000) |
||||
|
this.isValid = content.startsWith('http') |
||||
|
|
||||
|
// some more delay, so users have time to read the message |
||||
|
await this.timeout(2000) |
||||
|
|
||||
|
this.turnCameraOn() |
||||
|
}, |
||||
|
|
||||
|
turnCameraOn () { |
||||
|
this.camera = 'auto' |
||||
|
}, |
||||
|
|
||||
|
turnCameraOff () { |
||||
|
this.camera = 'off' |
||||
|
}, |
||||
|
|
||||
|
timeout (ms) { |
||||
|
return new Promise(resolve => { |
||||
|
window.setTimeout(resolve, ms) |
||||
|
}) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<style scoped> |
||||
|
.validation-success, |
||||
|
.validation-failure, |
||||
|
.validation-pending { |
||||
|
position: absolute; |
||||
|
width: 100%; |
||||
|
height: 100%; |
||||
|
|
||||
|
background-color: rgba(255, 255, 255, .8); |
||||
|
text-align: center; |
||||
|
font-weight: bold; |
||||
|
font-size: 1.4rem; |
||||
|
padding: 10px; |
||||
|
|
||||
|
display: flex; |
||||
|
flex-flow: column nowrap; |
||||
|
justify-content: center; |
||||
|
} |
||||
|
.validation-success { |
||||
|
color: green; |
||||
|
} |
||||
|
.validation-failure { |
||||
|
color: red; |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,41 @@ |
|||||
|
module.exports = { |
||||
|
title: 'Vue Qrcode Reader', |
||||
|
description: 'A set of Vue.js components for detecting and decoding QR codes.', |
||||
|
|
||||
|
base: '/vue-qrcode-reader/', |
||||
|
|
||||
|
extraWatchFiles: [ |
||||
|
'../src/' |
||||
|
], |
||||
|
|
||||
|
themeConfig: { |
||||
|
repo: 'gruhn/vue-qrcode-reader', |
||||
|
|
||||
|
sidebar: { |
||||
|
'/demos/': [ |
||||
|
'Simple', |
||||
|
'DecodeAll', |
||||
|
'CustomTracking', |
||||
|
'LoadingIndicator', |
||||
|
'ScanSameQrcodeMoreThanOnce', |
||||
|
'Validate', |
||||
|
'SwitchCamera', |
||||
|
'Fullscreen', |
||||
|
'Torch', |
||||
|
'DragDrop', |
||||
|
'Upload' |
||||
|
], |
||||
|
|
||||
|
'/api/': [ |
||||
|
'QrcodeStream', |
||||
|
'QrcodeDropZone', |
||||
|
'QrcodeCapture' |
||||
|
], |
||||
|
}, |
||||
|
|
||||
|
nav: [ |
||||
|
{ text: 'Live Demos', link: '/demos/CustomTracking' }, |
||||
|
{ text: 'API Reference', link: '/api/QrcodeStream' } |
||||
|
] |
||||
|
} |
||||
|
} |
||||
|
After Width: | Height: | Size: 445 B |
|
After Width: | Height: | Size: 499 B |
@ -0,0 +1,102 @@ |
|||||
|
<!DOCTYPE html> |
||||
|
<html lang="en"> |
||||
|
<head> |
||||
|
<meta charset="UTF-8"> |
||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
||||
|
<title>Document</title> |
||||
|
</head> |
||||
|
<body> |
||||
|
<video id="video" autoplay muted playsinline></video> |
||||
|
</body> |
||||
|
<script type="module"> |
||||
|
import BarcodeDetector from "https://cdn.skypack.dev/barcode-detector@0.5.0"; |
||||
|
|
||||
|
const adaptOldFormat = detectedCodes => { |
||||
|
if (detectedCodes.length > 0) { |
||||
|
const [ firstCode ] = detectedCodes; |
||||
|
|
||||
|
const [ |
||||
|
topLeftCorner, |
||||
|
topRightCorner, |
||||
|
bottomRightCorner, |
||||
|
bottomLeftCorner |
||||
|
] = firstCode.cornerPoints |
||||
|
|
||||
|
return { |
||||
|
content: firstCode.rawValue, |
||||
|
location: { |
||||
|
topLeftCorner, |
||||
|
topRightCorner, |
||||
|
bottomRightCorner, |
||||
|
bottomLeftCorner, |
||||
|
|
||||
|
// not supported by native API: |
||||
|
topLeftFinderPattern: {}, |
||||
|
topRightFinderPattern: {}, |
||||
|
bottomLeftFinderPattern: {} |
||||
|
}, |
||||
|
imageData: null |
||||
|
} |
||||
|
} else { |
||||
|
return { |
||||
|
content: null, |
||||
|
location: null, |
||||
|
imageData: null |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export const keepScanning = (videoElement, options) => { |
||||
|
const barcodeDetector = new BarcodeDetector({ formats: ["qr_code"] }); |
||||
|
|
||||
|
const { detectHandler, locateHandler, minDelay } = options; |
||||
|
|
||||
|
const processFrame = state => async timeNow => { |
||||
|
if (videoElement.readyState > 1) { |
||||
|
console.log("scan") |
||||
|
const { lastScanned, contentBefore, locationBefore } = state |
||||
|
|
||||
|
if (timeNow - lastScanned >= minDelay) { |
||||
|
const detectedCodes = await barcodeDetector.detect(videoElement); |
||||
|
const { content, location, imageData } = adaptOldFormat(detectedCodes) |
||||
|
|
||||
|
if (content !== null && content !== contentBefore) { |
||||
|
detectHandler({ content, location, imageData }); |
||||
|
} |
||||
|
|
||||
|
if (location !== null || locationBefore !== null) { |
||||
|
locateHandler(detectedCodes); |
||||
|
} |
||||
|
|
||||
|
window.requestAnimationFrame(processFrame({ |
||||
|
lastScanned: timeNow, |
||||
|
contentBefore: content ?? contentBefore, |
||||
|
locationBefore: location |
||||
|
})) |
||||
|
} else { |
||||
|
window.requestAnimationFrame(processFrame(state)) |
||||
|
} |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
processFrame({ |
||||
|
contentBefore: null, |
||||
|
locationBefore: null, |
||||
|
lastScanned: performance.now() |
||||
|
})(); |
||||
|
}; |
||||
|
|
||||
|
(async () => { |
||||
|
const videoEl = document.querySelector('#video') |
||||
|
|
||||
|
const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: false }) |
||||
|
|
||||
|
videoEl.srcObject = stream |
||||
|
|
||||
|
videoEl.addEventListener("loadeddata", () => { |
||||
|
keepScanning(videoEl, { minDelay: 40, detectHandler: console.log, locateHandler: console.log }) |
||||
|
}) |
||||
|
|
||||
|
})() |
||||
|
</script> |
||||
|
</html> |
||||
|
After Width: | Height: | Size: 396 B |
|
After Width: | Height: | Size: 325 B |
|
After Width: | Height: | Size: 380 B |
|
After Width: | Height: | Size: 381 B |
|
After Width: | Height: | Size: 22 KiB |
@ -0,0 +1,76 @@ |
|||||
|
<!DOCTYPE html> |
||||
|
<html lang="en"> |
||||
|
<head> |
||||
|
<meta charset="UTF-8"> |
||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
||||
|
<meta http-equiv="X-UA-Compatible" content="ie=edge"> |
||||
|
<script src="https://cdn.jsdelivr.net/npm/webrtc-adapter@7.6.1/out/adapter.js"></script> |
||||
|
</head> |
||||
|
<body> |
||||
|
cameras: <br> |
||||
|
<ul></ul> |
||||
|
<br> |
||||
|
<video autoplay muted playsinline></video> |
||||
|
|
||||
|
<script> |
||||
|
const listEl = document.querySelector("ul") |
||||
|
|
||||
|
const renderOptions = (currentDeviceId, devices) => { |
||||
|
listEl.innerHTML = "" |
||||
|
|
||||
|
devices |
||||
|
.filter(deviceInfo => deviceInfo.kind === "videoinput") |
||||
|
.map(({ label, deviceId }) => { |
||||
|
const el = document.createElement('li') |
||||
|
|
||||
|
if (deviceId == currentDeviceId) |
||||
|
el.innerHTML = `<a href="#" onclick="selectCamera('${deviceId}')">${label}</a> [PREFERRED]` |
||||
|
else |
||||
|
el.innerHTML = `<a href="#" onclick="selectCamera('${deviceId}')">${label}</a>` |
||||
|
|
||||
|
return el |
||||
|
}) |
||||
|
.forEach(el => listEl.appendChild(el)) |
||||
|
} |
||||
|
|
||||
|
|
||||
|
let stream |
||||
|
|
||||
|
const selectCamera = async deviceId => { |
||||
|
try { |
||||
|
console.log(deviceId) |
||||
|
|
||||
|
if (stream) { |
||||
|
stream.getTracks().forEach(track => track.stop()) |
||||
|
} |
||||
|
|
||||
|
const videoConstraints = {}; |
||||
|
if (!deviceId) { |
||||
|
videoConstraints.facingMode = 'environment'; |
||||
|
} else { |
||||
|
videoConstraints.deviceId = { exact: deviceId }; |
||||
|
} |
||||
|
|
||||
|
stream = await navigator.mediaDevices.getUserMedia({ |
||||
|
audio: false, |
||||
|
video: videoConstraints, |
||||
|
}) |
||||
|
|
||||
|
const videoEl = document.querySelector('video') |
||||
|
videoEl.srcObject = stream |
||||
|
|
||||
|
const [ videoTrack ] = stream.getVideoTracks() |
||||
|
|
||||
|
renderOptions( |
||||
|
videoTrack.getSettings().deviceId, |
||||
|
await navigator.mediaDevices.enumerateDevices() |
||||
|
) |
||||
|
} catch (error) { |
||||
|
console.error(error) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
selectCamera() |
||||
|
</script> |
||||
|
</body> |
||||
|
</html> |
||||
@ -0,0 +1,7 @@ |
|||||
|
--- |
||||
|
home: true |
||||
|
heroImage: /logo.png |
||||
|
actionText: See Demos → |
||||
|
actionLink: /demos/CustomTracking |
||||
|
footer: MIT Licensed | Copyright © 2017-present Niklas Gruhn |
||||
|
--- |
||||
@ -0,0 +1,95 @@ |
|||||
|
# QrcodeCapture |
||||
|
|
||||
|
## Browser Support |
||||
|
|
||||
|
The newest API this component depend on is the [FileReader API](https://caniuse.com/#feat=filereader). Vue Native is not supported (see [#206](https://github.com/gruhn/vue-qrcode-reader/issues/206)). |
||||
|
|
||||
|
|  |  |  |  |  | |
||||
|
| :---------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------: | :--------------------------------------------------------------------------------------------------: | :--------------------------------------------------------------------------------------------------: | |
||||
|
| 10+ | Yes | Yes | Yes | Yes¹ | |
||||
|
|
||||
|
1. It doesn't work in web apps added to home screen (PWA mode) on iOS prior to 11.3 (see [this StackOverflow question](https://stackoverflow.com/questions/46228218/how-to-access-camera-on-ios11-home-screen-web-app)) |
||||
|
|
||||
|
## Events |
||||
|
|
||||
|
### `decode` |
||||
|
* **Payload Type:** `String` |
||||
|
|
||||
|
The component renders to a simple file picker `input` element. Clicking opens a file dialog. On supporting mobile devices the camera is started to take a picture. The selected images are directly scanned and positive results are indicated by the `decode` event. You can also select multiple images at the same time (still one event per image though). |
||||
|
|
||||
|
However, if no QR code is pictured or an error occurs, `decode` silently fails. |
||||
|
|
||||
|
```html |
||||
|
<qrcode-capture @decode="onDecode" /> |
||||
|
``` |
||||
|
```javascript |
||||
|
methods: { |
||||
|
onDecode (decodedString) { |
||||
|
// ... |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
### `detect` |
||||
|
* **Payload Type:** `Promise<Object>` |
||||
|
|
||||
|
The `detect` event is basically a verbose version of `decode`. `detect` is emitted as soon as you confirm your file selection or the foto you took with your camera. `detect` carries a Promise which resolves when scanning the images has finished. The Promise rejects in case of errors. Additionally, `detect` gives you the unprocessed raw image data and the coordinates of the QR code in the image. |
||||
|
|
||||
|
```html |
||||
|
<qrcode-capture @detect="onDetect" /> |
||||
|
``` |
||||
|
```javascript |
||||
|
methods: { |
||||
|
async onDetect (promise) { |
||||
|
try { |
||||
|
const { |
||||
|
imageData, // raw image data of image/frame |
||||
|
content, // decoded String or null |
||||
|
location // QR code coordinates or null |
||||
|
} = await promise |
||||
|
|
||||
|
if (content === null) { |
||||
|
// decoded nothing |
||||
|
} else { |
||||
|
// ... |
||||
|
} |
||||
|
} catch (error) { |
||||
|
// ... |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
## Props |
||||
|
|
||||
|
`QrcodeCapture` has no explicitly defined props. |
||||
|
However, checkout the components template: |
||||
|
|
||||
|
```html |
||||
|
<template lang="html"> |
||||
|
<input |
||||
|
@change="onChangeInput" |
||||
|
type="file" |
||||
|
name="image" |
||||
|
accept="image/*" |
||||
|
capture="environment" |
||||
|
multiple |
||||
|
/> |
||||
|
</template> |
||||
|
``` |
||||
|
|
||||
|
Because the `input` element is the root element of the component and because Vue components accept [non-prop attributes](https://vuejs.org/v2/guide/components-props.html#Non-Prop-Attributes) you can make use of any valid `input` attribute: |
||||
|
|
||||
|
```html |
||||
|
<qrcode-capture disabled /> |
||||
|
``` |
||||
|
|
||||
|
You can even remove or replace already defined attributes: |
||||
|
|
||||
|
```html |
||||
|
<qrcode-capture :multiple="false" capture="user" /> |
||||
|
``` |
||||
|
|
||||
|
## Slots |
||||
|
|
||||
|
> no slots |
||||
@ -0,0 +1,118 @@ |
|||||
|
# QrcodeDropZone |
||||
|
|
||||
|
## Browser Support |
||||
|
|
||||
|
The newest API this component depend on is the [FileReader API](https://caniuse.com/#feat=filereader). |
||||
|
Vue Native is not supported (see [#206](https://github.com/gruhn/vue-qrcode-reader/issues/206)). |
||||
|
|
||||
|
|  |  |  |  |  | |
||||
|
| :---------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------: | :--------------------------------------------------------------------------------------------------: | :--------------------------------------------------------------------------------------------------: | |
||||
|
| 10+ | Yes | Yes | Yes | Yes | |
||||
|
|
||||
|
|
||||
|
## Events |
||||
|
|
||||
|
### `decode` |
||||
|
* **Payload Type:** `String` |
||||
|
|
||||
|
You can drag-and-drop image files from your desktop or images embedded into other web pages anywhere in the area the component occupies. The images are directly scanned and positive results are indicated by the `decode` event. You can also drop multiple images at the same time (still one event per image though). |
||||
|
|
||||
|
However, if no QR code is pictured or an error occurs, `decode` silently fails. |
||||
|
|
||||
|
```html |
||||
|
<qrcode-drop-zone @decode="onDecode"> |
||||
|
<!-- ... --> |
||||
|
</qrcode-drop-zone> |
||||
|
``` |
||||
|
```javascript |
||||
|
methods: { |
||||
|
onDecode (decodedString) { |
||||
|
// ... |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
### `detect` |
||||
|
* **Payload Type:** `Promise<Object>` |
||||
|
|
||||
|
The `detect` event is basically a verbose version of `decode`. `detect` is emitted as soon as you drop an image. It carries a Promise which resolves when scanning the dropped image has finished. The Promise rejects in case of errors. Additionally, `detect` gives you the unprocessed raw image data and the coordinates of the QR code in the image. |
||||
|
|
||||
|
```html |
||||
|
<qrcode-drop-zone @detect="onDetect"> |
||||
|
<!-- ... --> |
||||
|
</qrcode-drop-zone> |
||||
|
``` |
||||
|
```javascript |
||||
|
methods: { |
||||
|
async onDetect (promise) { |
||||
|
try { |
||||
|
const { |
||||
|
imageData, // raw image data of image/frame |
||||
|
content, // decoded String or null |
||||
|
location // QR code coordinates or null |
||||
|
} = await promise |
||||
|
|
||||
|
if (content === null) { |
||||
|
// decoded nothing |
||||
|
} else { |
||||
|
// ... |
||||
|
} |
||||
|
} catch (error) { |
||||
|
if (error.name === 'DropImageFetchError') { |
||||
|
// drag-and-dropped URL (probably just an <img> element) from different |
||||
|
// domain without CORS header caused same-origin-policy violation |
||||
|
} else if (error.name === 'DropImageDecodeError') { |
||||
|
// drag-and-dropped file is not of type image and can't be decoded |
||||
|
} else { |
||||
|
// idk, open an issue ¯\_(ツ)_/¯ |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
### `dragover` |
||||
|
* **Payload Type:** `Boolean` |
||||
|
|
||||
|
When the user is dragging something over the the component you might want to apply some emphasizing styling. Do that by reacting to the `dragover` event. |
||||
|
|
||||
|
```html |
||||
|
<qrcode-drop-zone @dragover="onDragOver"> |
||||
|
<div :class="{ highlight: draggingOver }"> |
||||
|
<!-- ... --> |
||||
|
</div> |
||||
|
</qrcode-drop-zone> |
||||
|
``` |
||||
|
```javascript |
||||
|
data () { |
||||
|
return { |
||||
|
draggingOver: false |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
methods: { |
||||
|
onDragOver (draggingOver) { |
||||
|
this.draggingOver = draggingOver |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
::: warning |
||||
|
This is a custom event not to be confused with [native `dragover`](https://developer.mozilla.org/en-US/docs/Web/Events/dragover). If you really need to listen for the DOM event instead, use [Vues `native` event modifier](https://vuejs.org/v2/guide/components-custom-events.html#Binding-Native-Events-to-Components). |
||||
|
::: |
||||
|
|
||||
|
## Props |
||||
|
|
||||
|
> no props |
||||
|
|
||||
|
## Slots |
||||
|
|
||||
|
### default |
||||
|
|
||||
|
This component merely renders a wrapper `div`. Its height is defined by the content inside so it will have zero height if you don't provide any content. |
||||
|
|
||||
|
```html |
||||
|
<qrcode-drop-zone> |
||||
|
<b>put anything here</b> |
||||
|
</qrcode-drop-zone> |
||||
|
``` |
||||
@ -0,0 +1,226 @@ |
|||||
|
# QrcodeStream |
||||
|
|
||||
|
## Browser Support |
||||
|
|
||||
|
This component fundamentally depends on the [Stream API](https://caniuse.com/#feat=stream). |
||||
|
Vue Native is not supported (see [#206](https://github.com/gruhn/vue-qrcode-reader/issues/206)). |
||||
|
|
||||
|
|  |  |  |  |  | |
||||
|
| :---------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------: | :--------------------------------------------------------------------------------------------------: | :--------------------------------------------------------------------------------------------------: | |
||||
|
| No | Yes | Yes | Yes¹ | Yes² | |
||||
|
|
||||
|
1. Chrome requires [HTTPS or localhost](https://sites.google.com/a/chromium.org/dev/Home/chromium-security/deprecating-powerful-features-on-insecure-origins) (see _Troubleshooting_ for help) |
||||
|
2. Safari also requires HTTPS **even** on localhost (see [#48](https://github.com/gruhn/vue-qrcode-reader/issues/48)). Support is limited for: |
||||
|
- web apps added to home screen (PWA mode): at least **iOS 13.4** (see [#76](https://github.com/gruhn/vue-qrcode-reader/issues/76)) |
||||
|
- iOS browsers other than Safari (_Chrome for iOS_, _Firefox for iOS_, ...): at least **iOS 14.3** (see [#29](https://github.com/gruhn/vue-qrcode-reader/issues/29)) |
||||
|
- WkWebView component in native iOS apps: at least **iOS 14.3** (see [#29](https://github.com/gruhn/vue-qrcode-reader/issues/29)) |
||||
|
|
||||
|
|
||||
|
## Events |
||||
|
|
||||
|
### `decode` |
||||
|
* **Payload Type:** `String` |
||||
|
|
||||
|
Once a stream from the users camera is loaded, it's displayed and continuously scanned for QR codes. Results are indicated by the `decode` event. |
||||
|
|
||||
|
```html |
||||
|
<qrcode-stream @decode="onDecode"></qrcode-stream> |
||||
|
``` |
||||
|
```javascript |
||||
|
methods: { |
||||
|
onDecode (decodedString) { |
||||
|
// ... |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
::: tip |
||||
|
If you scan the same QR code multiple times in a row, `decode` is still only emitted once. When you hold a QR code in the camera, frames are actually decoded multiple times a second but you don't want to be flooded with `decode` events that often. That's why the last decoded QR code is always cached and only new results are propagated. However changing the value of `camera` resets this internal cache. |
||||
|
::: |
||||
|
|
||||
|
### `detect` |
||||
|
* **Payload Type:** `Promise<Object>` |
||||
|
|
||||
|
The `detect` event is basically a verbose version of `decode`. `decode` only gives you the string encoded by QR codes. `detect` on the other hand ... |
||||
|
|
||||
|
* is always emitted before `decode` |
||||
|
* gives you the coordinates of the QR code in the camera frame |
||||
|
* does NOT silently fail in case of errors |
||||
|
|
||||
|
```html |
||||
|
<qrcode-stream @detect="onDetect"></qrcode-stream> |
||||
|
``` |
||||
|
```javascript |
||||
|
methods: { |
||||
|
async onDetect (promise) { |
||||
|
try { |
||||
|
const { |
||||
|
content, // decoded String |
||||
|
location // QR code coordinates |
||||
|
} = await promise |
||||
|
|
||||
|
// ... |
||||
|
} catch (error) { |
||||
|
// ... |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
### `init` |
||||
|
* **Payload Type:** `Promise<MediaTrackCapabilities>` |
||||
|
|
||||
|
It might take a while before the component is ready and the scanning process starts. The user has to be asked for camera access permission first and the camera stream has to be loaded. |
||||
|
|
||||
|
If you want to show a loading indicator, you can listen for the `init` event. It's emitted as soon as the component is mounted. It carries a promise which resolves with the cameras [MediaTrackCapabilities](https://developer.mozilla.org/en-US/docs/Web/API/MediaStreamTrack/getCapabilities) when everything is ready. The promise is rejected if initialization fails. This can have [a couple of reasons](https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia#Exceptions). |
||||
|
|
||||
|
::: warning |
||||
|
In Chrome you can't prompt users for permissions a second time. Once denied, users can only manually grant them. Make sure your users understand why you need access to their camera **before** you mount this component. Otherwise they might panic and deny and then get frustrated because they don't know how to change their decision. |
||||
|
::: |
||||
|
|
||||
|
```html |
||||
|
<qrcode-stream @init="onInit"></qrcode-stream> |
||||
|
``` |
||||
|
```javascript |
||||
|
methods: { |
||||
|
async onInit (promise) { |
||||
|
// show loading indicator |
||||
|
|
||||
|
try { |
||||
|
const { capabilities } = await promise |
||||
|
|
||||
|
// successfully initialized |
||||
|
} catch (error) { |
||||
|
if (error.name === 'NotAllowedError') { |
||||
|
// user denied camera access permisson |
||||
|
} else if (error.name === 'NotFoundError') { |
||||
|
// no suitable camera device installed |
||||
|
} else if (error.name === 'NotSupportedError') { |
||||
|
// page is not served over HTTPS (or localhost) |
||||
|
} else if (error.name === 'NotReadableError') { |
||||
|
// maybe camera is already in use |
||||
|
} else if (error.name === 'OverconstrainedError') { |
||||
|
// did you requested the front camera although there is none? |
||||
|
} else if (error.name === 'StreamApiNotSupportedError') { |
||||
|
// browser seems to be lacking features |
||||
|
} |
||||
|
} finally { |
||||
|
// hide loading indicator |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
## Props |
||||
|
|
||||
|
### `track` |
||||
|
* **Input Type:** `Function` |
||||
|
* **Default:** `undefined` |
||||
|
|
||||
|
You can visually highlight detected QR codes in real-time. |
||||
|
A transparent canvas overlays the camera stream. |
||||
|
When a QR code is detected, its location is painted to the canvas. |
||||
|
|
||||
|
To enable this feature, pass a function to `track` that defines how this should look like. |
||||
|
This function is called to produce each frame. |
||||
|
It receives the location object as the first argument and a `CanvasRenderingContext2D` instance as the second argument. |
||||
|
|
||||
|
For example check out the [Custom Tracking Demo](../demos/CustomTracking.md) |
||||
|
|
||||
|
Note that this scanning frequency has to be increased. |
||||
|
So if you want to go easy on your target device you might not want to enable tracking. |
||||
|
|
||||
|
::: danger |
||||
|
Avoid access to reactive properties in this function (like stuff in `data`, `computed` or your Vuex store). The function is called several times a second and might cause memory leaks. To be safe don't access `this` at all. |
||||
|
::: |
||||
|
|
||||
|
|
||||
|
### `camera` |
||||
|
* **Input Type:** `String` |
||||
|
* **Default:** `auto` |
||||
|
* **Valid Inputs:** `auto`, `rear`, `front`, `off` |
||||
|
|
||||
|
With the `camera` prop you can control which camera to access on the users device. |
||||
|
|
||||
|
* Use `front` or `rear` to force request the front or rear camera respectively. |
||||
|
* If you choose `auto` the rear camera is requested by default. |
||||
|
But if a device like a laptop has only a front camera installed, `auto` will fallback to that. |
||||
|
* Use `off` to not request a camera at all or in other words: turn the camera off. |
||||
|
|
||||
|
Every time the camera prop is modified, a new camera stream is requested so the `init` event is emitted again. |
||||
|
That way you can catch errors. |
||||
|
For example when the front camera is requested on a device that doesn't have one. |
||||
|
|
||||
|
```html |
||||
|
<qrcode-stream :camera="camera" @init="onCameraChange"></qrcode-stream> |
||||
|
``` |
||||
|
```js |
||||
|
data () { |
||||
|
return { |
||||
|
camera: 'auto' |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
methods: { |
||||
|
startFrontCamera () { |
||||
|
this.camera = 'front' |
||||
|
}, |
||||
|
|
||||
|
onCameraChange (promise) { |
||||
|
promise.catch(error => { |
||||
|
const cameraMissingError = error.name === 'OverconstrainedError' |
||||
|
const triedFrontCamera = this.camera === 'front' |
||||
|
|
||||
|
if (triedFrontCamera && cameraMissingError) { |
||||
|
// no front camera on this device |
||||
|
} |
||||
|
}) |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
### `torch` |
||||
|
* **Input Type:** `Boolean` |
||||
|
* **Default:** `false` |
||||
|
|
||||
|
With the `torch` prop you can turn a devices flashlight on/off. |
||||
|
This is not consistently supported by all devices and browsers. |
||||
|
Support can even vary on the same device with the same browser. |
||||
|
For example the rear camera often has a flashlight but the front camera doesn't. |
||||
|
We can only tell if flashlight control is supported once the camera is loaded and the `init` event has been emitted. |
||||
|
At the moment, `torch` silently fails on unsupported devices. |
||||
|
But from the `init` events payload you can access the [MediaTrackCapabilities](https://developer.mozilla.org/en-US/docs/Web/API/MediaStreamTrack/getCapabilities) object. |
||||
|
This will tell you whether or not `torch` is supported. |
||||
|
|
||||
|
Due to API limitations the camera stream must be reloaded when turning the torch on/off. |
||||
|
That means the `init` event will be emitted again. |
||||
|
|
||||
|
|
||||
|
```html |
||||
|
<qrcode-stream :torch="true" @init="onInit"></qrcode-stream> |
||||
|
``` |
||||
|
```js |
||||
|
methods: { |
||||
|
async onInit (promise) { |
||||
|
const { capabilities } = await promise |
||||
|
|
||||
|
const TORCH_IS_SUPPORTED = !!capabilities.torch |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
### `worker` <Badge text="removed in v3.0.0" type="error" /> |
||||
|
|
||||
|
[old documentation](https://github.com/gruhn/vue-qrcode-reader/blob/3608e0e04b0fbc8d2b57a5713fef92eef1e84c41/docs/api/QrcodeStream.md#worker-) |
||||
|
|
||||
|
## Slots |
||||
|
|
||||
|
### default |
||||
|
|
||||
|
Any distributed content overlays the camera stream, wrapped in a `position: absolute` container. |
||||
|
|
||||
|
```html |
||||
|
<qrcode-stream> |
||||
|
<b>stuff here overlays the camera stream</b> |
||||
|
</qrcode-stream> |
||||
|
``` |
||||
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 746 B |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
@ -0,0 +1,13 @@ |
|||||
|
# Visual Tracking |
||||
|
|
||||
|
Hold a QR code into view of the camera. |
||||
|
It should be visually highlighted in real-time. |
||||
|
Use the track function select below to change the flavor. |
||||
|
|
||||
|
<ClientOnly> |
||||
|
<DemoWrapper component="CustomTracking" /> |
||||
|
</ClientOnly> |
||||
|
|
||||
|
### Source |
||||
|
|
||||
|
<<< @/docs/.vuepress/components/demos/CustomTracking.vue |
||||
@ -0,0 +1,12 @@ |
|||||
|
# Decode Continuously |
||||
|
|
||||
|
Hold a QR code in the camera and see what happens. Note, you can't scan the same |
||||
|
QR code multiple time in a row. |
||||
|
|
||||
|
<ClientOnly> |
||||
|
<DemoWrapper component="DecodeAll" /> |
||||
|
</ClientOnly> |
||||
|
|
||||
|
### Source |
||||
|
|
||||
|
<<< @/docs/.vuepress/components/demos/DecodeAll.vue |
||||
@ -0,0 +1,13 @@ |
|||||
|
# Decode by Drag&Drop |
||||
|
|
||||
|
With the `QrcodeDropZone` component you can also drag-and-drop images that |
||||
|
should be scanned. Use it as a standalone feature or as a fallback for desktop |
||||
|
users. |
||||
|
|
||||
|
<ClientOnly> |
||||
|
<DemoWrapper component="DragDrop" /> |
||||
|
</ClientOnly> |
||||
|
|
||||
|
### Source |
||||
|
|
||||
|
<<< @/docs/.vuepress/components/demos/DragDrop.vue |
||||
@ -0,0 +1,13 @@ |
|||||
|
# Fullscreen |
||||
|
|
||||
|
`QrcodeStream` always covers the entire space available. |
||||
|
Not more, not less. |
||||
|
So to go fullscreen, simply put the component in a wrapper element that occupies the entire screen. |
||||
|
|
||||
|
<ClientOnly> |
||||
|
<DemoWrapper component="Fullscreen" /> |
||||
|
</ClientOnly> |
||||
|
|
||||
|
### Source |
||||
|
|
||||
|
<<< @/docs/.vuepress/components/demos/Fullscreen.vue |
||||
@ -0,0 +1,14 @@ |
|||||
|
# Show Loading Indicator |
||||
|
|
||||
|
There is some delay between mounting the component and the camera stream |
||||
|
becoming visible. Listen for the `init` event to show a loading indicator. |
||||
|
|
||||
|
Push the button below to force destroy and re-create the component. |
||||
|
|
||||
|
<ClientOnly> |
||||
|
<DemoWrapper component="LoadingIndicator" /> |
||||
|
</ClientOnly> |
||||
|
|
||||
|
### Source |
||||
|
|
||||
|
<<< @/docs/.vuepress/components/demos/LoadingIndicator.vue |
||||
@ -0,0 +1,17 @@ |
|||||
|
# Scan Same QR Code More Than Once |
||||
|
|
||||
|
You might have noticed that scanning the same QR code again doesn't work. |
||||
|
The thing is when a QR code is in the view of your the camera it's decoded multiple times a second. |
||||
|
You don't want to be flooded with decode events that often though. |
||||
|
That's why the last decoded QR code is "cached" and an event is only emitted, when the decoded content changes. |
||||
|
|
||||
|
However this cache is reset when you change the `camera` prop. |
||||
|
We can exploit that to scan same QR codes multiple times in a row. |
||||
|
|
||||
|
<ClientOnly> |
||||
|
<DemoWrapper component="ScanSameQrcodeMoreThanOnce" /> |
||||
|
</ClientOnly> |
||||
|
|
||||
|
### Source |
||||
|
|
||||
|
<<< @/docs/.vuepress/components/demos/ScanSameQrcodeMoreThanOnce.vue |
||||
@ -0,0 +1,14 @@ |
|||||
|
# Simple |
||||
|
|
||||
|
All other demos on this page utilize [single-file components](https://vuejs.org/v2/guide/single-file-components.html). |
||||
|
To use them in your project you need a bundler like webpack. |
||||
|
|
||||
|
For simpler example that works without a build step and right in the browser, checkout this: |
||||
|
|
||||
|
|
||||
|
<p class="codepen" data-height="265" data-theme-id="light" data-default-tab="js,result" data-user="gruhn" data-slug-hash="rNxQPay" style="height: 265px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="rNxQPay"> |
||||
|
<span>See the Pen <a href="https://codepen.io/gruhn/pen/rNxQPay"> |
||||
|
rNxQPay</a> by Niklas (<a href="https://codepen.io/gruhn">@gruhn</a>) |
||||
|
on <a href="https://codepen.io">CodePen</a>.</span> |
||||
|
</p> |
||||
|
<script async src="https://static.codepen.io/assets/embed/ei.js"></script> |
||||
@ -0,0 +1,12 @@ |
|||||
|
# Switch to Front Camera |
||||
|
|
||||
|
You can also allow users to choose the front or rear camera on their |
||||
|
device. |
||||
|
|
||||
|
<ClientOnly> |
||||
|
<DemoWrapper component="SwitchCamera" /> |
||||
|
</ClientOnly> |
||||
|
|
||||
|
### Source |
||||
|
|
||||
|
<<< @/docs/.vuepress/components/demos/SwitchCamera.vue |
||||
@ -0,0 +1,15 @@ |
|||||
|
# Torch (Flashlight) |
||||
|
|
||||
|
In low-light conditions you might want to make use of the cameras flashlight. |
||||
|
Using the `torch` prop, you can turn the flashlight on/off. |
||||
|
Note that support is inconsistent across devices and browers. |
||||
|
|
||||
|
Feature sponsored by [aeschbacher.ch](https://aeschbacher.ch) |
||||
|
|
||||
|
<ClientOnly> |
||||
|
<DemoWrapper component="Torch" /> |
||||
|
</ClientOnly> |
||||
|
|
||||
|
### Source |
||||
|
|
||||
|
<<< @/docs/.vuepress/components/demos/Torch.vue |
||||
@ -0,0 +1,18 @@ |
|||||
|
# Decode by Upload |
||||
|
|
||||
|
Finally, with `QrcodeCapture` comes another component which allows image scanning via classic file upload. |
||||
|
Nothing is actually uploaded. Everything is happening client-side. |
||||
|
|
||||
|
If you are on mobile and your browser supports it, |
||||
|
you are not prompted with a file dialog but with your camera. |
||||
|
So you can directly take the picture to be uploaded. |
||||
|
Adjust this behavior with the following dropdown: |
||||
|
|
||||
|
|
||||
|
<ClientOnly> |
||||
|
<DemoWrapper component="Upload" /> |
||||
|
</ClientOnly> |
||||
|
|
||||
|
### Source |
||||
|
|
||||
|
<<< @/docs/.vuepress/components/demos/Upload.vue |
||||
@ -0,0 +1,13 @@ |
|||||
|
# Pause & Validate |
||||
|
|
||||
|
By turning off the camera you can process each scanned QR-code one at a time. |
||||
|
The last received frame is still displayed so it just looks like the stream is |
||||
|
paused. |
||||
|
|
||||
|
<ClientOnly> |
||||
|
<DemoWrapper component="Validate" /> |
||||
|
</ClientOnly> |
||||
|
|
||||
|
### Source |
||||
|
|
||||
|
<<< @/docs/.vuepress/components/demos/Validate.vue |
||||
@ -0,0 +1,63 @@ |
|||||
|
{ |
||||
|
"name": "vue-qrcode-reader", |
||||
|
"description": "A set of Vue.js components for detecting and decoding QR codes.", |
||||
|
"version": "0.0.0-development", |
||||
|
"author": { |
||||
|
"name": "Niklas Gruhn", |
||||
|
"email": "niklas@gruhn.me" |
||||
|
}, |
||||
|
"repository": "github:gruhn/vue-qrcode-reader", |
||||
|
"bugs": "https://github.com/gruhn/vue-qrcode-reader/issues", |
||||
|
"homepage": "https://gruhn.github.io/vue-qrcode-reader", |
||||
|
"main": "dist/VueQrcodeReader.common.js", |
||||
|
"unpkg": "dist/VueQrcodeReader.umd.min.js", |
||||
|
"keywords": [ |
||||
|
"vue", |
||||
|
"vuejs", |
||||
|
"vue-component", |
||||
|
"qrcode", |
||||
|
"qrcode-reader", |
||||
|
"qrcode-scanner", |
||||
|
"webrtc" |
||||
|
], |
||||
|
"license": "MIT", |
||||
|
"types": "./src/types/vue-qrcode-reader.d.ts", |
||||
|
"scripts": { |
||||
|
"dev": "vuepress dev docs", |
||||
|
"build:docs": "vuepress build docs", |
||||
|
"build": "vue-cli-service build --target lib --name VueQrcodeReader src/index.js", |
||||
|
"lint": "vue-cli-service lint" |
||||
|
}, |
||||
|
"dependencies": { |
||||
|
"barcode-detector": "^1.0.0", |
||||
|
"callforth": "^0.3.1", |
||||
|
"core-js": "^3.6.5", |
||||
|
"vue": "^2.6.11", |
||||
|
"webrtc-adapter": "7.7.0" |
||||
|
}, |
||||
|
"devDependencies": { |
||||
|
"@vue/cli-plugin-babel": "~4.4.0", |
||||
|
"@vue/cli-plugin-eslint": "~4.4.0", |
||||
|
"@vue/cli-service": "~4.4.0", |
||||
|
"@vue/eslint-config-prettier": "^6.0.0", |
||||
|
"babel-eslint": "^10.1.0", |
||||
|
"eslint": "^6.7.2", |
||||
|
"eslint-plugin-prettier": "^3.1.3", |
||||
|
"eslint-plugin-vue": "^6.2.2", |
||||
|
"husky": "^4.2.5", |
||||
|
"lint-staged": "^9.5.0", |
||||
|
"prettier": "^1.19.1", |
||||
|
"semantic-release": "^17.1.1", |
||||
|
"vue-template-compiler": "^2.6.11", |
||||
|
"vuepress": "^1.5.2" |
||||
|
}, |
||||
|
"gitHooks": { |
||||
|
"pre-commit": "lint-staged" |
||||
|
}, |
||||
|
"lint-staged": { |
||||
|
"*.{js,jsx,vue}": [ |
||||
|
"vue-cli-service lint", |
||||
|
"git add" |
||||
|
] |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,30 @@ |
|||||
|
<template lang="html"> |
||||
|
<input |
||||
|
@change="onChangeInput" |
||||
|
type="file" |
||||
|
name="image" |
||||
|
accept="image/*" |
||||
|
capture="environment" |
||||
|
multiple |
||||
|
/> |
||||
|
</template> |
||||
|
|
||||
|
<script> |
||||
|
import { processFile } from "../misc/scanner.js"; |
||||
|
import CommonAPI from "../mixins/CommonAPI.vue"; |
||||
|
|
||||
|
export default { |
||||
|
name: "qrcode-capture", |
||||
|
|
||||
|
mixins: [CommonAPI], |
||||
|
|
||||
|
methods: { |
||||
|
onChangeInput(event) { |
||||
|
const files = [...event.target.files]; |
||||
|
const resultPromises = files.map(processFile); |
||||
|
|
||||
|
resultPromises.forEach(this.onDetect); |
||||
|
} |
||||
|
} |
||||
|
}; |
||||
|
</script> |
||||
@ -0,0 +1,42 @@ |
|||||
|
<template lang="html"> |
||||
|
<div |
||||
|
@drop.prevent.stop="onDrop" |
||||
|
@dragenter.prevent.stop="onDragOver(true)" |
||||
|
@dragleave.prevent.stop="onDragOver(false)" |
||||
|
@dragover.prevent.stop |
||||
|
> |
||||
|
<slot></slot> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script> |
||||
|
import { processFile, processUrl } from "../misc/scanner.js"; |
||||
|
import CommonAPI from "../mixins/CommonAPI.vue"; |
||||
|
|
||||
|
export default { |
||||
|
name: "qrcode-drop-zone", |
||||
|
|
||||
|
mixins: [CommonAPI], |
||||
|
|
||||
|
methods: { |
||||
|
onDragOver(isDraggingOver) { |
||||
|
this.$emit("dragover", isDraggingOver); |
||||
|
}, |
||||
|
|
||||
|
onDrop({ dataTransfer }) { |
||||
|
this.onDragOver(false); |
||||
|
|
||||
|
const droppedFiles = [...dataTransfer.files]; |
||||
|
const droppedUrl = dataTransfer.getData("text/uri-list"); |
||||
|
|
||||
|
droppedFiles.forEach(file => { |
||||
|
this.onDetect(processFile(file)); |
||||
|
}); |
||||
|
|
||||
|
if (droppedUrl !== "") { |
||||
|
this.onDetect(processUrl(droppedUrl)); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
}; |
||||
|
</script> |
||||
@ -0,0 +1,324 @@ |
|||||
|
<template lang="html"> |
||||
|
<div class="qrcode-stream-wrapper"> |
||||
|
<!-- |
||||
|
Note, the following DOM elements are not styled with z-index. |
||||
|
If z-index is not defined, elements are stacked in the order they appear in the DOM. |
||||
|
The first element is at the very bottom and subsequent elements are added on top. |
||||
|
--> |
||||
|
<video |
||||
|
ref="video" |
||||
|
:class="{ 'qrcode-stream-camera--hidden': !shouldScan }" |
||||
|
class="qrcode-stream-camera" |
||||
|
autoplay |
||||
|
muted |
||||
|
playsinline |
||||
|
></video> |
||||
|
|
||||
|
<canvas |
||||
|
ref="pauseFrame" |
||||
|
v-show="!shouldScan" |
||||
|
class="qrcode-stream-camera" |
||||
|
></canvas> |
||||
|
|
||||
|
<canvas ref="trackingLayer" class="qrcode-stream-overlay"></canvas> |
||||
|
|
||||
|
<div class="qrcode-stream-overlay"> |
||||
|
<slot></slot> |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script> |
||||
|
import { keepScanning } from "../misc/scanner.js"; |
||||
|
import Camera from "../misc/camera.js"; |
||||
|
import CommonAPI from "../mixins/CommonAPI.vue"; |
||||
|
|
||||
|
export default { |
||||
|
name: "qrcode-stream", |
||||
|
|
||||
|
mixins: [CommonAPI], |
||||
|
|
||||
|
props: { |
||||
|
camera: { |
||||
|
type: String, |
||||
|
default: "auto", |
||||
|
|
||||
|
validator(camera) { |
||||
|
return ["auto", "rear", "front", "off"].includes(camera); |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
torch: { |
||||
|
type: Boolean, |
||||
|
default: false |
||||
|
}, |
||||
|
|
||||
|
track: { |
||||
|
type: Function |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
data() { |
||||
|
return { |
||||
|
cameraInstance: null, |
||||
|
destroyed: false |
||||
|
}; |
||||
|
}, |
||||
|
|
||||
|
computed: { |
||||
|
shouldStream() { |
||||
|
return this.destroyed === false && this.camera !== "off"; |
||||
|
}, |
||||
|
|
||||
|
shouldScan() { |
||||
|
return this.shouldStream === true && this.cameraInstance !== null; |
||||
|
}, |
||||
|
|
||||
|
/** |
||||
|
* Minimum delay in milliseconds between frames to be scanned. Don't scan |
||||
|
* so often when visual tracking is disabled to improve performance. |
||||
|
*/ |
||||
|
scanInterval() { |
||||
|
if (this.track === undefined) { |
||||
|
return 500; |
||||
|
} else { |
||||
|
return 40; // ~ 25fps |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
watch: { |
||||
|
shouldStream(shouldStream) { |
||||
|
if (!shouldStream) { |
||||
|
const canvas = this.$refs.pauseFrame; |
||||
|
const ctx = canvas.getContext("2d"); |
||||
|
const video = this.$refs.video; |
||||
|
|
||||
|
canvas.width = video.videoWidth; |
||||
|
canvas.height = video.videoHeight; |
||||
|
|
||||
|
ctx.drawImage(video, 0, 0, video.videoWidth, video.videoHeight); |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
shouldScan(shouldScan) { |
||||
|
if (shouldScan) { |
||||
|
this.clearCanvas(this.$refs.pauseFrame); |
||||
|
this.clearCanvas(this.$refs.trackingLayer); |
||||
|
this.startScanning(); |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
torch() { |
||||
|
this.init(); |
||||
|
}, |
||||
|
|
||||
|
camera() { |
||||
|
this.init(); |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
mounted() { |
||||
|
this.init(); |
||||
|
}, |
||||
|
|
||||
|
beforeDestroy() { |
||||
|
this.beforeResetCamera(); |
||||
|
this.destroyed = true; |
||||
|
}, |
||||
|
|
||||
|
methods: { |
||||
|
init() { |
||||
|
const promise = (async () => { |
||||
|
this.beforeResetCamera(); |
||||
|
|
||||
|
if (this.camera === "off") { |
||||
|
this.cameraInstance = null; |
||||
|
|
||||
|
return { |
||||
|
capabilities: {} |
||||
|
}; |
||||
|
} else { |
||||
|
this.cameraInstance = await Camera(this.$refs.video, { |
||||
|
camera: this.camera, |
||||
|
torch: this.torch |
||||
|
}); |
||||
|
|
||||
|
const capabilities = this.cameraInstance.getCapabilities(); |
||||
|
|
||||
|
// if the component is destroyed before `cameraInstance` resolves a |
||||
|
// `beforeDestroy` hook has no chance to clear the remaining camera |
||||
|
// stream. |
||||
|
if (this.destroyed) { |
||||
|
this.cameraInstance.stop(); |
||||
|
} |
||||
|
|
||||
|
return { |
||||
|
capabilities |
||||
|
}; |
||||
|
} |
||||
|
})(); |
||||
|
|
||||
|
this.$emit("init", promise); |
||||
|
}, |
||||
|
|
||||
|
startScanning() { |
||||
|
const detectHandler = result => { |
||||
|
this.onDetect(Promise.resolve(result)); |
||||
|
}; |
||||
|
|
||||
|
keepScanning(this.$refs.video, { |
||||
|
detectHandler, |
||||
|
locateHandler: this.onLocate, |
||||
|
minDelay: this.scanInterval |
||||
|
}); |
||||
|
}, |
||||
|
|
||||
|
beforeResetCamera() { |
||||
|
if (this.cameraInstance !== null) { |
||||
|
this.cameraInstance.stop(); |
||||
|
this.cameraInstance = null; |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
onLocate(detectedCodes) { |
||||
|
const canvas = this.$refs.trackingLayer; |
||||
|
const video = this.$refs.video; |
||||
|
|
||||
|
if (canvas !== undefined) { |
||||
|
if (detectedCodes.length > 0 && this.track !== undefined && video !== undefined) { |
||||
|
// The visually occupied area of the video element. |
||||
|
// Because the component is responsive and fills the available space, |
||||
|
// this can be more or less than the actual resolution of the camera. |
||||
|
const displayWidth = video.offsetWidth; |
||||
|
const displayHeight = video.offsetHeight; |
||||
|
|
||||
|
// The actual resolution of the camera. |
||||
|
// These values are fixed no matter the screen size. |
||||
|
const resolutionWidth = video.videoWidth; |
||||
|
const resolutionHeight = video.videoHeight; |
||||
|
|
||||
|
// Dimensions of the video element as if there would be no |
||||
|
// object-fit: cover; |
||||
|
// Thus, the ratio is the same as the cameras resolution but it's |
||||
|
// scaled down to the size of the visually occupied area. |
||||
|
const largerRatio = Math.max( |
||||
|
displayWidth / resolutionWidth, |
||||
|
displayHeight / resolutionHeight |
||||
|
); |
||||
|
const uncutWidth = resolutionWidth * largerRatio; |
||||
|
const uncutHeight = resolutionHeight * largerRatio; |
||||
|
|
||||
|
const xScalar = uncutWidth / resolutionWidth; |
||||
|
const yScalar = uncutHeight / resolutionHeight; |
||||
|
const xOffset = (displayWidth - uncutWidth) / 2; |
||||
|
const yOffset = (displayHeight - uncutHeight) / 2; |
||||
|
|
||||
|
const scale = ({ x, y }) => { |
||||
|
return { |
||||
|
x: Math.floor(x * xScalar), |
||||
|
y: Math.floor(y * yScalar) |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
const translate = ({ x, y }) => { |
||||
|
return { |
||||
|
x: Math.floor(x + xOffset), |
||||
|
y: Math.floor(y + yOffset) |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
const adjustedCodes = detectedCodes.map(detectedCode => { |
||||
|
const { boundingBox, cornerPoints } = detectedCode |
||||
|
|
||||
|
const { x, y } = translate(scale({ |
||||
|
x: boundingBox.x, |
||||
|
y: boundingBox.y |
||||
|
})) |
||||
|
const { x: width, y: height } = scale({ |
||||
|
x: boundingBox.width, |
||||
|
y: boundingBox.height |
||||
|
}) |
||||
|
|
||||
|
return { |
||||
|
...detectedCode, |
||||
|
cornerPoints: cornerPoints.map(point => translate(scale(point))), |
||||
|
boundingBox: DOMRectReadOnly.fromRect({ x, y, width, height }) |
||||
|
} |
||||
|
}) |
||||
|
|
||||
|
canvas.width = video.offsetWidth; |
||||
|
canvas.height = video.offsetHeight; |
||||
|
|
||||
|
const ctx = canvas.getContext('2d'); |
||||
|
|
||||
|
this.track(adjustedCodes, ctx); |
||||
|
} else { |
||||
|
this.clearCanvas(canvas); |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
repaintTrackingLayer(video, canvas, location) { |
||||
|
const ctx = canvas.getContext("2d"); |
||||
|
|
||||
|
|
||||
|
window.requestAnimationFrame(() => { |
||||
|
canvas.width = displayWidth; |
||||
|
canvas.height = displayHeight; |
||||
|
|
||||
|
this.trackRepaintFunction(coordinatesAdjusted, ctx); |
||||
|
}); |
||||
|
}, |
||||
|
|
||||
|
clearCanvas(canvas) { |
||||
|
const ctx = canvas.getContext("2d"); |
||||
|
ctx.clearRect(0, 0, canvas.width, canvas.height); |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
}; |
||||
|
</script> |
||||
|
|
||||
|
<style lang="css" scoped> |
||||
|
.qrcode-stream-wrapper { |
||||
|
width: 100%; |
||||
|
height: 100%; |
||||
|
|
||||
|
position: relative; |
||||
|
z-index: 0; |
||||
|
} |
||||
|
|
||||
|
.qrcode-stream-overlay { |
||||
|
width: 100%; |
||||
|
height: 100%; |
||||
|
|
||||
|
position: absolute; |
||||
|
top: 0; |
||||
|
left: 0; |
||||
|
} |
||||
|
|
||||
|
.qrcode-stream-camera { |
||||
|
width: 100%; |
||||
|
height: 100%; |
||||
|
|
||||
|
display: block; |
||||
|
object-fit: cover; |
||||
|
} |
||||
|
/* When a camera stream is loaded, we assign the stream to the `video` |
||||
|
* element via `video.srcObject`. At this point the element used to be |
||||
|
* hidden with `v-show="false"` aka. `display: none`. We do that because |
||||
|
* at this point the videos dimensions are not known yet. We have to |
||||
|
* wait for the `loadeddata` event first. Only after that event we |
||||
|
* display the video element. Otherwise the elements size awkwardly flickers. |
||||
|
* |
||||
|
* However, it appears in iOS 15 all iOS browsers won't properly render |
||||
|
* the video element if the `video.srcObject` was assigned *while* the |
||||
|
* element was hidden with `display: none`. Using `visibility: hidden` |
||||
|
* instead seems to have fixed the problem though. |
||||
|
*/ |
||||
|
.qrcode-stream-camera--hidden { |
||||
|
visibility: hidden; |
||||
|
position: absolute; |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,32 @@ |
|||||
|
import QrcodeStream from "./components/QrcodeStream.vue"; |
||||
|
import QrcodeCapture from "./components/QrcodeCapture.vue"; |
||||
|
import QrcodeDropZone from "./components/QrcodeDropZone.vue"; |
||||
|
|
||||
|
// Install the components
|
||||
|
export function install(Vue) { |
||||
|
Vue.component("qrcode-stream", QrcodeStream); |
||||
|
Vue.component("qrcode-capture", QrcodeCapture); |
||||
|
Vue.component("qrcode-drop-zone", QrcodeDropZone); |
||||
|
} |
||||
|
|
||||
|
// Expose the components
|
||||
|
export { QrcodeStream, QrcodeCapture, QrcodeDropZone }; |
||||
|
|
||||
|
/* -- Plugin definition & Auto-install -- */ |
||||
|
/* You shouldn't have to modify the code below */ |
||||
|
|
||||
|
// Plugin
|
||||
|
const plugin = { install }; |
||||
|
|
||||
|
export default plugin; |
||||
|
|
||||
|
// Auto-install
|
||||
|
let GlobalVue = null; |
||||
|
if (typeof window !== "undefined") { |
||||
|
GlobalVue = window.Vue; |
||||
|
} else if (typeof global !== "undefined") { |
||||
|
GlobalVue = global.Vue; |
||||
|
} |
||||
|
if (GlobalVue) { |
||||
|
GlobalVue.use(plugin); |
||||
|
} |
||||
@ -0,0 +1,132 @@ |
|||||
|
import { StreamApiNotSupportedError, InsecureContextError } from "./errors.js"; |
||||
|
import { eventOn, timeout } from "callforth"; |
||||
|
import shimGetUserMedia from "./shimGetUserMedia"; |
||||
|
|
||||
|
class Camera { |
||||
|
constructor(videoEl, stream) { |
||||
|
this.videoEl = videoEl; |
||||
|
this.stream = stream; |
||||
|
} |
||||
|
|
||||
|
stop() { |
||||
|
this.videoEl.srcObject = null; |
||||
|
|
||||
|
this.stream.getTracks().forEach(track => { |
||||
|
this.stream.removeTrack(track); |
||||
|
track.stop(); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
getCapabilities() { |
||||
|
const [track] = this.stream.getVideoTracks(); |
||||
|
// Firefox does not yet support getCapabilities as of August 2020
|
||||
|
return track?.getCapabilities?.() ?? {}; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Modern phones often have multipe front/rear cameras.
|
||||
|
// Sometimes special purpose cameras like the wide-angle camera are picked
|
||||
|
// by default. Those are not optimal for scanning QR codes but standard
|
||||
|
// media constraints don't allow us to specify which camera we want exactly.
|
||||
|
const narrowDownFacingMode = async camera => { |
||||
|
// Filter some devices, known to be bad choices.
|
||||
|
const deviceBlackList = ["OBS Virtual Camera", "OBS-Camera"]; |
||||
|
|
||||
|
const devices = (await navigator.mediaDevices.enumerateDevices()) |
||||
|
.filter(({ kind }) => kind === "videoinput") |
||||
|
.filter(({ label }) => !deviceBlackList.includes(label)) |
||||
|
.filter(({ label }) => !label.includes("infrared")); |
||||
|
|
||||
|
if (devices.length > 2) { |
||||
|
// Explicitly picking the first entry in the list of all videoinput
|
||||
|
// devices for as the default front camera and the last entry as the default
|
||||
|
// rear camera seems to be a good heuristic on some devices.
|
||||
|
const frontCamera = devices[0]; |
||||
|
const rearCamera = devices[devices.length - 1]; |
||||
|
|
||||
|
switch (camera) { |
||||
|
case "auto": |
||||
|
return { deviceId: { exact: rearCamera.deviceId } }; |
||||
|
case "rear": |
||||
|
return { deviceId: { exact: rearCamera.deviceId } }; |
||||
|
case "front": |
||||
|
return { deviceId: { exact: frontCamera.deviceId } }; |
||||
|
default: |
||||
|
return undefined; |
||||
|
} |
||||
|
} else { |
||||
|
switch (camera) { |
||||
|
case "auto": |
||||
|
return { facingMode: { ideal: "environment" } }; |
||||
|
case "rear": |
||||
|
return { facingMode: { exact: "environment" } }; |
||||
|
case "front": |
||||
|
return { facingMode: { exact: "user" } }; |
||||
|
default: |
||||
|
return undefined; |
||||
|
} |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
export default async function(videoEl, { camera, torch }) { |
||||
|
// At least in Chrome `navigator.mediaDevices` is undefined when the page is
|
||||
|
// loaded using HTTP rather than HTTPS. Thus `STREAM_API_NOT_SUPPORTED` is
|
||||
|
// initialized with `false` although the API might actually be supported.
|
||||
|
// So although `getUserMedia` already should have a built-in mechanism to
|
||||
|
// detect insecure context (by throwing `NotAllowedError`), we have to do a
|
||||
|
// manual check before even calling `getUserMedia`.
|
||||
|
if (window.isSecureContext !== true) { |
||||
|
throw new InsecureContextError(); |
||||
|
} |
||||
|
|
||||
|
if (navigator?.mediaDevices?.getUserMedia === undefined) { |
||||
|
throw new StreamApiNotSupportedError(); |
||||
|
} |
||||
|
|
||||
|
// This is a browser API only shim. It patches the global window object which
|
||||
|
// is not available during SSR. So we lazily apply this shim at runtime.
|
||||
|
await shimGetUserMedia(); |
||||
|
|
||||
|
const constraints = { |
||||
|
audio: false, |
||||
|
video: { |
||||
|
width: { min: 360, ideal: 640, max: 1920 }, |
||||
|
height: { min: 240, ideal: 480, max: 1080 }, |
||||
|
...(await narrowDownFacingMode(camera)) |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
const stream = await navigator.mediaDevices.getUserMedia(constraints); |
||||
|
|
||||
|
if (videoEl.srcObject !== undefined) { |
||||
|
videoEl.srcObject = stream; |
||||
|
} else if (videoEl.mozSrcObject !== undefined) { |
||||
|
videoEl.mozSrcObject = stream; |
||||
|
} else if (window.URL.createObjectURL) { |
||||
|
videoEl.src = window.URL.createObjectURL(stream); |
||||
|
} else if (window.webkitURL) { |
||||
|
videoEl.src = window.webkitURL.createObjectURL(stream); |
||||
|
} else { |
||||
|
videoEl.src = stream; |
||||
|
} |
||||
|
|
||||
|
await eventOn(videoEl, "loadeddata"); |
||||
|
|
||||
|
// According to: https://oberhofer.co/mediastreamtrack-and-its-capabilities/#queryingcapabilities
|
||||
|
// On some devices, getCapabilities only returns a non-empty object after
|
||||
|
// some delay. There is no appropriate event so we have to add a constant timeout
|
||||
|
await timeout(500); |
||||
|
|
||||
|
if (torch) { |
||||
|
const [track] = stream.getVideoTracks(); |
||||
|
const capabilities = track.getCapabilities(); |
||||
|
|
||||
|
if (capabilities.torch) { |
||||
|
track.applyConstraints({ advanced: [{ torch: true }] }); |
||||
|
} else { |
||||
|
console.warn("device does not support torch capability"); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return new Camera(videoEl, stream); |
||||
|
} |
||||
@ -0,0 +1,25 @@ |
|||||
|
export class DropImageFetchError extends Error { |
||||
|
constructor() { |
||||
|
super("can't process cross-origin image"); |
||||
|
|
||||
|
this.name = "DropImageFetchError"; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export class StreamApiNotSupportedError extends Error { |
||||
|
constructor() { |
||||
|
super("this browser has no Stream API support"); |
||||
|
|
||||
|
this.name = "StreamApiNotSupportedError"; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export class InsecureContextError extends Error { |
||||
|
constructor() { |
||||
|
super( |
||||
|
"camera access is only permitted in secure context. Use HTTPS or localhost rather than HTTP." |
||||
|
); |
||||
|
|
||||
|
this.name = "InsecureContextError"; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,108 @@ |
|||||
|
import { DropImageFetchError } from "./errors.js"; |
||||
|
import { eventOn } from "callforth"; |
||||
|
|
||||
|
const adaptOldFormat = detectedCodes => { |
||||
|
if (detectedCodes.length > 0) { |
||||
|
const [ firstCode ] = detectedCodes; |
||||
|
|
||||
|
const [ |
||||
|
topLeftCorner, |
||||
|
topRightCorner, |
||||
|
bottomRightCorner, |
||||
|
bottomLeftCorner |
||||
|
] = firstCode.cornerPoints |
||||
|
|
||||
|
return { |
||||
|
content: firstCode.rawValue, |
||||
|
location: { |
||||
|
topLeftCorner, |
||||
|
topRightCorner, |
||||
|
bottomRightCorner, |
||||
|
bottomLeftCorner, |
||||
|
|
||||
|
// not supported by native API:
|
||||
|
topLeftFinderPattern: {}, |
||||
|
topRightFinderPattern: {}, |
||||
|
bottomLeftFinderPattern: {} |
||||
|
}, |
||||
|
imageData: null |
||||
|
} |
||||
|
} else { |
||||
|
return { |
||||
|
content: null, |
||||
|
location: null, |
||||
|
imageData: null |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Continuously extracts frames from camera stream and tries to read |
||||
|
* potentially pictured QR codes. |
||||
|
*/ |
||||
|
export const keepScanning = (videoElement, options) => { |
||||
|
const barcodeDetector = new BarcodeDetector({ formats: ["qr_code"] }); |
||||
|
|
||||
|
const { detectHandler, locateHandler, minDelay } = options; |
||||
|
|
||||
|
const processFrame = state => async timeNow => { |
||||
|
if (videoElement.readyState > 1) { |
||||
|
const { lastScanned, contentBefore, locationBefore } = state |
||||
|
|
||||
|
if (timeNow - lastScanned >= minDelay) { |
||||
|
const detectedCodes = await barcodeDetector.detect(videoElement); |
||||
|
const { content, location, imageData } = adaptOldFormat(detectedCodes) |
||||
|
|
||||
|
if (content !== null && content !== contentBefore) { |
||||
|
detectHandler({ content, location, imageData }); |
||||
|
} |
||||
|
|
||||
|
if (location !== null || locationBefore !== null) { |
||||
|
locateHandler(detectedCodes); |
||||
|
} |
||||
|
|
||||
|
window.requestAnimationFrame(processFrame({ |
||||
|
lastScanned: timeNow, |
||||
|
contentBefore: content ?? contentBefore, |
||||
|
locationBefore: location |
||||
|
})) |
||||
|
} else { |
||||
|
window.requestAnimationFrame(processFrame(state)) |
||||
|
} |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
processFrame({ |
||||
|
contentBefore: null, |
||||
|
locationBefore: null, |
||||
|
lastScanned: performance.now() |
||||
|
})(); |
||||
|
}; |
||||
|
|
||||
|
const imageElementFromUrl = async url => { |
||||
|
if (url.startsWith("http") && url.includes(location.host) === false) { |
||||
|
throw new DropImageFetchError(); |
||||
|
} |
||||
|
|
||||
|
const image = document.createElement("img"); |
||||
|
image.src = url; |
||||
|
|
||||
|
await eventOn(image, "load"); |
||||
|
|
||||
|
return image; |
||||
|
} |
||||
|
|
||||
|
export const processFile = async file => { |
||||
|
const barcodeDetector = new BarcodeDetector({ formats: ["qr_code"] }) |
||||
|
const detectedCodes = await barcodeDetector.detect(file) |
||||
|
|
||||
|
return adaptOldFormat(detectedCodes) |
||||
|
} |
||||
|
|
||||
|
export const processUrl = async url => { |
||||
|
const barcodeDetector = new BarcodeDetector({ formats: ["qr_code"] }) |
||||
|
const image = await imageElementFromUrl(url); |
||||
|
const detectedCodes = await barcodeDetector.detect(image) |
||||
|
|
||||
|
return adaptOldFormat(detectedCodes) |
||||
|
} |
||||
@ -0,0 +1,29 @@ |
|||||
|
import { shimGetUserMedia as chromeShim } from "webrtc-adapter/src/js/chrome/getusermedia"; |
||||
|
import { shimGetUserMedia as edgeShim } from "webrtc-adapter/src/js/edge/getusermedia"; |
||||
|
import { shimGetUserMedia as firefoxShim } from "webrtc-adapter/src/js/firefox/getusermedia"; |
||||
|
import { shimGetUserMedia as safariShim } from "webrtc-adapter/src/js/safari/safari_shim"; |
||||
|
import { detectBrowser } from "webrtc-adapter/src/js/utils"; |
||||
|
|
||||
|
import { StreamApiNotSupportedError } from "./errors"; |
||||
|
import { indempotent } from "./util"; |
||||
|
|
||||
|
export default indempotent(() => { |
||||
|
const { browser } = detectBrowser(window); |
||||
|
|
||||
|
switch (browser) { |
||||
|
case "chrome": |
||||
|
chromeShim(window); |
||||
|
break; |
||||
|
case "firefox": |
||||
|
firefoxShim(window); |
||||
|
break; |
||||
|
case "edge": |
||||
|
edgeShim(window); |
||||
|
break; |
||||
|
case "safari": |
||||
|
safariShim(window); |
||||
|
break; |
||||
|
default: |
||||
|
throw new StreamApiNotSupportedError(); |
||||
|
} |
||||
|
}); |
||||
@ -0,0 +1,15 @@ |
|||||
|
export const indempotent = action => { |
||||
|
let called = false; |
||||
|
let result = undefined; |
||||
|
|
||||
|
return (...args) => { |
||||
|
if (called) { |
||||
|
return result; |
||||
|
} else { |
||||
|
result = action(...args); |
||||
|
called = true; |
||||
|
|
||||
|
return result; |
||||
|
} |
||||
|
}; |
||||
|
}; |
||||
@ -0,0 +1,27 @@ |
|||||
|
<script> |
||||
|
import BarcodeDetector from "barcode-detector" |
||||
|
|
||||
|
export default { |
||||
|
beforeMount() { |
||||
|
// if (!('BarcodeDetector' in window)) { |
||||
|
window.BarcodeDetector = BarcodeDetector |
||||
|
// } |
||||
|
}, |
||||
|
|
||||
|
methods: { |
||||
|
async onDetect(resultPromise) { |
||||
|
this.$emit("detect", resultPromise); |
||||
|
|
||||
|
try { |
||||
|
const { content } = await resultPromise; |
||||
|
|
||||
|
if (content !== null) { |
||||
|
this.$emit("decode", content); |
||||
|
} |
||||
|
} catch (error) { |
||||
|
// fail silently |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
}; |
||||
|
</script> |
||||
@ -0,0 +1,4 @@ |
|||||
|
declare module "vue-qrcode-reader"; |
||||
|
declare module 'QrcodeCapture'; |
||||
|
declare module 'QrcodeDropZone'; |
||||
|
declare module 'QrcodeStream'; |
||||
@ -0,0 +1,7 @@ |
|||||
|
module.exports = { |
||||
|
css: { extract: false }, |
||||
|
|
||||
|
lintOnSave: false, |
||||
|
|
||||
|
transpileDependencies: ["webrtc-adapter"] |
||||
|
}; |
||||
@ -0,0 +1,144 @@ |
|||||
|
<template> |
||||
|
<view class="content"> |
||||
|
<view class="text-area"> |
||||
|
<text>{{code}}</text> |
||||
|
<text class="title">{{result}}</text> |
||||
|
</view> |
||||
|
<view v-if="select_view == 'credible_view'" class="open_view"> |
||||
|
<button type="primary">受信地址</button> |
||||
|
</view> |
||||
|
|
||||
|
<view v-if="select_view == 'open_view'" class="open_view"> |
||||
|
<button type="primary">不受信地址</button> |
||||
|
</view> |
||||
|
</view> |
||||
|
</template> |
||||
|
|
||||
|
<script> |
||||
|
import helper from "@/common/helper.js"; |
||||
|
|
||||
|
export default{ |
||||
|
data() { |
||||
|
return { |
||||
|
domain:"www.zunkr.com", |
||||
|
code: '默认的内容',// 获取的二维码 |
||||
|
result: '', |
||||
|
select_view:"no", |
||||
|
model:"pda", |
||||
|
callbackurl:'' |
||||
|
} |
||||
|
}, |
||||
|
onLoad:function(option){ |
||||
|
var that = this; |
||||
|
if(!option.code){ |
||||
|
this.result = "扫码内容为空"; |
||||
|
} |
||||
|
if(option.model){ |
||||
|
this.model = option.model; |
||||
|
} |
||||
|
this.callbackurl = decodeURIComponent(option.callbackurl);// 回调地址 |
||||
|
const code = decodeURIComponent(option.code); |
||||
|
// code = "https://www.baidu.com?51b6d2c0-1964-11ea-a286-00163e0dfa3d"; |
||||
|
this.code = code; |
||||
|
// 分解是类型 |
||||
|
switch (typeof(code)){ |
||||
|
case 'number': |
||||
|
this.result = code+"number 类型"; |
||||
|
break; |
||||
|
case 'string': |
||||
|
this.result = code+"string 类型"; |
||||
|
|
||||
|
if(helper.IsURL(code)){ |
||||
|
|
||||
|
// 解析内部地址 |
||||
|
var domain = helper.AnalysisURL(code)[2] |
||||
|
console.log(domain) |
||||
|
console.log(that.domain) |
||||
|
console.log(that.model) |
||||
|
if(domain === that.domain){ |
||||
|
// 检测工作模式 |
||||
|
if (that.model == 'pda'){ |
||||
|
// 分解参数 |
||||
|
var param = code.split("?"); |
||||
|
var uuid = param[1]; |
||||
|
this.result = domain+"为可控域名\r\nuuid为"+uuid; |
||||
|
if(that.callbackurl){ |
||||
|
uni.redirectTo({ |
||||
|
url:that.callbackurl+'?item='+uuid |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
}else if(that.model == 'browser'){ |
||||
|
this.result = domain+"可信域验证通过"; |
||||
|
this.select_view = "credible_view" |
||||
|
} |
||||
|
}else{ |
||||
|
// 检测工作模式 |
||||
|
console.log(that.model) |
||||
|
if (that.model == 'pda'){ |
||||
|
uni.showToast({ |
||||
|
title:"PDA模式权限验证失败" |
||||
|
}) |
||||
|
this.result = '模式权限验证失败'; |
||||
|
this.select_view = "no" |
||||
|
}else if(that.model == 'browser'){ |
||||
|
this.result = domain+"不属于内部域名可以第二webview打开"; |
||||
|
this.select_view = "open_view" |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
|
||||
|
}else{ |
||||
|
this.result = code+"外部数据禁止使用"; |
||||
|
} |
||||
|
|
||||
|
break; |
||||
|
case 'boolean': |
||||
|
this.result = code+"boolean 类型"; |
||||
|
break; |
||||
|
case 'array': |
||||
|
this.result = code+"array 类型"; |
||||
|
break; |
||||
|
default: |
||||
|
this.result = code+"default 类型"; |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
console.log('App Load') |
||||
|
}, |
||||
|
onShow: function() { |
||||
|
var that = this; |
||||
|
|
||||
|
console.log('App Show') |
||||
|
}, |
||||
|
methods:{ |
||||
|
back(){ |
||||
|
console.log("回去"); |
||||
|
uni.navigateBack({ |
||||
|
delta:1 |
||||
|
}) |
||||
|
}, |
||||
|
open_view(){ |
||||
|
console.log(encodeURI(this.code)); |
||||
|
uni.navigateTo({ |
||||
|
url: "/pages/webviews/openview?url="+ encodeURI(this.code) , |
||||
|
animationType: 'pop-in', |
||||
|
animationDuration: 200, |
||||
|
success: res => {}, |
||||
|
fail: () => {}, |
||||
|
complete: () => {} |
||||
|
}); |
||||
|
|
||||
|
|
||||
|
} |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<style> |
||||
|
.scan-result { |
||||
|
min-height: 50upx; |
||||
|
line-height: 50upx; |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,435 @@ |
|||||
|
<!DOCTYPE html> |
||||
|
<html lang="en" xmlns:th="http://www.thymeleaf.org"> |
||||
|
<head> |
||||
|
<meta charset="utf-8"> |
||||
|
<title>分步表单</title> |
||||
|
<meta name="renderer" content="webkit"> |
||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"> |
||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"> |
||||
|
<link rel="stylesheet" href="/static/lib/layui-v2.6.3/css/layui.css" media="all"> |
||||
|
<link rel="stylesheet" href="/static/css/public.css" media="all"> |
||||
|
<link rel="stylesheet" href="/static/js/lay-module/step-lay/step.css" media="all"> |
||||
|
</head> |
||||
|
<body> |
||||
|
<style> |
||||
|
.inputdiv{ |
||||
|
display:flex;background-color: #fff;height: 38px;line-height: 38px;border: 1px solid rgb(238, 238, 238); |
||||
|
} |
||||
|
.layui-form-label{ |
||||
|
padding: 9px 0px; |
||||
|
text-align: left; |
||||
|
} |
||||
|
.layui-input-block{ |
||||
|
margin-left: 80px; |
||||
|
} |
||||
|
|
||||
|
</style> |
||||
|
<div class="layuimini-container"> |
||||
|
<div class="layuimini-main"> |
||||
|
<div class="layui-fluid"> |
||||
|
<!-- 出库申请--> |
||||
|
<div class="layui-carousel" id="stepForm" lay-filter="stepForm" style="margin: 0 auto;"> |
||||
|
<div carousel-item style="overflow: inherit"> |
||||
|
<div> |
||||
|
<form class="layui-form" style="margin: 0 auto;max-width: 460px;padding-top: 40px;"> |
||||
|
<div class="layui-card" id="cardParent"> |
||||
|
<div class="layui-card-body" id="cardItem1"> |
||||
|
<hr> |
||||
|
<i class="layui-icon layui-icon-subtraction" style="display: inline" onclick="deleteItem(this)"></i> |
||||
|
<div class="layui-form-item"> |
||||
|
<label class="layui-form-label">物料名称:</label> |
||||
|
<div class="layui-input-block"> |
||||
|
<div class="inputdiv"> |
||||
|
<input type="text" placeholder="请选择物料" class="layui-input" style="border-style: none" |
||||
|
id="openSonByMaterial" onblur="selectMaterialByName(this)" |
||||
|
lay-verify="required"/> |
||||
|
<i class="layui-icon layui-icon-search" style="display: inline" id="selectMaterial" onclick="selectMaterial(this)"></i> |
||||
|
</div> |
||||
|
<input type="text" name="mid" class="layui-input" id="mid" |
||||
|
style="display: none" lay-verify="required"/> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="layui-form-item"> |
||||
|
<label class="layui-form-label">物料编码:</label> |
||||
|
<div class="layui-input-block"> |
||||
|
<input id="code" name="code" type="text" placeholder="请填写入物料编码" value="" onblur="selectCode(this)" |
||||
|
class="layui-input" lay-verify="required"> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="layui-form-item"> |
||||
|
<label class="layui-form-label">所处库位:</label> |
||||
|
<div class="layui-input-block"> |
||||
|
<select name="placeId" id="place"></select> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="layui-form-item"> |
||||
|
<label class="layui-form-label">物料数量:</label> |
||||
|
<div class="layui-input-block"> |
||||
|
<input name="quantity" type="number" placeholder="请填写入物料数量" value="" |
||||
|
class="layui-input" lay-verify="number" required> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="layui-form-item"> |
||||
|
<label class="layui-form-label">备注说明:</label> |
||||
|
<div class="layui-input-block"> |
||||
|
<textarea name="applyRemark" placeholder="请填写相关原因及申请原因" value="" |
||||
|
class="layui-textarea"></textarea> |
||||
|
</div> |
||||
|
</div> |
||||
|
<i class="layui-icon layui-icon-addition" style="display: inline" onclick="addItem(this)"></i> |
||||
|
</div> |
||||
|
|
||||
|
<!-- 提交按钮--> |
||||
|
<div class="layui-form-item"> |
||||
|
<div class="layui-input-block"> |
||||
|
<button class="layui-btn" lay-submit lay-filter="formStep" style="margin-bottom: 30px;margin-left: 15%"> |
||||
|
 提交申请  |
||||
|
</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</form> |
||||
|
</div> |
||||
|
<div> |
||||
|
<form class="layui-form" style="margin: 0 auto;max-width: 460px;padding-top: 40px;"> |
||||
|
<div style="text-align: center;margin-top: 90px;"> |
||||
|
<i class="layui-icon layui-circle" |
||||
|
style="color: white;font-size:30px;font-weight:bold;background: #52C41A;padding: 20px;line-height: 80px;"></i> |
||||
|
<div style="font-size: 24px;color: #333;font-weight: 500;margin-top: 30px;"> |
||||
|
提交申请成功 |
||||
|
</div> |
||||
|
<div style="font-size: 14px;color: #666;margin-top: 20px;">预计审核时间为1天</div> |
||||
|
</div> |
||||
|
<div style="text-align: center;margin-top: 50px;"> |
||||
|
<button class="layui-btn next">再申请一次</button> |
||||
|
</div> |
||||
|
</form> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<hr> |
||||
|
<div style="color: #666;margin-top: 30px;margin-bottom: 40px;padding-left: 30px;"> |
||||
|
<h3>说明</h3><br> |
||||
|
申请提交后,24小时内审核完毕 |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
</div> |
||||
|
</div> |
||||
|
<script src="/static/lib/layui-v2.6.3/layui.js" charset="utf-8"></script> |
||||
|
<script src="/static/js/lay-config.js?v=1.0.4" charset="utf-8"></script> |
||||
|
<script> |
||||
|
// 用于添加标签 |
||||
|
function addItem(obj){} |
||||
|
// 用于删除标签 |
||||
|
function deleteItem(obj){} |
||||
|
// 用于编码查询 |
||||
|
function selectCode(obj){} |
||||
|
// 用于点击搜索按钮 |
||||
|
function selectMaterial(obj){} |
||||
|
// 用于物料名称查询 |
||||
|
function selectMaterialByName(obj){} |
||||
|
// 用于暂存卡片个数 |
||||
|
var params = []; |
||||
|
// 用于卡片编号 |
||||
|
var NewIdNumber = 1; |
||||
|
layui.use(['form', 'step', 'layer', 'jquery'], function () { |
||||
|
var $ = layui.$, |
||||
|
form = layui.form, |
||||
|
step = layui.step; |
||||
|
// 用于分步表单加载 |
||||
|
step.render({ |
||||
|
elem: '#stepForm', |
||||
|
filter: 'stepForm', |
||||
|
width: '100%', //设置容器宽度 |
||||
|
height: '600px', |
||||
|
stepItems: [{ |
||||
|
title: '填写信息' |
||||
|
}, { |
||||
|
title: '提交成功' |
||||
|
}] |
||||
|
}); |
||||
|
|
||||
|
// 提交 |
||||
|
form.on('submit(formStep)', function (data) { |
||||
|
data = data.field; |
||||
|
data.params = params; |
||||
|
$.ajax({ |
||||
|
url: "/depositoryRecord/applicationOut", |
||||
|
type: 'post', |
||||
|
dataType: 'json', |
||||
|
contentType: "application/json;charset=utf-8", |
||||
|
data: JSON.stringify(data), |
||||
|
beforeSend: function () { |
||||
|
this.layerIndex = layer.load(0, {shade: [0.5, '#393D49']}); |
||||
|
}, |
||||
|
success: function (data) { |
||||
|
layer.close(this.layerIndex); |
||||
|
if (data.status >= 300) { |
||||
|
layer.msg(data.statusInfo.message);//失败的表情 |
||||
|
return; |
||||
|
} else { |
||||
|
layer.msg("申请提交成功", { |
||||
|
icon: 6,//成功的表情 |
||||
|
time: 500 //1秒关闭(如果不配置,默认是3秒) |
||||
|
}, function () { |
||||
|
step.next('#stepForm'); |
||||
|
}); |
||||
|
} |
||||
|
}, |
||||
|
complete: function () { |
||||
|
layer.close(this.layerIndex); |
||||
|
} |
||||
|
}) |
||||
|
return false; |
||||
|
}); |
||||
|
|
||||
|
form.on('submit(formStep2)', function (data) { |
||||
|
step.next('#stepForm'); |
||||
|
return false; |
||||
|
}); |
||||
|
|
||||
|
$('.pre').click(function () { |
||||
|
step.pre('#stepForm'); |
||||
|
}); |
||||
|
|
||||
|
$('.next').click(function () { |
||||
|
step.next('#stepForm'); |
||||
|
}); |
||||
|
|
||||
|
// 实现卡片添加 |
||||
|
addItem = function (obj) { |
||||
|
// 获取父元素id |
||||
|
var parentId = obj.parentNode.id; |
||||
|
NewIdNumber = NewIdNumber + 1; |
||||
|
// 物料名称栏目 |
||||
|
var materialItem = ` |
||||
|
<div class="layui-card-body" id=`+"cardItem"+NewIdNumber+`> |
||||
|
<hr> |
||||
|
<i class="layui-icon layui-icon-subtraction" style="display: inline" onclick="deleteItem(this)"></i> |
||||
|
<div class="layui-form-item"> |
||||
|
<label class="layui-form-label">物料名称</label> |
||||
|
<div class="layui-input-block"> |
||||
|
<div class="inputdiv"> |
||||
|
<input type="text" placeholder="请选择物料" class="layui-input" style="border-style: none" |
||||
|
id="openSonByMaterial" lay-verify="required" onblur="selectMaterialByName(this)"/> |
||||
|
<i class="layui-icon layui-icon-search" style="display: inline" id="selectMaterial" onclick="selectMaterial(this)"></i> |
||||
|
</div> |
||||
|
<input type="text" name=`+"mid"+NewIdNumber+` class="layui-input" id="mid" |
||||
|
style="display: none" lay-verify="required" /> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="layui-form-item"> |
||||
|
<label class="layui-form-label">物料编码:</label> |
||||
|
<div class="layui-input-block"> |
||||
|
<input id="code" name=`+"code"+NewIdNumber+` type="text" placeholder="请填写入物料编码" value="" onblur="selectCode(this)" |
||||
|
class="layui-input" lay-verify="required"> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="layui-form-item"> |
||||
|
<label class="layui-form-label">所处库位:</label> |
||||
|
<div class="layui-input-block"> |
||||
|
<select name=`+"placeId"+NewIdNumber +` id="place"></select> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="layui-form-item"> |
||||
|
<label class="layui-form-label">物料数量:</label> |
||||
|
<div class="layui-input-block"> |
||||
|
<input name=`+"quantity"+NewIdNumber+` type="number" placeholder="请填写入物料数量" value="" |
||||
|
class="layui-input" lay-verify="number"> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="layui-form-item"> |
||||
|
<label class="layui-form-label">备注说明:</label> |
||||
|
<div class="layui-input-block"> |
||||
|
<textarea name=`+"applyRemark"+NewIdNumber+` placeholder="请填写相关原因及申请原因" value="" |
||||
|
class="layui-textarea"></textarea> |
||||
|
</div> |
||||
|
</div> |
||||
|
<i class="layui-icon layui-icon-addition" style="display: inline" onclick="addItem(this)"></i> |
||||
|
</div>`; |
||||
|
// 获取当前高度 |
||||
|
var height = parseInt(($("#stepForm").css('height')).split("px")[0]); |
||||
|
params.push(NewIdNumber) |
||||
|
$("#stepForm").css("height",height+430 +'px'); |
||||
|
$("#"+parentId).after(materialItem); |
||||
|
}; |
||||
|
|
||||
|
// 实现卡片删除 |
||||
|
deleteItem = function (obj) { |
||||
|
// 获取父节点 |
||||
|
var parent = obj.parentNode; |
||||
|
var parentId = parent.id; |
||||
|
parentId = parseInt(parentId.split("cardItem")[1]); |
||||
|
console.log(parentId); |
||||
|
// 获取祖父节点 |
||||
|
var reparent = parent.parentNode; |
||||
|
var height = parseInt(($("#stepForm").css('height')).split("px")[0]); |
||||
|
$("#stepForm").css("height",height-430 +'px'); |
||||
|
params = remove(params,parentId); |
||||
|
reparent.removeChild(parent); |
||||
|
}; |
||||
|
//删除数组中指定元素 |
||||
|
function remove(arr, item) { |
||||
|
var result = []; |
||||
|
for (let i = 0; i < arr.length; i++) { |
||||
|
if(arr[i] === item){ |
||||
|
continue; |
||||
|
} |
||||
|
result.push(arr[i]); |
||||
|
} |
||||
|
return result; |
||||
|
} |
||||
|
|
||||
|
// 用于实现通过编码查询物料 |
||||
|
selectCode = function(obj){ |
||||
|
// 输入code |
||||
|
var code = obj.value; |
||||
|
// 获取对应元素 |
||||
|
var parent = obj.parentNode.parentNode.parentNode; |
||||
|
var children = parent.childNodes[5]; |
||||
|
var materialItem = children.childNodes[3].childNodes[1].childNodes; |
||||
|
var materialName = materialItem[1]; |
||||
|
var materialId = materialName.parentNode.parentNode.childNodes[3]; |
||||
|
var req = {}; |
||||
|
req.code = code; |
||||
|
req.type = "out"; |
||||
|
$.ajax({ |
||||
|
url: "/material/findMatrialByCode", |
||||
|
type: "get", |
||||
|
dataType: 'json', |
||||
|
data:(req), |
||||
|
contentType: "application/json;charset=utf-8", |
||||
|
success: function (d) { |
||||
|
var d = d.data; |
||||
|
if(d == null){ |
||||
|
layer.msg("没有该编码,请确认是否输入正确"); |
||||
|
materialName.value = ""; |
||||
|
materialId.value = ""; |
||||
|
obj.value = ""; |
||||
|
$('#place').empty(); |
||||
|
}else{ |
||||
|
req.mid = d.id; |
||||
|
$.ajax({ |
||||
|
url: "/place/findPlaceByMid", |
||||
|
type: "post", |
||||
|
dataType: 'json', |
||||
|
data:JSON.stringify(req), |
||||
|
contentType: "application/json;charset=utf-8", |
||||
|
success:function (res) { |
||||
|
$('#place').empty(); |
||||
|
$.each(res.data, function (index, item) { |
||||
|
$('#place').append(new Option(item.depositoryName+"-"+item.code, item.id));//往下拉菜单里添加元素 |
||||
|
}); |
||||
|
form.render(); |
||||
|
materialName.value = d.mname; |
||||
|
materialId.value = d.id; |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
} |
||||
|
} |
||||
|
}); |
||||
|
}; |
||||
|
|
||||
|
// 用于实现点击搜索按钮 |
||||
|
selectMaterial = function (obj) { |
||||
|
var parent = obj.parentNode.parentNode.parentNode.parentNode; |
||||
|
var parentId = parent.id; |
||||
|
var codeChildren = parent.childNodes[7]; |
||||
|
var materialChildren = parent.childNodes[5]; |
||||
|
var codeItem = codeChildren.childNodes[3].childNodes; |
||||
|
var codeValue = codeItem[1]; |
||||
|
var materialItem = materialChildren.childNodes[3].childNodes[1].childNodes; |
||||
|
var materialName = materialItem[1]; |
||||
|
var materialId = materialName.parentNode.parentNode.childNodes[3]; |
||||
|
var mname = materialName.value; |
||||
|
layer.open({ |
||||
|
type: 2, |
||||
|
title: '弹窗内容', |
||||
|
skin: 'layui-layer-rim', |
||||
|
maxmin: true, |
||||
|
shadeClose: true, //点击遮罩关闭层 |
||||
|
area: ['70%', '70%'], |
||||
|
content: '/selectMaterialByCard?mname='+mname+'&type=2&clickObj='+parentId, |
||||
|
move : '.layui-layer-title', |
||||
|
fixed:false, |
||||
|
end: function () { |
||||
|
var mid = materialId.value; |
||||
|
$.ajax({ |
||||
|
url: "/material/findMatrialById?mid=" + mid, |
||||
|
type: "get", |
||||
|
dataType: 'json', |
||||
|
contentType: "application/json;charset=utf-8", |
||||
|
success: function (d) { |
||||
|
var material = d.data.materialById; |
||||
|
var placeList = d.data.placeList; |
||||
|
var code = material.code; |
||||
|
if(code === undefined){ |
||||
|
code = ""; |
||||
|
} |
||||
|
codeValue.value = code; |
||||
|
$('#place').empty(); |
||||
|
$.each(placeList, function (index, item) { |
||||
|
$('#place').append(new Option(item.depositoryName+"-"+item.code, item.id));//往下拉菜单里添加元素 |
||||
|
}); |
||||
|
form.render(); |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
} |
||||
|
}); |
||||
|
}; |
||||
|
|
||||
|
//用于实现物料名称搜索 |
||||
|
selectMaterialByName = function (obj) { |
||||
|
// 输入code |
||||
|
var data = obj.value; |
||||
|
// 获取对应元素 |
||||
|
var parent = obj.parentNode.parentNode.parentNode.parentNode; |
||||
|
var materialChildren = parent.childNodes[5]; |
||||
|
var codeChildren = parent.childNodes[7]; |
||||
|
var codeItem = codeChildren.childNodes[3].childNodes; |
||||
|
var codeValue = codeItem[1]; |
||||
|
var materialItem = materialChildren.childNodes[3].childNodes[1].childNodes; |
||||
|
var materialName = materialItem[1]; |
||||
|
var materialId = materialName.parentNode.parentNode.childNodes[3]; |
||||
|
var req = {}; |
||||
|
req.mname = data; |
||||
|
$.ajax({ |
||||
|
url: "/material/findMaterialByCondition", |
||||
|
type: "post", |
||||
|
dataType: 'json', |
||||
|
data:JSON.stringify(req), |
||||
|
contentType: "application/json;charset=utf-8", |
||||
|
success: function (d) { |
||||
|
if(d.count > 1){ |
||||
|
layer.msg("请点击右侧搜索确定物品"); |
||||
|
materialId.value = ""; |
||||
|
codeValue.value = ""; |
||||
|
return false; |
||||
|
}else if(d.count === 0){ |
||||
|
layer.msg("没有该物品,请确认输入是否正确"); |
||||
|
materialId.value = ""; |
||||
|
codeValue.value = ""; |
||||
|
materialName.value = ""; |
||||
|
return false; |
||||
|
} |
||||
|
else{ |
||||
|
var material = d.data[0]; |
||||
|
materialName.value = material.mname; |
||||
|
materialId.value = material.id; |
||||
|
codeValue.value = material.code; |
||||
|
|
||||
|
} |
||||
|
} |
||||
|
}); |
||||
|
}; |
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
}) |
||||
|
</script> |
||||
|
</body> |
||||
|
</html> |
||||
@ -0,0 +1,17 @@ |
|||||
|
<script> |
||||
|
export default { |
||||
|
onLaunch: function() { |
||||
|
console.log('App Launch') |
||||
|
}, |
||||
|
onShow: function() { |
||||
|
console.log('App Show') |
||||
|
}, |
||||
|
onHide: function() { |
||||
|
console.log('App Hide') |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<style> |
||||
|
/*每个页面公共css */ |
||||
|
</style> |
||||